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. The first release intentionally supports text-to-video // and a single first-frame image only; request validation 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 = []Material{{URL: r.InputURLs[0], Type: MaterialImage}} } if len(materials) > 0 { content = append(content, map[string]any{ "type": "image_url", "image_url": map[string]any{"url": materials[0].URL}, "role": "first_frame", }) } 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)