Files
cmbot/docs/11-ai-outfit.md
T
adminandClaude Opus 4.8 c47e9dee95 refactor(ai-outfit): 标题模型改为「配置定名」、去掉下拉 (§19.19)
标题模型是「配一次就固定」的,不像分辨率/话术需每次选;左栏 ~360px 已叠两个
提示词组,去掉下拉更干净,还消掉「误选图片模型」的坑。模型名放 app_config.title_model
(默认 GPT-5.5 文本),运行时按名字查 ai_models.json —— 换模型只改配置不改代码(docs/11 §17.3/§17.6)。

- ai_outfit_panel.py:_build_title_group 去掉标题模型下拉;apply_config 改记 title_model
  名字、_emit_config 不再写(UI 不覆盖);_selected_title_model_config → 纯函数
  _find_title_model(按 name 查 + 校验,返回 (config, error))+ UI 包装
  _resolve_title_model_config;找不到/未配置/图片接口时明确报错
- config_service:DEFAULT_CONFIG.title_model 默认改为 "GPT-5.5 文本"
- docs/ai_models.sample.json:补一条 name=「GPT-5.5 文本」的 chat 文本模型样板(占位 key)
- 测试:面板断言无标题模型下拉;_find_title_model 命中/缺失/无模型三例;全套 py37 通过
- 冒烟:seeded ai_models.json + apply_config → title_model 名字解析到 gpt-5.5(chat),缺失→报错

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:45:36 +08:00

