"""套图规划 / 一键生成 / 单张 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