catch-22:「生成标题」用 read_all_rows 取行,而它要求 标题(A)+原始图片路径(C) 都非空;但标题生成的目的就是填 A。印花导出表若 A 空(被清空/早期导出), 所有行被跳过 → 误报「该表没有可处理的行」。实测 output/20260623_094529.xlsx: A 全空、C 是印花子目录。 - excel_service.read_all_rows 增 require_title=True 开关:False 时只在 原始图片路径(C) 空时跳过、允许 标题(A) 空;默认 True 行为不变(预览/ 无待处理统计/图片生成 load_outfit_tasks 仍要求 A) - ai_outfit_panel._TitleWorker.run 改调 read_all_rows(..., require_title=False) 测试:A 空+C 有的行 require_title=False 收录、默认跳过;C 空两种都跳过。 全套 py37 通过(test_config_service 的 packaging 模板失败属并行 §19.13,无关)。 离屏冒烟:真表 20260623_094529.xlsx 默认 0 行→require_title=False 加载 6 行; worker 把 6 条标题按序回填 A、C(印花目录)不动。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
46 KiB
AI 穿搭模块设计
1. 文档定位
本文档定义 cmbot「2 AI 穿搭」页签的设计。它是一项独立能力,按 docs/09 第 262 行「新能力单独成文」要求成文,不混入印花合成相关文档。
参考来源:同事旧项目「标题生成产品图工具」的分析见 docs/旧ai穿搭项目.md。本模块把该项目验证过的「本地图 + 提示词 → 调 AI 图像 API → 写回 Excel」机制,移植进 cmbot 的分层架构(app / core / services)。
一句话目标:以 Excel 为数据源,按行读取「衣服图 + 标题/货号」,套提示词调 AI 图像 API 生成「人物穿着该衣服」的效果图,保存 JPG 并把结果路径写回 Excel。
2. 目标与非目标
2.1 目标
- Excel 批量驱动:逐行读取标题、货号、衣服图路径,生成后把结果写回 Excel(与旧项目列约定一致)。
- AI 生成:衣服图作为图像输入 + 提示词(可插标题/货号占位符)→ 生成人物上身/穿搭效果图,保留款式、版型、颜色、印花。
- 复用 cmbot 现有设施:配置(
config_service/~/.cmbot)、日志、输出目录、后台线程模式、打包/更新,不重造。 - 稳态批量:并发、限速、阶梯重试、温和停止、实时日志、进度与失败清单。
- 分层可测:AI 调用与 Excel 适配放
services,无 GUI 逻辑放core,关键逻辑有单测。
2.2 非目标
- 不改变数据源形态:输入固定为 Excel(不在本期改为文件夹/队列;但核心对内部模型工作,将来加别的输入源不必改核心)。
- 不做模特/姿态/背景的精细可视化编辑(由提示词控制,不做画布微调)。
- 不内置 AI 模型,依赖外部中转 API。
- 不照搬旧项目的 bootstrap / offline_runtime / .bat(cmbot 已有自己的打包与更新体系)。
3. 总体架构
把 Excel 当作 I/O 适配边界,中间用 cmbot 内部数据模型,核心 AI 逻辑与数据源解耦:
Excel(数据源,固定)
│ services/excel_service:读行 → List[OutfitTask](转成内部模型)
▼
core/ai_outfit + services/ai_image_service:
衣服图 + 提示词 → 调 AI 图像 API → 人物上身图(JPG)
│ 复用:并发/日志/配置/进度/~/.cmbot
▼
services/excel_service:把结果写回 Excel(D 新图路径 / E 状态 / F 原因)
分层落点:
core/models.py:新增OutfitTask/OutfitResult(纯数据,无 PySide6)。services/excel_service.py:Excel 读取、写回、占用检测、跳过/重试判断。services/ai_image_service.py:移植旧项目ImageApiClient(多模型、多请求格式、传图、取图、重试、超时)。core/ai_outfit.py(或 service 内):单行生成的纯逻辑编排(提示词渲染、调用、保存、产出结果),可单测。app/widgets/ai_outfit_panel.py+ 主窗口「2 AI 穿搭」页签:界面与后台线程(QThread+Worker(QObject)+ 信号)。
4. Excel 适配(数据源)
沿用旧项目列约定(固定,先不做可配置):
| 列 | 含义 | 读/写 |
|---|---|---|
| A | 标题 | 读 |
| B | 货号(商品 ID,可空) | 读 |
| C | 衣服图路径(本机绝对路径,文件或目录) | 读 |
| D | 生成结果图片路径 | 写回 |
| E | 完成状态:完成 / 失败(空=未处理) |
写回 |
| F | 失败原因(仅人工查看) | 写回 |
规则:
- 默认读第一个工作表,第 1 行表头,从第 2 行开始。
- 跳过 E=
完成的行;是否处理 E=失败的行由界面「重试失败行」决定(勾选则处理「空白 + 失败」,否则只处理「空白」)。 - 行有效性只看「标题 + 衣服图路径」两者非空;货号(B 列)可空——它不进提示词,仅用于单文件行的输出命名(见 §9),为空时命名回退用源图名,因此不应再因缺货号而跳过整行。标题或衣服图为空 → 跳过该行,不写状态、不中断。
- 字段按原样读取,不清理空格、不回写清理值。
- 开始前检测 Excel 是否被占用(Office 打开会锁文件,openpyxl 无法写回)→ 提示「请关闭 Excel 后再开始」。
- 每处理完一行即保存 Excel(降低崩溃丢结果风险)。
- 成功 → D=新图绝对路径、E=
完成;失败 → E=失败、F=原因(同步写日志)。
4.1 C 列为「图片目录」(多图扇出,§9.1)
C 列除了单张图片文件,也可以是一个目录(如 d:/images/a/)。约定:
- 判定:
os.path.isdir(C)为真,或路径以分隔符结尾。仅取该目录顶层的图片 (扩展名.png/.jpg/.jpeg/.webp/.gif),不递归子目录,按文件名排序。 - 该行仍是一个任务、一次回写;对目录内每一张图各生成一张穿搭图(N→N), 共用本行的标题/货号与话术。
- 输出落到
AI 穿搭输出目录/<目录叶子名>/(默认是程序旁的穿搭图片\,见 §9.1);Excel 回写仍是整行一个状态: D=子目录绝对路径、E=全部成功才完成否则失败、F=失败张数/原因。 - Excel 行本身按顺序处理;并发只发生在当前目录行内部(见 §8 / §9.1)。
- 目录不存在或目录内没有图片 → 该行
失败并记录原因,不中断其它行。
5. 内部数据模型
core/models.py 新增(纯 dataclass,Python 3.7 兼容,不依赖 PySide6):
OutfitTask:row_index:int、title:str、product_id:str、garment_path:str、status:str(待处理/完成/失败)。OutfitResult:task、success:bool、output_path:str、error:str、attempts:int。
Excel 行 → OutfitTask 列表的转换由 excel_service 完成;核心只认 OutfitTask,因此将来换数据源只需另写适配器。
6. AI 图像服务(移植旧项目)
services/ai_image_service.py 移植旧项目 ImageApiClient,要点(详见 docs/旧ai穿搭项目.md §5.1/5.2):
- 多模型配置:
url / model / api_key / api_type / timeout_seconds / connect_timeout_seconds / extra_body,记住上次所选模型。 - 多请求格式(按
api_type:auto/chat/gemini/images/images_edits):- chat(OpenAI 兼容
/v1/chat/completions):messages[].content={type:text}+{type:image_url, image_url:{url:data_url}}。 - gemini(
generateContent):contents[].parts={text}+{inlineData:{mimeType,data}}。 - images:
{model,prompt,image_urls:[data_url],aspect_ratio,resolution,n}。 - images_edits:multipart
data+files={"image":...}。
- chat(OpenAI 兼容
- 传图:本地衣服图 →
data:<mime>;base64,...(data-url)或 multipart 文件。 - 取图:递归遍历响应 JSON 找 base64 / data-url / 图片 URL(再下载),对中转 API 结构差异强兼容。
- URL 归一化、
extra_body合并、字段校验(缺url/model/api_key时阻止开始)。
6.1 ai_models.json 出厂模板
为降低首次部署成本,发布包应在 app\config\ai_models.json 内带一份模型配置模板。
用户数据仍以 ~/.cmbot/config/ai_models.json 为准,程序不会直接把出厂模板当成用户配置使用。
播种/补种规则:
- 首次安装:启动器可从
app\config\ai_models.json播种到~/.cmbot/config/ai_models.json。 - 自更新:不能只依赖启动器播种。启动器目前先播种旧
app\config,再应用新版app;同时Launcher.exe本身不自更新,旧启动器也可能不知道ai_models.json。因此新版 app 在启动或load_ai_models()时必须做运行时兜底:如果~/.cmbot/config/ai_models.json不存在,且当前app\config\ai_models.json存在,则复制一次。 - 任何播种/补种都不得覆盖用户已有
~/.cmbot/config/ai_models.json。
模板格式采用当前程序可直接加载的结构:
{
"models": [
{
"name": "GPT Image 2",
"url": "https://api.vectorengine.ai/v1/images/edits",
"model": "gpt-image-2",
"api_key": "",
"api_type": "images_edits",
"timeout_seconds": 0,
"connect_timeout_seconds": 30,
"extra_body": {}
},
{
"name": "Nano Banana 2",
"url": "https://api.vectorengine.ai/v1/chat/completions",
"model": "gemini-3.1-flash-image-preview",
"api_key": "",
"api_type": "auto",
"timeout_seconds": 0,
"connect_timeout_seconds": 30,
"extra_body": {}
}
]
}
规则:
name是界面下拉框显示值,也是app_config.json中outfit_model的持久化值。timeout_seconds=0表示按分辨率自动取超时(见 §8),connect_timeout_seconds=30只控制连接阶段。- 出厂模板不得提交真实
api_key;管理员在目标机器的~/.cmbot/config/ai_models.json中填写真实 key。 - 旧项目的
api_config.json如为{last_selected_model, models:{id:{display_name,...}}}结构,迁移时把models对象转成列表,并把display_name映射为name。 - 当前旧配置里的
last_selected_model=nano_banana_2对应outfit_model="Nano Banana 2"。
Python 3.7 注意:旧项目用了
dict[str, Any]等 PEP 585 写法,移植时需from __future__ import annotations或改用typing.Dict,以兼容 cmbot 的 Python 3.7.9。
7. 提示词
- 提示词模板存
~/.cmbot/config/outfit_prompt.txt,界面可查看/编辑/保存;点「开始」前自动保存一次。 - 占位符:仅
{title}(标题);界面提供「插入标题」按钮,把占位符插入光标处。标题由用户经{title}自行放置(可前、可中、可省),程序不自动前置——这样标题不会被焊死在固定位置,也不会与旧话术里的{title}重复。货号(product_id)不进提示词——只用于 Excel B 列读取与输出文件命名(货号.jpg)。(render_prompt仍替换偶现的{product_id}以兼容,但界面不引导插入。) - 「保存」按钮(原「保存话术」改名、移到模板按钮行「重命名」之后):把当前编辑的模板原文(占位符原样保留,不存替换后的结果)经
config_service写入当前模板。用途是"改完先存、暂不开跑";与"开始前自动保存"并存、互为兜底。 - 最终提示词预览改为按需弹窗(§7.3):左栏要同时容纳「标题生成」组与「穿搭话术」组,纵向空间紧张(常驻预览会撑到 ~690px > 视口 ~664px、逼出滚动),故预览不做常驻面板,改成话术编辑框下方一个「预览最终提示词」按钮 → 弹非模态小窗,内含数据行下拉 + 只读的替换后提示词(含 §7.1 输出要求)。要"边改边看"就开着弹窗,话术/分辨率/数据行变化时刷新。
- 缺标题占位符时开始前弹窗询问是否继续。
- 默认话术方向(穿搭/上身,区别于旧项目的主图场景,需另调):人物上身实穿、保留衣服款式/版型/颜色/印花、合适身材与场景、电商可用、默认纯净不加促销牛皮癣。
7.1 自动附加「批量生成输出要求」
最终发给 AI 的提示词 = 用户话术(替换 {title} 后) + 一段自动附加的输出要求(移植自旧项目 app/main.py 的 render_prompt)。用户话术框只写创意部分,结构性要求由程序统一拼接,保证每张图都带:
批量生成输出要求:
- 参考解析度:{resolution} ← 动态,取「分辨率」设置(512/1K/2K/4K)
- 固定 1:1 正方形主图
- 必须结合商品标题与参考商品图片
- 服装本身、版型、颜色与图案不可跑版
- 仅「参考解析度」动态(= 分辨率设置),其余 3 行固定;简体文案(与软件一致)。
- 始终自动附加(同旧项目,不做开关)。
- 由
core/ai_outfit.render_prompt(template, task, resolution)实现:resolution为空时不附加(保持纯函数单测兼容);generate_outfit_image生成时传入当前分辨率。不做标题自动前置——标题由话术里的{title}决定(§7)。 - 预览弹窗必须包含这段(带当前选中分辨率),否则预览与实际发送不一致;分辨率下拉变化时弹窗同步刷新(§7.3)。
7.2 话术模板(多套命名话术)
把当前"单一一份话术"升级为多套命名话术:下拉切换、新建、另存为、重命名、删除、保存;记住上次所选。借鉴印花页「模板」区(template_panel.py / template_service.py)的范式,但全部自定义(不分内置)。
存储(config_service 扩展)
~/.cmbot/config/outfit_prompts.json=[{ "name", "text" }, …],始终 ≥1 套。- 当前选中名字存进
app_config.json新键outfit_prompt_name(启动恢复;名字失效则回退第一套)。 - 迁移:首次无
outfit_prompts.json时——有旧outfit_prompt.txt就转成一套「默认」,否则用DEFAULT_OUTFIT_PROMPT建「默认」。 - 新函数
load_outfit_prompts()/save_outfit_prompts(list)(沿用 utf-8-sig 读、无 BOM 写、损坏回退「默认」那套的约定)。
界面(「穿搭生成话术」组,套印花页范式)
- 编辑框上方一行:「穿搭生成话术」标签 + 模板下拉;下一行模板按钮
新建 / 另存为 / 重命名 / 保存 / 删除(「保存」从原编辑框下方移到此行「重命名」之后、改名自「保存话术」)。 - 编辑框下方一行:「插入标题」+「预览最终提示词」(弹窗,§7.3)。
- 左栏约 360px,5 个模板按钮偏多 → 实现时可排两行或用紧凑小按钮。
行为
- 切换 / 重命名 / 删除 / 新建 / 另存为之前:若编辑框与当前套已存文本不同(脏)→ 弹「是否保存当前修改?」(保存 / 不保存 / 取消;取消则下拉还原到原选项)。
- 保存=覆盖当前套;另存为=存为新名并选中;重命名=改当前套名;删除=二次确认,删后选邻近,不可删到 0。
- 名字唯一(重名拒绝或自动加序号)。
- 记住上次所选;生成用编辑框当前文本(
render_prompt(text, task, resolution)+ §7.1 尾巴不变);「开始生成」前把当前编辑存回所选套(沿用现有自动存逻辑)。
解耦:核心 render_prompt(text, task, resolution) 不动;"用哪段文本"由面板的模板选择决定。
7.3 最终提示词预览(按需弹窗)
左栏要同时放「标题生成」组与「穿搭话术」组,纵向吃紧(常驻预览 ~690px > 视口 ~664px → 逼出滚动)。故预览不常驻,改成话术编辑框下方一个「预览最终提示词」按钮 → 弹非模态 QDialog:
[预览最终提示词] ← 话术编辑框下方按钮
↓ 点开
┌─ 最终提示词预览 ─────────────┐
│ 数据行: [第 2 行 · 纯棉短袖 ▾] │
│ ┌─ 只读 ──────────────────┐ │
│ │ 为 纯棉短袖 生成… │ │ ← {title} 换成选中行标题
│ │ 批量生成输出要求: │ │ ← §7.1,随分辨率刷新
│ │ - 参考解析度: 1K … │ │
│ └────────────────────────┘ │
└────────────────────────────┘
- 非模态:弹窗开着时仍可编辑左栏话术;话术文本 / 分辨率 / 数据行下拉任一变化 → 弹窗内只读预览实时刷新(
render_prompt(话术, 选中行, 分辨率))。要"边改边看"就开着它。 - 数据行下拉在弹窗内(不占左栏):复用
excel_service.read_all_rows(excel)(状态无关,整表完成后仍可选;§10.3 的read_all_rows因此保留)。未选 Excel / 无有效行 → 显示带占位符的话术原文 + 提示;缺{title}占位符时在预览顶部提示。 - 零常驻高度:左栏只剩两个编辑框、不滚动;想验证替换效果时才弹窗。
- 预览只读,不改话术本身;标题仍由话术里的
{title}决定(不自动前置,§7)。
8. 并发、限速、重试、停止
复用旧项目的限速、重试、心跳与温和停止策略(docs/旧ai穿搭项目.md §5.3),但并发语义改为更适合「一行一个目录」的模型:
- Excel 行并发固定为 1:批次按 Excel 行顺序处理。这样一行一个目录时,D/E/F 仍按行聚合写回,避免多行同时写表或同时刷状态导致排查困难。
- 界面参数命名为图片并发数,只控制当前目录行内部同时生成多少张图片。默认 1;调到 2/3 时,同一目录内最多同时发起对应数量的图片请求。单文件行不参与图片并发,仍一行一张顺序处理。
- 并发由 Python 线程负责,PySide6 不参与:
requests在等网络响应时释放 GIL,所以目录内ThreadPoolExecutor(max_workers=图片并发数)的多个 HTTP 请求是真并发。Qt 只负责把进度/结果通过 signal 跨线程排队回主线程刷新 UI——子线程绝不直接操作控件(违反会崩溃/随机出错)。 RateLimiter(新请求间隔)限制同一目录内每个新图片请求的启动间隔;任务间单任务冷却作用于 Excel 行之间。因为行并发固定为 1,批次的最大 HTTP 并发约等于图片并发数。- 每行最多「首次 + 重试次数」尝试;限流/429 用短阶梯等待,普通错误短等待。
- 单次请求放子线程 + 主线程秒级检查,等待 >30 秒持续打心跳日志;超时按分辨率动态决定(512/1K/2K/4K → 180/240/360/600 秒,可被
timeout_seconds覆盖)。 - 温和停止(含目录行内打断,§19.24):置位停止后不再提交新 Excel 行;当前目录行内部也据
should_stop提前收尾——_generate_directory_outfit改为「有界提交」(始终最多图片并发数张在飞),每张完成后提交下一张前检查should_stop(),已停止则不再提交剩余图片、让在飞的收尾即返回(结果标注「已停止,N 张未生成」,已生成的照常带output_paths)。这样停止后最多再等 ~图片并发数 张图、秒级返回,而不是把当前目录整目录跑完。should_stop由OutfitBatchRunner.run()传给generate_func(=self._stop_event.is_set)。 - 停止反馈(§19.24):点「停止生成」后该按钮文案变「停止中…」(保持禁用),明确是在收尾不是卡死;批次结束
_set_running(False)复位为「停止生成」。期间「开始生成」仍禁用,待finished/failed后恢复。
9. 输出
- 格式 JPG,1:1,压缩到 ≤2MB;质量三档(小文件 75 / 均衡 85 / 高清 92)。
- AI 穿搭使用独立默认输出目录:打包态优先为安装根(
Launcher.exe旁)的穿搭图片\,不可写时回退到~/.cmbot/output/穿搭图片\;开发态可使用项目目录下的穿搭图片\。界面仍可改为任意目录,用户选择值继续通过outfit_output_dir记住。 穿搭图片\与添加印花页的合并后的图片\分开,避免两类产物混在一起;二者都不放在app\内,避免自更新替换app\时误删输出。- 命名:
货号.jpg,重名自动_1/_2,非法字符替换为_(不改 Excel 原始货号)。货号为空时命名回退用源图名(<源图名>.jpg,与目录行一致)。 - 成功后把实际新图绝对路径写回 Excel D 列。
9.1 目录行的输出(多图 → 同名子目录)
当 C 列是目录(§4.1)时:
- 每张源图各生成一张穿搭图,存到
AI 穿搭输出目录/<目录叶子名>/<源图名>.jpg(子目录名 = 目录叶子名,文件名沿用源图名;二者均按 §9 规则替换非法字符)。 例:d:/images/a/img1.png→穿搭图片/a/img1.jpg。 - 不加
_1/_2去重后缀:目标文件已存在视为「已生成」并跳过,使整行可 幂等重试——重试只补做缺失/失败的那几张,已成功的不重复调 API。 - 整行回写 Excel 时,D 列写子目录绝对路径(而非单个文件)。
- 目录内按图片并发数生成;默认 1 时等同旧的逐张顺序生成。为避免压垮中转 API,每个新图片请求启动前仍按「新请求间隔」节流。图片并发数 >1 时,完成日志和缩略图出现顺序不保证与文件名排序完全一致。
- 进度条按 Excel 行前进(一个目录=1 格),逐张进度通过实时日志与最近结果缩略图反馈。整行全部图片收尾后才写回 Excel D/E/F。
10. 界面(「2 AI 穿搭」页签)
界面效果图见 docs/ui-ai-outfit.html(浏览器打开)/ docs/ui-ai-outfit.png,完全沿用 cmbot 现有视觉、与「1 添加印花」严格统一(同 docs/ui-v1:浅灰底、#0067c0 蓝单一主色、12px 雅黑、3px 圆角、pill 状态徽章;不引入第二识别色)。「生成中」状态用蓝,与印花页「导出中」同色。把主窗口当前禁用的「2 AI 穿搭」页签启用,做成独立工作页(与「1 添加印花」并列、互不干扰)。
三栏布局:
- 左栏(数据源 + 标题/话术创作,~360px):
Excel/输出各为行内一行(标签 + 路径 + 浏览,省纵向空间);数据源概览(共 N 行 / 完成 / 待处理 / 失败);「标题生成」组(§17)在上:标题生成提示词编辑 +「保存」+「生成标题」(无标题模型下拉,模型由title_model配置定名,§17.3);「穿搭生成话术」组在下:模板下拉 + 按钮行新建/另存为/重命名/保存/删除+ 话术编辑框 + 下方「插入标题」「预览最终提示词」(预览为弹窗,数据行下拉在弹窗内,§7.3)。图片 AI 模型下拉在右栏;标题模型不放界面。 - 中栏(结果 + 明细):顶部「最近结果」缩略图条——只展示已完成的人物效果图,新图自动加到最左、首图标「最新」,单击看大图、右键开所在文件夹;下方「处理明细」表(行 / 标题 / 货号 / 衣服图 / 状态 / 结果或原因),按 Excel 行顺序,状态用 完成 / 失败 / 生成中 / 待处理 / 跳过 徽章。
- 列宽(§19.25 / §19.26):「行 / 货号 / 状态」是窄固定列(
QHeaderView.Fixed,约 46 / 88 / 76px);「衣服图 / 结果·原因」也收窄为可手拖的小初值(Interactive,约 90 / 130px,§19.26 再缩);省下的横向空间全部让给标题——「标题」列设QHeaderView.Stretch吃满剩余宽度(其余列越窄,标题越宽)。由_configure_detail_table_columns()统一设置。
- 列宽(§19.25 / §19.26):「行 / 货号 / 状态」是窄固定列(
- 右栏(设置 + 运行,~400px):生成设置标题与「重试上次失败的行」同排;图片并发数 / 新请求间隔 / 单任务冷却 / 失败重试 / 分辨率 / JPG 质量保持 3 列 × 2 行;
AI 模型标签与下拉框同排;本次进度 + 统计(完成 / 失败 / 待处理);开始生成 / 停止生成同排;导出失败清单 / 打开输出目录同排;节省的纵向空间给实时日志(含 §8 心跳行)。
设计取舍:不做"实时单图大预览"。 这是"开了走人、回头抽查"的批量工具;单图实时预览在并发时会产生"该显示哪一行"的歧义。改为「最近结果缩略图条」——只展示已落地成品,既保留"早发现话术/模型不对、及时停掉改话术"的价值,又因只显示成品而消除并发歧义。
- 后台用
QThread+Worker(QObject)+ 信号回主线程(与 cmbot 更新检查/导出一致;并发细节见 §8)。
页签内容切换:当前页签栏不切换内容面板(见
docs/07§4.2)。启用 AI 穿搭需要为页签接一个QStackedWidget(「1 添加印花」=现有工作区,「2 AI 穿搭」=本面板);这部分在实现阶段一并补。
10.1 已知问题:左栏内容被中间面板裁掉(下拉框撑宽)
§17/§7.3 更新。 撑宽元凶「样本行下拉」随预览移除(§17);§7.3 把数据行下拉收进预览弹窗(不在左栏),故本节裁切问题在左栏不再出现。弹窗内的下拉仍建议套
_compact_combo的 elide 策略以防长项撑宽弹窗。标题模型仍不放界面(§17.3 配置定名)。
现象:「2 AI 穿搭」左栏组件右侧显示不完整,像被中间面板遮挡了一部分。
定位(离屏实测 1280×720):
- 左栏是
QScrollArea(ai_outfit_panel.py_build_left),widgetResizable(True)且横向滚动条AlwaysOff。其内容的最小宽度高达 829px,而视口仅 ~428px;既不能再缩、又没有横向滚动条,于是内容按 829px 摆放、超出 ~428px 的右侧被裁掉,看起来就是"被中间盖住"。 - 829px 由单个控件撑出:「样本行」
QComboBox的minimumSizeHint ≈ 743px,因为它当前项是长字符串「第 N 行 · 货号 · 标题」。这把「最终提示词预览」分组撑到 ~805px 最小宽,主导了整列。「AI 模型」下拉有同样隐患(选到长模型名时一样会撑宽)。 - 纵向正常(内容 666 vs 视口 664)——纯横向裁切,与"占位太多"无关。
根因:QComboBox 默认会让 minimumSizeHint 随当前/最长项文本增长;长文本下拉框把列撑宽,而"无横向滚动"的滚动区只能裁切。
修复方向:
- 让两个下拉框不再决定列宽:
setSizeAdjustPolicy(AdjustToMinimumContentsLengthWithIcon)+setMinimumContentsLength(6)+setSizePolicy(Ignored, Preferred),长项改为省略号(elide)而非撑宽。 - 兜底:左栏滚动区横向策略由
AlwaysOff改AsNeeded,将来再有宽控件就滚动而非裁切。 - 可选:
_build_ui的setSizes([430,760,300])(和=1490)大于默认窗口 1280,首屏即被压缩;改成和≤窗口的一组(如[400,600,300]),非根因。
10.2 切换分辨率 / AI 模型时的信息提示
用户手动切换「分辨率」或「AI 模型」下拉时,弹一个信息框告知影响——只告知、不拦截、不还原(保持新选项)。
- 触发:用
QComboBox.activated信号(只在用户点选时发),不用currentIndexChanged——否则启动 /apply_config/_set_combo/_fill_sample_combo等程序化赋值会误弹。仅在值实际改变时弹(同项重选不弹)。 - 形式:
QMessageBox.information(只有「确定」),不阻断、不回退。预览刷新逻辑不变(分辨率仍照常实时刷预览)。 - 文案:
- 分辨率(超时取
ai_image_service.resolution_timeout:512→180 / 1K→240 / 2K→360 / 4K→600 秒):已切换分辨率到 4K。单任务超时约 600 秒,分辨率越高越慢。仅在下次「开始生成」生效,不影响正在进行的批次。
- AI 模型(带该模型
api_type):已切换模型到 GPT Image 2。调用方式:images_edits。不同模型的计费与效果可能不同。仅在下次「开始生成」生效。
- 分辨率(超时取
- 频率:每次实际更换都弹一次(不是整会话只弹一次)。
- 理由:下拉本可逆,不做强制确认(反模式);但分辨率/模型有"慢 / 贵 / 下次才生效"的隐含代价,一次性告知最划算。
10.3 已知问题:全表生成完成后,预览样本行下拉为空
§17/§7.3 更新。 一度随预览移除(§17),§7.3 把「数据行下拉」请回预览弹窗。本节结论重新生效:弹窗内数据行下拉必须用
read_all_rows(状态无关)填,否则整表完成后又会变空。read_all_rows同时是「标题生成」的行来源(§17)。
现象:选中的 Excel 整表生成成功后,再次选中该文件,「最终生成要求预览」的样本行下拉只显示「(选 Excel 后显示替换效果)」,无可选行。
原因:预览样本行用 _reload_sample_rows() 填,调的是给生成用的 load_outfit_tasks()——它按设计跳过 E=「完成」的行(失败行也只在勾「重试失败行」时返回)。整表都「完成」→ 返回空 → 下拉空 → 占位文案。预览本只是"拿一行真实数据看话术替换效果",与"这行要不要重新生成"无关,不该受状态过滤。
修复方向:
excel_service新增read_all_rows(excel):返回每一有效数据行(标题/货号/衣服图齐全)对应的OutfitTask,忽略 E 列状态。_reload_sample_rows()改调read_all_rows(预览专用);生成仍走load_outfit_tasks(待处理过滤不变)。运行时填下拉的_on_tasks_loaded仍用实际任务。- 补
read_all_rows单测(含「完成」行也返回、空字段行跳过)。
10.4 改进:整表无待处理行时的提示 + 生成不清空预览样本
§17/§7.3 更新。 改进 1(无待处理行提示)一直有效;改进 2 一度作废,§7.3 把数据行下拉请回预览弹窗后重新生效:
_on_tasks_loaded不动弹窗的数据行下拉,下拉只由read_all_rows维护,与运行解耦。
承接 §10.3。整表都「完成」后点「开始生成」时当前体验不清晰,两项改进:
改进 1 — 无待处理行时给明确提示。
现状:整表都「完成」(或都失败但未勾「重试失败行」)→ load_outfit_tasks 返回空 → 仅日志「已加载 0 行待处理任务」+ 结束弹窗「完成 0,失败 0」,用户看不懂为何没出图。
改为:检测到 0 行待处理时,明确提示——例如「该表没有待处理的行:已完成 N 行会跳过;失败 M 行可勾『重试失败行』重做;要重做已完成行,请清空对应行的状态(E)列。」(N/M 可由 read_all_rows 的各行状态统计得到。)在 tasks_loaded 为空或 _on_finished(total==0) 时弹 QMessageBox.information。
改进 2 — 生成不清空预览样本行。
现状:_on_tasks_loaded 里调 _fill_sample_combo(tasks),空任务时把预览下拉清成占位文案,破坏 §10.3 的修复。
改为:_on_tasks_loaded 不再动预览样本下拉;预览样本行只由 read_all_rows(_reload_sample_rows)维护,与运行解耦。
10.5 按钮配色(对齐「添加印花」)
现状:AI 穿搭面板没有 stylesheet,按钮全是系统灰。复用印花页各 widget 已有的按钮规范,让两页一致。
三类按钮(取自印花页 QSS):
- 主操作(蓝填充) =
#openFolderBtn/#queueBatchBtn:color:#fff; font-weight:bold; border:none; border-radius:3px; background:#0078d4; :hover #106ebe; :pressed #005a9e。 - 次级(灰描边) =
#queueActionBtn/#templateBtn:border:1px solid #d6d6d6; border-radius:3px; background:#f0f0f0; :hover #e0e0e0; :pressed #d0d0d0; :disabled color #aaa, bg #f7f7f7。 - 危险(安静红) =
#templateDeleteBtn:次级底 +color:#c42b1c; :hover bg #fde7e9 border #c42b1c; :pressed #f9d4d7。
映射:
| 按钮 | 配色 |
|---|---|
| 开始生成 | 主操作蓝(运行中禁用 → 灰) |
| 停止生成 | 危险安静红(空闲禁用 → 灰) |
| 话术「删除」 | 危险安静红 |
| 浏览…(×2) / 新建 / 另存为 / 重命名 / 插入标题 / 保存话术 / 导出失败清单 / 打开输出目录 | 次级灰 |
原则:蓝色只留给唯一主动作(开始生成,对齐「开始批量导出 / 打开文件夹」),红色标停止/删除(对齐「删除」),其余用印花页通用的次级灰。
实现:面板 setObjectName("aiOutfitPanel") + _apply_styles(),QSS 全部以 #aiOutfitPanel 前缀限定作用域(避免波及 QMessageBox/QInputDialog 等弹窗按钮);开始/停止/话术删除三个按钮各设 objectName(aiStartBtn / aiStopBtn / aiPromptDeleteBtn),其余走面板内 QPushButton 默认次级样式。纯样式改动,不动行为。
11. 配置与数据位置
遵循 cmbot「配置集中、放数据目录、凭据不入库」约定:
- AI 模型与密钥 →
~/.cmbot/config/ai_models.json(管理员预置或界面填写,含明文 key、不提交 git;与更新源凭据同等对待,见docs/10§14)。 - 发布包可附带
app\config\ai_models.json出厂模板(见 §6.1),用于首次播种;模板中的api_key必须为空或占位,真实 key 只写入用户数据目录。 - 提示词 →
~/.cmbot/config/outfit_prompt.txt。 - 批量设置 + 上次 Excel/输出路径 → 并入
app_config.json(config_service集中读写,UI 不直接读写配置文件,遵守docs/04第 6 节 /docs/054.12)。 - AI 穿搭默认输出目录 → 安装根旁的
穿搭图片\(不可写时回退~/.cmbot/output/穿搭图片\);用户手动选择的目录存入app_config.json的outfit_output_dir。 - 失败记录 / 日志沿用
~/.cmbot/logs与现有日志服务。
12. 依赖与兼容
- 新增依赖:
requests(HTTP)、openpyxl(Excel);Pillow、PySide6已有。写入requirements.txt并锁版本(兼容 Python 3.7:requests>=2.31,<3、openpyxl>=3.1,<4、urllib3<2)。 - 所有新代码保持 Python 3.7.9 兼容(注意类型注解写法)。
docs/03-technical-stack.md增列上述依赖;docs/02-prd.md、docs/07-ui-design.md§4.2 加指针指向本文档。
13. 安全考量
- AI 中转 API key 为明文,仅放
~/.cmbot,不入库、不随包分发到公网;内部使用可接受(与更新凭据一致)。 - 中转 API 会 429 限流:图片并发数默认 1,阶梯重试。
- 衣服图为本机路径;不存在/打不开/非图片 → 该行失败并记录,继续下一行。
14. 实现阶段建议
分步落地,每步可独立验证:
- 核心与服务(无 GUI,可测):
core.models.OutfitTask/OutfitResult、services/excel_service.py(读/写回/占用检测)、services/ai_image_service.py(移植 ImageApiClient)、core/ai_outfit.py(单行编排)+ 单测(Excel 读写、提示词渲染、取图、命名去重;API 用 mock)。 - 批量编排:并发/限速/重试/停止的
Worker,纯逻辑部分尽量可测。 - UI 页签:页签
QStackedWidget+ai_outfit_panel,接线后台线程、日志、进度、失败清单。 - 配置与提示词:
ai_models.json/outfit_prompt.txt/app_config.json接入,集中读写。 - 真机联调:用真实中转 API + 小批量 Excel 跑通端到端,调穿搭提示词。
15. 暂不做
- 文件夹/队列作为输入源(保留扩展位,本期不做)。
- 模特/姿态/背景的可视化精细编辑。
- 多 Excel / 多工作表批处理、列映射可配置化。
- 生成结果的应用内预览画廊(先以写回 Excel + 输出目录为准)。
16. 验收要点
- 选 Excel + 输出目录并记住上次;能读/存提示词。
- 能校验 AI 配置缺失;缺
url/model/api_key时阻止开始并提示。 - 按行处理:跳过「完成」、按设置重试「失败」、空字段行安全跳过。
- 衣服图 + 提示词 → 生成人物上身 JPG(1:1、≤2MB、命名去重)。
- 每行实时写回 D/E/F 并保存 Excel;Excel 被占用时明确提示。
- Excel 行顺序处理、图片并发/限速/重试/温和停止生效;实时日志 + 进度 + 结束摘要 + 失败清单导出。
- 日志写入
~/.cmbot/logs;AI 密钥不入库。 - 核心逻辑单测通过、不依赖 GUI、Python 3.7 可运行。
17. 标题生成(左栏独立功能)
「添加印花」批量导出生成的 Excel(docs/02 §6.12),A 列「标题」只是占位的印花名,不是真正的电商标题。本功能在 AI 穿搭页左栏新增一个**独立的「生成标题」**流程:用户写标题提示词(含数量),一次请求、纯提示词(不传图)生成多条电商标题,按序回填 Excel A 列(第 i 条 → 第 i 行),再点「开始生成」跑图时图片提示词的 {title} 即用上新标题。与「开始生成」(图片)互不绑定、各自一个按钮。
17.1 数据流与回填语义(一次请求 · 纯提示词 · 多条 · 按序填行)
§17.7 改版:早期为「逐行看图、各生成 1 条」(每行一次请求、带图)。现改为 一次请求、纯提示词(不传图)、生成多条、按序回填(用户在提示词里自写数量)。
- 行来源(§19.27):用
excel_service.read_all_rows(excel, require_title=False)(状态无关)——拿到全部数据行(仅为「填到哪些行 + 行号」),覆盖式写 A,不看 E 列状态、不引入新列。- 关键:标题生成的行来源只要求 原始图片路径(C) 非空,标题(A) 可空。因为 A 正是要生成/覆盖的列;若沿用「A 必须非空」(图片生成/预览的规则),则 A 空的印花表(恰恰最需要生成标题)会被全部跳过 → 误报「该表没有可处理的行」(§19.27 修复的 catch-22)。
read_all_rows(excel, require_title=True)(默认)行为不变:要求 标题(A)+原始图片路径(C) 都非空——预览弹窗、_show_no_pending_message统计仍用默认;图片生成的load_outfit_tasks也仍要求 A({title}是输入)。
- 一次请求、纯提示词:把用户标题提示词原样(不带图、不替换
{title})发一次给文本模型;提示词里由用户自写数量并约定标题之间用逗号隔开(如「生成 6 条…标题之间用逗号分隔,不要表格/换行/序号」)。 - 按逗号拆分多条:把响应文本按逗号(半角
,/ 全角,,并兼容换行作兜底分隔)拆成多条,每条清洗(去首尾空白、去行首序号/符号1. / - / ①、去首尾引号、丢空)→ 得到标题列表。不再按行/不解析 Markdown 表格——靠提示词约定逗号分隔(§19.23)。 - 按序回填:第 i 条 → 第 i 行 A(
write_title_result(excel, row_index, title)),逐条写、刷新中栏「处理明细」表该行「标题」列。 - 条数对不上时兜底:
N = len(标题),R = len(行)。N > R→ 多出的N-R条丢弃 + 日志提示;N < R→ 后R-N行留空 + 日志提示。不报错中止。 - 生成完重载 Excel:刷新界面与内存任务,紧接「开始生成」跑图即用新标题(图片生成当场
load_outfit_tasks读 A 列)。
17.2 文本服务(复用图像服务的 HTTP 管道)
- 新增
services/ai_text_service.py的AiTextClient(config, session=None):复用ai_image_service的AiModelConfig/image_to_data_url/detect_api_type/normalize_api_url/ Bearer 鉴权 / 超时 / session,不重写 HTTP。 generate_texts(prompt, image_path=None) -> List[str](标题生成走此:image_path=None纯文本):chat:messages=[{role:user, content:[{type:text,text:prompt}]}](无图时不含image_url)。gemini:contents[].parts=[{text}],generationConfig.responseModalities=["TEXT"]。images/images_edits:纯图片接口,不能返回文字 → 抛AiTextServiceError。- 一次 POST →
extract_titles_from_response返回多条清洗后标题。
generate_text(prompt, image_path=None) -> str:保留(返回第一条,=generate_texts的[0]),供单条场景与既有单测;与generate_texts共用同一段 POST。extract_titles_from_response(data) -> List[str]:取choices[0].message.content/ geminicandidates[0].content.parts[].text的原始文本,按逗号(,/,,兼容换行)拆分 + 逐段清洗(去首尾空白/序号/符号/引号、丢空),返回标题列表(§19.23)。extract_text_from_response= 取其首条(兼容保留)。
17.3 模型与提示词
- 标题模型「配置定名」,不放下拉(§17.6 决策):图片/文本模型类型不同(图片模型返回图、不返回文字),且标题模型是「配一次就固定」的东西,不像分辨率/话术需要每次选。因此去掉「标题模型」下拉,改由配置决定用哪条:
app_config.json的title_model记模型名字(对应ai_models.json某条的name),默认"GPT-5.5 文本"。- 运行时按该名字在
load_ai_models()里查对应模型来用;界面上没有下拉,效果等于"固定用 gpt-5.5"。 - 换模型只改配置、不改代码:管理员在
ai_models.json加/换文本模型条目,再把title_model改成它的name即可,无需重新打包。 - 找不到/未配置时明确报错(不静默猜测):「未找到标题模型『{名字}』,请在 ai_models.json 添加可生成文字的模型(chat/gemini),并在 app_config 的 title_model 指定其 name」。
- 命中的模型是纯图片接口(images/images_edits)时,开跑前就拦截(§19.21):
_find_title_model检出api_type ∈ {images, images_edits}→ 返回错误「『{名字}』是图片模型({api_type}),不能生成文字标题,请改选 chat/gemini 文本模型」→_resolve_title_model_config弹窗 + 中止,不逐行失败。 - 边界:
api_type=chat但实际返回图片的模型(如 Nano Banana)类型上无法预判,仍在运行时由AiTextClient/「未找到文字标题」逐行暴露——这是固有限制。
ai_models.json需有一条文本/视觉模型(管理员维护,含真实 key,不入库):api_type设chat、model填中转的真实 id(如gpt-5.5)、name与title_model一致(默认GPT-5.5 文本)。docs/ai_models.sample.json已含 chat 示例可参照。- 标题提示词:单份,存
~/.cmbot/config/title_prompt.txt(load_title_prompt/save_title_prompt,仿旧式单份,标题侧不做多套模板);默认文案为批量风格 + 逗号分隔——让模型生成多条电商女装标题、标题之间用逗号分隔、不要表格/换行/序号/引号/表情,数量由用户在提示词里写(默认示例可写「生成 10 条」)。与 §17.1 的逗号拆分配套。
17.4 界面与运行
- 左栏布局(§10 已同步):「标题生成」组在上、「穿搭生成话术」组在下;预览块移除。标题组含:标题提示词编辑、「保存」、「生成标题」(无标题模型下拉,模型由
title_model配置定名,§17.3)。 - 运行:
_TitleWorker(QObject)跑在QThread(仿_OutfitWorker),一次请求(纯提示词,无图)拿到标题列表 → 按序逐条回填write_title_result+ 刷新该行 GUI;条数对不上按 §17.1 兜底(多丢、少留空、记日志);请求失败记日志并以「成功 0」收尾;温和停止。模型用_resolve_title_model_config()(按title_model名字查ai_models.json)。只有一次请求,不再用「新请求间隔」做行间节流。 - 互斥:「生成标题」与「开始生成」运行时互斥(避免同表并发写)。
- 进度/结果走中栏「处理明细」表与右栏日志(不写 Excel 状态列)。
17.5 验收要点(标题生成)
- 左栏有「标题生成」组(提示词 + 保存 + 生成标题按钮),无标题模型下拉、无预览块。
app_config.title_model指向的模型在ai_models.json存在 → 生成走该模型;改title_model名字即换模型,无需改代码。- 选印花生成的 Excel,点「生成标题」→ 一次请求返回多条 → 按序回填各行 A、明细表标题列实时刷新、D/E/F 未动。
- 条数对不上:N>行数多的丢弃 + 日志;N<行数后面行留空 + 日志(不报错中止)。
title_model找不到对应模型 / 命中条目是纯图片接口(images/images_edits)时,开跑前弹窗报错并中止(§19.21)。- 生成完重载 Excel,紧接「开始生成」跑图用的是新标题。
extract_titles_from_response(按逗号拆分→多条、清洗、兼容换行、Markdown 表格不再当多条)、generate_titles(一次请求纯文本、客户端异常)、write_title_result(只改 A)、_find_title_model(命中/缺失/图片模型报错)单测通过,Python 3.7。
17.6 决策:标题模型「配置定名」而非下拉
最初实现(§19.18)给标题模型放了独立下拉。落地后复盘改为配置定名、去下拉:
- 去下拉的理由:标题模型是「配一次就固定」的(一个店铺/平台通常固定一个文本模型),不像分辨率/话术需要每次切换;左栏仅 ~360px、已叠两个提示词组,少一个下拉更干净;还消掉「误选图片模型导致报错」的坑。
- 为什么不把模型名写死在代码里:写死后换模型/升级要改
.py重新打包分发。改放app_config.title_model(默认GPT-5.5 文本)后,换模型只改配置一行、重启生效,对非技术管理员友好。 - 界面效果一致:两种做法用户都看不到下拉、都固定一个模型;差别只在「换模型」的代价(改配置 vs 改代码)。
- 标题提示词暂不做多套模板(同 §17 决策;真有多套切换需求再按 §7.2 范式补)。
17.7 决策:标题改为「一次请求、纯提示词、多条按序填」
§19.18 初版为「逐行看图、各生成 1 条」(每行一次请求、带衣服图做视觉)。用户复盘后改为 一次请求、纯提示词(不传图)、生成多条、按序回填(§19.22):
- 为什么去掉图片:印花 Excel 一行=一个印花子目录、多张图,"看哪张"本就要取首图近似;且用户的标题更偏通用电商 SEO 风格(同批女装标题可互换),不必逐件看图。去图后改为纯文本批量,一次请求拿多条,更快更省(N 行从 N 次请求降到 1 次)。
- 数量由用户在提示词里写(已确认):代码不自动附加数量、不替换
{title},原样发;解析返回的多行为多条标题。 - 条数对不上不强求:
N>行数多的丢、N<行数后面行留空 + 日志,不报错中止(用户可改提示词数量重跑)。 - 代价:标题不再与具体某件衣服一一对应(无图);若将来要"每件看图各出标题",那是另一种模式,按 §17.1 旧版思路另做。
- 文本服务保留
generate_text(单条)+ 新增generate_texts(多条),共用同一段 POST;标题流程走generate_texts(prompt, image_path=None)。