fix: align video generation payloads
This commit is contained in:
@@ -7,10 +7,14 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var volcesElementReferencePattern = regexp.MustCompile(`(?i)<<<[[:space:]]*element[_-]?([0-9]+)[[:space:]]*>>>|@element([0-9]+)`)
|
||||
|
||||
type VolcesClient struct {
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
@@ -215,11 +219,9 @@ func volcesVideoBody(request Request) map[string]any {
|
||||
content = buildVolcesContentFromBody(body)
|
||||
}
|
||||
appendMultiShotTimeline(&content)
|
||||
convertVolcesElementsToImageReferences(&content)
|
||||
normalizeVolcesContentRoles(content)
|
||||
appendVolcesVideoParams(&content, body)
|
||||
body["content"] = content
|
||||
stripVolcesVideoConvenienceFields(body)
|
||||
return body
|
||||
return volcesVideoTaskBody(body, content)
|
||||
}
|
||||
|
||||
func cleanProviderBody(body map[string]any) map[string]any {
|
||||
@@ -286,56 +288,234 @@ func buildVolcesContentFromBody(body map[string]any) []map[string]any {
|
||||
return content
|
||||
}
|
||||
|
||||
func stripVolcesVideoConvenienceFields(body map[string]any) {
|
||||
for _, key := range []string{
|
||||
"prompt",
|
||||
"input",
|
||||
"image",
|
||||
"images",
|
||||
"image_url",
|
||||
"imageUrl",
|
||||
"image_urls",
|
||||
"imageUrls",
|
||||
"reference_image",
|
||||
"referenceImage",
|
||||
"first_frame",
|
||||
"firstFrame",
|
||||
"last_frame",
|
||||
"lastFrame",
|
||||
"video",
|
||||
"video_url",
|
||||
"videoUrl",
|
||||
"reference_video",
|
||||
"referenceVideo",
|
||||
"audio_url",
|
||||
"audioUrl",
|
||||
"reference_audio",
|
||||
"referenceAudio",
|
||||
} {
|
||||
delete(body, key)
|
||||
func volcesVideoTaskBody(body map[string]any, content []map[string]any) map[string]any {
|
||||
out := map[string]any{
|
||||
"model": body["model"],
|
||||
"content": sanitizeVolcesVideoContent(content),
|
||||
}
|
||||
addVolcesVideoTaskParams(out, body)
|
||||
return out
|
||||
}
|
||||
|
||||
func addVolcesVideoTaskParams(out map[string]any, body map[string]any) {
|
||||
copyVolcesStringParam(out, "callback_url", body, "callback_url", "callbackUrl")
|
||||
copyVolcesBoolParam(out, "return_last_frame", body, "return_last_frame", "returnLastFrame")
|
||||
copyVolcesIntParam(out, "execution_expires_after", body, "execution_expires_after", "executionExpiresAfter")
|
||||
copyVolcesBoolParam(out, "generate_audio", body, "generate_audio", "generateAudio", "audio")
|
||||
copyVolcesBoolParam(out, "draft", body, "draft")
|
||||
copyVolcesStringParam(out, "resolution", body, "resolution", "size")
|
||||
copyVolcesStringParam(out, "ratio", body, "ratio", "aspect_ratio", "aspectRatio")
|
||||
if copyVolcesIntParam(out, "frames", body, "frames") {
|
||||
delete(out, "duration")
|
||||
} else {
|
||||
copyVolcesIntParam(out, "duration", body, "duration", "duration_seconds", "durationSeconds", "dur")
|
||||
}
|
||||
copyVolcesIntParam(out, "seed", body, "seed")
|
||||
copyVolcesBoolParam(out, "camera_fixed", body, "camera_fixed", "cameraFixed", "camerafixed", "cf")
|
||||
copyVolcesBoolParam(out, "watermark", body, "watermark")
|
||||
}
|
||||
|
||||
func copyVolcesStringParam(out map[string]any, target string, body map[string]any, keys ...string) bool {
|
||||
for _, key := range keys {
|
||||
if value := strings.TrimSpace(stringFromAny(body[key])); value != "" {
|
||||
out[target] = value
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func copyVolcesIntParam(out map[string]any, target string, body map[string]any, keys ...string) bool {
|
||||
for _, key := range keys {
|
||||
if value, ok := volcesIntFromAny(body[key]); ok {
|
||||
out[target] = value
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func copyVolcesBoolParam(out map[string]any, target string, body map[string]any, keys ...string) bool {
|
||||
for _, key := range keys {
|
||||
if value, ok := volcesBoolFromAny(body[key]); ok {
|
||||
out[target] = value
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func volcesIntFromAny(value any) (int, bool) {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return 0, false
|
||||
case int:
|
||||
return typed, true
|
||||
case int64:
|
||||
return int(typed), true
|
||||
case float64:
|
||||
return int(math.Round(typed)), true
|
||||
case string:
|
||||
text := strings.TrimSpace(typed)
|
||||
if text == "" {
|
||||
return 0, false
|
||||
}
|
||||
if parsed, err := strconv.ParseFloat(text, 64); err == nil {
|
||||
return int(math.Round(parsed)), true
|
||||
}
|
||||
return 0, false
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func contentItems(value any) []map[string]any {
|
||||
rawItems, ok := value.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
func volcesBoolFromAny(value any) (bool, bool) {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return false, false
|
||||
case bool:
|
||||
return typed, true
|
||||
case int:
|
||||
if typed == 1 {
|
||||
return true, true
|
||||
}
|
||||
if typed == 0 {
|
||||
return false, true
|
||||
}
|
||||
case int64:
|
||||
if typed == 1 {
|
||||
return true, true
|
||||
}
|
||||
if typed == 0 {
|
||||
return false, true
|
||||
}
|
||||
case float64:
|
||||
if typed == 1 {
|
||||
return true, true
|
||||
}
|
||||
if typed == 0 {
|
||||
return false, true
|
||||
}
|
||||
case string:
|
||||
normalized := strings.ToLower(strings.TrimSpace(typed))
|
||||
if normalized == "true" || normalized == "1" {
|
||||
return true, true
|
||||
}
|
||||
if normalized == "false" || normalized == "0" {
|
||||
return false, true
|
||||
}
|
||||
}
|
||||
out := make([]map[string]any, 0, len(rawItems))
|
||||
for _, raw := range rawItems {
|
||||
item, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
return false, false
|
||||
}
|
||||
|
||||
func sanitizeVolcesVideoContent(content []map[string]any) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(content))
|
||||
for _, item := range content {
|
||||
switch stringFromAny(item["type"]) {
|
||||
case "text":
|
||||
out = append(out, map[string]any{
|
||||
"type": "text",
|
||||
"text": strings.TrimSpace(stringFromAny(item["text"])),
|
||||
})
|
||||
case "image_url":
|
||||
url := volcesNestedURL(item, "image_url")
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"type": "image_url",
|
||||
"role": volcesImageRole(item),
|
||||
"image_url": map[string]any{"url": url},
|
||||
})
|
||||
case "video_url":
|
||||
url := volcesNestedURL(item, "video_url")
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
videoURL := map[string]any{"url": url}
|
||||
if value := strings.TrimSpace(stringFromAny(mapFromAny(item["video_url"])["refer_type"])); value != "" {
|
||||
videoURL["refer_type"] = value
|
||||
}
|
||||
if value := strings.TrimSpace(stringFromAny(mapFromAny(item["video_url"])["keep_original_sound"])); value != "" {
|
||||
videoURL["keep_original_sound"] = value
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"type": "video_url",
|
||||
"role": "reference_video",
|
||||
"video_url": videoURL,
|
||||
})
|
||||
case "audio_url":
|
||||
url := volcesNestedURL(item, "audio_url")
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"type": "audio_url",
|
||||
"role": "reference_audio",
|
||||
"audio_url": map[string]any{"url": url},
|
||||
})
|
||||
}
|
||||
copied := map[string]any{}
|
||||
for key, value := range item {
|
||||
copied[key] = value
|
||||
}
|
||||
out = append(out, copied)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []map[string]any{{"type": "text", "text": ""}}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func volcesImageRole(item map[string]any) string {
|
||||
switch strings.TrimSpace(stringFromAny(item["role"])) {
|
||||
case "first_frame":
|
||||
return "first_frame"
|
||||
case "last_frame":
|
||||
return "last_frame"
|
||||
default:
|
||||
return "reference_image"
|
||||
}
|
||||
}
|
||||
|
||||
func volcesNestedURL(item map[string]any, key string) string {
|
||||
nested := mapFromAny(item[key])
|
||||
return strings.TrimSpace(stringFromAny(nested["url"]))
|
||||
}
|
||||
|
||||
func mapFromAny(value any) map[string]any {
|
||||
if object, ok := value.(map[string]any); ok {
|
||||
return object
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func contentItems(value any) []map[string]any {
|
||||
switch typed := value.(type) {
|
||||
case []any:
|
||||
out := make([]map[string]any, 0, len(typed))
|
||||
for _, raw := range typed {
|
||||
item, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
copied := map[string]any{}
|
||||
for key, value := range item {
|
||||
copied[key] = value
|
||||
}
|
||||
out = append(out, copied)
|
||||
}
|
||||
return out
|
||||
case []map[string]any:
|
||||
out := make([]map[string]any, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
copied := map[string]any{}
|
||||
for key, value := range item {
|
||||
copied[key] = value
|
||||
}
|
||||
out = append(out, copied)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeVolcesContentRoles(content []map[string]any) {
|
||||
for _, item := range content {
|
||||
itemType := strings.TrimSpace(stringFromAny(item["type"]))
|
||||
@@ -353,32 +533,115 @@ func normalizeVolcesContentRoles(content []map[string]any) {
|
||||
}
|
||||
}
|
||||
|
||||
func appendVolcesVideoParams(content *[]map[string]any, body map[string]any) {
|
||||
textItem := ensureTextContent(content)
|
||||
current := strings.TrimSpace(stringFromAny(textItem["text"]))
|
||||
values := []struct {
|
||||
key string
|
||||
value any
|
||||
}{
|
||||
{"dur", firstPresent(body["duration"], body["dur"])},
|
||||
{"ratio", firstPresent(body["aspect_ratio"], body["aspectRatio"], body["ratio"])},
|
||||
{"fps", firstPresent(body["framespersecond"], body["framesPerSecond"], body["fps"])},
|
||||
{"watermark", firstPresent(body["watermark"], false)},
|
||||
{"seed", firstPresent(body["seed"], -1)},
|
||||
{"cf", firstPresent(body["camerafixed"], body["cameraFixed"])},
|
||||
{"rs", firstPresent(body["resolution"], body["size"])},
|
||||
}
|
||||
for _, item := range values {
|
||||
valueText := volcesParamString(item.value)
|
||||
if valueText == "" || strings.Contains(current, "--"+item.key) {
|
||||
func convertVolcesElementsToImageReferences(content *[]map[string]any) {
|
||||
referenced := referencedVolcesElementIndexes(*content)
|
||||
out := make([]map[string]any, 0, len(*content))
|
||||
elementIndex := 0
|
||||
for _, item := range *content {
|
||||
if stringFromAny(item["type"]) != "element" {
|
||||
out = append(out, item)
|
||||
continue
|
||||
}
|
||||
if current != "" {
|
||||
current += " "
|
||||
elementIndex++
|
||||
if !referenced[elementIndex] {
|
||||
continue
|
||||
}
|
||||
current += "--" + item.key + " " + valueText
|
||||
url := volcesElementFrontalImageURL(item)
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
role := stringFromAny(item["role"])
|
||||
if role != "first_frame" && role != "last_frame" {
|
||||
role = "reference_image"
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"type": "image_url",
|
||||
"role": role,
|
||||
"image_url": map[string]any{"url": url},
|
||||
})
|
||||
}
|
||||
*content = out
|
||||
}
|
||||
|
||||
func referencedVolcesElementIndexes(content []map[string]any) map[int]bool {
|
||||
out := map[int]bool{}
|
||||
for _, item := range content {
|
||||
if stringFromAny(item["type"]) != "text" {
|
||||
continue
|
||||
}
|
||||
text := stringFromAny(item["text"])
|
||||
if strings.TrimSpace(text) == "" {
|
||||
continue
|
||||
}
|
||||
for _, match := range volcesElementReferencePattern.FindAllStringSubmatch(text, -1) {
|
||||
raw := ""
|
||||
if len(match) > 1 && match[1] != "" {
|
||||
raw = match[1]
|
||||
} else if len(match) > 2 {
|
||||
raw = match[2]
|
||||
}
|
||||
index, err := strconv.Atoi(raw)
|
||||
if err == nil && index > 0 {
|
||||
out[index] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func volcesElementFrontalImageURL(item map[string]any) string {
|
||||
element := mapFromAny(item["element"])
|
||||
if element == nil {
|
||||
return ""
|
||||
}
|
||||
inline := mapFromAny(element["inline_element"])
|
||||
for _, value := range []any{
|
||||
inline["frontal_image_url"],
|
||||
element["frontal_image_url"],
|
||||
element["front_image_url"],
|
||||
element["image_url"],
|
||||
} {
|
||||
if url := strings.TrimSpace(stringFromAny(value)); url != "" {
|
||||
return url
|
||||
}
|
||||
}
|
||||
return volcesReferImageURL(firstPresent(inline["refer_images"], element["refer_images"]))
|
||||
}
|
||||
|
||||
func volcesReferImageURL(value any) string {
|
||||
images := mapListFromAny(value)
|
||||
firstURL := ""
|
||||
for _, image := range images {
|
||||
url := strings.TrimSpace(stringFromAny(image["url"]))
|
||||
if url == "" {
|
||||
continue
|
||||
}
|
||||
if firstURL == "" {
|
||||
firstURL = url
|
||||
}
|
||||
slot := strings.ToLower(strings.TrimSpace(stringFromAny(image["slot_key"])))
|
||||
if slot == "frontal" || slot == "front" {
|
||||
return url
|
||||
}
|
||||
}
|
||||
return firstURL
|
||||
}
|
||||
|
||||
func mapListFromAny(value any) []map[string]any {
|
||||
switch typed := value.(type) {
|
||||
case []any:
|
||||
out := make([]map[string]any, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
if object := mapFromAny(item); object != nil {
|
||||
out = append(out, object)
|
||||
}
|
||||
}
|
||||
return out
|
||||
case []map[string]any:
|
||||
return typed
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
textItem["text"] = current
|
||||
}
|
||||
|
||||
func appendMultiShotTimeline(content *[]map[string]any) {
|
||||
@@ -625,31 +888,6 @@ func firstNonEmptyStringListFromAny(values ...any) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func volcesParamString(value any) string {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return strings.TrimSpace(typed)
|
||||
case bool:
|
||||
if typed {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case int:
|
||||
return fmt.Sprintf("%d", typed)
|
||||
case int64:
|
||||
return fmt.Sprintf("%d", typed)
|
||||
case float64:
|
||||
if math.Mod(typed, 1) == 0 {
|
||||
return fmt.Sprintf("%d", int64(typed))
|
||||
}
|
||||
return fmt.Sprintf("%g", typed)
|
||||
default:
|
||||
return fmt.Sprintf("%v", typed)
|
||||
}
|
||||
}
|
||||
|
||||
func numericValue(value any, fallback float64) float64 {
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
|
||||
Reference in New Issue
Block a user