Files
cmbot/docs/11-ai-outfit.md

573 lines
58 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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":...}`。
- **传图**:本地衣服图 → `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`。
- **标题模型补全(升级兼容)**:如果用户目录的 `ai_models.json` 已存在,但缺少 `app_config.title_model` 指定的模型名(默认 `"GPT-5.5 文本"`),新版 app 在 `load_ai_models()` 时应从当前 `app\config\ai_models.json` 查找同名模型并**追加**到用户 `ai_models.json`。只追加缺失项,不覆盖、不合并、不改用户已有模型与 key;出厂条目的 `api_key` 仍为空,管理员需要在用户目录中填写真实 key。
- 如果用户已在 `app_config.title_model` 改成其它名字,则按该名字查找;出厂模板没有同名条目时只记录日志并保持现状,界面仍按 §17.3 给出「未找到标题模型」提示。
模板格式采用当前程序可直接加载的结构:
```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": "GPT-5.5 文本",
"url": "https://api.vectorengine.ai/v1/chat/completions",
"model": "gpt-5.5",
"api_key": "",
"api_type": "chat",
"timeout_seconds": 0,
"connect_timeout_seconds": 30,
"extra_body": {}
}
]
}
```
规则:
- `name` 是界面下拉框显示值,也是 `app_config.json` 中 `outfit_model` 的持久化值。
- `GPT-5.5 文本` 是标题生成默认模型名,对应 `app_config.json` 的 `title_model` 默认值;它必须是能返回文字的 `chat`/`gemini` 类模型,不应指向 `images`/`images_edits` 图片接口。
- `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`,界面可查看/编辑/保存;点「开始」前自动保存一次。
- 发布包应在 `app\config\outfit_prompt.txt` 带一份出厂穿搭话术;启动器首次播种或主程序运行时兜底会在用户目录缺失时复制到 `~/.cmbot/config/outfit_prompt.txt`。用户已有文件永不覆盖;出厂文件缺失时才退回代码内置 `DEFAULT_OUTFIT_PROMPT`。
- 占位符:仅 `{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);「标题生成提示词」label 右侧预留标题请求等待计时位(§17.10),点击「生成标题」后显示 `等待中 01:35 / 10:00`;**「穿搭生成话术」组在下**:模板下拉 + 按钮行 `新建/另存为/重命名/保存/删除` + 话术编辑框 + 下方「插入标题」「预览最终提示词」(预览为弹窗,数据行下拉在弹窗内,§7.3)。图片 AI 模型下拉在右栏;标题模型不放界面。
- **中栏(结果 + 明细)**:顶部「最近结果」缩略图条——**只展示已完成的人物效果图**,新图自动加到最左、首图标「最新」,单击看大图、右键开所在文件夹;下方「处理明细」表(行 / 标题 / 货号 / 衣服图 / 状态 / 结果或原因),按 Excel 行顺序,状态用 完成 / 失败 / 生成中 / 待处理 / 跳过 徽章。
- **列宽(§19.25 / §19.26)**:「行 / 货号 / 状态」是**窄固定列**(`QHeaderView.Fixed`,约 46 / 88 / 76px);「衣服图 / 结果·原因」也收窄为可手拖的小初值(`Interactive`,约 90 / 130px,§19.26 再缩);省下的横向空间全部让给**标题**——「标题」列设 `QHeaderView.Stretch` 吃满剩余宽度(其余列越窄,标题越宽)。由 `_configure_detail_table_columns()` 统一设置。
- **右栏(设置 + 运行,~400px)**:生成设置标题与「重试上次失败的行」同排;图片并发数 / 新请求间隔 / 单任务冷却 / 失败重试 / 分辨率 / JPG 质量保持 **3 列 × 2 行**;`AI 模型` 标签与下拉框同排;本次进度 + 统计(完成 / 失败 / 待处理);开始生成 / 停止生成同排;导出失败清单 / 打开输出目录同排;节省的纵向空间给实时日志(含 §8 心跳行)。
- **图片 AI 模型下拉过滤**:右栏 `AI 模型` 只用于「开始生成」的人物穿搭图片生成,应在填充下拉时跳过 `name == app_config.title_model` 的模型(即 `self._title_model_name`)。标题模型继续由 §17.3 的配置定名机制使用,不出现在图片模型下拉中,避免用户误选文本模型导致图片生成失败。除这条按名过滤外,暂不引入 `usage` 字段或更复杂的配置结构。
**状态色应用(遵守 `docs/07` §10.1)**:AI 穿搭页只给状态、风险和结果上色,不给普通 label / 按钮 / 输入框装饰性上色。标题生成等待计时已使用 `等待中=信息蓝`、`写入中/完成=完成绿`、`失败/超时=错误红`(§17.10)。后续若扩展颜色,优先覆盖这些组件:图片生成与批量导出状态(生成中 / 停止中 / 完成 / 失败)、处理明细表「状态」徽章、低对比 / 可见度结果、模型或路径校验错误。普通「标题生成提示词」「AI 模型」「输出」等静态标签保持黑灰色。
状态建议:
```text
处理明细状态:待处理=灰,生成中=蓝,完成=绿,失败=红,跳过/停止=灰
图片生成 / 批量导出:生成中/导出中/停止中=蓝,完成=绿,失败/超时=红
低对比 / 可见度:正常=默认或绿,偏低=橙,不明显=红
配置校验:API Key 缺失 / 模型不可用 / 路径无效=红色文字 + 明确原因
```
> **设计取舍:不做"实时单图大预览"。** 这是"开了走人、回头抽查"的批量工具;单图实时预览在并发时会产生"该显示哪一行"的歧义。改为「最近结果缩略图条」——只展示已落地成品,既保留"早发现话术/模型不对、及时停掉改话术"的价值,又因只显示成品而消除并发歧义。
- 后台用 `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 只写入用户数据目录。用户已有 `ai_models.json` 缺标题模型时,只追加同名出厂模型,不覆盖已有模型。
- **穿搭话术** → `~/.cmbot/config/outfit_prompt.txt`。发布包应带 `app\config\outfit_prompt.txt`;用户文件缺失时复制,已有不覆盖。
- **标题提示词** → `~/.cmbot/config/title_prompt.txt`。发布包应带 `app\config\title_prompt.txt`;用户文件缺失时复制,已有不覆盖。
- **批量设置 + 上次 Excel/输出路径** → 并入 `app_config.json`(`config_service` 集中读写,UI 不直接读写配置文件,遵守 `docs/04` 第 6 节 / `docs/05` 4.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. 实现阶段建议
分步落地,每步可独立验证:
1. **核心与服务(无 GUI,可测)**:`core.models.OutfitTask/OutfitResult`、`services/excel_service.py`(读/写回/占用检测)、`services/ai_image_service.py`(移植 ImageApiClient)、`core/ai_outfit.py`(单行编排)+ 单测(Excel 读写、提示词渲染、取图、命名去重;API 用 mock)。
2. **批量编排**:并发/限速/重试/停止的 `Worker`,纯逻辑部分尽量可测。
3. **UI 页签**:页签 `QStackedWidget` + `ai_outfit_panel`,接线后台线程、日志、进度、失败清单。
4. **配置与提示词**:`ai_models.json` / `outfit_prompt.txt` / `app_config.json` 接入,集中读写。
5. **真机联调**:用真实中转 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` / gemini `candidates[0].content.parts[].text` 的原始文本,**先剥离前导思考(§17.9)→ 按逗号(`,`/`,`,兼容换行)拆分 + 逐段清洗**(去首尾空白/序号/符号/引号、丢空),返回标题列表(§19.23)。`extract_text_from_response` = 取其首条(兼容保留)。
- **剥离前导思考 `_strip_title_preamble`(§17.9/§19.31)**:推理型模型常在标题前先输出一段"思考"散文(这段本身带逗号,直接逗号切会切出假标题)。切分前先剥离:
- **哨兵优先**:文本含 `===TITLES===`(正则 `=+\s*TITLES\s*=+`,忽略大小写)时,只取**最后一个哨兵之后**的内容——把思考连同哨兵一起丢掉。这是提示词侧约定的确定性切点(§17.3)。
- **句号兜底**:无哨兵时,切掉**最后一个句末标点(`。!?`)及其之前的全部**——真标题不含 `。`(提示词禁标点),故最后一个 `。` 即前导散文与标题列表的分界。仅当"其后仍含逗号/换行分隔符"时才剥离,避免把"末条标题的收尾句号"误当分界而清空。
- **读取超时:标题生成走 4K 的 600 秒(§17.8)**。`generate_texts`/`generate_text` 的 `resolution` 形参本是图像分辨率借来的超时档(`resolution_timeout`:512→180 / 1K→240 / 2K→360 / 4K→600 秒);纯文本请求没有"分辨率",只借那张超时表。`generate_titles` 显式传 `resolution="4K"`(`ai_title._TITLE_TIMEOUT_RESOLUTION`)→ 读取超时 **600 秒**,而非默认档 1K 的 240 秒。理由:一次请求要中转站排队、大模型生成多条标题再整段返回,240 秒对慢模型/排队偏紧。连接超时仍 30 秒不变;模型条目若显式设 `timeout_seconds>0` 仍**优先覆盖**这 600 秒(`_post`:`config.timeout_seconds if >0 else resolution_timeout(resolution)`)。
### 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 模型` 下拉在 `_fill_model_combo()` 填充时,应跳过 `name == title_model` 的模型;标题模型仍留在 `self._models` 里供 `_resolve_title_model_config()` 查找,不应从总模型列表删除。这样既保留标题生成能力,又避免默认 `GPT-5.5 文本` 出现在图片模型下拉里被误选。
- **`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`,仿旧式单份,标题侧不做多套模板)。发布包应带 `app\config\title_prompt.txt`;用户目录缺失时首次播种/运行时兜底复制,已有不覆盖;出厂文件缺失时才退回代码内置 `DEFAULT_TITLE_PROMPT`。默认文案为**批量风格 + 逗号分隔**——让模型生成**多条**电商女装标题、**标题之间用逗号分隔**、**不要表格/换行/序号/引号/表情**,数量由用户在提示词里写(默认示例可写「生成 10 条」)。与 §17.1 的逗号拆分配套。**代码内置 `DEFAULT_TITLE_PROMPT` 还约定「若需思考先写在最前面,思考完毕后单独一行输出 `===TITLES===`,其后只放逗号分隔的标题」——给解析一个确定性切点(§17.9)。用户自定义提示词(含发布包精细模板)建议照抄这行哨兵约定;不加也有 `_strip_title_preamble` 的句号兜底,但加了更稳。**
### 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`)。**只有一次请求,不再用「新请求间隔」做行间节流**。
- **互斥**:「生成标题」与「开始生成」运行时互斥(避免同表并发写)。
- **标题请求等待计时**:点击「生成标题」后,在「标题生成提示词」label 右侧显示 `等待中 mm:ss / 10:00`(§17.10),每秒刷新;请求返回后切到写回/完成状态,失败或超时后复位并提示。该计时只表达“已等待 / 最长等待”,不是模型真实生成百分比。
- 进度/结果走中栏「处理明细」表与右栏日志(不写 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)`。
### 17.8 决策:标题生成读取超时用 4K 档(600 秒)
标题生成默认读取超时从 **1K 的 240 秒改为 4K 的 600 秒**(§19.30)。
- **背景**:标题是"一次请求、纯提示词、生成多条"(§17.7)。这一次请求要经中转站转发、大模型排队、生成 N 条标题后**整段**返回;比"一张图"耗时更不可控(推理/排队型模型尤甚)。原先借用 1K 档 240 秒,偏紧,慢模型会在拿到结果前就 `ReadTimeout`。
- **改法**:`generate_titles` 显式传 `resolution="4K"`(常量 `ai_title._TITLE_TIMEOUT_RESOLUTION`),把读取超时抬到 600 秒。**只改标题这一条链路**——`generate_texts`/`generate_text` 的默认 `resolution="1K"` 不动,其它/未来调用方不受影响。
- **为什么复用分辨率档而非新增字段**:纯文本请求本无分辨率,`resolution` 只是 `resolution_timeout` 那张 `{512/1K/2K/4K → 180/240/360/600}` 超时表的键;复用它零新增配置、与图片侧同一套超时语义,最省。若将来要独立可调,再在模型条目上用 `timeout_seconds>0` 覆盖(已支持,优先级高于分辨率档)。
- **代价**:慢/挂死的请求现在最长等 600 秒才失败(而非 240 秒),但标题生成是单次、手动触发、非批量循环,多等的是最坏情况,正常返回不受影响。
### 17.9 决策:过滤模型「前导思考」,只取标题(哨兵 + 句号兜底)
推理型文本模型即便提示词写明「只返回逗号连接的标题」,仍常在标题**前面**先输出一段思考散文(例:「我會直接產出符合格式的標題,並先用字元計數檢查…然後一次輸出 42 個標題。【台灣現貨】…」)。这段散文**自身带逗号**,直接逗号切分会把它切成若干**假标题**混进结果。§19.31 在 `_clean_titles` 切分**前**加一步 `_strip_title_preamble`:
- **两层过滤**:
- **① 哨兵优先(确定性)**:提示词约定思考完毕后单独一行输出 `===TITLES===`(§17.3);解析取**最后一个哨兵之后**的内容(正则 `=+\s*TITLES\s*=+`、忽略大小写,容忍 `**===TITLES===**` 之类修饰)。思考连同哨兵一并丢弃,无歧义。
- **② 句号兜底(抗噪,无需改提示词)**:无哨兵时,切掉**最后一个 `。!?` 及其之前的全部**。依据:真标题不含句末标点(提示词禁标点、这批标题用半角空格分词),故最后一个 `。` 就是"前导散文 ↔ 标题列表"的分界。**仅当其后仍含逗号/换行分隔符时才剥离**,防止"末条标题恰好带收尾句号"被误当分界而清空。
- **为什么两层都要**:哨兵最干净但依赖模型照做(推理模型偶尔漏);句号兜底不依赖提示词、对现有精细模板(发布包 `【台灣現貨】` 模板,未含哨兵)当场生效。二者叠加:模型配合时确定性、不配合时仍能救。
- **为什么不选纯提示词约束 / JSON 数组**:纯靠"别输出思考"压不住推理模型(§本节前提)。JSON 数组(`["t1","t2",…]` + 抽第一个 `[...]` 解析)鲁棒性更高、还能容忍标题内嵌逗号,但要改输出格式、超长数组偶有截断/非法风险;当前保留用户熟悉的逗号格式 + 上述两层过滤,改动最小。若日后逗号内嵌成为真问题,再迁 JSON(把逗号切分降级为 fallback)。
- **边界**:② 假设标题不含 `。`;若某条标题真带句号会被误切——由提示词"禁标点"约束兜住,属可接受的固有边界(与 §17.3 一致)。中转站若能单独返回 `reasoning_content` 或支持关思考参数,则从源头无前导思考,比解析更干净(值得在中转文档确认;本次不依赖)。
### 17.10 决策:标题生成显示等待计时(已等待 / 最长等待)
标题生成支持用户在提示词里一次要求生成上百条标题。该流程是“一次请求、纯提示词、多条返回”(§17.7),请求发出后程序无法获得模型内部生成百分比;若只显示按钮禁用和旧的行进度条,用户容易误判为卡死。
交互规则:
- 点击「生成标题」后,在左栏「标题生成提示词」label 右侧显示 `等待中 mm:ss / 10:00`,例如 `等待中 01:35 / 10:00`。
- `10:00` 来自标题生成当前默认读取超时 600 秒(§17.8);若模型配置 `timeout_seconds > 0` 覆盖超时,则右侧最大等待应显示覆盖后的秒数。
- 计时每秒刷新,直到请求返回、失败、超时或用户停止。
- 请求返回后进入 Excel 写回阶段时,文案可改为 `写入中 已完成/总数`;全部写回后显示 `完成 N 条` 并在短暂停留后复位。
- 超时或失败时显示 `等待超时` / `生成失败`,同时保留弹窗和日志里的详细原因。
- 文字颜色按状态变化:`等待中` 使用信息蓝 `#0078d4`,`写入中` / `完成 N 条` 使用完成绿 `#107c10`,`等待超时` / `生成失败` / `完成 N 条,失败 M` 使用错误红 `#c42b1c`。
产品边界:
- 这不是模型真实进度条,不能用百分比暗示“已生成多少”。它只表达“已等待多久 / 最长等多久”。
- 现有右栏本次进度条继续按 Excel 行/写回进度推进;标题 label 右侧计时只覆盖“单次标题请求等待”这段长等待空窗。
- 只给文字本身上色,不加背景色、不做闪烁、不放大字号,避免在左栏窄空间里形成过强干扰。
- 其它 AI 穿搭状态色遵守 §10 的「状态色应用」和 `docs/07` §10.1:只给状态、风险、错误、完成结果上色,普通说明文字保持黑灰。