package service import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" "go.uber.org/zap" "github.com/topfans/backend/pkg/logger" ) // MinimaxClient MiniMax 图像生成 HTTP 客户端 type MinimaxClient struct { APIURL string APIKey string HTTPClient *http.Client } // NewMinimaxClient 创建 MiniMax 客户端 func NewMinimaxClient(apiURL, apiKey string) *MinimaxClient { return &MinimaxClient{ APIURL: apiURL, APIKey: apiKey, HTTPClient: &http.Client{ Timeout: 60 * time.Second, }, } } // SubjectReference 人像参考(图生图) type SubjectReference struct { Type string `json:"type"` // "character" ImageFile string `json:"image_file"` // URL 或 data: URL } // ImageGenReq 文生图/图生图请求 type ImageGenReq struct { Model string `json:"model"` Prompt string `json:"prompt"` AspectRatio string `json:"aspect_ratio"` N int `json:"n"` ResponseFormat string `json:"response_format"` SubjectReference []SubjectReference `json:"subject_reference,omitempty"` } // ImageGenResp 文生图响应 type ImageGenResp struct { ID string `json:"id"` Data struct { ImageURLs []string `json:"image_urls"` } `json:"data"` BaseResp struct { StatusCode int `json:"status_code"` StatusMsg string `json:"status_msg"` } `json:"base_resp"` } // GenerateImage 文生图(无参考图) func (c *MinimaxClient) GenerateImage(ctx context.Context, prompt string) (string, error) { return c.GenerateImageWithSubject(ctx, prompt, "") } // GenerateImageWithSubject 图生图 — 将人物抠图作为 subject_reference 传给 MiniMax // 这样 AI 生成的背景图会自然地融合人物主体 func (c *MinimaxClient) GenerateImageWithSubject(ctx context.Context, prompt, subjectImageURL string) (string, error) { req := ImageGenReq{ Model: "image-01", Prompt: prompt, AspectRatio: "3:4", N: 1, ResponseFormat: "url", } if subjectImageURL != "" { req.SubjectReference = []SubjectReference{ {Type: "character", ImageFile: subjectImageURL}, } } body, err := json.Marshal(req) if err != nil { return "", fmt.Errorf("marshal minimax request: %w", err) } httpReq, err := http.NewRequestWithContext(ctx, "POST", c.APIURL, bytes.NewReader(body)) if err != nil { return "", fmt.Errorf("create minimax request: %w", err) } httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey)) httpReq.Header.Set("Content-Type", "application/json") resp, err := c.HTTPClient.Do(httpReq) if err != nil { return "", fmt.Errorf("minimax http: %w", err) } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) var genResp ImageGenResp if err := json.Unmarshal(respBody, &genResp); err != nil { return "", fmt.Errorf("parse minimax response: %w, body=%s", err, string(respBody)) } if genResp.BaseResp.StatusCode != 0 { return "", fmt.Errorf("minimax error: code=%d msg=%s", genResp.BaseResp.StatusCode, genResp.BaseResp.StatusMsg) } if len(genResp.Data.ImageURLs) == 0 { return "", fmt.Errorf("minimax returned no images") } logger.Logger.Info("MiniMax generated image", zap.String("prompt_preview", prompt[:minLen(60, len(prompt))]), zap.Bool("with_subject", subjectImageURL != ""), ) return genResp.Data.ImageURLs[0], nil } func minLen(a, b int) int { if a < b { return a } return b }