Files
2026-10-02 19:56:32 +08:00

132 lines
4.0 KiB
Go

package providers
import (
"context"
"net/http"
"strings"
)
const MinimaxH3Model = "MiniMax-H3"
// Minimax adapts the MiniMax H3 V2 asynchronous video API to the common
// provider contract. Request validation for first-frame and multi-reference
// modes lives in jobs.
type Minimax struct{ *httpAdapter }
func NewMinimax(c Config, client HTTPClient) *Minimax {
if client == nil {
client = http.DefaultClient
}
if strings.TrimSpace(c.Model) == "" {
c.Model = MinimaxH3Model
}
a := &httpAdapter{
name: "minimax",
config: c,
client: client,
submitPath: func(Request) string { return "/v2/video_generation" },
queryPath: func(id string) string { return "/v2/query/video_generation/" + id },
payload: func(r Request) any {
content := []any{map[string]any{"type": "text", "text": r.Prompt}}
materials := r.Materials
if len(materials) == 0 && len(r.InputURLs) > 0 {
materials = make([]Material, 0, len(r.InputURLs))
for _, inputURL := range r.InputURLs {
materials = append(materials, Material{URL: inputURL, Type: MaterialImage})
}
}
role := "first_frame"
if r.Settings["inputMode"] == "reference" {
role = "reference_image"
}
for _, material := range materials {
content = append(content, map[string]any{
"type": "image_url",
"image_url": map[string]any{"url": material.URL},
"role": role,
})
}
return map[string]any{
"model": requestModel(r, c.Model),
"content": content,
"resolution": r.Settings["resolution"],
"duration": r.Settings["duration"],
"ratio": r.Settings["ratio"],
"aigc_watermark": false,
}
},
decode: decodeMinimax,
failureDetail: func(responseBody, requestBody []byte) string {
return minimaxSafeFailureDetail(responseBody, requestBody, c.APIKey)
},
}
return &Minimax{a}
}
func (a *Minimax) Submit(ctx context.Context, request Request) (Result, error) {
result, err := a.submit(ctx, request)
if err != nil {
logMinimaxFailedRequest(request, a.config, err)
}
return result, err
}
func (a *Minimax) Query(ctx context.Context, id string) (Result, error) {
return a.query(ctx, id)
}
func decodeMinimax(raw []byte) Result {
root := record(raw)
task := object(root["task"])
if len(task) == 0 {
return Result{TaskID: stringValue(root["task_id"]), Status: StatusQueued}
}
outputURLs := []string{}
collectURLs(object(task["content"])["url"], &outputURLs)
usage := map[string]int{}
for source, target := range map[string]string{
"total_seconds": "totalSeconds",
"input_seconds": "inputSeconds",
"output_seconds": "outputSeconds",
"input_image_count": "inputImageCount",
"input_audio_seconds": "inputAudioSeconds",
"total_tokens": "totalTokens",
"prompt_tokens": "promptTokens",
"completion_tokens": "completionTokens",
} {
if number, ok := object(task["usage"])[source].(float64); ok && number >= 0 {
usage[target] = int(number)
}
}
providerError := object(task["error"])
resultStatus := status(task["status"])
errorMessage := minimaxTaskFailureMessage(safeHTTPProviderDiagnosticToken(providerError["code"], 16), stringValue(providerError["message"]))
if resultStatus == StatusFailed && errorMessage == "" {
errorMessage = "MiniMax 视频生成失败,请调整提示词或素材后重试。"
}
return Result{
TaskID: stringValue(task["id"], root["task_id"]),
Status: resultStatus,
OutputURLs: outputURLs,
ErrorMessage: errorMessage,
Usage: usage,
}
}
func minimaxTaskFailureMessage(code, message string) string {
switch strings.TrimSpace(code) {
case "1008":
return "MiniMax API 账户余额不足,请充值后重试。"
case "1026":
return "视频描述或输入素材触发内容安全审核,请调整后重试。"
case "1002":
return "MiniMax 请求频率过高,请稍后重试。"
}
if strings.TrimSpace(message) != "" {
return "MiniMax 视频生成失败,请调整提示词或素材后重试。"
}
return ""
}
var _ Adapter = (*Minimax)(nil)