132 lines
3.4 KiB
Go
132 lines
3.4 KiB
Go
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
|
|
}
|