fix(canvas): 视频首尾帧 - 保留结构化帧并按协议顺序传参
This commit is contained in:
1 parent
2ceaa56cd8
commit
49133a7c2a
6 files changed
+82
-17
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user