diff --git a/app/gui/tabs/product_suite.py b/app/gui/tabs/product_suite.py index 148036c..39134a8 100644 --- a/app/gui/tabs/product_suite.py +++ b/app/gui/tabs/product_suite.py @@ -2209,9 +2209,24 @@ class ProductSuiteTab(QWidget): grid.addWidget(combo, 1, column) grid.setColumnStretch(column, 1) layout.addLayout(grid) + per_image_row = QHBoxLayout() + per_image_row.setContentsMargins(0, 0, 0, 0) + per_image_row.setSpacing(8) self.per_image_checkbox = QCheckBox("每张上传图分别作为主图生成") self.per_image_checkbox.setObjectName("suitePerImageCheckbox") - layout.addWidget(self.per_image_checkbox) + per_image_row.addWidget(self.per_image_checkbox) + self.per_image_helper_label = QLabel( + "多款式或多SKU图请勾选;同一商品多角度图不勾选,其余图会作为参考图一同提交。" + ) + self.per_image_helper_label.setObjectName("suitePerImageHelperLabel") + self.per_image_helper_label.setWordWrap(True) + self.per_image_helper_label.setSizePolicy( + QSizePolicy.Expanding, + QSizePolicy.Preferred, + ) + self.per_image_helper_label.setStyleSheet("color: #57606a; font-size: 12px;") + per_image_row.addWidget(self.per_image_helper_label, 1) + layout.addLayout(per_image_row) return frame def _value_combo(self, object_name, label, values): @@ -4852,6 +4867,15 @@ class ProductSuiteTab(QWidget): if counts.get(name, 0) > 0 ] per_image_primary = bool(state.settings.get("per_image_primary")) + reference_count = 0 + if not per_image_primary: + reference_count = max( + [len(spec.get("reference_asset_ids") or []) for spec in specs] or [0] + ) + ignored_reference_count = max( + 0, + len(local_assets) - 1 - reference_count, + ) lines = [ "店铺:%s" % self._account_context_label(state), "商品ID:%s" % (state.item_id or "未绑定商品"), @@ -4861,10 +4885,22 @@ class ProductSuiteTab(QWidget): ( "说明:白底图只使用第一张原图;场景图、卖点图和自定义分类会按每张原图分别生成。" if per_image_primary - else "说明:所有分类都只使用第一张可用原图生成。" + else ( + "说明:所有分类都使用第1张可用原图作为主图,另有%d张参考图会随每次请求一同提交。" + % reference_count + if reference_count + else "说明:所有分类都只使用第一张可用原图生成。" + ) ), "", ] + if not per_image_primary and ignored_reference_count: + lines.extend( + [ + "参考图仅取前7张,其余%d张不参与本轮生成。" % ignored_reference_count, + "", + ] + ) lines.extend(category_lines) lines.extend( [ diff --git a/docs/04-architecture.md b/docs/04-architecture.md index 2bcec3e..2ba61ed 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -439,6 +439,7 @@ data/images///__new. # AI 生成的新 - 商品套图固定使用设置保存的 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-658a 后商品套图异步生图提交统一使用 `images` 数组,单图也使用单元素 `{"image_base64": ...}`,不再提交顶层 `image_base64`。提交层最多传 8 张本地图片,单图原文件上限10MiB、编码后总输入上限32MiB;超限在请求前以中文错误阻断,后续多图参考的资产快照和提示词语义由 T-658b/T-658c 负责。 - T-658b 后 `image_studio_jobs.reference_asset_ids` 以有序 JSON 图片 ID 列表冻结每个商品套图 job 的参考图;新一轮未勾选逐图主图时,第1张为主图、后续最多7张写入快照,勾选时写空数组。历史 `NULL` 行继续按单图任务处理。恢复或重试只读 job 快照,不回读当前原图列表;参考图资产或本地文件缺失时提交前失败,不静默减少提交数量。 +- T-658d 后逐图主图 checkbox 右侧显示可换行中文 helper,说明多SKU应逐图生成、同商品多角度应作为参考图一同提交。常规生成确认框从最终 specs 计算主图/参考图数量;未开启逐图主图且可用原图超过8张时,明确第1张主图加前7张参考图的上限和忽略数量。确认总点数仍只按 job 请求数计算,不因单请求图片数量重复计算。 - 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/主参考图序号、参考图规则、商品卖点和四个只读规则。旧模板必须同时包含 `{套图名称}` 与 `{补充描述}` 才能按兼容路径继续使用;必需变量缺失、未知/未闭合花括号、只读规则变量未独占一行都视为无效。`product_suite_reference_rule()` 是 `{参考图规则}` 的唯一事实来源,变量值都以 `参考图规则:` 开头:逐图主图开启或实际参考图数为0时为单图主参考规则;未开启且快照有1至7张参考图时为“第1张主商品图、第2至N张仅作参考”的多图规则,N 必须等于实际提交总图片数。提示词弹窗按当前可用原图数计算,job 按冻结的 `reference_asset_ids` 渲染,两者共用 renderer。默认模板独占一行,不额外重复标签或序号。四个只读规则覆盖尺寸与长图(含禁止多宫格拼接)、政治标识、价格和尺码;商品主体一致性与禁止编造并入参考图规则。比例仍同时传入 `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`。服务不删除本地文件或蝦皮线上图片,任一资产校验失败时整批回滚。 diff --git a/docs/api.md b/docs/api.md index 7147721..6a4bfc9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -439,6 +439,7 @@ export_generation_round(project_id, generation_round_key, parent_dir, path=None, - 所有真实网络/CDP/图片生成/下载/转码由 GUI worker 调用;Widget slot 不直接执行长耗时操作。 - 商品套图默认提示词首行使用 `{套图名称}` 与 `{生成目标}`,结构为“生成目标:{套图名称},{生成目标}”;五个内置分类使用固定目标描述,自定义分类使用“生成自定义分类图片:分类名称。”并注入实际分类名称。旧模板同时包含 `{套图名称}` 和 `{补充描述}` 时继续兼容,预览和 `build_job_specs()` 使用同一 renderer,job 保存最终提示词快照。 - `product_suite_reference_rule(settings, reference_asset_count)` 是 `{参考图规则}` 的唯一文案来源:逐图主图开启或实际参考图数为0时返回单图规则;未开启且参考图数为1至7时返回“第1张为主商品图、第2至N张仅作参考”的多图规则。`product_suite_prompt_context()`、提示词弹窗预览和 `build_job_specs()` 都使用该 helper;默认模板独占该变量行,不额外重复标签和序号。 +- `ProductSuiteTab._generation_confirmation_message()` 从最终 specs 的 `reference_asset_ids` 计算本次主图与参考图说明;未开启逐图主图且上传超过8张时,必须在确认框显示“参考图仅取前7张”和忽略数量。该说明不改变 `len(specs)`、点数估算或 cmhub 并发计划。 ## gui 模块(`app/gui/` 包,已建,PySide6) diff --git a/docs/routes.md b/docs/routes.md index c1e74fb..fb98566 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -206,6 +206,7 @@ - 套图按分类配置生成图片,固定分类顺序为白底图、场景图、模特场景图、细节说明图、卖点图,之后是自定义分类;默认数量为白底图1、场景图2、模特场景图0、细节说明图0、卖点图2。新增分类默认数量为0,用户未主动配置时不增加生成任务或点数消耗。自定义分类名称非空、无空格、最多10字且不可重名。逐图主图开启后,白底图只生成一次,其余分类按每张有效原图展开。 - 套图提示词设置弹窗默认模板首行使用“生成目标:{套图名称},{生成目标}”。五个内置分类使用统一固定目标描述;自定义分类使用“生成自定义分类图片:分类名称。”,实际分类名称必须进入最终提示词。旧模板若仍使用 `{套图名称}` 与 `{补充描述}` 会继续读取和渲染,不静默覆盖用户文件;新模板与旧模板都由同一个 renderer 提供预览和 job prompt。 - `{参考图规则}` 由实际提交图片数决定:逐图主图开启、或当前只有1张可用图时,使用“参考图规则:当前上传图片是本任务唯一主参考图;保持商品主体、款式、颜色和关键细节准确;不编造用户与参考图均未提供的信息。”;未开启且有2至8张可用图时,第1张为主商品图,第2至N张仅作风格、构图、场景或排版参考,N 为本次实际提交总数。提示词弹窗按当前可用原图数预览,`build_job_specs()` 按冻结的参考图快照渲染;默认模板中的 `{参考图规则}` 独占一行,不额外重复标签或序号。 +- 「每张上传图分别作为主图生成」右侧固定显示可换行 helper:“多款式或多SKU图请勾选;同一商品多角度图不勾选,其余图会作为参考图一同提交。”确认生成框必须说明当前主图/参考图模式和实际参考图数;未开启且上传超过8张时,明确仅提交第1张主图加前7张参考图,以及未参与本轮的剩余数量。 - 平台、国家地区、语言和比例以四个带独立标签的同行下拉展示,选项只显示真实值;四项都写进每个 job 的完整提示词,比例还透传到 cmhub 生图请求,不是装饰字段。已有项目保存自己的完整设置;未绑定商品的新任务在重启后采用 `config.json` 的最近四项选择。生成仍走 `image_studio_generation.run_jobs()` 的 submit → poll → download 管线。 - 平台、站点、语言和比例四个生成设置下拉仅在闭合时拦截自身滚轮改值,并把滚动交给 `suiteConfigScroll`;展开时滚轮只浏览候选列表,不静默提交当前值。鼠标点击、方向键、回车和 `Alt+↓` 保持原有显式选择语义;账号、历史筛选和设置页等其它下拉不受影响。 - 生成按钮按当前总数显示并在运行时切换为「停止生成」;确认停止后显示「正在停止...」,重复点击不再弹确认框。每轮生成用独立运行标识隔离旧信号,本轮全部 job 终态或线程结束时都会统一恢复按钮;最终 worker 信号缺失时由数据库终态看门狗兜底,不要求用户重启。停止会取消未开始任务,已提交任务停止本地等待并保留后续继续查询语义;客户端不承诺取消服务端任务或退回点数。T-643 后项目持久化当前生成轮次:常规新轮至少成功一张才替换主结果区,全部失败/取消保留上一当前轮;主结果按稳定槽位显示同轮最新 job,单张重试留在原槽位。旧版无轮次 job 临时显示为“旧版历史记录”,不按时间或图片数量猜测归属。成功图可预览、复制路径、打开目录、重新生成、移入项目废纸篓并撤销,失败卡显示脱敏中文摘要与重试入口。 @@ -213,7 +214,7 @@ - T-646 后「历史生成」打开全局非模态「套图历史生成记录」窗口,默认显示所有未删除商品项目最近创建的生成轮次,主结果区不切换。T-649 将店铺筛选固定为首项「全部店铺」的下拉:当前账号显示「账号名(账号别名)」,已删除但仍有历史项目的账号显示「历史店铺:别名(账号已删除)」,选择值使用 `account_alias` 精确查询;商品 ID 关键字和“仅当前商品”可与其叠加,默认不限制当前任务。每一行就是一次正常生成轮次,单张失败重试仍归入原行。行内固定显示时间、店铺/账号、商品 ID、成功/失败/停止/重试统计、最多5张缩略图及余量 `+N`,当前轮标记“当前”,NULL 轮次标记“旧版历史记录”,临时项目显示“临时草稿”。双击缩略图或整行从对应图片打开该轮所有可用图的自适应原尺寸浏览;“导出本轮”后台复制该轮成功且本地存在的图片到用户选择目录下的新安全子目录,不覆盖或修改内部图片。旧版记录、全失败轮和本地文件缺失项保留中文说明;不提供批量导出、删除、重试、切换当前轮或再次生成。重复点击复用同一窗口;关闭任务页不关闭全局窗口,应用退出时正常释放。 - AI帮写和生图按任务独立运行。AI帮写只使用设置的「图片理解别名」调用图片理解能力,不走②标题生成;按商品原图 `source_order` 取1至8张已下载的本地图片,在一次请求中作为同商品的多角度/细节/包装/场景证据集联合理解,超过8张时状态提示只使用前8张,原图勾选不改变输入图片。返回一份可直接编辑的商品级卖点与套图画面要求,按商品概述、可确认卖点、人群与场景、套图画面要求、待确认或避免编造的信息组织,不按图1、图2逐图说明;图片有可见差异时明确为待确认项。单图超过10MiB、总计超过32MiB、没有可用本地图、别名未配置或服务异常时不改现有卖点;图片理解读超时或网络中断提示“结果未确认,请先查看点数余额或稍后重试”,不自动重发。成功状态显示理解图片张数、扣点和余额;AI帮写期间若用户改过卖点,返回后必须确认才覆盖;全部用户可见错误隐藏图片路径、URL、接口路径、base64、完整提示词和敏感信息。 - AI帮写提交图片理解前先显示「开始AI帮写」确认框:按 `source_order` 说明会理解当前商品前1至8张可用原图并生成商品卖点与要求。模型目录只走后台读取或进程内短期缓存;仅当前图片理解别名有唯一无条件价格时显示「预计扣点:X 点」,否则明确实际以网关返回为准。确认框默认、Esc 和关闭均取消,不提交图片;开始后可取消本地等待,但已提交网关的请求仍可能产生扣点。预估不写入业务数据,完成后仍只显示接口返回的实际扣点和余额。 -- 常规「生成套图」保留“已有成功历史”优先确认,选择继续后才后台读取或复用同一模型目录缓存,并显示正式生成确认。确认严格按最终 planned `specs` 展示各分类实际张数、总张数和比例;逐图主图开启时明确白底图只用第一张原图,其他分类按每张原图生成,关闭时所有分类只用第一张。仅唯一无条件的生图价格显示预计单张和总扣点,总价只按 `len(specs)` 计算;价格未知时不显示数字。默认、Esc、关闭、切换任务、取消读取或计划变化均不创建生图 job;单图失败重试和恢复未完成任务不增加这一层批量确认。 +- 常规「生成套图」保留“已有成功历史”优先确认,选择继续后才后台读取或复用同一模型目录缓存,并显示正式生成确认。确认严格按最终 planned `specs` 展示各分类实际张数、总张数和比例;逐图主图开启时明确白底图只用第一张原图,其他分类按每张原图生成;关闭时所有分类使用第1张主图及同一批冻结参考图。仅唯一无条件的生图价格显示预计单张和总扣点,总价只按 `len(specs)` 计算;价格未知时不显示数字。默认、Esc、关闭、切换任务、取消读取或计划变化均不创建生图 job;单图失败重试和恢复未完成任务不增加这一层批量确认。 - 商品套图只管理本地图片资产,不自动上传或修改蝦皮;③线上更新边界不受影响。旧 `ImageStudioTab` 留作代码兼容但不再作为主窗口入口。 ## 流程导航 diff --git a/docs/tasks/T-658d.md b/docs/tasks/T-658d.md index df1d87f..c95c73f 100644 --- a/docs/tasks/T-658d.md +++ b/docs/tasks/T-658d.md @@ -1,7 +1,7 @@ --- id: T-658d title: 商品套图多图参考 GUI 与全流程回归 -status: TODO +status: DONE phase: 7 deps: [T-658a, T-658b, T-658c] created: 2026-07-17 @@ -52,4 +52,7 @@ git diff --check ## 执行记录 -- 待实现。 +- 2026-07-17:在「每张上传图分别作为主图生成」右侧新增可换行中文 helper,说明多SKU逐图生成与同商品多角度参考图两种使用场景;在 1180x760 下覆盖布局断言。 +- 2026-07-17:生成确认框根据最终 specs 展示主图/参考图提交语义;未开启逐图主图且上传超过8张时,明确“前7张参考图”和忽略数量,点数估算仍只按 job 请求数。 +- 2026-07-17:补充 helper、9张原图上限提示及确认框文案的 GUI 测试;同步路由、架构和 API 文档。 +- 验证通过:`py -3.10 -m unittest tests.test_product_suite_gui`(58项)、`py -3.10 -m unittest discover -s tests`(587项)、`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check`。离屏 Qt 的字体/插件警告不影响测试结果。 diff --git a/tests/test_product_suite_gui.py b/tests/test_product_suite_gui.py index a21bd62..62d5322 100644 --- a/tests/test_product_suite_gui.py +++ b/tests/test_product_suite_gui.py @@ -3285,11 +3285,50 @@ class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase): ) self.assertIn("逐图主图:未开启", message) - self.assertIn("所有分类都只使用第一张可用原图生成", message) + self.assertIn("第1张可用原图作为主图,另有1张参考图会随每次请求一同提交", message) self.assertIn("本次生成总数:5张", message) self.assert_removed(temp_dir) + def test_per_image_helper_and_confirmation_explain_reference_limit(self): + with self.make_temp_dir() as temp_dir: + config = self._config(temp_dir) + project, assets = self._create_project_with_assets(temp_dir, config, 9) + tab = ProductSuiteTab(config=config, db_path=config["db_path"]) + self.addCleanup(tab.close) + tab.resize(1180, 760) + tab.show() + self.app.processEvents() + state = tab._displayed_state + state.account_alias = "alias-a" + state.item_id = project.item_id + state.project_id = project.id + state.project_binding_state = project.binding_state + state.settings["per_image_primary"] = False + tab._load_state(state) + self.app.processEvents() + specs = product_suite.build_job_specs( + assets, + "商品卖点", + state.settings, + state.item_id, + template_text=prompts.load_product_suite_prompt(tab.product_suite_prompt_path), + ) + + self.assertTrue(tab.per_image_helper_label.wordWrap()) + self.assertGreater(tab.per_image_helper_label.width(), 0) + self.assertIn("多SKU图请勾选", tab.per_image_helper_label.text()) + self.assertGreater( + tab.per_image_helper_label.geometry().left(), + tab.per_image_checkbox.geometry().left(), + ) + + message = tab._generation_confirmation_message(state, assets, specs) + self.assertIn("另有7张参考图会随每次请求一同提交", message) + self.assertIn("参考图仅取前7张,其余1张不参与本轮生成", message) + + self.assert_removed(temp_dir) + def test_generation_confirmation_rejects_plan_changed_while_reading_price(self): with self.make_temp_dir() as temp_dir: config = self._config(temp_dir)