| package handler |
|
|
| import ( |
| "encoding/base64" |
| "encoding/json" |
| "fmt" |
| "io" |
| "mime/multipart" |
| "net/http" |
| "strconv" |
| "strings" |
|
|
| "aurora/httpclient" |
| "aurora/httpclient/bogdanfinn" |
| "aurora/internal/accounts" |
| "aurora/internal/chatgpt" |
| "aurora/internal/config" |
| officialtypes "aurora/typings/official" |
|
|
| "github.com/gin-gonic/gin" |
| ) |
|
|
| type ImageHandler struct { |
| accountPool *accounts.Pool |
| cfg *config.Config |
| } |
|
|
| func NewImageHandler(pool *accounts.Pool, cfg *config.Config) *ImageHandler { |
| return &ImageHandler{accountPool: pool, cfg: cfg} |
| } |
|
|
| |
|
|
| type imageStreamChunk struct { |
| Object string `json:"object"` |
| Index int `json:"index"` |
| Total int `json:"total"` |
| Created int64 `json:"created"` |
| ProgressText string `json:"progress_text,omitempty"` |
| UpstreamEventType string `json:"upstream_event_type,omitempty"` |
| Model string `json:"model,omitempty"` |
| AccountEmail string `json:"_account_email,omitempty"` |
| ConversationID string `json:"_conversation_id,omitempty"` |
| } |
|
|
| type imageStreamResult struct { |
| Object string `json:"object"` |
| Index int `json:"index"` |
| Total int `json:"total"` |
| Created int64 `json:"created"` |
| Model string `json:"model,omitempty"` |
| Data []officialtypes.ImageGenerationData `json:"data"` |
| } |
|
|
| type imageStreamCompleted struct { |
| Object string `json:"object"` |
| Created int64 `json:"created"` |
| Model string `json:"model,omitempty"` |
| Data []officialtypes.ImageGenerationData `json:"data"` |
| } |
|
|
| func writeImageStreamHeader(c *gin.Context) { |
| c.Writer.Header().Set("Content-Type", "text/event-stream") |
| c.Writer.Header().Set("Cache-Control", "no-cache") |
| c.Writer.Header().Set("Connection", "keep-alive") |
| c.Writer.Header().Set("X-Accel-Buffering", "no") |
| c.Writer.WriteHeader(200) |
| } |
|
|
| func writeImageStreamEvent(c *gin.Context, event string, payload interface{}) bool { |
| data, err := json.Marshal(payload) |
| if err != nil { |
| return false |
| } |
| if event != "" { |
| if _, err := c.Writer.WriteString("event: " + event + "\n"); err != nil { |
| return false |
| } |
| } |
| if _, err := c.Writer.WriteString("data: "); err != nil { |
| return false |
| } |
| if _, err := c.Writer.Write(data); err != nil { |
| return false |
| } |
| if _, err := c.Writer.WriteString("\n\n"); err != nil { |
| return false |
| } |
| c.Writer.Flush() |
| return true |
| } |
|
|
| func writeImageStreamDone(c *gin.Context) bool { |
| if _, err := c.Writer.WriteString("data: [DONE]\n\n"); err != nil { |
| return false |
| } |
| c.Writer.Flush() |
| return true |
| } |
|
|
| |
| func requestStreamFlag(c *gin.Context, jsonStream bool) bool { |
| if jsonStream { |
| return true |
| } |
| if v := strings.ToLower(strings.TrimSpace(c.Query("stream"))); v == "true" || v == "1" || v == "yes" { |
| return true |
| } |
| if v := strings.ToLower(strings.TrimSpace(c.PostForm("stream"))); v == "true" || v == "1" || v == "yes" { |
| return true |
| } |
| return false |
| } |
|
|
| |
| func isStreamTrue(v string) bool { |
| switch strings.ToLower(strings.TrimSpace(v)) { |
| case "true", "1", "yes", "on": |
| return true |
| } |
| return false |
| } |
|
|
| |
|
|
| func (h *ImageHandler) Generations(c *gin.Context) { |
| var imageRequest officialtypes.ImageGenerationRequest |
| err := c.BindJSON(&imageRequest) |
| if err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Request must be proper JSON", |
| "type": "invalid_request_error", |
| "param": nil, |
| "code": err.Error(), |
| }}) |
| return |
| } |
| if imageRequest.Prompt == "" { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Missing required parameter: prompt", |
| "type": "invalid_request_error", |
| "param": "prompt", |
| "code": "missing_required_parameter", |
| }}) |
| return |
| } |
| if imageRequest.N <= 0 { |
| imageRequest.N = 1 |
| } |
| if imageRequest.N > 10 { |
| imageRequest.N = 10 |
| } |
| if imageRequest.ResponseFormat == "" { |
| imageRequest.ResponseFormat = "b64_json" |
| } |
|
|
| account, _, err := resolveAccount(c, h.accountPool, h.cfg, true) |
| if err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "authorization_error", |
| "param": "Authorization", |
| "code": 400, |
| }}) |
| return |
| } |
| if account == nil || account.Token == "" { |
| c.JSON(400, gin.H{"error": "Images API requires a logged-in ChatGPT access token."}) |
| c.Abort() |
| return |
| } |
| if !account.Type.Satisfies(accounts.CapImageGenerate) { |
| c.JSON(403, gin.H{"error": "Images API requires a logged-in ChatGPT account."}) |
| return |
| } |
|
|
| proxyUrl := account.Proxy |
| client := setupClientWithProxy(proxyUrl) |
| client.SetCookies("https://chatgpt.com", chatgpt.BasicCookies) |
| turnStile, status, err := chatgpt.InitSentinel(client, account, proxyUrl, 0) |
| if err != nil { |
| if status == http.StatusUnauthorized { |
| h.accountPool.ReportFailure(account) |
| } |
| c.JSON(status, gin.H{ |
| "message": err.Error(), |
| "type": "InitTurnStile_request_error", |
| "param": err, |
| "code": status, |
| }) |
| return |
| } |
|
|
| stream := requestStreamFlag(c, imageRequest.Stream) |
| if stream { |
| writeImageStreamHeader(c) |
| } |
|
|
| var data []officialtypes.ImageGenerationData |
| for i := 0; i < imageRequest.N; i++ { |
| if stream { |
| writeImageStreamEvent(c, "image.generation.chunk", imageStreamChunk{ |
| Object: "image.generation.chunk", |
| Index: i, |
| Total: imageRequest.N, |
| Created: 0, |
| Model: imageRequest.Model, |
| ProgressText: fmt.Sprintf("Generating image %d/%d ...", i+1, imageRequest.N), |
| }) |
| } |
| imageResults, upstreamText, err := chatgpt.GeneratePictureConversationImages(client, account, turnStile, imageRequest.Prompt, imageRequest.Model, proxyUrl) |
| if err != nil { |
| if stream { |
| writeImageStreamEvent(c, "image.generation.error", gin.H{ |
| "object": "image.generation.error", |
| "index": i, |
| "total": imageRequest.N, |
| "message": err.Error(), |
| }) |
| writeImageStreamDone(c) |
| return |
| } |
| c.JSON(500, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "image_generation_error", |
| "param": nil, |
| "code": "image_generation_error", |
| }}) |
| return |
| } |
| for _, imageResult := range imageResults { |
| item := officialtypes.ImageGenerationData{ |
| RevisedPrompt: imageRequest.Prompt, |
| } |
| if imageRequest.ResponseFormat == "b64_json" { |
| if imageResult.B64JSON != "" { |
| item.B64JSON = imageResult.B64JSON |
| } else if imageResult.URL != "" { |
| imageBytes, err := chatgpt.DownloadImageBytes(client, imageResult.URL, account) |
| if err != nil { |
| if stream { |
| writeImageStreamEvent(c, "image.generation.error", gin.H{ |
| "object": "image.generation.error", |
| "index": i, |
| "total": imageRequest.N, |
| "message": err.Error(), |
| }) |
| writeImageStreamDone(c) |
| return |
| } |
| c.JSON(500, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "image_download_error", |
| "param": nil, |
| "code": "image_download_error", |
| }}) |
| return |
| } |
| item.B64JSON = base64.StdEncoding.EncodeToString(imageBytes) |
| } |
| } else { |
| item.URL = imageResult.URL |
| if item.URL == "" && imageResult.B64JSON != "" { |
| item.B64JSON = imageResult.B64JSON |
| } |
| } |
| data = append(data, item) |
| if stream { |
| writeImageStreamEvent(c, "image.generation.result", imageStreamResult{ |
| Object: "image.generation.result", |
| Index: len(data) - 1, |
| Total: imageRequest.N, |
| Created: 0, |
| Model: imageRequest.Model, |
| Data: []officialtypes.ImageGenerationData{item}, |
| }) |
| } |
| if len(data) >= imageRequest.N { |
| break |
| } |
| } |
| if len(imageResults) == 0 && upstreamText != "" { |
| if stream { |
| writeImageStreamEvent(c, "image.generation.error", gin.H{ |
| "object": "image.generation.error", |
| "index": i, |
| "total": imageRequest.N, |
| "message": "No image result found in response: " + upstreamText, |
| }) |
| writeImageStreamDone(c) |
| return |
| } |
| c.JSON(500, gin.H{"error": gin.H{ |
| "message": "No image result found in response: " + upstreamText, |
| "type": "image_generation_error", |
| "param": nil, |
| "code": "image_generation_error", |
| }}) |
| return |
| } |
| if len(data) >= imageRequest.N { |
| break |
| } |
| } |
| if len(data) == 0 { |
| if stream { |
| writeImageStreamEvent(c, "image.generation.error", gin.H{ |
| "object": "image.generation.error", |
| "message": "No image result found in response", |
| }) |
| writeImageStreamDone(c) |
| return |
| } |
| c.JSON(500, gin.H{"error": gin.H{ |
| "message": "No image result found in response", |
| "type": "image_generation_error", |
| "param": nil, |
| "code": "image_generation_error", |
| }}) |
| return |
| } |
| if stream { |
| writeImageStreamEvent(c, "image.generation.completed", imageStreamCompleted{ |
| Object: "image.generation.completed", |
| Created: 0, |
| Model: imageRequest.Model, |
| Data: data, |
| }) |
| writeImageStreamDone(c) |
| return |
| } |
| c.JSON(200, officialtypes.NewImageGenerationResponse(data)) |
| } |
|
|
| |
|
|
| |
| type editImageInput struct { |
| Data []byte |
| Filename string |
| ContentType string |
| } |
|
|
| |
| var imageEditImageReferenceFields = map[string]bool{ |
| "image": true, |
| "image[]": true, |
| "images": true, |
| "images[]": true, |
| "image_url": true, |
| "image_url[]": true, |
| } |
|
|
| func normalizeImageEditImages(rawImages []interface{}) []editImageInput { |
| out := make([]editImageInput, 0, len(rawImages)) |
| for _, raw := range rawImages { |
| switch v := raw.(type) { |
| case *multipart.FileHeader: |
| if v == nil { |
| continue |
| } |
| f, err := v.Open() |
| if err != nil { |
| continue |
| } |
| data, err := io.ReadAll(f) |
| f.Close() |
| if err != nil || len(data) == 0 { |
| continue |
| } |
| ct := v.Header.Get("Content-Type") |
| if ct == "" { |
| ct = "image/png" |
| } |
| name := v.Filename |
| if name == "" { |
| name = "image.png" |
| } |
| out = append(out, editImageInput{Data: data, Filename: name, ContentType: ct}) |
| case editImageInput: |
| if len(v.Data) > 0 { |
| out = append(out, v) |
| } |
| default: |
| |
| } |
| } |
| return out |
| } |
|
|
| func imageEditReadJSONImage(data []byte, filename, contentType string) (editImageInput, error) { |
| if len(data) == 0 { |
| return editImageInput{}, fmt.Errorf("image data is empty") |
| } |
| if filename == "" { |
| filename = "image.png" |
| } |
| if contentType == "" { |
| contentType = "image/png" |
| } |
| return editImageInput{Data: data, Filename: filename, ContentType: contentType}, nil |
| } |
|
|
| func imageEditDecodeDataURL(url string) (editImageInput, error) { |
| comma := strings.Index(url, ",") |
| if comma < 0 { |
| return editImageInput{}, fmt.Errorf("invalid data URL") |
| } |
| header := url[:comma] |
| payload := url[comma+1:] |
| contentType := "image/png" |
| if semi := strings.Index(header, ";"); semi > 5 { |
| contentType = header[5:semi] |
| if !strings.HasPrefix(contentType, "image/") { |
| return editImageInput{}, fmt.Errorf("data URL must be an image, got %q", contentType) |
| } |
| } |
| dec := base64.StdEncoding |
| if strings.Contains(header, ";base64") { |
| dec = base64.StdEncoding |
| } else { |
| dec = base64.URLEncoding |
| } |
| raw, err := dec.DecodeString(payload) |
| if err != nil { |
| raw, err = base64.StdEncoding.DecodeString(payload) |
| if err != nil { |
| return editImageInput{}, err |
| } |
| contentType = "image/png" |
| } |
| ext := "png" |
| switch strings.ToLower(contentType) { |
| case "image/jpeg": |
| ext = "jpg" |
| case "image/gif": |
| ext = "gif" |
| case "image/webp": |
| ext = "webp" |
| } |
| return imageEditReadJSONImage(raw, "image_url."+ext, contentType) |
| } |
|
|
| func imageEditDownloadHTTPURL(client httpclient.AuroraHttpClient, url string) (editImageInput, error) { |
| if client == nil { |
| client = bogdanfinn.NewStdClient() |
| } |
| headers := httpclient.AuroraHeaders{ |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", |
| "Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", |
| } |
| resp, err := client.Request(httpclient.GET, url, headers, nil, nil) |
| if err != nil { |
| return editImageInput{}, err |
| } |
| defer resp.Body.Close() |
| if resp.StatusCode < 200 || resp.StatusCode >= 300 { |
| return editImageInput{}, fmt.Errorf("download image failed: status %d", resp.StatusCode) |
| } |
| data, err := io.ReadAll(resp.Body) |
| if err != nil { |
| return editImageInput{}, err |
| } |
| contentType := resp.Header.Get("Content-Type") |
| if contentType == "" { |
| contentType = http.DetectContentType(data) |
| } |
| filename := "image_url" |
| if idx := strings.LastIndex(url, "/"); idx >= 0 && idx < len(url)-1 { |
| filename = url[idx+1:] |
| } |
| if dot := strings.Index(filename, "?"); dot >= 0 { |
| filename = filename[:dot] |
| } |
| if filename == "" { |
| filename = "image_url.png" |
| } |
| return imageEditReadJSONImage(data, filename, contentType) |
| } |
|
|
| func imageEditConvertURL(client httpclient.AuroraHttpClient, raw string) (editImageInput, bool, error) { |
| raw = strings.TrimSpace(raw) |
| if raw == "" { |
| return editImageInput{}, false, nil |
| } |
| if strings.HasPrefix(raw, "data:") { |
| item, err := imageEditDecodeDataURL(raw) |
| return item, true, err |
| } |
| if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") { |
| item, err := imageEditDownloadHTTPURL(client, raw) |
| return item, true, err |
| } |
| |
| item, err := imageEditReadJSONImage([]byte(raw), "image.png", "image/png") |
| return item, true, err |
| } |
|
|
| func resolveEditImageSources(c *gin.Context, body map[string]interface{}, client httpclient.AuroraHttpClient) ([]editImageInput, error) { |
| out := make([]editImageInput, 0, 2) |
| appendValue := func(v interface{}) error { |
| switch t := v.(type) { |
| case string: |
| item, ok, err := imageEditConvertURL(client, t) |
| if err != nil { |
| return err |
| } |
| if ok { |
| out = append(out, item) |
| } |
| case map[string]interface{}: |
| if urlVal, ok := t["image_url"]; ok { |
| if s, ok := urlVal.(string); ok { |
| item, _, err := imageEditConvertURL(client, s) |
| if err != nil { |
| return err |
| } |
| out = append(out, item) |
| } |
| } else if u, ok := t["url"]; ok { |
| if s, ok := u.(string); ok { |
| item, _, err := imageEditConvertURL(client, s) |
| if err != nil { |
| return err |
| } |
| out = append(out, item) |
| } |
| } else if b64, ok := t["b64_json"].(string); ok && b64 != "" { |
| item, err := imageEditReadJSONImage([]byte(b64), "image.png", "image/png") |
| if err != nil { |
| return err |
| } |
| out = append(out, item) |
| } else if b64, ok := t["base64"].(string); ok && b64 != "" { |
| item, err := imageEditReadJSONImage([]byte(b64), "image.png", "image/png") |
| if err != nil { |
| return err |
| } |
| out = append(out, item) |
| } |
| } |
| return nil |
| } |
|
|
| for _, key := range []string{"images", "image", "image_url"} { |
| val, ok := body[key] |
| if !ok || val == nil { |
| continue |
| } |
| switch arr := val.(type) { |
| case []interface{}: |
| for _, item := range arr { |
| if err := appendValue(item); err != nil { |
| return nil, err |
| } |
| } |
| case string: |
| if err := appendValue(arr); err != nil { |
| return nil, err |
| } |
| case map[string]interface{}: |
| if err := appendValue(arr); err != nil { |
| return nil, err |
| } |
| } |
| } |
| return out, nil |
| } |
|
|
| |
| func collectResponsesAPIParts(raw interface{}) (string, []string) { |
| if raw == nil { |
| return "", nil |
| } |
| var textParts []string |
| var imageURLs []string |
|
|
| appendFromContent := func(content interface{}) { |
| switch c := content.(type) { |
| case string: |
| if strings.TrimSpace(c) != "" { |
| textParts = append(textParts, c) |
| } |
| case []interface{}: |
| for _, item := range c { |
| part, ok := item.(map[string]interface{}) |
| if !ok { |
| continue |
| } |
| partType := strings.ToLower(strings.TrimSpace(stringFromAny(part["type"]))) |
| switch partType { |
| case "input_text", "text", "output_text": |
| if s := stringFromAny(part["text"]); s != "" { |
| textParts = append(textParts, s) |
| } |
| case "input_image", "image", "image_url": |
| switch u := part["image_url"].(type) { |
| case string: |
| imageURLs = append(imageURLs, u) |
| case map[string]interface{}: |
| if s := stringFromAny(u["url"]); s != "" { |
| imageURLs = append(imageURLs, s) |
| } |
| } |
| if s := stringFromAny(part["url"]); s != "" { |
| imageURLs = append(imageURLs, s) |
| } |
| } |
| } |
| } |
| } |
|
|
| switch v := raw.(type) { |
| case string: |
| textParts = append(textParts, v) |
| case map[string]interface{}: |
| appendFromContent(v["content"]) |
| case []interface{}: |
| for _, item := range v { |
| switch m := item.(type) { |
| case string: |
| textParts = append(textParts, m) |
| case map[string]interface{}: |
| partType := strings.ToLower(strings.TrimSpace(stringFromAny(m["type"]))) |
| if partType == "input_image" || partType == "image" || partType == "image_url" { |
| switch u := m["image_url"].(type) { |
| case string: |
| imageURLs = append(imageURLs, u) |
| case map[string]interface{}: |
| if s := stringFromAny(u["url"]); s != "" { |
| imageURLs = append(imageURLs, s) |
| } |
| } |
| if s := stringFromAny(m["url"]); s != "" { |
| imageURLs = append(imageURLs, s) |
| } |
| continue |
| } |
| appendFromContent(m["content"]) |
| } |
| } |
| } |
| return strings.Join(textParts, "\n"), imageURLs |
| } |
|
|
| func stringFromAny(v interface{}) string { |
| if s, ok := v.(string); ok { |
| return s |
| } |
| return "" |
| } |
|
|
| |
|
|
| func (h *ImageHandler) Edits(c *gin.Context) { |
| h.runImageEditFlow(c, false) |
| } |
|
|
| func (h *ImageHandler) Variations(c *gin.Context) { |
| h.runImageEditFlow(c, true) |
| } |
|
|
| func (h *ImageHandler) runImageEditFlow(c *gin.Context, asVariation bool) { |
| contentType := strings.Split(c.GetHeader("Content-Type"), ";")[0] |
| contentType = strings.ToLower(strings.TrimSpace(contentType)) |
|
|
| var imageSources []editImageInput |
| var prompt, model, responseFormat string |
| var n int |
| var stream bool |
|
|
| parseFormFields := func(promptVal, modelVal, nVal, responseFormatVal, streamVal string) { |
| prompt = strings.TrimSpace(promptVal) |
| model = strings.TrimSpace(modelVal) |
| responseFormat = strings.TrimSpace(responseFormatVal) |
| if nVal != "" { |
| if v, err := strconv.Atoi(nVal); err == nil { |
| n = v |
| } |
| } |
| stream = isStreamTrue(streamVal) |
| } |
|
|
| if contentType == "application/json" { |
| var body struct { |
| Prompt string `json:"prompt"` |
| Model string `json:"model"` |
| N int `json:"n"` |
| Size string `json:"size"` |
| ResponseFormat string `json:"response_format"` |
| Stream bool `json:"stream"` |
| Images []officialtypes.ImageEditSource `json:"images,omitempty"` |
| Image *officialtypes.ImageEditSource `json:"image,omitempty"` |
| ImageURL interface{} `json:"image_url,omitempty"` |
| Input interface{} `json:"input,omitempty"` |
| Content interface{} `json:"content,omitempty"` |
| Messages interface{} `json:"messages,omitempty"` |
| } |
| if err := c.BindJSON(&body); err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Request must be proper JSON", |
| "type": "invalid_request_error", |
| "param": nil, |
| "code": err.Error(), |
| }}) |
| return |
| } |
| prompt = strings.TrimSpace(body.Prompt) |
| model = body.Model |
| n = body.N |
| responseFormat = body.ResponseFormat |
| stream = body.Stream |
|
|
| client := bogdanfinn.NewStdClient() |
|
|
| for _, src := range body.Images { |
| item, _, err := imageEditConvertURL(client, src.ImageURL) |
| if err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "invalid image reference: " + err.Error(), |
| "type": "invalid_request_error", |
| "param": "images", |
| "code": "invalid_image", |
| }}) |
| return |
| } |
| if len(item.Data) > 0 { |
| imageSources = append(imageSources, item) |
| } |
| } |
| if body.Image != nil { |
| item, _, err := imageEditConvertURL(client, body.Image.ImageURL) |
| if err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "invalid image reference: " + err.Error(), |
| "type": "invalid_request_error", |
| "param": "image", |
| "code": "invalid_image", |
| }}) |
| return |
| } |
| if len(item.Data) > 0 { |
| imageSources = append(imageSources, item) |
| } |
| } |
| if body.ImageURL != nil { |
| switch t := body.ImageURL.(type) { |
| case string: |
| item, _, err := imageEditConvertURL(client, t) |
| if err == nil && len(item.Data) > 0 { |
| imageSources = append(imageSources, item) |
| } |
| case map[string]interface{}: |
| if u, ok := t["url"].(string); ok { |
| item, _, err := imageEditConvertURL(client, u) |
| if err == nil && len(item.Data) > 0 { |
| imageSources = append(imageSources, item) |
| } |
| } |
| } |
| } |
|
|
| promptFromParts, imageParts := collectResponsesAPIParts(body.Input) |
| if len(imageParts) == 0 { |
| if p, imgs := collectResponsesAPIParts(body.Content); len(imgs) > 0 { |
| promptFromParts = p |
| imageParts = imgs |
| } |
| } |
| if len(imageParts) == 0 { |
| if p, imgs := collectResponsesAPIParts(body.Messages); len(imgs) > 0 { |
| promptFromParts = p |
| imageParts = imgs |
| } |
| } |
| for _, p := range imageParts { |
| item, _, err := imageEditConvertURL(client, p) |
| if err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "invalid image reference: " + err.Error(), |
| "type": "invalid_request_error", |
| "param": "input", |
| "code": "invalid_image", |
| }}) |
| return |
| } |
| if len(item.Data) > 0 { |
| imageSources = append(imageSources, item) |
| } |
| } |
| if prompt == "" { |
| prompt = strings.TrimSpace(promptFromParts) |
| } |
| } else { |
| form, err := c.MultipartForm() |
| if err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Request must be multipart/form-data or application/json: " + err.Error(), |
| "type": "invalid_request_error", |
| "param": nil, |
| "code": "invalid_multipart", |
| }}) |
| return |
| } |
| parseFormFields( |
| strings.TrimSpace(c.PostForm("prompt")), |
| strings.TrimSpace(c.PostForm("model")), |
| c.PostForm("n"), |
| strings.TrimSpace(c.PostForm("response_format")), |
| c.PostForm("stream"), |
| ) |
| rawSources := make([]interface{}, 0, 4) |
| for _, key := range []string{"image", "image[]", "images", "images[]"} { |
| if vs, ok := form.File[key]; ok { |
| for _, fh := range vs { |
| rawSources = append(rawSources, fh) |
| } |
| } |
| } |
| if vs, ok := form.Value["image_url"]; ok { |
| client := bogdanfinn.NewStdClient() |
| for _, s := range vs { |
| item, _, err := imageEditConvertURL(client, s) |
| if err == nil && len(item.Data) > 0 { |
| imageSources = append(imageSources, item) |
| } |
| } |
| } |
| imageSources = append(imageSources, normalizeImageEditImages(rawSources)...) |
| } |
|
|
| if asVariation { |
| if prompt == "" { |
| prompt = "Generate a variation of the provided image(s). Return only the generated image, not a text description." |
| } |
| } |
|
|
| if len(imageSources) == 0 { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Missing required image input. Provide multipart 'image'/'images' field, or JSON 'image'/'images' field with image_url.", |
| "type": "invalid_request_error", |
| "param": "image", |
| "code": "missing_required_parameter", |
| }}) |
| return |
| } |
| if !asVariation && prompt == "" { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": "Missing required parameter: prompt", |
| "type": "invalid_request_error", |
| "param": "prompt", |
| "code": "missing_required_parameter", |
| }}) |
| return |
| } |
| if n <= 0 { |
| n = 1 |
| } |
| if n > 10 { |
| n = 10 |
| } |
| if responseFormat == "" { |
| responseFormat = "b64_json" |
| } |
| stream = requestStreamFlag(c, stream) |
|
|
| account, _, err := resolveAccount(c, h.accountPool, h.cfg, true) |
| if err != nil { |
| c.JSON(400, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "authorization_error", |
| "param": "Authorization", |
| "code": 400, |
| }}) |
| return |
| } |
| if account == nil || account.Token == "" { |
| c.JSON(400, gin.H{"error": "Images API requires a logged-in ChatGPT access token."}) |
| c.Abort() |
| return |
| } |
| if !account.Type.Satisfies(accounts.CapImageGenerate) { |
| c.JSON(403, gin.H{"error": "Images API requires a logged-in ChatGPT account."}) |
| return |
| } |
| if asVariation { |
| if !account.Type.Satisfies(accounts.CapImageVariation) { |
| c.JSON(403, gin.H{"error": "Image variation requires a logged-in ChatGPT account."}) |
| return |
| } |
| } else { |
| if !account.Type.Satisfies(accounts.CapImageEdit) { |
| c.JSON(403, gin.H{"error": "Image edit requires a logged-in ChatGPT account."}) |
| return |
| } |
| } |
|
|
| proxyUrl := account.Proxy |
| client := setupClientWithProxy(proxyUrl) |
| client.SetCookies("https://chatgpt.com", chatgpt.BasicCookies) |
| turnStile, status, err := chatgpt.InitSentinel(client, account, proxyUrl, 0) |
| if err != nil { |
| if status == http.StatusUnauthorized { |
| h.accountPool.ReportFailure(account) |
| } |
| c.JSON(status, gin.H{ |
| "message": err.Error(), |
| "type": "InitTurnStile_request_error", |
| "param": err, |
| "code": status, |
| }) |
| return |
| } |
|
|
| if stream { |
| writeImageStreamHeader(c) |
| } |
|
|
| |
| references := make([]chatgpt.ImageEditReference, 0, len(imageSources)) |
| for idx, src := range imageSources { |
| uploaded, upStatus, upErr := chatgpt.UploadFile(client, account, proxyUrl, src.Filename, src.ContentType, src.Data) |
| if upErr != nil { |
| if stream { |
| writeImageStreamEvent(c, "image.generation.error", gin.H{ |
| "object": "image.generation.error", |
| "index": idx, |
| "message": "upload image failed: " + upErr.Error(), |
| }) |
| writeImageStreamDone(c) |
| return |
| } |
| c.JSON(upStatus, gin.H{"error": gin.H{ |
| "message": "upload image failed: " + upErr.Error(), |
| "type": "image_upload_error", |
| "param": fmt.Sprintf("image[%d]", idx), |
| "code": "image_upload_error", |
| }}) |
| return |
| } |
| references = append(references, chatgpt.ImageEditReference{ |
| FileID: uploaded.FileID, |
| Width: uploaded.Width, |
| Height: uploaded.Height, |
| Size: int(uploaded.Bytes), |
| MimeType: uploaded.MimeType, |
| Filename: uploaded.Filename, |
| }) |
| } |
|
|
| |
| var data []officialtypes.ImageGenerationData |
| for i := 0; i < n; i++ { |
| if stream { |
| writeImageStreamEvent(c, "image.generation.chunk", imageStreamChunk{ |
| Object: "image.generation.chunk", |
| Index: i, |
| Total: n, |
| Created: 0, |
| Model: model, |
| ProgressText: fmt.Sprintf("Generating image %d/%d ...", i+1, n), |
| }) |
| } |
| imageResults, upstreamText, err := chatgpt.GeneratePictureConversationImagesWithReferences(client, account, turnStile, prompt, model, proxyUrl, references) |
| if err != nil { |
| if stream { |
| writeImageStreamEvent(c, "image.generation.error", gin.H{ |
| "object": "image.generation.error", |
| "index": i, |
| "total": n, |
| "message": err.Error(), |
| }) |
| writeImageStreamDone(c) |
| return |
| } |
| c.JSON(500, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "image_generation_error", |
| "param": nil, |
| "code": "image_generation_error", |
| }}) |
| return |
| } |
| for _, imageResult := range imageResults { |
| item := officialtypes.ImageGenerationData{ |
| RevisedPrompt: prompt, |
| } |
| if responseFormat == "b64_json" { |
| if imageResult.B64JSON != "" { |
| item.B64JSON = imageResult.B64JSON |
| } else if imageResult.URL != "" { |
| imageBytes, err := chatgpt.DownloadImageBytes(client, imageResult.URL, account) |
| if err != nil { |
| if stream { |
| writeImageStreamEvent(c, "image.generation.error", gin.H{ |
| "object": "image.generation.error", |
| "index": i, |
| "total": n, |
| "message": err.Error(), |
| }) |
| writeImageStreamDone(c) |
| return |
| } |
| c.JSON(500, gin.H{"error": gin.H{ |
| "message": err.Error(), |
| "type": "image_download_error", |
| "param": nil, |
| "code": "image_download_error", |
| }}) |
| return |
| } |
| item.B64JSON = base64.StdEncoding.EncodeToString(imageBytes) |
| } |
| } else { |
| item.URL = imageResult.URL |
| if item.URL == "" && imageResult.B64JSON != "" { |
| item.B64JSON = imageResult.B64JSON |
| } |
| } |
| data = append(data, item) |
| if stream { |
| writeImageStreamEvent(c, "image.generation.result", imageStreamResult{ |
| Object: "image.generation.result", |
| Index: len(data) - 1, |
| Total: n, |
| Created: 0, |
| Model: model, |
| Data: []officialtypes.ImageGenerationData{item}, |
| }) |
| } |
| if len(data) >= n { |
| break |
| } |
| } |
| if len(imageResults) == 0 && upstreamText != "" { |
| if stream { |
| writeImageStreamEvent(c, "image.generation.error", gin.H{ |
| "object": "image.generation.error", |
| "index": i, |
| "total": n, |
| "message": "No image result found in response: " + upstreamText, |
| }) |
| writeImageStreamDone(c) |
| return |
| } |
| c.JSON(500, gin.H{"error": gin.H{ |
| "message": "No image result found in response: " + upstreamText, |
| "type": "image_generation_error", |
| "param": nil, |
| "code": "image_generation_error", |
| }}) |
| return |
| } |
| if len(data) >= n { |
| break |
| } |
| } |
| if len(data) == 0 { |
| if stream { |
| writeImageStreamEvent(c, "image.generation.error", gin.H{ |
| "object": "image.generation.error", |
| "message": "No image result found in response", |
| }) |
| writeImageStreamDone(c) |
| return |
| } |
| c.JSON(500, gin.H{"error": gin.H{ |
| "message": "No image result found in response", |
| "type": "image_generation_error", |
| "param": nil, |
| "code": "image_generation_error", |
| }}) |
| return |
| } |
| if stream { |
| writeImageStreamEvent(c, "image.generation.completed", imageStreamCompleted{ |
| Object: "image.generation.completed", |
| Created: 0, |
| Model: model, |
| Data: data, |
| }) |
| writeImageStreamDone(c) |
| return |
| } |
| if asVariation { |
| c.JSON(200, officialtypes.NewImageVariationResponse(data)) |
| } else { |
| c.JSON(200, officialtypes.NewImageEditResponse(data)) |
| } |
| } |
|
|