fix: align video generation payloads
This commit is contained in:
@@ -329,6 +329,12 @@ func TestVolcesClientVideoSubmitsAndPollsTask(t *testing.T) {
|
||||
var gotModel string
|
||||
var gotText string
|
||||
var gotFirstFrameRole string
|
||||
var gotDuration float64
|
||||
var gotRatio string
|
||||
var gotResolution string
|
||||
var gotSeed float64
|
||||
var gotCameraFixed bool
|
||||
var gotWatermark bool
|
||||
var submittedRemoteTaskID string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotAuth = r.Header.Get("Authorization")
|
||||
@@ -343,6 +349,17 @@ func TestVolcesClientVideoSubmitsAndPollsTask(t *testing.T) {
|
||||
if body["prompt"] != nil || body["first_frame"] != nil {
|
||||
t.Fatalf("video convenience fields leaked upstream: %+v", body)
|
||||
}
|
||||
for _, key := range []string{"duration_seconds", "aspect_ratio", "audio", "cameraFixed"} {
|
||||
if _, ok := body[key]; ok {
|
||||
t.Fatalf("volces video task body should not include top-level %s: %+v", key, body)
|
||||
}
|
||||
}
|
||||
gotDuration, _ = body["duration"].(float64)
|
||||
gotRatio, _ = body["ratio"].(string)
|
||||
gotResolution, _ = body["resolution"].(string)
|
||||
gotSeed, _ = body["seed"].(float64)
|
||||
gotCameraFixed, _ = body["camera_fixed"].(bool)
|
||||
gotWatermark, _ = body["watermark"].(bool)
|
||||
content, _ := body["content"].([]any)
|
||||
textItem, _ := content[0].(map[string]any)
|
||||
gotText, _ = textItem["text"].(string)
|
||||
@@ -375,6 +392,10 @@ func TestVolcesClientVideoSubmitsAndPollsTask(t *testing.T) {
|
||||
"first_frame": "https://example.com/first.png",
|
||||
"duration": 6,
|
||||
"aspect_ratio": "16:9",
|
||||
"resolution": "720p",
|
||||
"seed": 11,
|
||||
"cameraFixed": false,
|
||||
"watermark": true,
|
||||
},
|
||||
Candidate: store.RuntimeModelCandidate{
|
||||
BaseURL: server.URL,
|
||||
@@ -406,10 +427,11 @@ func TestVolcesClientVideoSubmitsAndPollsTask(t *testing.T) {
|
||||
if gotModel != "doubao-seedance-2-0-260128" || gotFirstFrameRole != "first_frame" {
|
||||
t.Fatalf("unexpected submitted model=%s role=%s", gotModel, gotFirstFrameRole)
|
||||
}
|
||||
for _, fragment := range []string{"A clean product reveal", "--dur 6", "--ratio 16:9", "--watermark false", "--seed -1"} {
|
||||
if !strings.Contains(gotText, fragment) {
|
||||
t.Fatalf("expected text to contain %q, got %q", fragment, gotText)
|
||||
}
|
||||
if gotText != "A clean product reveal" {
|
||||
t.Fatalf("video params should not be appended to prompt text, got %q", gotText)
|
||||
}
|
||||
if gotDuration != 6 || gotRatio != "16:9" || gotResolution != "720p" || gotSeed != 11 || gotCameraFixed != false || gotWatermark != true {
|
||||
t.Fatalf("unexpected submitted video params duration=%v ratio=%s resolution=%s seed=%v camera_fixed=%v watermark=%v", gotDuration, gotRatio, gotResolution, gotSeed, gotCameraFixed, gotWatermark)
|
||||
}
|
||||
data, _ := response.Result["data"].([]any)
|
||||
item, _ := data[0].(map[string]any)
|
||||
@@ -418,6 +440,147 @@ func TestVolcesClientVideoSubmitsAndPollsTask(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolcesVideoBodyAllowsOnlyTaskPayloadFields(t *testing.T) {
|
||||
body := volcesVideoBody(Request{
|
||||
Kind: "videos.generations",
|
||||
ModelType: "omni_video",
|
||||
Model: "豆包Seedance",
|
||||
Body: map[string]any{
|
||||
"model": "豆包Seedance",
|
||||
"duration": 8,
|
||||
"duration_seconds": 8,
|
||||
"aspect_ratio": "9:16",
|
||||
"resolution": "720p",
|
||||
"audio": true,
|
||||
"callback_url": "https://example.com/callback",
|
||||
"returnLastFrame": true,
|
||||
"executionExpiresAfter": 3600,
|
||||
"draft": false,
|
||||
"cameraFixed": false,
|
||||
"watermark": true,
|
||||
"seed": -1,
|
||||
"task_id": "local-task-id",
|
||||
"runMode": "simulation",
|
||||
"fps": 24,
|
||||
"content": []any{
|
||||
map[string]any{"type": "text", "text": "Use <<<element_1>>> in a product reveal"},
|
||||
map[string]any{
|
||||
"type": "element",
|
||||
"element": map[string]any{
|
||||
"inline_element": map[string]any{
|
||||
"name": "subject",
|
||||
"frontal_image_url": "https://example.com/subject.png",
|
||||
"refer_images": []any{map[string]any{"url": "https://example.com/side.png", "slot_key": "side"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "image_url",
|
||||
"role": "unexpected_role",
|
||||
"name": "drop-me",
|
||||
"image_url": map[string]any{"url": "https://example.com/ref.png", "extra": "drop-me"},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "video_url",
|
||||
"duration": 3,
|
||||
"video_url": map[string]any{
|
||||
"url": "https://example.com/ref.mp4",
|
||||
"refer_type": "feature",
|
||||
"keep_original_sound": "yes",
|
||||
"extra": "drop-me",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "audio_url",
|
||||
"audio_url": map[string]any{"url": "https://example.com/ref.mp3", "extra": "drop-me"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Candidate: store.RuntimeModelCandidate{
|
||||
ModelName: "豆包Seedance",
|
||||
ProviderModelName: "doubao-seedance-2-0-260128",
|
||||
Credentials: map[string]any{"apiKey": "volces-key"},
|
||||
},
|
||||
})
|
||||
|
||||
allowedTopLevel := map[string]bool{
|
||||
"model": true, "content": true, "callback_url": true, "return_last_frame": true, "execution_expires_after": true,
|
||||
"generate_audio": true, "draft": true, "resolution": true, "ratio": true, "duration": true,
|
||||
"seed": true, "camera_fixed": true, "watermark": true,
|
||||
}
|
||||
for key := range body {
|
||||
if !allowedTopLevel[key] {
|
||||
t.Fatalf("unexpected top-level volces field %q in %+v", key, body)
|
||||
}
|
||||
}
|
||||
if body["model"] != "doubao-seedance-2-0-260128" ||
|
||||
body["generate_audio"] != true ||
|
||||
body["callback_url"] != "https://example.com/callback" ||
|
||||
body["return_last_frame"] != true ||
|
||||
body["execution_expires_after"] != 3600 ||
|
||||
body["draft"] != false ||
|
||||
body["resolution"] != "720p" ||
|
||||
body["ratio"] != "9:16" ||
|
||||
body["duration"] != 8 ||
|
||||
body["seed"] != -1 ||
|
||||
body["camera_fixed"] != false ||
|
||||
body["watermark"] != true {
|
||||
t.Fatalf("unexpected direct video fields: %+v", body)
|
||||
}
|
||||
|
||||
content, ok := body["content"].([]map[string]any)
|
||||
if !ok || len(content) != 5 {
|
||||
t.Fatalf("unexpected sanitized content: %#v", body["content"])
|
||||
}
|
||||
text := content[0]
|
||||
if text["type"] != "text" || strings.Contains(text["text"].(string), "--dur") || strings.Contains(text["text"].(string), "--ratio") {
|
||||
t.Fatalf("video params should not be appended to the text item: %+v", text)
|
||||
}
|
||||
elementImage := content[1]
|
||||
if elementImage["type"] != "image_url" || elementImage["role"] != "reference_image" {
|
||||
t.Fatalf("referenced element should be converted to reference image: %+v", elementImage)
|
||||
}
|
||||
imageURL, _ := elementImage["image_url"].(map[string]any)
|
||||
if imageURL["url"] != "https://example.com/subject.png" || len(imageURL) != 1 {
|
||||
t.Fatalf("element image payload should only include url: %+v", imageURL)
|
||||
}
|
||||
referenceImage := content[2]
|
||||
if referenceImage["role"] != "reference_image" || referenceImage["name"] != nil {
|
||||
t.Fatalf("image references should be role-normalized and scrubbed: %+v", referenceImage)
|
||||
}
|
||||
videoItem := content[3]
|
||||
videoURL, _ := videoItem["video_url"].(map[string]any)
|
||||
if videoItem["role"] != "reference_video" || videoURL["url"] != "https://example.com/ref.mp4" || videoURL["refer_type"] != "feature" || videoURL["extra"] != nil {
|
||||
t.Fatalf("video references should keep only allowed nested fields: %+v", videoItem)
|
||||
}
|
||||
audioItem := content[4]
|
||||
audioURL, _ := audioItem["audio_url"].(map[string]any)
|
||||
if audioItem["role"] != "reference_audio" || audioURL["url"] != "https://example.com/ref.mp3" || len(audioURL) != 1 {
|
||||
t.Fatalf("audio references should keep only url: %+v", audioItem)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolcesVideoBodyPrefersFramesOverDuration(t *testing.T) {
|
||||
body := volcesVideoBody(Request{
|
||||
Kind: "videos.generations",
|
||||
ModelType: "video_generate",
|
||||
Body: map[string]any{
|
||||
"prompt": "A quick camera move",
|
||||
"duration": 8,
|
||||
"frames": 57,
|
||||
},
|
||||
Candidate: store.RuntimeModelCandidate{
|
||||
ProviderModelName: "doubao-seedance-1-0-pro-250528",
|
||||
},
|
||||
})
|
||||
if body["frames"] != 57 {
|
||||
t.Fatalf("frames should be passed through as the official duration control: %+v", body)
|
||||
}
|
||||
if _, ok := body["duration"]; ok {
|
||||
t.Fatalf("duration should not be sent when frames is present: %+v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolcesClientVideoResumePollsExistingTaskID(t *testing.T) {
|
||||
var submitCalled bool
|
||||
var pollPath string
|
||||
|
||||
Reference in New Issue
Block a user