Merge pull request #22 from CyrusAuyeung/agent/fix-xai-video-payload
fix(media): xAI 视频 - 使用官方生成请求字段
This commit is contained in:
@@ -993,6 +993,10 @@ func validateGenerationInterface(mode string, interfaceType string) error {
|
||||
}
|
||||
|
||||
func grokVideoBody(input canvasGenerationInput) (map[string]interface{}, error) {
|
||||
if input.Config.InterfaceType == "xai-video" {
|
||||
return xaiVideoBody(input)
|
||||
}
|
||||
|
||||
seconds := defaultString(input.Config.VideoSeconds, "6")
|
||||
duration, err := strconv.Atoi(seconds)
|
||||
if err != nil || duration <= 0 {
|
||||
@@ -1022,6 +1026,29 @@ func grokVideoBody(input canvasGenerationInput) (map[string]interface{}, error)
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// xAI 生成接口与 legacy /videos 使用不同字段,保持独立可避免兼容字段触发上游 422。
|
||||
func xaiVideoBody(input canvasGenerationInput) (map[string]interface{}, error) {
|
||||
body := map[string]interface{}{
|
||||
"model": input.Config.Model,
|
||||
"prompt": strings.TrimSpace(input.Prompt),
|
||||
"duration": normalizeXAIVideoDuration(input.Config.VideoSeconds),
|
||||
"aspect_ratio": normalizeXAIVideoAspectRatio(input.Config.Size),
|
||||
"resolution": normalizeXAIVideoResolution(input.Config.VQuality),
|
||||
}
|
||||
if !shouldSendNewAPIVideoImages(input) || len(input.ReferenceImages) == 0 {
|
||||
return body, nil
|
||||
}
|
||||
if len(input.ReferenceImages) > 1 {
|
||||
return nil, fmt.Errorf("xAI 图生视频只支持 1 张起始图,当前连接了 %d 张", len(input.ReferenceImages))
|
||||
}
|
||||
imageURL, err := openAIImageInputURL(input.ReferenceImages[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body["image"] = map[string]interface{}{"url": imageURL}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func runSeedanceVideosTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) {
|
||||
id := resumedProviderRequestID(ctx)
|
||||
var created map[string]interface{}
|
||||
@@ -1787,6 +1814,74 @@ func normalizeVideoResolution(value string) string {
|
||||
return value + "p"
|
||||
}
|
||||
|
||||
func normalizeXAIVideoDuration(value string) int {
|
||||
duration, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil || duration <= 0 {
|
||||
return 6
|
||||
}
|
||||
if duration > 15 {
|
||||
return 15
|
||||
}
|
||||
return duration
|
||||
}
|
||||
|
||||
func normalizeXAIVideoResolution(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "480", "480p", "low":
|
||||
return "480p"
|
||||
case "1080", "1080p":
|
||||
return "1080p"
|
||||
default:
|
||||
return "720p"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeXAIVideoAspectRatio(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
allowed := map[string]bool{
|
||||
"1:1": true, "16:9": true, "9:16": true, "4:3": true,
|
||||
"3:4": true, "3:2": true, "2:3": true,
|
||||
}
|
||||
if allowed[value] {
|
||||
return value
|
||||
}
|
||||
parts := strings.Split(value, "x")
|
||||
if len(parts) != 2 {
|
||||
return "16:9"
|
||||
}
|
||||
width, widthErr := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||
height, heightErr := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||||
if widthErr != nil || heightErr != nil || width <= 0 || height <= 0 {
|
||||
return "16:9"
|
||||
}
|
||||
ratio := float64(width) / float64(height)
|
||||
candidates := []struct {
|
||||
name string
|
||||
ratio float64
|
||||
}{
|
||||
{name: "1:1", ratio: 1},
|
||||
{name: "16:9", ratio: 16.0 / 9},
|
||||
{name: "9:16", ratio: 9.0 / 16},
|
||||
{name: "4:3", ratio: 4.0 / 3},
|
||||
{name: "3:4", ratio: 3.0 / 4},
|
||||
{name: "3:2", ratio: 3.0 / 2},
|
||||
{name: "2:3", ratio: 2.0 / 3},
|
||||
}
|
||||
bestName := "16:9"
|
||||
bestDifference := 2.0
|
||||
for _, candidate := range candidates {
|
||||
difference := ratio - candidate.ratio
|
||||
if difference < 0 {
|
||||
difference = -difference
|
||||
}
|
||||
if difference < bestDifference {
|
||||
bestName = candidate.name
|
||||
bestDifference = difference
|
||||
}
|
||||
}
|
||||
return bestName
|
||||
}
|
||||
|
||||
func normalizeSeedanceDuration(value string) int {
|
||||
if strings.TrimSpace(value) == "-1" {
|
||||
return -1
|
||||
|
||||
@@ -387,6 +387,14 @@ func TestRunVideoTaskUsesXAIVideoGenerationEndpoint(t *testing.T) {
|
||||
if body["model"] != "grok-imagine-video-1.5" || body["prompt"] != "make it move" {
|
||||
t.Errorf("request body = %#v", body)
|
||||
}
|
||||
if body["duration"] != float64(10) || body["aspect_ratio"] != "1:1" || body["resolution"] != "720p" {
|
||||
t.Errorf("xAI settings = %#v", body)
|
||||
}
|
||||
for _, legacyField := range []string{"seconds", "size", "images"} {
|
||||
if _, exists := body[legacyField]; exists {
|
||||
t.Errorf("request body includes legacy field %q: %#v", legacyField, body)
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"request_id":"video-1"}`))
|
||||
case "GET /v1/videos/video-1":
|
||||
@@ -412,6 +420,8 @@ func TestRunVideoTaskUsesXAIVideoGenerationEndpoint(t *testing.T) {
|
||||
Model: "grok-imagine-video-1.5",
|
||||
InterfaceType: "xai-video",
|
||||
VideoSeconds: "10",
|
||||
Size: "1:1",
|
||||
VQuality: "720",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -427,6 +437,50 @@ func TestRunVideoTaskUsesXAIVideoGenerationEndpoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestXAIVideoBodyUsesOfficialImageShapeAndNormalizesSettings(t *testing.T) {
|
||||
body, err := grokVideoBody(canvasGenerationInput{
|
||||
Prompt: "make it move",
|
||||
Config: providerConfig{
|
||||
Model: "grok-imagine-video-1.5",
|
||||
InterfaceType: "xai-video",
|
||||
VideoSeconds: "20",
|
||||
Size: "1024x1792",
|
||||
VQuality: "1080",
|
||||
},
|
||||
ReferenceImages: []providerMedia{{ID: "image-1", DataURL: testReferenceImageDataURL}},
|
||||
Metadata: map[string]interface{}{"videoEditOperation": "image_to_video"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("grokVideoBody() error = %v", err)
|
||||
}
|
||||
if body["duration"] != 15 || body["aspect_ratio"] != "9:16" || body["resolution"] != "1080p" {
|
||||
t.Fatalf("xAI settings = %#v", body)
|
||||
}
|
||||
image, ok := body["image"].(map[string]interface{})
|
||||
if !ok || image["url"] != testReferenceImageDataURL {
|
||||
t.Fatalf("image = %#v", body["image"])
|
||||
}
|
||||
for _, legacyField := range []string{"seconds", "size", "images"} {
|
||||
if _, exists := body[legacyField]; exists {
|
||||
t.Fatalf("body includes legacy field %q: %#v", legacyField, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXAIVideoBodyRejectsMultipleStartImages(t *testing.T) {
|
||||
_, err := grokVideoBody(canvasGenerationInput{
|
||||
Config: providerConfig{Model: "grok-imagine-video-1.5", InterfaceType: "xai-video"},
|
||||
ReferenceImages: []providerMedia{
|
||||
{ID: "image-1", DataURL: testReferenceImageDataURL},
|
||||
{ID: "image-2", DataURL: testReferenceImageDataURL},
|
||||
},
|
||||
Metadata: map[string]interface{}{"videoEditOperation": "image_to_video"},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "只支持 1 张起始图") {
|
||||
t.Fatalf("grokVideoBody() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAPIVideoPromptKeepsTextOnlyPromptUnchanged(t *testing.T) {
|
||||
input := canvasGenerationInput{
|
||||
Prompt: "make it move",
|
||||
|
||||
Reference in New Issue
Block a user