加回 nano-banana-2 与 nano-banana-pro 并存
This commit is contained in:
@@ -452,6 +452,17 @@ func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error)
|
|||||||
"max_reference_images": 4,
|
"max_reference_images": 4,
|
||||||
"description": "Adobe Flux Kontext Max",
|
"description": "Adobe Flux Kontext Max",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "nano-banana-2",
|
||||||
|
"provider": "runway",
|
||||||
|
"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"},
|
||||||
|
"resolutions": []string{"1K", "2K", "4K"},
|
||||||
|
"image_to_image": true,
|
||||||
|
"max_reference_images": 6,
|
||||||
|
"reference_mode": "asset",
|
||||||
|
"description": "Runway Nano Banana 2 (图/参考图)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "nano-banana-pro",
|
"id": "nano-banana-pro",
|
||||||
"provider": "runway",
|
"provider": "runway",
|
||||||
@@ -613,6 +624,15 @@ func (h *UserGenerationHandler) publicModels() ([]gin.H, error) {
|
|||||||
"description": "Adobe Flux Kontext Max",
|
"description": "Adobe Flux Kontext Max",
|
||||||
"stub": false,
|
"stub": false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "nano-banana-2",
|
||||||
|
"provider": "runway",
|
||||||
|
"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"},
|
||||||
|
"resolutions": []string{"1K", "2K", "4K"},
|
||||||
|
"description": "Runway Nano Banana 2",
|
||||||
|
"stub": false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "nano-banana-pro",
|
"id": "nano-banana-pro",
|
||||||
"provider": "runway",
|
"provider": "runway",
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateImage runs the Runway "Nano Banana Pro" (workflow_gemini_image /
|
// GenerateImage runs a Runway gemini image text/image-to-image pipeline:
|
||||||
// gemini-3-pro-image-preview) text/image-to-image pipeline: upload each
|
// upload each reference image (DATASET + DATASET_PREVIEW → dataset) to obtain
|
||||||
// reference image (DATASET + DATASET_PREVIEW → dataset) to obtain its
|
// its {assetId, url}, create a gemini image task and poll it to completion,
|
||||||
// {assetId, url}, create a gemini image task and poll it to completion, then
|
// then download the rendered PNG. modelID selects the variant: "nano-banana-2"
|
||||||
// download the rendered PNG. teamID is the workspace id; if empty it's derived
|
// → gemini_3_1_flash_image / gemini-3.1-flash-image-preview (with aspect_ratio),
|
||||||
// from the token. imageSize is the "1K"/"2K"/"4K" tier (Pro honors 2K/4K).
|
// anything else → workflow_gemini_image / gemini-3-pro-image-preview. imageSize
|
||||||
// refs may be empty (pure text-to-image).
|
// is the "1K"/"2K"/"4K" tier. teamID is the workspace id; if empty it's derived
|
||||||
func (c *Client) GenerateImage(ctx context.Context, token, teamID, prompt, imageSize string, refs [][]byte) ([]byte, map[string]any, error) {
|
// from the token. refs may be empty (pure text-to-image).
|
||||||
|
func (c *Client) GenerateImage(ctx context.Context, token, teamID, modelID, prompt, aspectRatio, 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
|
||||||
@@ -63,7 +64,7 @@ func (c *Client) GenerateImage(ctx context.Context, token, teamID, prompt, image
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
taskID, err := c.createImageTask(ctx, submitClient, token, teamID, prompt, imageSize, refImages)
|
taskID, err := c.createImageTask(ctx, submitClient, token, teamID, modelID, prompt, aspectRatio, imageSize, refImages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -108,9 +109,10 @@ func (c *Client) uploadReference(ctx context.Context, client tlsclient.HttpClien
|
|||||||
return assetID, refURL, nil
|
return assetID, refURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createImageTask creates a workflow_gemini_image (Nano Banana Pro) task and
|
// createImageTask creates a gemini image task (workflow_gemini_image for Pro,
|
||||||
// returns its id.
|
// gemini_3_1_flash_image for Nano Banana 2) and returns its id.
|
||||||
func (c *Client) createImageTask(ctx context.Context, client tlsclient.HttpClient, token, teamID, prompt, imageSize string, refImages []map[string]any) (string, error) {
|
func (c *Client) createImageTask(ctx context.Context, client tlsclient.HttpClient, token, teamID, modelID, prompt, aspectRatio, imageSize string, refImages []map[string]any) (string, error) {
|
||||||
|
taskType := "workflow_gemini_image"
|
||||||
opts := map[string]any{
|
opts := map[string]any{
|
||||||
"name": "Nano Banana Pro - " + prompt,
|
"name": "Nano Banana Pro - " + prompt,
|
||||||
"text_prompt": prompt,
|
"text_prompt": prompt,
|
||||||
@@ -120,11 +122,20 @@ func (c *Client) createImageTask(ctx context.Context, client tlsclient.HttpClien
|
|||||||
"exploreMode": false,
|
"exploreMode": false,
|
||||||
"creationSource": "tool-mode",
|
"creationSource": "tool-mode",
|
||||||
}
|
}
|
||||||
|
if modelID == "nano-banana-2" {
|
||||||
|
taskType = "gemini_3_1_flash_image"
|
||||||
|
opts["name"] = "Nano Banana 2 - " + prompt
|
||||||
|
opts["model"] = "gemini-3.1-flash-image-preview"
|
||||||
|
if strings.TrimSpace(aspectRatio) == "" {
|
||||||
|
aspectRatio = "16:9"
|
||||||
|
}
|
||||||
|
opts["aspect_ratio"] = aspectRatio
|
||||||
|
}
|
||||||
if len(refImages) > 0 {
|
if len(refImages) > 0 {
|
||||||
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": "workflow_gemini_image",
|
"taskType": taskType,
|
||||||
"options": opts,
|
"options": opts,
|
||||||
"asTeamId": jsonNumberOrString(teamID),
|
"asTeamId": jsonNumberOrString(teamID),
|
||||||
"sessionId": uuid.NewString(),
|
"sessionId": uuid.NewString(),
|
||||||
|
|||||||
@@ -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, resolution)
|
b, execErr := s.generateRunwayImage(genCtx, eventID, modelItem, in, aspectRatio, 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 Pro" (workflow_gemini_image /
|
// generateRunwayImage runs the Runway gemini image pipeline (Nano Banana Pro or
|
||||||
// gemini-3-pro-image-preview) image pipeline across the runway pool. Unlike the
|
// Nano Banana 2, selected by the model id) across the runway pool. Unlike the
|
||||||
// video path it does NOT pre-deduct credits: it simply round-robins the pool and
|
// video path it does NOT pre-deduct credits: it simply round-robins the pool and
|
||||||
// generates. Per ops decision an out-of-credits account is treated like a dead
|
// generates. Per ops decision an out-of-credits account is treated like a dead
|
||||||
// 401 — marked dead (status=disabled) and skipped — because Runway credits don't
|
// 401 — marked dead (status=disabled) and skipped — because Runway credits don't
|
||||||
// refill daily, so a "quota" mark (which the maintenance loop would revive) is
|
// refill daily, so a "quota" mark (which the maintenance loop would revive) is
|
||||||
// wrong. 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, resolution string) ([]byte, error) {
|
func (s *V1Service) generateRunwayImage(ctx context.Context, eventID string, modelItem *model.ModelConfig, in V1ImageRequest, aspectRatio, 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, imageSize, refs)
|
d, _, genErr := s.runway.GenerateImage(ctx, token.Value, teamID, modelItem.ID, in.Prompt, aspectRatio, 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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user