feat(product-suite): add category targets and prompt compatibility

This commit is contained in:
chengma
2026-07-17 15:35:28 +08:00
parent 7426c181a7
commit f6a1ad9ffc
9 changed files with 120 additions and 26 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
套图名称:{套图名称}{补充描述}
生成目标:{生成目标}
平台:{平台};国家地区:{国家地区};输出语言:{输出语言}。
{尺寸与长图规则}
{禁用内容规则}
+58 -10
View File
@@ -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"
+3 -3
View File
@@ -436,9 +436,9 @@ data/images/<batch_id>/<slug>/<task_id>_<item_id>_new.<ext> # 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_<uuid>` 作为仅数据库可见的非空 `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/<batch_id>/<slug>/<task_id>_<item_id>_new.<ext> # 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)
+2 -1
View File
@@ -422,7 +422,7 @@ export_generation_round(project_id, generation_round_key, parent_dir, path=None,
要点:
- 项目唯一键为账号别名 + 商品 ID;图片文件默认在 `data/images/pool/<slug>/<item_id>/` 下分 `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)
+3 -2
View File
@@ -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 临时显示为“旧版历史记录”,不按时间或图片数量猜测归属。成功图可预览、复制路径、打开目录、重新生成、移入项目废纸篓并撤销,失败卡显示脱敏中文摘要与重试入口。
+3 -1
View File
@@ -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 个测试通过。
+2 -1
View File
@@ -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):
+42 -6
View File
@@ -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() + (
+6 -1
View File
@@ -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))