455 lines
39 KiB
Markdown
Raw 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`。
模板格式采用当前程序可直接加载的结构:
```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}`(标题);界面只提供「插入标题」按钮,把占位符插入光标处。货号(`product_id`)不进提示词——它只用于 Excel B 列读取与输出文件命名(`货号.jpg`),不影响 AI 生成内容。(`render_prompt` 仍会替换偶然出现的 `{product_id}`,保持兼容,但界面不再引导插入。)
- **「保存话术」按钮**:把当前编辑的模板原文(占位符原样保留,**不**存替换后的结果)经 `config_service` 写入 `outfit_prompt.txt`。用途是"改完先存、暂不开跑"并给用户明确反馈;与"开始前自动保存"并存、互为兜底(保留按钮是有意为之,对非技术用户更安心)。
- ~~**最终提示词预览(内嵌、不弹窗)**~~ **(§17 已移除)**:原左栏话术编辑下方的只读预览区 + 样本行下拉,为给左栏让出「标题生成」组(§17)已整体移除。相关历史问题(§10.1 下拉撑宽、§10.3 样本行为空、§10.4 不清空样本)随之作废。
- 缺标题占位符时开始前弹窗询问是否继续。
- 默认话术方向(穿搭/上身,区别于旧项目的主图场景,需另调):人物上身实穿、保留衣服款式/版型/颜色/印花、合适身材与场景、电商可用、默认纯净不加促销牛皮癣。
### 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` 生成时传入当前分辨率。
- **左栏「最终提示词预览」必须包含这段**(带当前选中分辨率),否则预览与实际发送不一致;分辨率下拉变化时预览同步刷新。
### 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 写、损坏回退「默认」那套的约定)。
**界面**(「穿搭生成话术」组,套印花页范式)
- 编辑框**上方一行**:「穿搭生成话术」标签 + 模板下拉;下一行放「新建」「另存为」「重命名」「删除」。
- 编辑框**下方**:保留「插入标题」「保存」。
- 左栏约 360px,5 个模板按钮偏多 → 实现时可排两行或用紧凑小按钮。
**行为**
- **切换 / 重命名 / 删除 / 新建 / 另存为之前**:若编辑框与当前套已存文本不同(脏)→ 弹「是否保存当前修改?」(保存 / 不保存 / 取消;取消则下拉还原到原选项)。
- 保存=覆盖当前套;另存为=存为新名并选中;重命名=改当前套名;删除=二次确认,删后选邻近,**不可删到 0**。
- 名字**唯一**(重名拒绝或自动加序号)。
- 记住上次所选;生成/预览用编辑框当前文本(`render_prompt` + §7.1 尾巴不变);「开始生成」前把当前编辑存回所选套(沿用现有自动存逻辑)。
**解耦**:核心 `render_prompt(text, task, resolution)` 不动;"用哪段文本"由面板的模板选择决定。
## 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` 覆盖)。
- **温和停止**:置位停止后不再提交新任务,已发请求收尾后正常写回。
## 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.2)。**最终提示词预览已移除(§17)**。图片 AI 模型下拉在右栏;标题模型不放界面。
- **中栏(结果 + 明细)**:顶部「最近结果」缩略图条——**只展示已完成的人物效果图**,新图自动加到最左、首图标「最新」,单击看大图、右键开所在文件夹;下方「处理明细」表(行 / 标题 / 货号 / 衣服图 / 状态 / 结果或原因),按 Excel 行顺序,状态用 完成 / 失败 / 生成中 / 待处理 / 跳过 徽章。
- **右栏(设置 + 运行,~400px)**:生成设置标题与「重试上次失败的行」同排;图片并发数 / 新请求间隔 / 单任务冷却 / 失败重试 / 分辨率 / JPG 质量保持 **3 列 × 2 行**;`AI 模型` 标签与下拉框同排;本次进度 + 统计(完成 / 失败 / 待处理);开始生成 / 停止生成同排;导出失败清单 / 打开输出目录同排;节省的纵向空间给实时日志(含 §8 心跳行)。
> **设计取舍:不做"实时单图大预览"。** 这是"开了走人、回头抽查"的批量工具;单图实时预览在并发时会产生"该显示哪一行"的歧义。改为「最近结果缩略图条」——只展示已落地成品,既保留"早发现话术/模型不对、及时停掉改话术"的价值,又因只显示成品而消除并发歧义。
- 后台用 `QThread` + `Worker(QObject)` + 信号回主线程(与 cmbot 更新检查/导出一致;并发细节见 §8)。
> 页签内容切换:当前页签栏不切换内容面板(见 `docs/07` §4.2)。启用 AI 穿搭需要为页签接一个 `QStackedWidget`(「1 添加印花」=现有工作区,「2 AI 穿搭」=本面板);这部分在实现阶段一并补。
### 10.1 已知问题:左栏内容被中间面板裁掉(下拉框撑宽)
> **§17 更新:已作废。** 撑宽元凶「样本行下拉」随预览一并移除(§17);保留本节仅作历史记录。标题模型改为「配置定名」、不放界面(§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 更新:已作废。** 预览与样本行下拉已移除(§17),本问题不复存在。`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 更新:改进 2 作废**(预览样本已移除,§17);**改进 1(无待处理行提示)继续有效**。
承接 §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/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 穿搭页**左栏**新增一个**独立的「生成标题」**流程:用户写标题提示词,AI **看该行衣服图**(视觉)生成电商标题,**写回 Excel A 列**,再点「开始生成」跑图时图片提示词的 `{title}` 即用上新标题。与「开始生成」(图片)**互不绑定**、各自一个按钮。
### 17.1 数据流与回填语义
- **行来源**:复用 `excel_service.read_all_rows(excel)`(状态无关)——对全部有效行生成、**覆盖式**写 A,不看 E 列状态、不引入新列(保持与 `标题生成产品图.xlsx` 七列一致)。
- **逐行看图、各生成 1 条、顺序回填**:按行顺序处理,第 i 行看第 i 行衣服图 → 生成 1 条标题 → **立即**写回第 i 行 A(`write_title_result(excel, row_index, title)`)→ 刷新中栏「处理明细」表该行「标题」列。即「第 n 条标题 → 第 n 行 A,顺序回填 + GUI 实时刷新」。
- **目录行取首图**:C 列为图片目录时(印花 Excel 即如此),标题写回**一个 A 单元格**(整行一个标题),故只取 `ai_outfit.list_directory_images(dir)` 的**第一张**作视觉参考;标题提示词宜写成概括款式/印花的通用句式。
- **条数对齐**:因逐行各生成 1 条,标题数永远 = 行数,不存在「AI 返回条数和行数对不上」的兜底问题。提示词即使写「生成 N 条」,每行也**只取第一条**(解析时取首个非空行、去行首序号/符号与首尾引号、单行化)。
- **"覆盖图片组件的标题" = 写回 A + 重载**:图片生成在「开始生成」时当场 `load_outfit_tasks(excel)` 读 A 列,故标题写回 A 后无需另改图片组件;标题全部生成完**重新加载一次 Excel**,刷新界面与内存任务,紧接着「开始生成」即用新标题。
### 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_text(prompt, image_path=None) -> str`:
- `chat`:`messages=[{role:user, content:[{type:text,text:prompt}, {type:image_url,...}]}]`(带图=视觉)。
- `gemini`:`contents[].parts=[{text},{inlineData}]`,`generationConfig.responseModalities=["TEXT"]`。
- `images`/`images_edits`:纯图片接口,不能返回文字 → 抛 `AiTextServiceError`,提示「该模型不能生成文字,请改选文本/视觉模型」。
- `extract_text_from_response(data)`:取 `choices[0].message.content`(str 或 content 列表的 text)/ gemini `candidates[0].content.parts[].text`;都取不到则抛错;返回**第一条标题**(见 §17.1 解析规则)。
### 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),由 `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`,仿旧式单份,标题侧不做多套模板);默认文案面向电商女装(结合款式/版型/颜色/印花,输出一行中文标题,不加引号/表情/促销词)。
### 17.4 界面与运行
- **左栏布局**(§10 已同步):「标题生成」组在上、「穿搭生成话术」组在下;预览块移除。标题组含:标题提示词编辑、「保存」、「生成标题」(**无标题模型下拉**,模型由 `title_model` 配置定名,§17.3)。
- **运行**:`_TitleWorker(QObject)` 跑在 `QThread`(仿 `_OutfitWorker`),按行顺序逐行生成、立即回填、刷新该行 GUI;失败记日志、跳过该行、继续;用右栏「新请求间隔」做行间节流;温和停止。模型用 `_resolve_title_model_config()`(按 `title_model` 名字查 `ai_models.json`)。
- **互斥**:「生成标题」与「开始生成」运行时互斥(避免同表并发写)。
- 进度/结果走中栏「处理明细」表与右栏日志(不写 Excel 状态列)。
### 17.5 验收要点(标题生成)
- 左栏有「标题生成」组(提示词 + 保存 + 生成标题按钮),**无标题模型下拉**、无预览块。
- `app_config.title_model` 指向的模型在 `ai_models.json` 存在 → 生成走该模型;改 `title_model` 名字即换模型,无需改代码。
- 选印花生成的 Excel(A=印花名、C=目录),点「生成标题」→ 每行 A 被改写为 AI 标题、明细表标题列实时刷新、D/E/F 未动。
- `title_model` 找不到对应模型 / 指向纯图片接口(images/images_edits)时明确报错提示。
- 生成完重载 Excel,紧接「开始生成」跑图用的是新标题。
- 文本解析、`generate_title`(单文件/目录取首图/无图失败/客户端异常)、`write_title_result`(只改 A)、`_resolve_title_model_config`(命中/缺失报错)单测通过,Python 3.7。
### 17.6 决策:标题模型「配置定名」而非下拉
最初实现(§19.18)给标题模型放了独立下拉。落地后复盘改为**配置定名、去下拉**:
- **去下拉的理由**:标题模型是「配一次就固定」的(一个店铺/平台通常固定一个文本模型),不像分辨率/话术需要每次切换;左栏仅 ~360px、已叠两个提示词组,少一个下拉更干净;还消掉「误选图片模型导致报错」的坑。
- **为什么不把模型名写死在代码里**:写死后换模型/升级要改 `.py` 重新打包分发。改放 `app_config.title_model`(默认 `GPT-5.5 文本`)后,换模型只改配置一行、重启生效,对非技术管理员友好。
- **界面效果一致**:两种做法用户都看不到下拉、都固定一个模型;差别只在「换模型」的代价(改配置 vs 改代码)。
- 标题提示词暂不做多套模板(同 §17 决策;真有多套切换需求再按 §7.2 范式补)。