Files
gen2d/backend/internal/service/gptimage.go
T
Gmarker689 d9d3b51262 feat(inference): 集成 GPT Image 2 异步文生图 API
- 新增 GptImage2Config 配置(yuntts 等 GPT Image 2 兼容服务)
- 新增 gptimage.go 异步客户端:提交任务 → 轮询状态 → 下载图片
- GenerateImages 优先级调整为:GPT Image 2 > OpenAI 兼容 ImageGen > Mock
- 支持环境变量 GEN2D_GPT_IMAGE2_* 系列配置
2026-05-25 12:36:37 +08:00

269 lines
7.3 KiB
Go
Executable File

package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"gen2d/internal/config"
)
// gptImage2Cfg 保存 GPT Image 2 配置。
var gptImage2Cfg config.GptImage2Config
// InitGptImage2Config 注入 GPT Image 2 配置。
func InitGptImage2Config(cfg config.GptImage2Config) {
gptImage2Cfg = cfg
}
// ======================== GPT Image 2 API 类型 ========================
// gpt2SubmitRequest 提交生图任务请求体。
type gpt2SubmitRequest struct {
Prompt string `json:"prompt"`
AspectRatio string `json:"aspect_ratio,omitempty"`
ReferenceImages []string `json:"reference_images,omitempty"`
XChannel string `json:"x_channel,omitempty"`
}
// gpt2SubmitResponse 提交生图任务响应体。
type gpt2SubmitResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
TaskID string `json:"task_id"`
Status string `json:"status"`
} `json:"data"`
}
// gpt2StatusRequest 查询任务状态请求体。
type gpt2StatusRequest struct {
TaskID string `json:"task_id"`
}
// gpt2StatusResponse 查询任务状态响应体。
type gpt2StatusResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
TaskID string `json:"task_id"`
Status string `json:"status"`
Progress int `json:"progress"`
ResultImageURL string `json:"result_image_url"`
ErrorMessage string `json:"error_message"`
} `json:"data"`
}
// ======================== 公开接口 ========================
// GenerateImagesGpt2 通过 GPT Image 2 异步 API 生成图片。
// count 张图通过并行提交+轮询实现。
func GenerateImagesGpt2(ctx context.Context, prompt string, count int) ([]GeneratedImage, error) {
if gptImage2Cfg.APIKey == "" {
return nil, fmt.Errorf("gpt_image2 api_key not configured")
}
// 并行提交任务
type submitResult struct {
index int
taskID string
err error
}
results := make(chan submitResult, count)
for i := 0; i < count; i++ {
go func(idx int) {
taskID, err := submitGpt2Task(ctx, prompt)
results <- submitResult{index: idx, taskID: taskID, err: err}
}(i)
}
// 收集 taskID
taskIDs := make([]string, count)
for i := 0; i < count; i++ {
r := <-results
if r.err != nil {
return nil, fmt.Errorf("submit task %d: %w", r.index, r.err)
}
taskIDs[r.index] = r.taskID
}
// 并行轮询+下载
type imageResult struct {
index int
image GeneratedImage
err error
}
imgResults := make(chan imageResult, count)
for i, tid := range taskIDs {
go func(idx int, taskID string) {
img, err := pollAndDownloadGpt2(ctx, taskID, prompt)
imgResults <- imageResult{index: idx, image: img, err: err}
}(i, tid)
}
images := make([]GeneratedImage, count)
for i := 0; i < count; i++ {
r := <-imgResults
if r.err != nil {
return nil, fmt.Errorf("task %d: %w", r.index, r.err)
}
images[r.index] = r.image
}
return images, nil
}
// ======================== 内部实现 ========================
// submitGpt2Task 提交生图任务,返回 taskID。
func submitGpt2Task(ctx context.Context, prompt string) (string, error) {
reqBody := gpt2SubmitRequest{
Prompt: prompt,
AspectRatio: gptImage2Cfg.AspectRatio,
XChannel: gptImage2Cfg.XChannel,
}
body, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("marshal: %w", err)
}
url := strings.TrimRight(gptImage2Cfg.BaseURL, "/") + "/gpt-image2/generate"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return "", fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+gptImage2Cfg.APIKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("send: %w", err)
}
defer resp.Body.Close()
var submitResp gpt2SubmitResponse
if err := json.NewDecoder(resp.Body).Decode(&submitResp); err != nil {
return "", fmt.Errorf("decode: %w", err)
}
if submitResp.Code != 200 {
return "", fmt.Errorf("submit failed: %s", submitResp.Message)
}
log.Printf("[gpt_image2] task submitted: %s", submitResp.Data.TaskID)
return submitResp.Data.TaskID, nil
}
// pollAndDownloadGpt2 轮询任务状态直到完成,然后下载图片。
func pollAndDownloadGpt2(ctx context.Context, taskID, prompt string) (GeneratedImage, error) {
pollInterval := gptImage2Cfg.PollInterval
if pollInterval <= 0 {
pollInterval = 3
}
maxWait := gptImage2Cfg.PollMaxWait
if maxWait <= 0 {
maxWait = 120
}
deadline := time.Now().Add(time.Duration(maxWait) * time.Second)
ticker := time.NewTicker(time.Duration(pollInterval) * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return GeneratedImage{}, ctx.Err()
case <-ticker.C:
status, err := queryGpt2Status(ctx, taskID)
if err != nil {
return GeneratedImage{}, fmt.Errorf("query status: %w", err)
}
switch status.Data.Status {
case "completed":
log.Printf("[gpt_image2] task %s completed, downloading from %s", taskID, status.Data.ResultImageURL)
data, err := downloadGpt2Image(ctx, status.Data.ResultImageURL)
if err != nil {
return GeneratedImage{}, fmt.Errorf("download: %w", err)
}
return GeneratedImage{Data: data, Format: "png"}, nil
case "failed":
errMsg := status.Data.ErrorMessage
if errMsg == "" {
errMsg = "unknown error"
}
return GeneratedImage{}, fmt.Errorf("generation failed: %s", errMsg)
default:
log.Printf("[gpt_image2] task %s status=%s progress=%d", taskID, status.Data.Status, status.Data.Progress)
}
if time.Now().After(deadline) {
return GeneratedImage{}, fmt.Errorf("poll timeout after %ds", maxWait)
}
}
}
}
// queryGpt2Status 查询任务状态。
func queryGpt2Status(ctx context.Context, taskID string) (*gpt2StatusResponse, error) {
reqBody := gpt2StatusRequest{TaskID: taskID}
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("marshal: %w", err)
}
url := strings.TrimRight(gptImage2Cfg.BaseURL, "/") + "/gpt-image2/status"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+gptImage2Cfg.APIKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("send: %w", err)
}
defer resp.Body.Close()
var statusResp gpt2StatusResponse
if err := json.NewDecoder(resp.Body).Decode(&statusResp); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
if statusResp.Code != 200 {
return nil, fmt.Errorf("status query failed: %s", statusResp.Message)
}
return &statusResp, nil
}
// downloadGpt2Image 下载生成的图片。
func downloadGpt2Image(ctx context.Context, imageURL string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, imageURL, nil)
if err != nil {
return nil, fmt.Errorf("create download request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("download: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("download status %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}