feat(seedance): 同步输入图片约束与自动转换

This commit is contained in:
2026-07-23 21:55:42 +08:00
parent 46cdb1a288
commit 8b7d3e9c9a
7 changed files with 656 additions and 0 deletions
@@ -0,0 +1,140 @@
package runner
import (
"bytes"
"context"
"encoding/base64"
"image"
"image/color"
"image/jpeg"
"image/png"
"strings"
"testing"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func seedanceImageConstraintCandidate() store.RuntimeModelCandidate {
return store.RuntimeModelCandidate{
ModelType: "image_to_video",
Capabilities: map[string]any{
"image_to_video": map[string]any{
"input_image_resolution_range": map[string]any{
"min": map[string]any{"long_edge": float64(360), "short_edge": float64(360)},
"max": map[string]any{"long_edge": float64(1920), "short_edge": float64(1080)},
},
"input_image_aspect_ratio_range": []any{float64(0.39), float64(2.5)},
},
},
}
}
func pngImageDataURL(t *testing.T, width int, height int) string {
t.Helper()
source := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
source.Set(x, y, color.RGBA{R: 80, G: 120, B: 160, A: 255})
}
}
var payload bytes.Buffer
if err := png.Encode(&payload, source); err != nil {
t.Fatal(err)
}
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(payload.Bytes())
}
func decodedImageSize(t *testing.T, source string) image.Point {
t.Helper()
if !strings.HasPrefix(source, "data:image/jpeg;base64,") {
t.Fatalf("expected converted JPEG data URL, got prefix %.32q", source)
}
payload, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(source, "data:image/jpeg;base64,"))
if err != nil {
t.Fatal(err)
}
config, err := jpeg.DecodeConfig(bytes.NewReader(payload))
if err != nil {
t.Fatal(err)
}
return image.Pt(config.Width, config.Height)
}
func TestNormalizeVideoInputImagesResizesOversizedSeedanceImage(t *testing.T) {
service := &Service{}
body := map[string]any{
"content": []any{
map[string]any{
"type": "image_url",
"role": "first_frame",
"image_url": map[string]any{"url": pngImageDataURL(t, 3840, 2160)},
},
},
}
normalized, err := service.normalizeVideoInputImages(context.Background(), body, seedanceImageConstraintCandidate())
if err != nil {
t.Fatal(err)
}
content := normalized["content"].([]any)
item := content[0].(map[string]any)
imageURL := item["image_url"].(map[string]any)["url"].(string)
if got := decodedImageSize(t, imageURL); got != image.Pt(1920, 1080) {
t.Fatalf("unexpected converted size: %v", got)
}
}
func TestNormalizeVideoInputImagesPadsAspectRatioViolation(t *testing.T) {
service := &Service{}
source := pngImageDataURL(t, 1530, 500)
body := map[string]any{"first_frame": source}
normalized, err := service.normalizeVideoInputImages(context.Background(), body, seedanceImageConstraintCandidate())
if err != nil {
t.Fatal(err)
}
if got := decodedImageSize(t, normalized["first_frame"].(string)); got != image.Pt(1530, 612) {
t.Fatalf("unexpected converted size: %v", got)
}
}
func TestNormalizeVideoInputImagesUpscalesBelowMinimumResolution(t *testing.T) {
service := &Service{}
body := map[string]any{"first_frame": pngImageDataURL(t, 200, 100)}
normalized, err := service.normalizeVideoInputImages(context.Background(), body, seedanceImageConstraintCandidate())
if err != nil {
t.Fatal(err)
}
if got := decodedImageSize(t, normalized["first_frame"].(string)); got != image.Pt(720, 360) {
t.Fatalf("unexpected converted size: %v", got)
}
}
func TestNormalizeVideoInputImagesKeepsCompliantImage(t *testing.T) {
service := &Service{}
source := pngImageDataURL(t, 1280, 720)
body := map[string]any{"first_frame": source}
normalized, err := service.normalizeVideoInputImages(context.Background(), body, seedanceImageConstraintCandidate())
if err != nil {
t.Fatal(err)
}
if normalized["first_frame"] != source {
t.Fatal("compliant image should remain unchanged")
}
}
func TestNormalizeVideoInputImagesReturnsActionableDecodeError(t *testing.T) {
service := &Service{}
body := map[string]any{"first_frame": "data:image/png;base64,bm90LWltYWdl"}
_, err := service.normalizeVideoInputImages(context.Background(), body, seedanceImageConstraintCandidate())
if err == nil {
t.Fatal("expected decode error")
}
if clients.ErrorCode(err) != "invalid_parameter" || clients.ErrorParam(err) != "first_frame" {
t.Fatalf("unexpected error contract: code=%s param=%s err=%v", clients.ErrorCode(err), clients.ErrorParam(err), err)
}
}