refactor(ai-outfit): 标题生成改为「一次请求·纯提示词·多条按序填」 (§19.22)
把标题生成从「逐行看图、各生成1条、每行一次请求」改为一次请求、纯提示词 (不传图)、生成多条、按序回填(数量由用户写进提示词,docs/11 §17.1/§17.7)。 - ai_text_service:新增 extract_titles_from_response(多行→多条、逐行去 序号/引号、丢空)+ AiTextClient.generate_texts(一次POST返回多条,与 generate_text 共用 _post);generate_text/extract_text 改为取首条 - ai_title:generate_titles(一次请求、image_path=None 纯文本);移除 generate_title/_reference_image/render_title_prompt(不再逐行看图/替换占位符) - config_service:DEFAULT_TITLE_PROMPT 改批量风格(生成多条、每行一条) - 面板 _TitleWorker:一次 generate_titles → 按序 write_title_result 回填; N>行数多的丢+日志、N<行数后面行留空+日志;请求异常→日志+成功0;去掉行间节流 - _start_title 不再传 request_interval 测试:extract_titles 多行→多条/去序号引号、generate_texts 多条且无图、 generate_titles 一次请求/异常。全套 py37 通过(test_config_service 的 packaging 模板失败属并行 §19.13,与本改动无关)。离屏冒烟:3/3 精确、 5/3 丢弃、2/3 留空 三种分发均按序回填 Excel A + 日志正确。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+35
-17
@@ -420,24 +420,30 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
|
||||
## 17. 标题生成(左栏独立功能)
|
||||
|
||||
「添加印花」批量导出生成的 Excel(`docs/02` §6.12),A 列「标题」只是占位的**印花名**,不是真正的电商标题。本功能在 AI 穿搭页**左栏**新增一个**独立的「生成标题」**流程:用户写标题提示词,AI **看该行衣服图**(视觉)生成电商标题,**写回 Excel A 列**,再点「开始生成」跑图时图片提示词的 `{title}` 即用上新标题。与「开始生成」(图片)**互不绑定**、各自一个按钮。
|
||||
「添加印花」批量导出生成的 Excel(`docs/02` §6.12),A 列「标题」只是占位的**印花名**,不是真正的电商标题。本功能在 AI 穿搭页**左栏**新增一个**独立的「生成标题」**流程:用户写标题提示词(含数量),**一次请求、纯提示词(不传图)生成多条电商标题**,**按序回填 Excel A 列**(第 i 条 → 第 i 行),再点「开始生成」跑图时图片提示词的 `{title}` 即用上新标题。与「开始生成」(图片)**互不绑定**、各自一个按钮。
|
||||
|
||||
### 17.1 数据流与回填语义
|
||||
### 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.7 改版**:早期为「逐行看图、各生成 1 条」(每行一次请求、带图)。现改为
|
||||
> **一次请求、纯提示词(不传图)、生成多条、按序回填**(用户在提示词里自写数量)。
|
||||
|
||||
- **行来源**:复用 `excel_service.read_all_rows(excel)`(状态无关)——拿到全部有效行(仅为「填到哪些行 + 行号」),**覆盖式**写 A,不看 E 列状态、不引入新列。
|
||||
- **一次请求、纯提示词**:把用户标题提示词**原样**(不带图、不替换 `{title}`)发一次给文本模型;提示词里由用户自写数量(如「生成 6 条…每行一条」)。
|
||||
- **解析多条**:从响应里把文本**按行**拆成多条,每条清洗(去行首序号/符号 `1. / - / ①`、去首尾引号、单行化、丢空行)→ 得到标题列表。
|
||||
- **按序回填**:第 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_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 解析规则)。
|
||||
- `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` 的原始文本,**按行拆分 + 逐行清洗**(去序号/符号/引号、单行化、丢空),返回标题列表。`extract_text_from_response` = 取其首条(兼容保留)。
|
||||
|
||||
### 17.3 模型与提示词
|
||||
|
||||
@@ -449,12 +455,12 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
- **命中的模型是纯图片接口(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`,仿旧式单份,标题侧不做多套模板);默认文案面向电商女装(结合款式/版型/颜色/印花,输出一行中文标题,不加引号/表情/促销词)。
|
||||
- **标题提示词**:单份,存 `~/.cmbot/config/title_prompt.txt`(`load_title_prompt`/`save_title_prompt`,仿旧式单份,标题侧不做多套模板);默认文案为**批量风格**——让模型生成**多条**电商女装标题、**每行一条**、不带序号/引号/表情,数量由用户在提示词里写(默认示例可写「生成 10 条」)。
|
||||
|
||||
### 17.4 界面与运行
|
||||
|
||||
- **左栏布局**(§10 已同步):「标题生成」组在上、「穿搭生成话术」组在下;预览块移除。标题组含:标题提示词编辑、「保存」、「生成标题」(**无标题模型下拉**,模型由 `title_model` 配置定名,§17.3)。
|
||||
- **运行**:`_TitleWorker(QObject)` 跑在 `QThread`(仿 `_OutfitWorker`),按行顺序逐行生成、立即回填、刷新该行 GUI;失败记日志、跳过该行、继续;用右栏「新请求间隔」做行间节流;温和停止。模型用 `_resolve_title_model_config()`(按 `title_model` 名字查 `ai_models.json`)。
|
||||
- **运行**:`_TitleWorker(QObject)` 跑在 `QThread`(仿 `_OutfitWorker`),**一次请求**(纯提示词,无图)拿到标题列表 → **按序逐条回填** `write_title_result` + 刷新该行 GUI;条数对不上按 §17.1 兜底(多丢、少留空、记日志);请求失败记日志并以「成功 0」收尾;温和停止。模型用 `_resolve_title_model_config()`(按 `title_model` 名字查 `ai_models.json`)。**只有一次请求,不再用「新请求间隔」做行间节流**。
|
||||
- **互斥**:「生成标题」与「开始生成」运行时互斥(避免同表并发写)。
|
||||
- 进度/结果走中栏「处理明细」表与右栏日志(不写 Excel 状态列)。
|
||||
|
||||
@@ -462,10 +468,11 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
|
||||
- 左栏有「标题生成」组(提示词 + 保存 + 生成标题按钮),**无标题模型下拉**、无预览块。
|
||||
- `app_config.title_model` 指向的模型在 `ai_models.json` 存在 → 生成走该模型;改 `title_model` 名字即换模型,无需改代码。
|
||||
- 选印花生成的 Excel(A=印花名、C=目录),点「生成标题」→ 每行 A 被改写为 AI 标题、明细表标题列实时刷新、D/E/F 未动。
|
||||
- `title_model` 找不到对应模型 / 命中条目是纯图片接口(images/images_edits)时,**开跑前**弹窗报错并中止(不逐行失败,§19.21)。
|
||||
- 选印花生成的 Excel,点「生成标题」→ **一次请求**返回多条 → 按序回填各行 A、明细表标题列实时刷新、D/E/F 未动。
|
||||
- 条数对不上:N>行数多的丢弃 + 日志;N<行数后面行留空 + 日志(不报错中止)。
|
||||
- `title_model` 找不到对应模型 / 命中条目是纯图片接口(images/images_edits)时,**开跑前**弹窗报错并中止(§19.21)。
|
||||
- 生成完重载 Excel,紧接「开始生成」跑图用的是新标题。
|
||||
- 文本解析、`generate_title`(单文件/目录取首图/无图失败/客户端异常)、`write_title_result`(只改 A)、`_find_title_model`(命中/缺失/图片模型报错)单测通过,Python 3.7。
|
||||
- `extract_titles_from_response`(多行→多条、清洗)、`generate_titles`(一次请求纯文本、客户端异常)、`write_title_result`(只改 A)、`_find_title_model`(命中/缺失/图片模型报错)单测通过,Python 3.7。
|
||||
|
||||
### 17.6 决策:标题模型「配置定名」而非下拉
|
||||
|
||||
@@ -475,3 +482,14 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
- **为什么不把模型名写死在代码里**:写死后换模型/升级要改 `.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)`。
|
||||
|
||||
Reference in New Issue
Block a user