fix(acceptance): 校准 Seedance 图片转换验收
按 Volces 官方输入边界补齐 Seedance 2.0 候选能力,将错误的合法 4K 转换样本替换为真实越界图片,并让协议模拟器校验物化后的 Base64 data URL。\n\n验证:Go 全量测试、迁移安全检查、gofmt。
This commit is contained in:
@@ -28,7 +28,10 @@ import (
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
const maxProtocolBodyBytes = 128 << 20
|
||||
const (
|
||||
maxProtocolBodyBytes = 128 << 20
|
||||
maxImageReferenceBytes = 32 << 20
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Now func() time.Time
|
||||
@@ -434,37 +437,30 @@ func (s *Server) validateVideoRequest(ctx context.Context, body map[string]any)
|
||||
}
|
||||
if forceConversion {
|
||||
for _, ref := range refs {
|
||||
if ref.Width >= 3840 || ref.Height >= 2160 {
|
||||
return nil, false, false, errors.New("forced 4K image did not pass through automatic normalization")
|
||||
if !seedanceImageWithinOfficialInputRange(ref.Width, ref.Height) {
|
||||
return nil, false, false, errors.New("forced oversized image did not pass through automatic normalization")
|
||||
}
|
||||
}
|
||||
}
|
||||
return refs, longRun, forceConversion, nil
|
||||
}
|
||||
|
||||
func seedanceImageWithinOfficialInputRange(width int, height int) bool {
|
||||
if width < 300 || width > 6000 || height < 300 || height > 6000 {
|
||||
return false
|
||||
}
|
||||
ratio := float64(width) / float64(height)
|
||||
return ratio >= 0.4 && ratio <= 2.5
|
||||
}
|
||||
|
||||
func (s *Server) fetchImageReference(ctx context.Context, role string, rawURL string) (imageReference, error) {
|
||||
if rawURL == "" {
|
||||
return imageReference{}, errors.New("image reference URL is required")
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
||||
payload, err := s.readImageReference(ctx, rawURL)
|
||||
if err != nil {
|
||||
return imageReference{}, err
|
||||
}
|
||||
response, err := s.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return imageReference{}, fmt.Errorf("fetch image reference: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return imageReference{}, fmt.Errorf("fetch image reference: status %d", response.StatusCode)
|
||||
}
|
||||
payload, err := io.ReadAll(io.LimitReader(response.Body, 32<<20))
|
||||
if err != nil {
|
||||
return imageReference{}, err
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
return imageReference{}, errors.New("image reference is empty")
|
||||
}
|
||||
config, format, err := image.DecodeConfig(bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return imageReference{}, fmt.Errorf("decode image reference: %w", err)
|
||||
@@ -476,6 +472,67 @@ func (s *Server) fetchImageReference(ctx context.Context, role string, rawURL st
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) readImageReference(ctx context.Context, rawURL string) ([]byte, error) {
|
||||
if strings.HasPrefix(strings.ToLower(rawURL), "data:") {
|
||||
return decodeImageDataURL(rawURL)
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := s.httpClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch image reference: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("fetch image reference: status %d", response.StatusCode)
|
||||
}
|
||||
payload, err := io.ReadAll(io.LimitReader(response.Body, maxImageReferenceBytes+1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
return nil, errors.New("image reference is empty")
|
||||
}
|
||||
if len(payload) > maxImageReferenceBytes {
|
||||
return nil, errors.New("image reference exceeds 32 MiB")
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func decodeImageDataURL(raw string) ([]byte, error) {
|
||||
header, encoded, ok := strings.Cut(raw, ",")
|
||||
if !ok || len(header) <= len("data:") || encoded == "" {
|
||||
return nil, errors.New("image data URL is malformed")
|
||||
}
|
||||
metadata := strings.Split(header[len("data:"):], ";")
|
||||
if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(metadata[0])), "image/") {
|
||||
return nil, errors.New("image data URL must declare an image media type")
|
||||
}
|
||||
base64Encoded := false
|
||||
for _, item := range metadata[1:] {
|
||||
if strings.EqualFold(strings.TrimSpace(item), "base64") {
|
||||
base64Encoded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !base64Encoded {
|
||||
return nil, errors.New("image data URL must use base64 encoding")
|
||||
}
|
||||
if base64.StdEncoding.DecodedLen(len(encoded)) > maxImageReferenceBytes {
|
||||
return nil, errors.New("image reference exceeds 32 MiB")
|
||||
}
|
||||
payload, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return nil, errors.New("image data URL contains invalid base64")
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
return nil, errors.New("image reference is empty")
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func validateGeminiImageRequest(body map[string]any) (int, error) {
|
||||
generationConfig, _ := body["generationConfig"].(map[string]any)
|
||||
modalities, _ := generationConfig["responseModalities"].([]any)
|
||||
@@ -662,10 +719,10 @@ func buildFixtures() map[string]fixture {
|
||||
fixtures[fmt.Sprintf("image-%02d.webp", index+8)] = fixture{ContentType: "image/webp", Payload: payload}
|
||||
}
|
||||
for index := 0; index < 4; index++ {
|
||||
imageValue := patternedImage(4096, 2160, index+21)
|
||||
imageValue := patternedImage(6144, 2160, index+21)
|
||||
var encoded bytes.Buffer
|
||||
_ = jpeg.Encode(&encoded, imageValue, &jpeg.Options{Quality: 90})
|
||||
fixtures[fmt.Sprintf("image-%02d-4k.jpg", index+12)] = fixture{ContentType: "image/jpeg", Payload: encoded.Bytes()}
|
||||
fixtures[fmt.Sprintf("image-%02d-oversized.jpg", index+12)] = fixture{ContentType: "image/jpeg", Payload: encoded.Bytes()}
|
||||
}
|
||||
return fixtures
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user