From f6a1ad9ffcff887515eef50e0258afde3fd0fccd Mon Sep 17 00:00:00 2001 From: chengma Date: Fri, 17 Jul 2026 15:35:28 +0800 Subject: [PATCH] feat(product-suite): add category targets and prompt compatibility --- app/default_prompts/product_suite/base.txt | 2 +- app/product_suite.py | 68 ++++++++++++++++++---- docs/04-architecture.md | 6 +- docs/api.md | 3 +- docs/routes.md | 5 +- docs/tasks/T-654.md | 4 +- tests/test_packaging.py | 3 +- tests/test_product_suite.py | 48 +++++++++++++-- tests/test_product_suite_gui.py | 7 ++- 9 files changed, 120 insertions(+), 26 deletions(-) diff --git a/app/default_prompts/product_suite/base.txt b/app/default_prompts/product_suite/base.txt index 140de3a..a6da51c 100644 --- a/app/default_prompts/product_suite/base.txt +++ b/app/default_prompts/product_suite/base.txt @@ -1,4 +1,4 @@ -套图名称:{套图名称}{补充描述} +生成目标:{生成目标} 平台:{平台};国家地区:{国家地区};输出语言:{输出语言}。 {尺寸与长图规则} {禁用内容规则} diff --git a/app/product_suite.py b/app/product_suite.py index 70459ee..5903d1f 100644 --- a/app/product_suite.py +++ b/app/product_suite.py @@ -6,14 +6,29 @@ from collections import OrderedDict import re -FIXED_CATEGORIES = ("白底图", "场景图", "卖点图") +FIXED_CATEGORIES = ("白底图", "场景图", "模特场景图", "细节说明图", "卖点图") FIXED_CATEGORY_HELPERS = { "白底图": "白底主图,多角度呈现商品细节", "场景图": "生活化场景展示商品使用方式", + "模特场景图": "模特或手持场景展示商品真实使用", + "细节说明图": "突出材质、做工和关键细节", "卖点图": "突出核心卖点和差异化优势", } +FIXED_CATEGORY_DESCRIPTIONS = { + "白底图": "生成 Shopee 台灣商品白底主圖,純白背景,商品清晰置中,不添加多餘文字。", + "场景图": "生成生活化使用場景圖,展示商品在真實情境中的用途與氛圍,畫面自然可信。", + "模特场景图": "生成模特或手持使用情境圖,畫面自然可信,商品為主角。", + "细节说明图": "生成商品細節特寫說明圖,突出材質、做工、接口、紋理或關鍵細節。", + "卖点图": "生成賣點詳解圖,使用繁體中文短文案呈現核心優勢,版面乾淨。", +} DEFAULT_CATEGORY_COUNTS = OrderedDict( - (("白底图", 1), ("场景图", 2), ("卖点图", 2)) + ( + ("白底图", 1), + ("场景图", 2), + ("模特场景图", 0), + ("细节说明图", 0), + ("卖点图", 2), + ) ) PLATFORMS = ("Shopee", "Lazada", "TikTok Shop", "Amazon") COUNTRIES = ("中国台湾", "新加坡", "马来西亚", "菲律宾", "泰国", "越南") @@ -23,6 +38,7 @@ LAST_SETTING_KEYS = ("platform", "country", "language", "ratio") MAX_CATEGORY_NAME_LENGTH = 10 MAX_GENERATION_COUNT_WITHOUT_CONFIRM = 16 PRODUCT_SUITE_PLACEHOLDERS = ( + "生成目标", "套图名称", "补充描述", "平台", @@ -38,9 +54,7 @@ PRODUCT_SUITE_PLACEHOLDERS = ( "价格信息规则", "尺码信息规则", ) -PRODUCT_SUITE_REQUIRED_PLACEHOLDERS = ( - "套图名称", - "补充描述", +PRODUCT_SUITE_COMMON_REQUIRED_PLACEHOLDERS = ( "图片比例", "参考图规则", "商品卖点与要求", @@ -49,6 +63,15 @@ PRODUCT_SUITE_REQUIRED_PLACEHOLDERS = ( "价格信息规则", "尺码信息规则", ) +PRODUCT_SUITE_REQUIRED_PLACEHOLDERS = ( + "生成目标", + *PRODUCT_SUITE_COMMON_REQUIRED_PLACEHOLDERS, +) +PRODUCT_SUITE_LEGACY_REQUIRED_PLACEHOLDERS = ( + "套图名称", + "补充描述", + *PRODUCT_SUITE_COMMON_REQUIRED_PLACEHOLDERS, +) PRODUCT_SUITE_READ_ONLY_RULE_PLACEHOLDERS = ( "尺寸与长图规则", "禁用内容规则", @@ -154,8 +177,13 @@ def category_helper(category): def category_description(category): - helper = category_helper(category) - return "," + helper if helper else "" + name = str(category or "").strip() + fixed_description = FIXED_CATEGORY_DESCRIPTIONS.get(name) + if fixed_description: + return fixed_description + if name: + return "生成自定义分类图片:%s。" % name + return "生成自定义分类图片。" def suite_total_count(settings, image_count): @@ -186,10 +214,20 @@ def product_suite_prompt_errors(template_text): errors.append("模板包含未知变量:%s" % "、".join("{%s}" % name for name in unknown)) missing = [ - name for name in PRODUCT_SUITE_REQUIRED_PLACEHOLDERS if name not in names + name + for name in PRODUCT_SUITE_COMMON_REQUIRED_PLACEHOLDERS + if name not in names ] if missing: errors.append("模板缺少必需变量:%s" % "、".join("{%s}" % name for name in missing)) + has_new_target = "生成目标" in names + has_legacy_target = all( + name in names for name in ("套图名称", "补充描述") + ) + if not has_new_target and not has_legacy_target: + errors.append( + "模板缺少必需变量:{生成目标}(旧模板需同时包含{套图名称}和{补充描述})" + ) invalid_rule_lines = [] for line in text.splitlines(): @@ -228,8 +266,9 @@ def product_suite_prompt_context( item_text = "未绑定商品" reference_index = max(1, int(source_index or 1)) return { + "生成目标": category_description(category), "套图名称": str(category or ""), - "补充描述": category_description(category), + "补充描述": "," + category_description(category), "平台": normalized["platform"], "国家地区": normalized["country"], "输出语言": normalized["language"], @@ -255,9 +294,18 @@ def render_product_suite_prompt(template_text, context): name: str((context or {}).get(name, "")) for name in PRODUCT_SUITE_PLACEHOLDERS } + context = context or {} missing_context = [ - name for name in PRODUCT_SUITE_REQUIRED_PLACEHOLDERS if name not in (context or {}) + name + for name in PRODUCT_SUITE_COMMON_REQUIRED_PLACEHOLDERS + if name not in context ] + has_new_target = "生成目标" in context + has_legacy_target = all( + name in context for name in ("套图名称", "补充描述") + ) + if not has_new_target and not has_legacy_target: + missing_context.append("生成目标") if missing_context: raise ProductSuitePromptError( "提示词上下文缺少变量:%s" diff --git a/docs/04-architecture.md b/docs/04-architecture.md index 46e0ec6..0090763 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -436,9 +436,9 @@ data/images///__new. # AI 生成的新 - 进度:标题和图片两条进度分开显示;只生成标题时图片进度显示本轮未生成/0 张,并在运行日志写明本轮生成内容。 - 任一组件生成后 `stage=generated`;**不设逐条人工审核阶段**。若只有标题,③可选择只更新标题;若只有封面,③可选择只更新封面,②标题状态仍为待生成,后续补标题会保留已有封面且不重复生图。双击任务弹窗查看旧封面、新封面和历史候选图;T-577 后弹窗内「重置图片」只清当前任务 `new_cover_path` 并归档旧图,不启动单条 `GenerateWorker`,用户退出后用状态筛选「待生成」批量补生成封面。②「重置生成结果」提供标题/封面/全部的多选或当前筛选范围重置,默认不删除本地新封面文件;已生成且未提交线上的新标题可在②表格本地微调。 - 并发数、重试、分辨率、jpg 质量、模型/Key 均来自设置(`data/config.json` 的 `ai` 段;Key 存 `data/config/cmhub.json` 或 direct 兼容清单)。T-547 后标题并发和图片并发都限制为 1..5,失败重试次数限制为 0..10;旧 `config.json` 或手工配置的超限值会在加载/保存时夹紧。设置仍只展示一个「图片并发」设置;cmhub 模式下②运行日志显示“图片并发 X,cmhub实际生图并发 Y,下载并发 Y”。 -- 商品套图固定使用设置保存的 cmhub 生图 alias。T-651 后,常规新一轮生成先完成历史确认,再按最终 `build_job_specs()` 在后台读取或复用短期模型目录缓存,最后才显示数量和扣点确认;逐图主图开启时白底图仅使用第一张原图,其他分类按每张原图展开。只有当前生图别名存在唯一无条件 `points_cost` 时显示单张与总预估点数,总数严格按 planned job 数量计算;目录不可用或价格有条件时不显示数字。确认默认、Esc 或关闭均取消,计划在读取期间发生变化时不提交旧 specs;单图重试和恢复不进入该常规确认。预估不落库、不预扣,实际扣点仍以 job 的 cmhub 响应为准。T-637 后套图提示词事实来源分为:安装包只读默认 `app/default_prompts/product_suite/base.txt`、用户全局模板 `data/prompts/product_suite/base.txt`、`app/product_suite.py` 中的结构化上下文与只读规则常量。用户模板首次缺失或为空时,必须先按完整占位符契约校验内置模板,再通过原子写入初始化;已有用户模板不被升级静默覆盖。模板无效时在创建 project/job/worker 和调用 cmhub 前阻断新一轮生成,单张历史重试继续使用原 `image_studio_jobs.prompt` 快照。 +- 商品套图固定使用设置保存的 cmhub 生图 alias。T-651 后,常规新一轮生成先完成历史确认,再按最终 `build_job_specs()` 在后台读取或复用短期模型目录缓存,最后才显示数量和扣点确认;逐图主图开启时白底图仅使用第一张原图,其他分类按每张原图展开。固定分类顺序为白底图、场景图、模特场景图、细节说明图、卖点图;默认数量为1、2、0、0、2,新增分类不因升级自动产生任务。只有当前生图别名存在唯一无条件 `points_cost` 时显示单张与总预估点数,总数严格按 planned job 数量计算;目录不可用或价格有条件时不显示数字。确认默认、Esc 或关闭均取消,计划在读取期间发生变化时不提交旧 specs;单图重试和恢复不进入该常规确认。预估不落库、不预扣,实际扣点仍以 job 的 cmhub 响应为准。T-637 后套图提示词事实来源分为:安装包只读默认 `app/default_prompts/product_suite/base.txt`、用户全局模板 `data/prompts/product_suite/base.txt`、`app/product_suite.py` 中的分类目标、结构化上下文与只读规则常量。用户模板首次缺失或为空时,必须先按完整占位符契约校验内置模板,再通过原子写入初始化;已有用户模板不被升级静默覆盖。模板无效时在创建 project/job/worker 和调用 cmhub 前阻断新一轮生成,单张历史重试继续使用原 `image_studio_jobs.prompt` 快照。 - T-647 后商品套图中的「AI帮写」不是②标题生成的复用入口:它使用 `vision_alias` 调 `POST /api/v1/analyze/images`,把当前项目中1至8张可用本地原图按 `source_order` 在一次请求中发送;超过8张时明确只使用前8张,原图勾选状态不改变该输入集合。视觉提示词将这些图片定义为同一商品项目的多角度、细节、包装或场景证据集,要求产出一份商品级联合分析,不按“图1/图2”逐图罗列;结果按商品概述、可确认卖点、适用人群与场景、套图画面要求、待确认或避免编造的信息组织,并把可见差异明确为待确认项,不强行合并为事实。单图上限10MiB、总计上限32MiB,缺图、未下载、超限或未配置别名时不发请求且保留用户现有卖点。读取等待固定120秒、连接等待沿用设置;读超时或网络中断只提示“结果未确认”,不自动重发。成功只显示图片张数、扣点和余额,不显示路径、base64、接口URL、完整提示词或上游原始响应;取消只在请求前/返回后协作生效,不强杀网络线程。 -- `product_suite.render_product_suite_prompt()` 是弹窗预览和真实生成的唯一 renderer;`build_job_specs()` 建立本轮 specs 前只读取一次用户模板并冻结,每个 job 保存最终完整 prompt,运行中修改模板只影响下一轮。允许占位符为 `{套图名称}`、`{补充描述}`、平台/地区/语言/比例、可选商品ID/主参考图序号、参考图规则、商品卖点及四个只读规则;必需变量缺失、未知/未闭合花括号、只读规则变量未独占一行都视为无效。四个只读规则覆盖尺寸与长图(含禁止多宫格拼接)、政治标识、价格和尺码;商品主体一致性与禁止编造并入参考图规则。比例仍同时传入 `image_studio_generation.run_jobs(aspect_ratio=...)`,进入 cmhub 请求与输出资产元数据。 +- `product_suite.render_product_suite_prompt()` 是弹窗预览和真实生成的唯一 renderer;`build_job_specs()` 建立本轮 specs 前只读取一次用户模板并冻结,每个 job 保存最终完整 prompt,运行中修改模板只影响下一轮。新模板使用 `{生成目标}`,其值为内置分类固定目标描述或含实际名称的“生成自定义分类图片:分类名称。”;同时保留 `{套图名称}`、`{补充描述}` 及平台/地区/语言/比例、可选商品ID/主参考图序号、参考图规则、商品卖点和四个只读规则。旧模板必须同时包含 `{套图名称}` 与 `{补充描述}` 才能按兼容路径继续使用;必需变量缺失、未知/未闭合花括号、只读规则变量未独占一行都视为无效。四个只读规则覆盖尺寸与长图(含禁止多宫格拼接)、政治标识、价格和尺码;商品主体一致性与禁止编造并入参考图规则。比例仍同时传入 `image_studio_generation.run_jobs(aspect_ratio=...)`,进入 cmhub 请求与输出资产元数据。 - `image_studio_projects.suite_settings_json` 持久化套图设置,旧数据库由 `db.init_db()` 原位补列,默认 `{}`;`draft_prompt` 继续保存卖点文本。`image_studio_assets` 中有效商品原图最多16张,历史 missing 记录不占有效名额;手工原图不会因再次同步蝦皮 URL 被误标 missing。商品套图原图列表的批量勾选只保存在当前 `SuiteTaskState` 对应的界面上下文,不写库;批量移除由 `remove_original_assets_if_unused()` 一次校验项目归属、原图类型和 job/终选引用,并在单个 SQLite 事务中删除资产行、连续重排 `source_order`。服务不删除本地文件或蝦皮线上图片,任一资产校验失败时整批回滚。 - T-636 起,`image_studio_projects` 增加 `binding_state`(`draft` / `bound`)和稳定 `storage_key`。既有项目迁移为 `bound`,并以原 `item_id` 回填 `storage_key`;项目目录改用 `storage_key`,因此临时草稿绑定正式商品 ID 后不移动目录、不改写已有资产路径。草稿内部使用 `draft_` 作为仅数据库可见的非空 `item_id`,GUI 输入框始终保持空白,用户日志和 cmhub 提示词只使用“临时草稿”或“未绑定商品”,不得暴露该内部值。 - 在商品套图中,已选账号但未填写商品 ID 时允许输入卖点、导入、拖入或粘贴本地图片。非空卖点在现有防抖稳定后会创建一个可恢复临时草稿并保存到 `image_studio_projects.draft_prompt`;首次有效图片导入也会创建草稿。空白卖点、取消选择和全部导入失败不保留空草稿。只有卖点的草稿同样属于可恢复业务内容;卖点清空且没有资产/job 时仍可按空草稿规则清理。草稿可管理本地图片、AI 帮写、生成套图、查看历史和打开结果目录,但在创建 worker、启动 Chrome 或执行 CDP 前禁止「拉取蝦皮主图」。输入合法数字商品 ID 后,经确认原地绑定同一个 `project_id`;资产、job、selection、提示词、套图设置和 `storage_key` 均保持不变。若同账号目标 ID(含软删除项目)已存在则拒绝覆盖或合并。 @@ -455,7 +455,7 @@ data/images///__new. # AI 生成的新 - **标题提示词**:`data/title_prompt.txt` 是当前工作文本,「保存标题提示词」写入该文件;软件启动时加载该文件回显到输入框(缺失则空)。标题命名模板另存于 `data/prompts/title/*.txt`,左侧模板行提供下拉、新建、保存模板、另存为、重命名、删除;模板只负责被用户选中后载入编辑框,或把当前编辑框内容保存为命名模板,不改变生成读取路径,也不在启动时覆盖 `title_prompt.txt`。T-549 后标题提示词支持 `{旧标题}` 占位符:若提示词含 `{旧标题}`,生成前替换为该任务旧标题且不再自动追加旧标题块;若不含,则保持旧行为自动追加“旧标题:...”块。两种情况都会保留“请只返回新标题,不要解释。”输出约束。 - **封面提示词**:多模板。左侧模板行提供下拉(读 `data/prompts/cover/*.txt`)、新建、保存模板、模板操作(另存为/重命名/删除);重名校验、删除二次确认、删空给默认。 -- **商品套图提示词**:单一全局基础模板,用户文件为 `data/prompts/product_suite/base.txt`,内置恢复源为 `app/default_prompts/product_suite/base.txt`。商品套图中的「商品卖点与要求」标题行提供「提示词设置」;弹窗左侧编辑模板、右侧按白底图或其他当前分类只读预览最终请求,编辑使用 200ms 防抖。保存与「恢复默认」共用同一个 UTF-8 原子写入服务;恢复默认会先校验内置资源并经确认立即落盘。用户可移动只读规则占位符位置,但不能修改规则内容或删除必需变量;预览和新 job prompt 必须逐字一致。 +- **商品套图提示词**:单一全局基础模板,用户文件为 `data/prompts/product_suite/base.txt`,内置恢复源为 `app/default_prompts/product_suite/base.txt`。商品套图中的「商品卖点与要求」标题行提供「提示词设置」;弹窗左侧编辑模板、右侧按白底图或其他当前分类只读预览最终请求,默认模板首行是“生成目标:{生成目标}”,编辑使用 200ms 防抖。保存与「恢复默认」共用同一个 UTF-8 原子写入服务;恢复默认会先校验内置资源并经确认立即落盘。用户可移动只读规则占位符位置,但不能修改规则内容或删除必需变量;预览和新 job prompt 必须逐字一致。旧版用户模板中的 `{套图名称}` 与 `{补充描述}` 继续兼容,不被启动升级静默替换。 - **变量**:标题提示词本阶段只支持 `{旧标题}`,左侧按钮「插入旧标题」在标题提示词光标处插入 `{旧标题}`。封面提示词支持占位符 `{旧标题}`、`{新标题}`、`{商品id}`、`{店铺}`,生成前用该任务真实值替换(`render_prompt`)。「插入标题」= 在封面提示词光标处插入 `{新标题}`;「预览」= 用某条任务的值替换封面变量后展示,确认实际发送给 AI 的内容。 ### 6.3 应用更新(③ Tab) diff --git a/docs/api.md b/docs/api.md index 326f5d6..c0938f8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -422,7 +422,7 @@ export_generation_round(project_id, generation_round_key, parent_dir, path=None, 要点: - 项目唯一键为账号别名 + 商品 ID;图片文件默认在 `data/images/pool///` 下分 `originals/generated/exports`,删除生成图进入项目内 `.trash` 并可撤销。 -- `image_studio_projects.suite_settings_json` 保存平台/国家/语言/比例/逐图主图/分类数量;`draft_prompt` 保存商品卖点。有效原图上限16张,missing 历史不占名额。 +- `image_studio_projects.suite_settings_json` 保存平台/国家/语言/比例/逐图主图/分类数量;固定分类顺序为白底图、场景图、模特场景图、细节说明图、卖点图,默认数量为1、2、0、0、2;自定义分类名称和数量继续保存在同一分类映射中。新增分类默认数量为0,不会无提示增加任务或点数消耗。`draft_prompt` 保存商品卖点。有效原图上限16张,missing 历史不占名额。 - T-643 后 `image_studio_projects.current_generation_round_key` 记录项目主界面的当前生成轮次;`image_studio_jobs.generation_round_key` 与 `generation_slot_index` 记录整轮归属和稳定展示槽位。`promote_generation_round_if_success()` 只会在同项目、同轮已存在成功 job 时原子切换当前轮;`list_generation_rounds()` 返回轮次摘要,`list_generation_round_current_jobs()` 返回每个槽位最新尝试,`list_generation_round_attempts()` 返回完整尝试历史。旧 job 保持 NULL 并按“旧版历史记录”兼容,不猜测轮次边界。 - T-646 后 `list_global_generation_rounds()` 是商品套图跨项目历史的唯一查询入口:仅返回未软删除项目的轮次摘要,按创建时间倒序并支持账号/店铺、商品 ID、项目 ID、`limit`/`offset` 筛选;正常轮次只统计每个稳定槽位的最新有效 job,完整尝试数仅用于计算重试数,NULL 轮次保留“旧版历史记录”语义。`ProductSuiteGlobalHistoryDialog` 组合该查询和 `list_generation_round_current_jobs()`,默认读取30轮,显示受限缩略图并可按轮预览;窗口不写业务数据,也不改变当前轮次。`ProductSuiteHistoryExportWorker` 在后台调用 `export_generation_round()`,只复制该轮成功且存在的输出 asset 到用户选择目录中新建的安全子目录;同名目录追加稳定序号,不覆盖外部文件,部分文件失败只汇总中文结果,源 asset 保持不变。 - T-648 后 `get_successful_generation_history_summary()` 是商品套图常规新一轮生成前的只读判据:它只查询未软删除当前项目的最新有效成功 job,返回成功轮数、成功图片数和最近成功时间;正常轮次按稳定槽位取最新尝试,NULL 旧版轮次整体按一轮计数,只有失败/取消/进行中的 job 返回零摘要。该 API 不扫描文件目录、不创建 job/asset/轮次,也不改变当前轮;GUI 的“查看历史”仅调用全局历史窗口并临时启用当前商品筛选。 @@ -435,6 +435,7 @@ export_generation_round(project_id, generation_round_key, parent_dir, path=None, - 终选顺序由 `replace_selections()` 事务替换,主图/详情图同类别去重、跨类别可复用。 - 导出只写 JPEG 图片文件,透明图铺白底;商品目录已存在时只能覆盖受管命名文件或新建带时间目录,不合并、不递归清空。 - 所有真实网络/CDP/图片生成/下载/转码由 GUI worker 调用;Widget slot 不直接执行长耗时操作。 +- 商品套图默认提示词首行使用 `{生成目标}`;五个内置分类使用固定目标描述,自定义分类使用“生成自定义分类图片:分类名称。”并注入实际分类名称。旧模板同时包含 `{套图名称}` 和 `{补充描述}` 时继续兼容,预览和 `build_job_specs()` 使用同一 renderer,job 保存最终提示词快照。 ## gui 模块(`app/gui/` 包,已建,PySide6) diff --git a/docs/routes.md b/docs/routes.md index 570184f..9f5ee93 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -191,7 +191,7 @@ │ 平台 站点 语言 比例(同行) │ [结果卡][结果卡][失败卡·重试] │ │ 每张上传图分别作为主图生成 │ │ │ 商品卖点与要求 [AI帮写/取消] │ │ -│ 白底图/场景图/卖点图/自定义类 │ 进度条 套图X/Y(秒)·失败N │ +│ 白底图/场景图/模特图/细节图/卖点图 │ 进度条 套图X/Y(秒)·失败N │ │ [生成套图(N) ⇄ 停止生成] │ │ └─────────────────────────────┴──────────────────────────────────┘ ``` @@ -203,7 +203,8 @@ - 商品原图最多16张。前6个槽位固定显示主图与参考1~5;列表关闭内部滚动条,按可用宽度换行并自然向下展开,由左侧配置区统一滚动。支持文件选择、外部拖入、剪贴板粘贴和列表内排序。历史失效远程图不占有效名额;第1张是主参考图。 - 每张真实原图左上角提供独立勾选框,标题行显示「已选 N 张 / 全选 / 反选」;添加占位图不参与选择。勾选只在当前任务界面内临时保留,普通刷新和排序按资产 ID 保留,切换任务或删除成功后清空。右键或 Delete 可批量移除,确认框说明准确数量、主图变化及非破坏性边界;生成中或勾选项仍在下载时整批阻断。移除只删除当前项目的本地资产记录,不删除本地源文件或蝦皮线上图片。 - 「拉取蝦皮主图」复用只读 CDP,读取 URL 后由最多2个下载 worker 后台落盘;不改标题/封面、不拖拽、不点击更新。拉取、下载期间其余界面和其他任务仍可操作。 -- 套图只有一个图片类型,不再展示详情图、终选盘或模板 CRUD。默认分类为白底图1、场景图2、卖点图2;自定义分类名称非空、无空格、最多10字且不可重名。逐图主图开启后,白底图只生成一次,其余分类按每张有效原图展开。 +- 套图按分类配置生成图片,固定分类顺序为白底图、场景图、模特场景图、细节说明图、卖点图,之后是自定义分类;默认数量为白底图1、场景图2、模特场景图0、细节说明图0、卖点图2。新增分类默认数量为0,用户未主动配置时不增加生成任务或点数消耗。自定义分类名称非空、无空格、最多10字且不可重名。逐图主图开启后,白底图只生成一次,其余分类按每张有效原图展开。 +- 套图提示词设置弹窗默认模板首行使用“生成目标:{生成目标}”。五个内置分类使用统一固定目标描述;自定义分类使用“生成自定义分类图片:分类名称。”,实际分类名称必须进入最终提示词。旧模板若仍使用 `{套图名称}` 与 `{补充描述}` 会继续读取和渲染,不静默覆盖用户文件;新模板与旧模板都由同一个 renderer 提供预览和 job prompt。 - 平台、国家地区、语言和比例以四个带独立标签的同行下拉展示,选项只显示真实值;四项都写进每个 job 的完整提示词,比例还透传到 cmhub 生图请求,不是装饰字段。已有项目保存自己的完整设置;未绑定商品的新任务在重启后采用 `config.json` 的最近四项选择。生成仍走 `image_studio_generation.run_jobs()` 的 submit → poll → download 管线。 - 平台、站点、语言和比例四个生成设置下拉仅在闭合时拦截自身滚轮改值,并把滚动交给 `suiteConfigScroll`;展开时滚轮只浏览候选列表,不静默提交当前值。鼠标点击、方向键、回车和 `Alt+↓` 保持原有显式选择语义;账号、历史筛选和设置页等其它下拉不受影响。 - 生成按钮按当前总数显示并在运行时切换为「停止生成」;确认停止后显示「正在停止...」,重复点击不再弹确认框。每轮生成用独立运行标识隔离旧信号,本轮全部 job 终态或线程结束时都会统一恢复按钮;最终 worker 信号缺失时由数据库终态看门狗兜底,不要求用户重启。停止会取消未开始任务,已提交任务停止本地等待并保留后续继续查询语义;客户端不承诺取消服务端任务或退回点数。T-643 后项目持久化当前生成轮次:常规新轮至少成功一张才替换主结果区,全部失败/取消保留上一当前轮;主结果按稳定槽位显示同轮最新 job,单张重试留在原槽位。旧版无轮次 job 临时显示为“旧版历史记录”,不按时间或图片数量猜测归属。成功图可预览、复制路径、打开目录、重新生成、移入项目废纸篓并撤销,失败卡显示脱敏中文摘要与重试入口。 diff --git a/docs/tasks/T-654.md b/docs/tasks/T-654.md index 251dc57..6072c78 100644 --- a/docs/tasks/T-654.md +++ b/docs/tasks/T-654.md @@ -3,7 +3,7 @@ id: T-654 title: 扩展套图分类并统一生成目标提示词 phase: 7 deps: [T-637] -status: TODO +status: DONE created: 2026-07-17 --- @@ -114,3 +114,5 @@ created: 2026-07-17 ## 执行记录 - 2026-07-17:根据商品套图新增分类、生成目标提示词和自定义分类名缺失问题创建任务,待实现。 +- 2026-07-17:完成固定分类扩展、默认分类数量兼容、生成目标提示词、旧模板兼容和自定义分类名称注入;同步更新架构、路由与 API 文档。 +- 2026-07-17:验证通过 `python -m ruff check app tests main.py`、`python -m compileall app main.py`、`python -m unittest discover -s tests`,共 579 个测试通过。 diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 6baf8f1..fce4eb9 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -33,7 +33,8 @@ class PackagingTests(unittest.TestCase): self.assertIn("COLLECT(", spec) self.assertIn('name="cmshopee"', normalized) suite_prompt = self.read_text("app/default_prompts/product_suite/base.txt") - self.assertIn("{套图名称}", suite_prompt) + self.assertIn("{生成目标}", suite_prompt) + self.assertNotIn("套图名称:", suite_prompt) self.assertIn("{尺寸与长图规则}", suite_prompt) def test_build_script_blocks_user_data_in_release_output(self): diff --git a/tests/test_product_suite.py b/tests/test_product_suite.py index 016a2fd..19b4bdf 100644 --- a/tests/test_product_suite.py +++ b/tests/test_product_suite.py @@ -12,7 +12,10 @@ class ProductSuiteTests(unittest.TestCase): def test_defaults_and_per_image_total(self): settings = product_suite.default_suite_settings() - self.assertEqual(["白底图", "场景图", "卖点图"], product_suite.category_order(settings)) + self.assertEqual( + ["白底图", "场景图", "模特场景图", "细节说明图", "卖点图"], + product_suite.category_order(settings), + ) self.assertEqual(5, product_suite.suite_total_count(settings, 3)) settings["per_image_primary"] = True @@ -34,7 +37,10 @@ class ProductSuiteTests(unittest.TestCase): ) self.assertEqual("3:4", settings["ratio"]) - self.assertEqual(["白底图", "场景图", "卖点图", "尺寸图"], product_suite.category_order(settings)) + self.assertEqual( + ["白底图", "场景图", "模特场景图", "细节说明图", "卖点图", "尺寸图"], + product_suite.category_order(settings), + ) self.assertEqual(5, product_suite.suite_total_count(settings, 1)) def test_recent_settings_only_keep_allowed_dropdown_values(self): @@ -108,8 +114,14 @@ class ProductSuiteTests(unittest.TestCase): spec["prompt"].splitlines()[-1].startswith("本次生成比例:4:3") ) - self.assertIn("套图名称:白底图,白底主图", specs[0]["prompt"]) - self.assertIn("套图名称:场景图,生活化场景", specs[1]["prompt"]) + self.assertIn( + "生成目标:生成 Shopee 台灣商品白底主圖,純白背景,商品清晰置中,不添加多餘文字。", + specs[0]["prompt"], + ) + self.assertIn( + "生成目标:生成生活化使用場景圖,展示商品在真實情境中的用途與氛圍,畫面自然可信。", + specs[1]["prompt"], + ) white_prompt = specs[0]["prompt"] self.assertLess(white_prompt.index("重要尺寸要求"), white_prompt.index("重要禁用内容")) self.assertLess(white_prompt.index("重要禁用内容"), white_prompt.index("价格信息规则")) @@ -153,9 +165,33 @@ class ProductSuiteTests(unittest.TestCase): "draft_hidden", ) rendered = product_suite.render_product_suite_prompt(default_text, context) - self.assertIn("套图名称:尺寸图", rendered) + self.assertIn("生成目标:生成自定义分类图片:尺寸图。", rendered) self.assertNotIn("draft_", rendered) - self.assertNotIn("白底主图", rendered) + self.assertNotIn("套图名称:", rendered) + + legacy_template = default_text.replace( + "生成目标:{生成目标}", + "套图名称:{套图名称}{补充描述}", + ) + self.assertEqual([], product_suite.product_suite_prompt_errors(legacy_template)) + legacy_rendered = product_suite.render_product_suite_prompt(legacy_template, context) + self.assertIn("套图名称:尺寸图,生成自定义分类图片:尺寸图。", legacy_rendered) + + def test_category_descriptions_are_fixed_and_custom_name_is_explicit(self): + expected = { + "白底图": "生成 Shopee 台灣商品白底主圖,純白背景,商品清晰置中,不添加多餘文字。", + "场景图": "生成生活化使用場景圖,展示商品在真實情境中的用途與氛圍,畫面自然可信。", + "模特场景图": "生成模特或手持使用情境圖,畫面自然可信,商品為主角。", + "细节说明图": "生成商品細節特寫說明圖,突出材質、做工、接口、紋理或關鍵細節。", + "卖点图": "生成賣點詳解圖,使用繁體中文短文案呈現核心優勢,版面乾淨。", + } + for category, description in expected.items(): + self.assertEqual(description, product_suite.category_description(category)) + + self.assertEqual( + "生成自定义分类图片:结构展示。", + product_suite.category_description("结构展示"), + ) def test_product_suite_optional_item_and_reference_variables(self): template = prompts.load_default_product_suite_prompt() + ( diff --git a/tests/test_product_suite_gui.py b/tests/test_product_suite_gui.py index 2ece1b9..e3f76a8 100644 --- a/tests/test_product_suite_gui.py +++ b/tests/test_product_suite_gui.py @@ -330,7 +330,10 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase): self.assertEqual("白底图", dialog.category_combo.currentData()) self.assertTrue(dialog.preview_edit.isReadOnly()) - self.assertIn("套图名称:白底图,白底主图", dialog.preview_edit.toPlainText()) + self.assertIn( + "生成目标:生成 Shopee 台灣商品白底主圖,純白背景,商品清晰置中,不添加多餘文字。", + dialog.preview_edit.toPlainText(), + ) self.assertIn("40小时续航", dialog.preview_edit.toPlainText()) sizes = dialog.splitter.sizes() self.assertLessEqual(abs(sizes[0] - sizes[1]), 12) @@ -462,6 +465,8 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase): expected = { "白底图": (1, "白底主图,多角度呈现商品细节"), "场景图": (2, "生活化场景展示商品使用方式"), + "模特场景图": (0, "模特或手持场景展示商品真实使用"), + "细节说明图": (0, "突出材质、做工和关键细节"), "卖点图": (2, "突出核心卖点和差异化优势"), } self.assertEqual(list(expected), list(tab.category_rows))