Files
2026-09-27 22:41:33 +08:00

215 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""套图规划 / 一键生成 / 单张 AI 生图 契约(studio/src/services/suite.ts 同源,见 docs/v2.1/api.md)。"""
from __future__ import annotations
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
SUPPORTED_TYPES = [
"white_bg", "key_features", "selling_pt", "material",
"lifestyle", "multi_scene", "ecommerce_detail",
"size_chart", "sku_collection", "custom",
]
# 通义(DashScope)生图模型白名单
TONGYI_MODELS = ["qwen-image-3.0-pro", "wan2.7-image-pro", "wan2.6-image", "wan2.6-t2i"]
# RightAPI 生图模型白名单(gpt-image 系列 + Google nano-banana 系列,同一中转)
RIGHTAPI_MODELS = [
"gpt-image-2",
"gpt-image-2-vip",
"nano-banana",
"nano-banana-2",
"nano-banana-2-lite",
"nano-banana-pro",
]
# Pcore 生图模型白名单(pcore.ai 中转;同一模型名与 RightAPI 重名,用 @pcore 后缀区分路由)
PCORE_MODELS = ["gpt-image-2"]
# 豆包 Seedream 走火山方舟;模型由 ARK_IMAGE_MODEL 指定(不在下拉白名单内也允许直连豆包)
DOUBAO_MODELS = ["doubao-seedream-4-5-251128"]
# 模型 → provider 推断表:前端只传模型名,服务端据此路由(模型名优先于 provider 字段)
# 同名模型走多家中转时,前端传「api_model@provider」形式(如 gpt-image-2@pcore)
MODEL_PROVIDERS: dict[str, str] = {
**{m: "tongyi" for m in TONGYI_MODELS},
**{m: "rightapi" for m in RIGHTAPI_MODELS},
**{m: "doubao" for m in DOUBAO_MODELS},
**{f"{m}@pcore": "pcore" for m in PCORE_MODELS},
}
def api_model(model: str | None) -> str | None:
"""前端模型值 → 发给供应商 API 的模型名:'gpt-image-2@pcore' → 'gpt-image-2'。"""
return model.split("@", 1)[0] if model and "@" in model else model
def resolve_provider(model: str | None, requested: str | None, default: str) -> str:
"""已知模型名直接定位 provider;未知模型回退到请求指定的 provider 或默认值。"""
if model and model in MODEL_PROVIDERS:
return MODEL_PROVIDERS[model]
return requested or default
# 目标平台 → 文案语言 + 图片比例(平台决定规格,不再单独选语言)
PLATFORM_SPECS: dict[str, dict] = {
"ozon": {"lang": "ru", "ratio": "3:4", "label": "Ozon"},
"wb": {"lang": "ru", "ratio": "3:4", "label": "Wildberries"},
"cn": {"lang": "zh", "ratio": "1:1", "label": "中文(国内平台)"},
}
class TextMaterial(BaseModel):
kind: str = Field(..., description="title | title_ru | params | selling_point | desc | price | brand | sales | shop")
content: str = ""
pairs: list[dict] | None = None # [{key, value}]
class WatermarkOptions(BaseModel):
"""生成图水印:AI 出图后由服务端后处理合成(与生图模型无关)。右下角,默认文字 Panda Store。"""
enabled: bool = Field(default=False, description="是否开启水印")
type: Literal["image", "text"] = Field(default="image", description="图片水印 | 文字水印")
text: str = Field(default="Panda Store", description="文字水印内容")
opacity: int = Field(default=30, ge=1, le=100, description="不透明度(%)")
class ProductDimensions(BaseModel):
"""商品长宽高;尺寸图仅标注实际提供的方向。"""
length: float | None = Field(default=None, gt=0, description="长度(按 unit 单位)")
width: float | None = Field(default=None, gt=0, description="宽度(按 unit 单位)")
height: float | None = Field(default=None, gt=0, description="高度(按 unit 单位)")
unit: Literal["mm", "cm"] = Field(default="cm", description="尺寸单位")
class PlanItem(BaseModel):
"""出图方案项:一类图 × 数量,可绑定 SKU 规格。"""
kind: str = Field(default="custom", description="图类型(SUPPORTED_TYPES 之一)")
title: str = Field(..., description="方案标题,如「主图·粉色」")
detail: str = Field(default="", description="这张图展示什么(中文)")
prompt_hint: str = Field(default="", description="构图提示(英文,进生图 prompt)")
count: int = Field(default=1, ge=0, le=5)
variant_name: str | None = Field(default=None, description="绑定的 SKU 规格名")
class SuitePlanRequest(BaseModel):
"""POST /api/suite/plan:DeepSeek 根据商品信息生成出图方案。"""
product_id: str | None = Field(default=None, description="便于日志与上下文定位,可空")
texts: list[TextMaterial] = Field(default_factory=list)
sku_variants: list[str] = Field(default_factory=list, description="带图的 SKU 规格名")
image_stats: dict = Field(default_factory=dict, description="分组图片数量统计 {main: n, ...}")
platform: str = Field(default="ozon")
requirements: str | None = Field(default=None, description="生图要求(最高优先级,规划方案必须遵循)")
dimensions: ProductDimensions | None = Field(default=None, description="商品长宽高(mm 或 cm),用于规划尺寸标注图")
class PlanItemOut(BaseModel):
kind: str
title: str
detail: str = ""
prompt_hint: str = ""
count: int = 1
variant_name: str | None = None
class PlanResponse(BaseModel):
summary: str = ""
items: list[PlanItemOut]
class GenerateImageItem(BaseModel):
url: str = Field(..., description="勾选的参考底图 URL(stored_url 或源站原图)")
group_key: str = Field(default="main", description="main | sku | detail | upload")
variant_name: str | None = Field(default=None, description="SKU 规格名(方案绑定用)")
class SuiteGenerateRequest(BaseModel):
"""POST /api/suite/generate:提交套图任务,返回 suite_id 后前端轮询。"""
product_id: str | None = Field(default=None, description="用于生成图回写素材(generated 组),可空")
texts: list[TextMaterial] = Field(default_factory=list)
images: list[GenerateImageItem] = Field(default_factory=list, description="勾选的参考底图")
style_set: int = Field(default=1, ge=1, le=5)
style_prompt: str | None = Field(default=None, description="用户改写的风格提示词(覆盖 style_set 模板)")
requirements: str | None = Field(default=None, description="生图要求(最高优先级,强制约束,覆盖其他设定)")
plan: list[PlanItem] | None = Field(default=None, description="出图方案(优先于 types)")
types: list[str] = Field(default_factory=list, description="旧参数:无方案时按类型生成")
platform: str = Field(default="ozon")
model: str | None = Field(default=None, description="生图模型名(按 MODEL_PROVIDERS 路由 provider)")
watermark: WatermarkOptions | None = Field(default=None, description="生成图水印(服务端后处理合成);关闭时不传")
dimensions: ProductDimensions | None = Field(default=None, description="商品长宽高(mm 或 cm),用于尺寸标注图")
class SuiteCreateResponse(BaseModel):
suite_id: str
class SuiteImageOut(BaseModel):
item_id: str
asset_id: str | None = None
type_id: str
name: str
url: str
status: str
error: str | None = None
completed_at: datetime | None = None
class SuiteOut(BaseModel):
id: str
status: str
kind: str = "image_suite"
style_set: int
platform: str
lang: str
ratio: str
provider: str
model: str | None = None
total: int = 0 # 计划总张数(进度分母;images 是逐张追加,过程中 length < total)
completed: int = 0
failed: int = 0
cancel_requested: bool = False
created_at: datetime | None = None
started_at: datetime | None = None
completed_at: datetime | None = None
images: list[SuiteImageOut]
error: str | None = None
class ImageEditSingleRequest(BaseModel):
"""POST /api/suite/image-edit:单张 AI 生图,可携带多张参考底图。"""
product_id: str | None = Field(default=None, description="append=true 时回写到该商品的 generated 组")
main_image_url: str | None = Field(default=None, description="本次编辑的主图;新界面必须显式传入")
reference_image_urls: list[str] = Field(default_factory=list, max_length=2, description="最多两张辅助参考图,按选择顺序传给模型")
image_url: str | None = Field(default=None, description="兼容旧客户端的单张原图 URL")
image_urls: list[str] = Field(default_factory=list, description="选中的参考底图 URL,可多张")
prompt: str = Field(default="", description="用户要求(可选,作为额外生图指令)")
mode: Literal["edit", "outpaint"] = Field(
default="edit",
description="edit 为 AI 编辑;outpaint 只扩展到目标画布,不允许模型改动原图区域",
)
model: str = Field(default="gpt-image-2@pcore")
ratio: Literal["3:4", "1:1", "4:5"] = Field(default="3:4", description="输出画幅")
target_language: Literal["ru", "ar", "en"] = Field(default="ru", description="图内文字的目标语言")
translate_text: bool = Field(default=False, description="在本次生图调用中识别、翻译并重新排版图中文字")
style_set: int = Field(default=2, ge=1, le=5)
style_prompt: str | None = Field(default=None, description="用户修改后的风格提示词")
background: Literal["auto", "white", "solid", "scene", "reference"] = Field(default="auto")
background_description: str | None = Field(default=None, description="纯色或场景背景的具体要求")
append: bool = Field(default=True, description="结果是否追加为商品素材(generated 组)")
after_asset_id: str | None = Field(
default=None,
description="插入锚点:新素材排在该素材(generated 组)之后,方便与原图对比;缺省追加到组尾",
)
watermark: WatermarkOptions | None = Field(default=None, description="单张生图完成后的服务端水印;关闭时不传")
generate_size_chart: bool = Field(default=False, description="是否按尺寸标注图生成")
dimensions: ProductDimensions | None = Field(default=None, description="商品长宽高(mm 或 cm),用于尺寸标注图")
class ImageEditTaskResponse(BaseModel):
"""单张编辑已进入统一图片任务队列;用 GET /api/suites/{suite_id} 查询结果。"""
suite_id: str