nano-banana-2 升级为 nano-banana-pro (workflow_gemini_image / gemini-3-pro-image-preview, 支持 2K/4K)

This commit is contained in:
2026-07-10 11:19:51 +08:00
parent a636f79cd8
commit 2afb4e7857
3 changed files with 29 additions and 32 deletions
@@ -453,7 +453,7 @@ func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error)
"description": "Adobe Flux Kontext Max", "description": "Adobe Flux Kontext Max",
}, },
{ {
"id": "nano-banana-2", "id": "nano-banana-pro",
"provider": "runway", "provider": "runway",
"type": "image", "type": "image",
"ratios": []string{"1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"}, "ratios": []string{"1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"},
@@ -461,7 +461,7 @@ func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error)
"image_to_image": true, "image_to_image": true,
"max_reference_images": 6, "max_reference_images": 6,
"reference_mode": "asset", "reference_mode": "asset",
"description": "Runway Nano Banana 2 (图/参考图)", "description": "Runway Nano Banana Pro (图/参考图)",
}, },
{ {
"id": "gemini-veo31", "id": "gemini-veo31",
@@ -614,12 +614,12 @@ func (h *UserGenerationHandler) publicModels() ([]gin.H, error) {
"stub": false, "stub": false,
}, },
{ {
"id": "nano-banana-2", "id": "nano-banana-pro",
"provider": "runway", "provider": "runway",
"kind": "image", "kind": "image",
"ratios": []string{"1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"}, "ratios": []string{"1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"},
"resolutions": []string{"1K", "2K", "4K"}, "resolutions": []string{"1K", "2K", "4K"},
"description": "Runway Nano Banana 2", "description": "Runway Nano Banana Pro",
"stub": false, "stub": false,
}, },
{ {
+15 -18
View File
@@ -13,14 +13,14 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
// GenerateImage runs the Runway "Nano Banana 2" (gemini_3_1_flash_image) // GenerateImage runs the Runway "Nano Banana Pro" (workflow_gemini_image /
// text/image-to-image pipeline: upload each reference image (DATASET + // gemini-3-pro-image-preview) text/image-to-image pipeline: upload each
// DATASET_PREVIEW → dataset) to obtain its {assetId, url}, create a gemini image // reference image (DATASET + DATASET_PREVIEW → dataset) to obtain its
// task and poll it to completion, then download the rendered PNG. teamID is the // {assetId, url}, create a gemini image task and poll it to completion, then
// workspace id; if empty it's derived from the token. aspectRatio is passed // download the rendered PNG. teamID is the workspace id; if empty it's derived
// through as-is (e.g. "16:9"); imageSize is the "1K"/"2K"/"4K" tier. refs may be // from the token. imageSize is the "1K"/"2K"/"4K" tier (Pro honors 2K/4K).
// empty (pure text-to-image). // refs may be empty (pure text-to-image).
func (c *Client) GenerateImage(ctx context.Context, token, teamID, prompt, aspectRatio, imageSize string, refs [][]byte) ([]byte, map[string]any, error) { func (c *Client) GenerateImage(ctx context.Context, token, teamID, prompt, imageSize string, refs [][]byte) ([]byte, map[string]any, error) {
token = strings.TrimSpace(strings.TrimPrefix(token, "Bearer ")) token = strings.TrimSpace(strings.TrimPrefix(token, "Bearer "))
if token == "" { if token == "" {
return nil, nil, ErrAuth return nil, nil, ErrAuth
@@ -31,9 +31,6 @@ func (c *Client) GenerateImage(ctx context.Context, token, teamID, prompt, aspec
if teamID == "" { if teamID == "" {
return nil, nil, errors.New("runway: no team id") return nil, nil, errors.New("runway: no team id")
} }
if strings.TrimSpace(aspectRatio) == "" {
aspectRatio = "16:9"
}
if strings.TrimSpace(imageSize) == "" { if strings.TrimSpace(imageSize) == "" {
imageSize = "1K" imageSize = "1K"
} }
@@ -66,7 +63,7 @@ func (c *Client) GenerateImage(ctx context.Context, token, teamID, prompt, aspec
}) })
} }
taskID, err := c.createImageTask(ctx, submitClient, token, teamID, prompt, aspectRatio, imageSize, refImages) taskID, err := c.createImageTask(ctx, submitClient, token, teamID, prompt, imageSize, refImages)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -111,15 +108,15 @@ func (c *Client) uploadReference(ctx context.Context, client tlsclient.HttpClien
return assetID, refURL, nil return assetID, refURL, nil
} }
// createImageTask creates a gemini_3_1_flash_image task and returns its id. // createImageTask creates a workflow_gemini_image (Nano Banana Pro) task and
func (c *Client) createImageTask(ctx context.Context, client tlsclient.HttpClient, token, teamID, prompt, aspectRatio, imageSize string, refImages []map[string]any) (string, error) { // returns its id.
func (c *Client) createImageTask(ctx context.Context, client tlsclient.HttpClient, token, teamID, prompt, imageSize string, refImages []map[string]any) (string, error) {
opts := map[string]any{ opts := map[string]any{
"name": "Nano Banana 2 - " + prompt, "name": "Nano Banana Pro - " + prompt,
"text_prompt": prompt, "text_prompt": prompt,
"aspect_ratio": aspectRatio,
"num_images": 1, "num_images": 1,
"image_size": imageSize, "image_size": imageSize,
"model": "gemini-3.1-flash-image-preview", "model": "gemini-3-pro-image-preview",
"exploreMode": false, "exploreMode": false,
"creationSource": "tool-mode", "creationSource": "tool-mode",
} }
@@ -127,7 +124,7 @@ func (c *Client) createImageTask(ctx context.Context, client tlsclient.HttpClien
opts["reference_images"] = refImages opts["reference_images"] = refImages
} }
res, err := c.submitTask(ctx, client, token, teamID, map[string]any{ res, err := c.submitTask(ctx, client, token, teamID, map[string]any{
"taskType": "gemini_3_1_flash_image", "taskType": "workflow_gemini_image",
"options": opts, "options": opts,
"asTeamId": jsonNumberOrString(teamID), "asTeamId": jsonNumberOrString(teamID),
"sessionId": uuid.NewString(), "sessionId": uuid.NewString(),
+10 -10
View File
@@ -555,7 +555,7 @@ func (s *V1Service) prepareImageExecution(ctx context.Context, principal *APIPri
} }
imageBytes = b imageBytes = b
case "runway": case "runway":
b, execErr := s.generateRunwayImage(genCtx, eventID, modelItem, in, aspectRatio, resolution) b, execErr := s.generateRunwayImage(genCtx, eventID, modelItem, in, resolution)
if execErr != nil { if execErr != nil {
_ = s.refundIfNeeded(ctx, principal, eventID, price) _ = s.refundIfNeeded(ctx, principal, eventID, price)
_ = s.events.UpdateStatus(ctx, eventID, "failed", execErr.Error(), 0) _ = s.events.UpdateStatus(ctx, eventID, "failed", execErr.Error(), 0)
@@ -2137,14 +2137,14 @@ func (s *V1Service) generateGrokVideo(ctx context.Context, eventID string, model
return nil, "", lastErr return nil, "", lastErr
} }
// generateRunwayImage runs the Runway "Nano Banana 2" (gemini_3_1_flash_image) // generateRunwayImage runs the Runway "Nano Banana Pro" (workflow_gemini_image /
// image pipeline across the runway pool. Unlike the video path it does NOT // gemini-3-pro-image-preview) image pipeline across the runway pool. Unlike the
// pre-deduct credits: it simply round-robins the pool and generates. Per ops // video path it does NOT pre-deduct credits: it simply round-robins the pool and
// decision an out-of-credits account is treated like a dead 401 — marked // generates. Per ops decision an out-of-credits account is treated like a dead
// dead (status=disabled) and skipped — because Runway credits don't refill // 401 — marked dead (status=disabled) and skipped — because Runway credits don't
// daily, so a "quota" mark (which the maintenance loop would revive) is wrong. // refill daily, so a "quota" mark (which the maintenance loop would revive) is
// Reference images (up to the model's max) are uploaded per attempt. // wrong. Reference images (up to the model's max) are uploaded per attempt.
func (s *V1Service) generateRunwayImage(ctx context.Context, eventID string, modelItem *model.ModelConfig, in V1ImageRequest, aspectRatio, resolution string) ([]byte, error) { func (s *V1Service) generateRunwayImage(ctx context.Context, eventID string, modelItem *model.ModelConfig, in V1ImageRequest, resolution string) ([]byte, error) {
if s.runway == nil { if s.runway == nil {
return nil, errors.New("runway client not configured") return nil, errors.New("runway client not configured")
} }
@@ -2203,7 +2203,7 @@ func (s *V1Service) generateRunwayImage(ctx context.Context, eventID string, mod
if token.Meta != nil { if token.Meta != nil {
teamID = strings.TrimSpace(stringValue(token.Meta["team_id"])) teamID = strings.TrimSpace(stringValue(token.Meta["team_id"]))
} }
d, _, genErr := s.runway.GenerateImage(ctx, token.Value, teamID, in.Prompt, aspectRatio, imageSize, refs) d, _, genErr := s.runway.GenerateImage(ctx, token.Value, teamID, in.Prompt, imageSize, refs)
if genErr == nil { if genErr == nil {
_, _ = s.tokens.Update(ctx, "runway", token.ID, map[string]any{ _, _ = s.tokens.Update(ctx, "runway", token.ID, map[string]any{
"last_used_at": time.Now(), "last_used_at": time.Now(),