fix(canvas): 视频首尾帧 - 保留结构化帧并按协议顺序传参

This commit is contained in:
ddCat committed 2026-08-01 10:20:47 +08:00
1 parent 2ceaa56cd8
commit 49133a7c2a
6 files changed
+82 -17

No files matched your search

+27 -11
View File
@@ -1148,6 +1148,13 @@ func newAPIChannel2VideoBody(input canvasGenerationInput) (map[string]interface{
return nil, fmt.Errorf("NewAPI Video Generations 的 %s 必须且只能提供 1 张参考图(当前 %d 张)", input.Config.Model, len(images))
}
}
frameImages, err := videoFrameImageURLs(input, images)
if err != nil {
return nil, err
}
if len(frameImages) > 0 {
images = frameImages
}
seconds, secondsErr := strconv.Atoi(strings.TrimSpace(input.Config.VideoSeconds))
if secondsErr != nil || seconds < 1 {
@@ -2066,7 +2073,11 @@ func seedanceVideosBody(input canvasGenerationInput) (map[string]interface{}, er
}
imageURLs = append(imageURLs, url)
}
if frameImageURLs := seedanceVideosFrameImageURLs(input, imageURLs); len(frameImageURLs) > 0 {
frameImageURLs, err := videoFrameImageURLs(input, imageURLs)
if err != nil {
return nil, err
}
if len(frameImageURLs) > 0 {
body["image_urls"] = frameImageURLs
} else if len(imageURLs) > 0 {
body["image_url"] = imageURLs[0]
@@ -2117,36 +2128,41 @@ func seedanceImageRole(input canvasGenerationInput, image providerMedia) string
return "reference_image"
}
func seedanceVideosFrameImageURLs(input canvasGenerationInput, imageURLs []string) []string {
func videoFrameImageURLs(input canvasGenerationInput, imageURLs []string) ([]string, error) {
startFrameID := metadataString(input.Metadata, "videoStartFrameNodeId")
endFrameID := metadataString(input.Metadata, "videoEndFrameNodeId")
if startFrameID == "" && endFrameID == "" {
return nil
return nil, nil
}
// /v1/videos 的 image_urls 只接受字符串;首帧和尾帧通过数组顺序表达。
// image_urls 按首帧、尾帧、普通参考图排序,保持 JSON 视频协议的结构化帧语义。
ordered := make([]string, 0, len(imageURLs))
used := make([]bool, len(imageURLs))
appendFrame := func(frameID string) {
appendFrame := func(frameID string, label string) error {
if frameID == "" {
return
return nil
}
for index, image := range input.ReferenceImages {
if index >= len(imageURLs) || used[index] || image.ID != frameID {
if index >= len(imageURLs) || image.ID != frameID {
continue
}
ordered = append(ordered, imageURLs[index])
used[index] = true
return
return nil
}
return fmt.Errorf("已配置的%s参考图未包含在视频请求中", label)
}
if err := appendFrame(startFrameID, "首帧"); err != nil {
return nil, err
}
if err := appendFrame(endFrameID, "尾帧"); err != nil {
return nil, err
}
appendFrame(startFrameID)
appendFrame(endFrameID)
for index, imageURL := range imageURLs {
if !used[index] {
ordered = append(ordered, imageURL)
}
}
return ordered
return ordered, nil
}
func metadataString(metadata map[string]interface{}, key string) string {
+32
View File
@@ -8,6 +8,7 @@ import (
"mime/multipart"
"net/http"
"net/http/httptest"
"reflect"
"strconv"
"strings"
"testing"
@@ -915,6 +916,37 @@ func TestNewAPIChannel2SingleImageModelUsesReferenceForStaleTextToVideoMetadata(
}
}
func TestNewAPIChannel2OrdersFramesBeforeReferenceImages(t *testing.T) {
body, err := newAPIChannel2VideoBody(canvasGenerationInput{
Config: providerConfig{Model: "Seedance 2 Mini", VideoSeconds: "10"},
ReferenceImages: []providerMedia{
{ID: "character", DataURL: "data:image/png;base64,Y2hhcmFjdGVy"},
{ID: "last-frame", DataURL: "data:image/png;base64,bGFzdA=="},
{ID: "first-frame", DataURL: "data:image/png;base64,Zmlyc3Q="},
},
Metadata: map[string]interface{}{"videoStartFrameNodeId": "first-frame", "videoEndFrameNodeId": "last-frame", "videoEditOperation": "image_to_video"},
})
if err != nil {
t.Fatalf("newAPIChannel2VideoBody() error = %v", err)
}
images, ok := body["image_urls"].([]string)
want := []string{"data:image/png;base64,Zmlyc3Q=", "data:image/png;base64,bGFzdA==", "data:image/png;base64,Y2hhcmFjdGVy"}
if !ok || !reflect.DeepEqual(images, want) {
t.Fatalf("image_urls = %#v, want %#v", body["image_urls"], want)
}
}
func TestNewAPIChannel2RejectsMissingConfiguredFrame(t *testing.T) {
_, err := newAPIChannel2VideoBody(canvasGenerationInput{
Config: providerConfig{Model: "Seedance 2 Mini", VideoSeconds: "10"},
ReferenceImages: []providerMedia{{ID: "character", DataURL: testReferenceImageDataURL}},
Metadata: map[string]interface{}{"videoStartFrameNodeId": "missing-frame", "videoEditOperation": "image_to_video"},
})
if err == nil || !strings.Contains(err.Error(), "首帧参考图未包含") {
t.Fatalf("newAPIChannel2VideoBody() error = %v", err)
}
}
func TestValidateGenerationInterfaceRejectsMismatchedType(t *testing.T) {
if err := validateGenerationInterface("video", "chat-completion"); err == nil {
t.Fatal("validateGenerationInterface() error = nil")