feat(leonardo): 接入视频通道并新增 seedance-2.0/minimax-h3 模型

This commit is contained in:
2026-08-08 21:55:33 +08:00
parent 6ba80f5109
commit 6d17caaf80
11 changed files with 1128 additions and 106 deletions
+27 -36
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"mime/multipart"
"strings"
@@ -51,24 +52,25 @@ const mUploadImage = `mutation UploadImage($uploadImageInput: UploadImageInput!)
// uploadInitImage uploads a reference (init) image for image-to-image: it asks
// Leonardo for a presigned S3 POST, uploads the bytes, and returns the upload id
// to reference in the Generate request's image_reference guidance.
func (c *Client) uploadInitImage(ctx context.Context, accessToken string, img []byte) (string, error) {
func (c *Client) uploadInitImage(ctx context.Context, cookie string, img []byte) (string, error) {
return c.uploadAsset(ctx, cookie, "png", img)
}
// uploadAsset uploads one reference asset (extension png / mp3 / mp4 …) through
// the same UploadImage presigned-S3 flow images use, and returns its upload id.
func (c *Client) uploadAsset(ctx context.Context, cookie, extension string, asset []byte) (string, error) {
extension = strings.TrimPrefix(strings.ToLower(strings.TrimSpace(extension)), ".")
if extension == "" {
extension = "png"
}
payload, _ := json.Marshal(map[string]any{
"operationName": "UploadImage",
"query": mUploadImage,
"variables": map[string]any{"uploadImageInput": map[string]any{"uploadType": "INIT", "extension": "png"}},
"variables": map[string]any{"uploadImageInput": map[string]any{"uploadType": "INIT", "extension": extension}},
})
body, status, err := c.graphqlP(ctx, accessToken, payload, false)
body, err := c.callGraphQL(ctx, cookie, payload, false, "upload-init")
if err != nil {
return "", fmt.Errorf("%w: upload-init: %s", ErrTemporaryUpstream, err.Error())
}
if status == 401 || status == 403 {
return "", ErrAuth
}
if status != 200 {
return "", fmt.Errorf("%w: upload-init http %d: %s", ErrTemporaryUpstream, status, clip(body, 160))
}
if e := graphqlError(body); e != nil {
return "", e
return "", err
}
var ur struct {
Data struct {
@@ -97,11 +99,11 @@ func (c *Client) uploadInitImage(ctx context.Context, accessToken string, img []
for k, v := range fields {
_ = w.WriteField(k, v)
}
fw, err := w.CreateFormFile("file", "image.png")
fw, err := w.CreateFormFile("file", "asset."+extension)
if err != nil {
return "", err
}
if _, err := fw.Write(img); err != nil {
if _, err := fw.Write(asset); err != nil {
return "", err
}
_ = w.Close()
@@ -154,7 +156,7 @@ func (c *Client) GenerateImage(ctx context.Context, cookie, model, prompt string
if len(img) == 0 {
continue
}
uploadID, upErr := c.uploadInitImage(ctx, sess.AccessToken, img)
uploadID, upErr := c.uploadInitImage(ctx, cookie, img)
if upErr != nil {
return nil, nil, upErr
}
@@ -192,18 +194,9 @@ func (c *Client) GenerateImage(ctx context.Context, cookie, model, prompt string
},
}
payload, _ := json.Marshal(genReq)
body, status, err := c.graphql(ctx, sess.AccessToken, payload)
body, err := c.callGraphQL(ctx, cookie, payload, true, "generate")
if err != nil {
return nil, nil, fmt.Errorf("%w: %s", ErrTemporaryUpstream, err.Error())
}
if status == 401 || status == 403 {
return nil, nil, ErrAuth
}
if status != 200 {
return nil, nil, fmt.Errorf("%w: generate http %d: %s", ErrTemporaryUpstream, status, clip(body, 200))
}
if e := graphqlError(body); e != nil {
return nil, nil, e
return nil, nil, err
}
var genResp struct {
Data struct {
@@ -221,7 +214,7 @@ func (c *Client) GenerateImage(ctx context.Context, cookie, model, prompt string
}
// 2. poll until COMPLETE, then read the image url.
imageURL, err := c.pollImage(ctx, sess.AccessToken, genID)
imageURL, err := c.pollImage(ctx, cookie, genID)
if err != nil {
return nil, nil, err
}
@@ -244,7 +237,7 @@ func (c *Client) GenerateImage(ctx context.Context, cookie, model, prompt string
// pollImage polls one generation until it reports COMPLETE (returning the first
// image url) or FAILED (error). Honors ctx cancellation / deadline.
func (c *Client) pollImage(ctx context.Context, accessToken, genID string) (string, error) {
func (c *Client) pollImage(ctx context.Context, cookie, genID string) (string, error) {
payload, _ := json.Marshal(map[string]any{
"operationName": "GenerationImages",
"query": qGenerationImages,
@@ -264,14 +257,12 @@ func (c *Client) pollImage(ctx context.Context, accessToken, genID string) (stri
}
for {
body, status, err := c.graphqlP(ctx, accessToken, payload, false)
if err != nil {
return "", fmt.Errorf("%w: poll: %s", ErrTemporaryUpstream, err.Error())
body, err := c.callGraphQL(ctx, cookie, payload, false, "poll")
if errors.Is(err, ErrAuth) {
return "", err
}
if status == 401 || status == 403 {
return "", ErrAuth
}
if status == 200 {
// 其它错误(含上游临时抖动)不中断轮询,等 deadline 再判超时。
if err == nil {
var pr struct {
Data struct {
Generations []struct {