2026-06-18 16:30:26 +08:00
# 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=`失败` 的行由界面「重试失败行」决定(勾选则处理「空白 + 失败」,否则只处理「空白」)。
- 标题/货号/衣服图任一为空 → 跳过该行,不写状态、不中断。
- 字段按原样读取,不清理空格、不回写清理值。
- **开始前检测 Excel 是否被占用**( Office 打开会锁文件,openpyxl 无法写回)→ 提示「请关闭 Excel 后再开始」。
- **每处理完一行即保存 Excel**(降低崩溃丢结果风险)。
- 成功 → D=新图绝对路径、E=`完成` ;失败 → E=`失败` 、F=原因(同步写日志)。
## 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` 时阻止开始)。
> 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` ,界面可查看/编辑/保存;点「开始」前自动保存一次。
2026-06-22 09:16:04 +08:00
- 占位符:仅 `{title}` (标题);界面只提供「插入标题」按钮,把占位符插入光标处。货号(`product_id` )不进提示词——它只用于 Excel B 列读取与输出文件命名(`货号.jpg` ),不影响 AI 生成内容。(`render_prompt` 仍会替换偶然出现的 `{product_id}` ,保持兼容,但界面不再引导插入。)
2026-06-18 17:11:56 +08:00
- **「保存话术」按钮**:把当前编辑的模板原文(占位符原样保留,**不**存替换后的结果)经 `config_service` 写入 `outfit_prompt.txt` 。用途是"改完先存、暂不开跑"并给用户明确反馈;与"开始前自动保存"并存、互为兜底(保留按钮是有意为之,对非技术用户更安心)。
2026-06-22 09:35:52 +08:00
- **最终提示词预览(内嵌、不弹窗)**:左栏话术编辑下方常驻一块只读预览区,编辑话术时实时把占位符替换成样本行真值,显示最终要发送给 AI 的完整文字。样本行下拉默认取明细表当前选中行、没选则取第一条待处理行;未选 Excel / 无可预览行时显示带高亮占位符的模板原文 + 提示;缺 `{title}` 占位符时在预览区提示。预览只读,不改话术本身。(早期设计为弹窗 `QDialog` ,因调话术需"边改边看"改为内嵌。)
2026-06-18 16:30:26 +08:00
- 缺标题占位符时开始前弹窗询问是否继续。
- 默认话术方向(穿搭/上身,区别于旧项目的主图场景,需另调):人物上身实穿、保留衣服款式/版型/颜色/印花、合适身材与场景、电商可用、默认纯净不加促销牛皮癣。
2026-06-22 10:35:11 +08:00
### 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` 生成时传入当前分辨率。
- **左栏「最终提示词预览」必须包含这段**(带当前选中分辨率),否则预览与实际发送不一致;分辨率下拉变化时预览同步刷新。
2026-06-22 11:52:10 +08:00
### 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 写、损坏回退「默认」那套的约定)。
2026-06-22 14:36:38 +08:00
**界面** (「穿搭生成话术」组,套印花页范式)
2026-06-22 11:52:10 +08:00
2026-06-22 14:36:38 +08:00
- 编辑框**上方一行**:「穿搭生成话术」标签 + 模板下拉;下一行放「新建」「另存为」「重命名」「删除」。
2026-06-22 11:52:10 +08:00
- 编辑框**下方**:保留「插入标题」「保存」。
- 左栏约 360px,5 个模板按钮偏多 → 实现时可排两行或用紧凑小按钮。
**行为**
- **切换 / 重命名 / 删除 / 新建 / 另存为之前**:若编辑框与当前套已存文本不同(脏)→ 弹「是否保存当前修改?」(保存 / 不保存 / 取消;取消则下拉还原到原选项)。
- 保存=覆盖当前套;另存为=存为新名并选中;重命名=改当前套名;删除=二次确认,删后选邻近,**不可删到 0**。
- 名字**唯一**(重名拒绝或自动加序号)。
- 记住上次所选;生成/预览用编辑框当前文本(`render_prompt` + §7.1 尾巴不变);「开始生成」前把当前编辑存回所选套(沿用现有自动存逻辑)。
**解耦** :核心 `render_prompt(text, task, resolution)` 不动;"用哪段文本"由面板的模板选择决定。
2026-06-18 16:30:26 +08:00
## 8. 并发、限速、重试、停止
复用旧项目策略(`docs/旧ai穿搭项目.md` §5.3):
2026-06-18 17:11:56 +08:00
- **并发由 Python 线程负责,PySide6 不参与**: `requests` 在等网络响应时释放 GIL,所以 `ThreadPoolExecutor` 起多线程跑行任务时多个 HTTP 请求是真并发。Qt 只负责把进度/结果通过 **signal 跨线程排队回主线程**刷新 UI——**子线程绝不直接操作控件** (违反会崩溃/随机出错)。默认并发 1 是迁就中转 API 限流(429),非框架限制,可在界面调高。
2026-06-18 16:30:26 +08:00
- `ThreadPoolExecutor(max_workers=并发数)` 调度行任务;`RateLimiter(新请求间隔)` 限制请求开始间隔;任务间 `单任务冷却` 。
- 每行最多「首次 + 重试次数」尝试;限流/429 用短阶梯等待,普通错误短等待。
- 单次请求放子线程 + 主线程秒级检查,**等待 >30 秒持续打心跳日志**;超时按分辨率动态决定(512/1K/2K/4K → 180/240/360/600 秒,可被 `timeout_seconds` 覆盖)。
- **温和停止**:置位停止后不再提交新任务,已发请求收尾后正常写回。
## 9. 输出
- 格式 JPG,1:1,压缩到 ≤2MB;质量三档(小文件 75 / 均衡 85 / 高清 92)。
- 默认输出目录沿用 cmbot `get_output_dir()` (程序旁的「合并后的图片」,见 `docs/10` §5),界面可改。
- 命名:`货号.jpg` ,重名自动 `_1` /`_2` ,非法字符替换为 `_` (不改 Excel 原始货号)。
- 成功后把**实际新图绝对路径**写回 Excel D 列。
## 10. 界面(「2 AI 穿搭」页签)
2026-06-18 17:11:56 +08:00
界面效果图见 ** `docs/ui-ai-outfit.html` **(浏览器打开)/ ** `docs/ui-ai-outfit.png` **,完全沿用 cmbot 现有视觉、与「1 添加印花」严格统一(同 `docs/ui-v1` :浅灰底、`#0067c0` 蓝单一主色、12px 雅黑、3px 圆角、pill 状态徽章;不引入第二识别色)。「生成中」状态用蓝,与印花页「导出中」同色。把主窗口当前禁用的「2 AI 穿搭」页签启用,做成独立工作页(与「1 添加印花」并列、互不干扰)。
2026-06-18 16:30:26 +08:00
2026-06-18 17:11:56 +08:00
三栏布局:
2026-06-22 14:36:38 +08:00
- **左栏(数据源 + 话术创作,~360px)**:`Excel` / `输出` 各为**行内一行**(标签 + 路径 + 浏览,省纵向空间);数据源概览(共 N 行 / 完成 / 待处理 / 失败);**加大的**穿搭生成话术编辑 +「保存话术」+「插入标题」;下方常驻**加大的最终生成要求预览**(内嵌、实时,标题行右侧放样本行下拉,含 §7.1 自动附加的输出要求)。AI 模型下拉不在此(见右栏)。
2026-06-18 17:11:56 +08:00
- **中栏(结果 + 明细)**:顶部「最近结果」缩略图条——**只展示已完成的人物效果图**,新图自动加到最左、首图标「最新」,单击看大图、右键开所在文件夹;下方「处理明细」表(行 / 标题 / 货号 / 衣服图 / 状态 / 结果或原因),按 Excel 行顺序,状态用 完成 / 失败 / 生成中 / 待处理 / 跳过 徽章。
2026-06-22 11:01:23 +08:00
- **右栏(设置 + 运行,~400px)**:生成设置(重试失败行 + 并发数 / 新请求间隔 / 单任务冷却 / 失败重试 / 分辨率 / JPG 质量,**3 列 × 2 行**);其下 **AI 模型下拉** (来自 AI 模型配置);本次进度 + 统计(完成 / 失败 / 待处理);开始生成 / 停止生成;导出失败清单 / 打开输出目录;实时日志(含 §8 心跳行)。
2026-06-18 17:11:56 +08:00
> **设计取舍:不做"实时单图大预览"。** 这是"开了走人、回头抽查"的批量工具;单图实时预览在并发时会产生"该显示哪一行"的歧义。改为「最近结果缩略图条」——只展示已落地成品,既保留"早发现话术/模型不对、及时停掉改话术"的价值,又因只显示成品而消除并发歧义。
- 后台用 `QThread` + `Worker(QObject)` + 信号回主线程(与 cmbot 更新检查/导出一致;并发细节见 §8)。
2026-06-18 16:30:26 +08:00
> 页签内容切换:当前页签栏不切换内容面板(见 `docs/07` §4.2)。启用 AI 穿搭需要为页签接一个 `QStackedWidget`(「1 添加印花」=现有工作区,「2 AI 穿搭」=本面板);这部分在实现阶段一并补。
2026-06-22 10:04:24 +08:00
### 10.1 已知问题:左栏内容被中间面板裁掉(下拉框撑宽)
**现象** :「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]` ),非根因。
2026-06-22 11:26:52 +08:00
### 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。不同模型的计费与效果可能不同。仅在下次「开始生成」生效。
- **频率**:每次实际更换都弹一次(不是整会话只弹一次)。
- **理由**:下拉本可逆,不做强制确认(反模式);但分辨率/模型有"慢 / 贵 / 下次才生效"的隐含代价,一次性告知最划算。
2026-06-18 16:30:26 +08:00
## 11. 配置与数据位置
遵循 cmbot「配置集中、放数据目录、凭据不入库」约定:
- **AI 模型与密钥** → `~/.cmbot/config/ai_models.json` (管理员预置或界面填写,**含明文 key、不提交 git**;与更新源凭据同等对待,见 `docs/10` §14)。
- **提示词** → `~/.cmbot/config/outfit_prompt.txt` 。
- **批量设置 + 上次 Excel/输出路径** → 并入 `app_config.json` ( `config_service` 集中读写,UI 不直接读写配置文件,遵守 `docs/04` 第 6 节 / `docs/05` 4.12)。
- 失败记录 / 日志沿用 `~/.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 被占用时明确提示。
- 并发/限速/重试/温和停止生效;实时日志 + 进度 + 结束摘要 + 失败清单导出。
- 日志写入 `~/.cmbot/logs` ; AI 密钥不入库。
- 核心逻辑单测通过、不依赖 GUI、Python 3.7 可运行。