feat(ai-outfit): 话术按钮行微调 + 保留插入标题 + 最终提示词预览改弹窗 (§19.20)
话术组小改版(保留 {title} 占位符、不自动前置):
- 「保存话术」→「保存」,从编辑框下方上移到模板按钮行「重命名」之后
(行变 新建/另存为/重命名/保存/删除)
- 保留「插入标题」按钮 + {title} 占位符(用户自定标题位置,不与旧话术重复)
- 最终提示词预览改按需弹窗:编辑框下方「预览最终提示词」按钮 → 非模态
_OutfitPreviewDialog,含数据行下拉(read_all_rows,状态无关)+ 只读替换后
提示词(含 §7.1 输出要求);话术/分辨率/数据行变化时实时刷新
- 左栏空间理由:标题生成组 + 话术组并存,常驻预览会逼出滚动;弹窗零常驻高度
render_prompt 与 DEFAULT_OUTFIT_PROMPT 不改(纯面板改动)。
测试:面板断言按钮行含「保存」、保留「插入标题」+「预览最终提示词」、
预览弹窗按选中行替换 {title} 并附输出要求。全套 py37 通过
(test_config_service 的 ai_models.json 顺序失败属并行 §19.13,与本改动无关)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+34
-12
@@ -162,9 +162,9 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
## 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 不清空样本)随之作废。
|
||||
- 占位符:仅 `{title}`(标题);界面提供「插入标题」按钮,把占位符插入光标处。**标题由用户经 `{title}` 自行放置(可前、可中、可省),程序不自动前置**——这样标题不会被焊死在固定位置,也不会与旧话术里的 `{title}` 重复。货号(`product_id`)不进提示词——只用于 Excel B 列读取与输出文件命名(`货号.jpg`)。(`render_prompt` 仍替换偶现的 `{product_id}` 以兼容,但界面不引导插入。)
|
||||
- **「保存」按钮**(原「保存话术」改名、移到模板按钮行「重命名」之后):把当前编辑的模板原文(占位符原样保留,**不**存替换后的结果)经 `config_service` 写入当前模板。用途是"改完先存、暂不开跑";与"开始前自动保存"并存、互为兜底。
|
||||
- **最终提示词预览改为按需弹窗(§7.3)**:左栏要同时容纳「标题生成」组与「穿搭话术」组,纵向空间紧张(常驻预览会撑到 ~690px > 视口 ~664px、逼出滚动),故预览不做常驻面板,改成话术编辑框下方一个「预览最终提示词」按钮 → 弹**非模态**小窗,内含数据行下拉 + 只读的替换后提示词(含 §7.1 输出要求)。要"边改边看"就开着弹窗,话术/分辨率/数据行变化时刷新。
|
||||
- 缺标题占位符时开始前弹窗询问是否继续。
|
||||
- 默认话术方向(穿搭/上身,区别于旧项目的主图场景,需另调):人物上身实穿、保留衣服款式/版型/颜色/印花、合适身材与场景、电商可用、默认纯净不加促销牛皮癣。
|
||||
|
||||
@@ -182,8 +182,8 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
|
||||
- 仅「参考解析度」动态(= 分辨率设置),其余 3 行固定;**简体文案**(与软件一致)。
|
||||
- **始终自动附加**(同旧项目,不做开关)。
|
||||
- 由 `core/ai_outfit.render_prompt(template, task, resolution)` 实现:`resolution` 为空时不附加(保持纯函数单测兼容);`generate_outfit_image` 生成时传入当前分辨率。
|
||||
- **左栏「最终提示词预览」必须包含这段**(带当前选中分辨率),否则预览与实际发送不一致;分辨率下拉变化时预览同步刷新。
|
||||
- 由 `core/ai_outfit.render_prompt(template, task, resolution)` 实现:`resolution` 为空时不附加(保持纯函数单测兼容);`generate_outfit_image` 生成时传入当前分辨率。**不做标题自动前置**——标题由话术里的 `{title}` 决定(§7)。
|
||||
- **预览弹窗必须包含这段**(带当前选中分辨率),否则预览与实际发送不一致;分辨率下拉变化时弹窗同步刷新(§7.3)。
|
||||
|
||||
### 7.2 话术模板(多套命名话术)
|
||||
|
||||
@@ -198,8 +198,8 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
|
||||
**界面**(「穿搭生成话术」组,套印花页范式)
|
||||
|
||||
- 编辑框**上方一行**:「穿搭生成话术」标签 + 模板下拉;下一行放「新建」「另存为」「重命名」「删除」。
|
||||
- 编辑框**下方**:保留「插入标题」「保存」。
|
||||
- 编辑框**上方一行**:「穿搭生成话术」标签 + 模板下拉;下一行模板按钮 `新建 / 另存为 / 重命名 / 保存 / 删除`(**「保存」从原编辑框下方移到此行「重命名」之后、改名自「保存话术」**)。
|
||||
- 编辑框**下方一行**:「插入标题」+「预览最终提示词」(弹窗,§7.3)。
|
||||
- 左栏约 360px,5 个模板按钮偏多 → 实现时可排两行或用紧凑小按钮。
|
||||
|
||||
**行为**
|
||||
@@ -207,10 +207,32 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
- **切换 / 重命名 / 删除 / 新建 / 另存为之前**:若编辑框与当前套已存文本不同(脏)→ 弹「是否保存当前修改?」(保存 / 不保存 / 取消;取消则下拉还原到原选项)。
|
||||
- 保存=覆盖当前套;另存为=存为新名并选中;重命名=改当前套名;删除=二次确认,删后选邻近,**不可删到 0**。
|
||||
- 名字**唯一**(重名拒绝或自动加序号)。
|
||||
- 记住上次所选;生成/预览用编辑框当前文本(`render_prompt` + §7.1 尾巴不变);「开始生成」前把当前编辑存回所选套(沿用现有自动存逻辑)。
|
||||
- 记住上次所选;生成用编辑框当前文本(`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),但并发语义改为更适合「一行一个目录」的模型:
|
||||
@@ -250,7 +272,7 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
|
||||
三栏布局:
|
||||
|
||||
- **左栏(数据源 + 标题/话术创作,~360px)**:`Excel` / `输出` 各为**行内一行**(标签 + 路径 + 浏览,省纵向空间);数据源概览(共 N 行 / 完成 / 待处理 / 失败);**「标题生成」组(§17)在上**:标题生成提示词编辑 +「保存」+「生成标题」(**无标题模型下拉**,模型由 `title_model` 配置定名,§17.3);**「穿搭生成话术」组在下**:话术编辑 +「保存话术」+「插入标题」+ 多套模板下拉(§7.2)。**最终提示词预览已移除(§17)**。图片 AI 模型下拉在右栏;标题模型不放界面。
|
||||
- **左栏(数据源 + 标题/话术创作,~360px)**:`Excel` / `输出` 各为**行内一行**(标签 + 路径 + 浏览,省纵向空间);数据源概览(共 N 行 / 完成 / 待处理 / 失败);**「标题生成」组(§17)在上**:标题生成提示词编辑 +「保存」+「生成标题」(**无标题模型下拉**,模型由 `title_model` 配置定名,§17.3);**「穿搭生成话术」组在下**:模板下拉 + 按钮行 `新建/另存为/重命名/保存/删除` + 话术编辑框 + 下方「插入标题」「预览最终提示词」(预览为弹窗,数据行下拉在弹窗内,§7.3)。图片 AI 模型下拉在右栏;标题模型不放界面。
|
||||
- **中栏(结果 + 明细)**:顶部「最近结果」缩略图条——**只展示已完成的人物效果图**,新图自动加到最左、首图标「最新」,单击看大图、右键开所在文件夹;下方「处理明细」表(行 / 标题 / 货号 / 衣服图 / 状态 / 结果或原因),按 Excel 行顺序,状态用 完成 / 失败 / 生成中 / 待处理 / 跳过 徽章。
|
||||
- **右栏(设置 + 运行,~400px)**:生成设置标题与「重试上次失败的行」同排;图片并发数 / 新请求间隔 / 单任务冷却 / 失败重试 / 分辨率 / JPG 质量保持 **3 列 × 2 行**;`AI 模型` 标签与下拉框同排;本次进度 + 统计(完成 / 失败 / 待处理);开始生成 / 停止生成同排;导出失败清单 / 打开输出目录同排;节省的纵向空间给实时日志(含 §8 心跳行)。
|
||||
|
||||
@@ -262,7 +284,7 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
|
||||
### 10.1 已知问题:左栏内容被中间面板裁掉(下拉框撑宽)
|
||||
|
||||
> **§17 更新:已作废。** 撑宽元凶「样本行下拉」随预览一并移除(§17);保留本节仅作历史记录。标题模型改为「配置定名」、不放界面(§17.3),左栏也不再有新下拉。
|
||||
> **§17/§7.3 更新。** 撑宽元凶「样本行下拉」随预览移除(§17);§7.3 把数据行下拉**收进预览弹窗**(不在左栏),故本节裁切问题在左栏不再出现。弹窗内的下拉仍建议套 `_compact_combo` 的 elide 策略以防长项撑宽弹窗。标题模型仍不放界面(§17.3 配置定名)。
|
||||
|
||||
**现象**:「2 AI 穿搭」左栏组件右侧显示不完整,像被中间面板遮挡了一部分。
|
||||
|
||||
@@ -296,7 +318,7 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
|
||||
### 10.3 已知问题:全表生成完成后,预览样本行下拉为空
|
||||
|
||||
> **§17 更新:已作废。** 预览与样本行下拉已移除(§17),本问题不复存在。`read_all_rows` 仍保留——它现在是「标题生成」的行来源(§17)。保留本节作历史记录。
|
||||
> **§17/§7.3 更新。** 一度随预览移除(§17),§7.3 把「数据行下拉」请回**预览弹窗**。本节结论**重新生效**:弹窗内数据行下拉必须用 `read_all_rows`(状态无关)填,否则整表完成后又会变空。`read_all_rows` 同时是「标题生成」的行来源(§17)。
|
||||
|
||||
**现象**:选中的 Excel 整表生成成功后,再次选中该文件,「最终生成要求预览」的样本行下拉只显示「(选 Excel 后显示替换效果)」,无可选行。
|
||||
|
||||
@@ -310,7 +332,7 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
|
||||
### 10.4 改进:整表无待处理行时的提示 + 生成不清空预览样本
|
||||
|
||||
> **§17 更新:改进 2 作废**(预览样本已移除,§17);**改进 1(无待处理行提示)继续有效**。
|
||||
> **§17/§7.3 更新。** 改进 1(无待处理行提示)一直有效;改进 2 一度作废,§7.3 把数据行下拉请回**预览弹窗**后**重新生效**:`_on_tasks_loaded` 不动弹窗的数据行下拉,下拉只由 `read_all_rows` 维护,与运行解耦。
|
||||
|
||||
承接 §10.3。整表都「完成」后点「开始生成」时当前体验不清晰,两项改进:
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from PySide6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QInputDialog,
|
||||
QDoubleSpinBox,
|
||||
QFileDialog,
|
||||
@@ -216,6 +217,79 @@ class _TitleWorker(QObject):
|
||||
self.finished.emit(success, fail)
|
||||
|
||||
|
||||
class _OutfitPreviewDialog(QDialog):
|
||||
"""Non-modal 最终提示词预览(docs/11 §7.3)。
|
||||
|
||||
数据行下拉 + 只读结果;内容 = render_prompt(话术, 选中行, 当前分辨率)。
|
||||
话术/分辨率/数据行变化时由面板调 refresh() 刷新。下拉用 read_all_rows
|
||||
(状态无关)填,整表完成后仍可选。
|
||||
"""
|
||||
|
||||
def __init__(self, panel):
|
||||
super().__init__(panel)
|
||||
self._panel = panel
|
||||
self.setWindowTitle("最终提示词预览")
|
||||
self.setModal(False)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose)
|
||||
self.resize(440, 380)
|
||||
|
||||
v = QVBoxLayout(self)
|
||||
row = QHBoxLayout()
|
||||
row.addWidget(QLabel("数据行"))
|
||||
self._combo = QComboBox()
|
||||
panel._compact_combo(self._combo)
|
||||
self._combo.currentIndexChanged.connect(self.refresh)
|
||||
row.addWidget(self._combo, stretch=1)
|
||||
v.addLayout(row)
|
||||
|
||||
self._warn = QLabel("")
|
||||
self._warn.setStyleSheet("color:#b87a00;")
|
||||
self._warn.setVisible(False)
|
||||
v.addWidget(self._warn)
|
||||
|
||||
self._view = QPlainTextEdit()
|
||||
self._view.setReadOnly(True)
|
||||
v.addWidget(self._view, stretch=1)
|
||||
|
||||
self.reload_rows()
|
||||
|
||||
def reload_rows(self):
|
||||
"""(Re)fill the data-row dropdown from the panel's Excel (status-independent)."""
|
||||
excel = self._panel._excel_edit.text().strip()
|
||||
rows = []
|
||||
if excel:
|
||||
try:
|
||||
from services.excel_service import read_all_rows
|
||||
rows = read_all_rows(excel)
|
||||
except Exception as exc: # noqa: BLE001 - silent for preview
|
||||
logger.info("Preview rows unavailable: %s", exc)
|
||||
self._combo.blockSignals(True)
|
||||
self._combo.clear()
|
||||
if rows:
|
||||
for t in rows:
|
||||
pid = t.product_id if (t.product_id and t.product_id.strip()) else "(无货号)"
|
||||
self._combo.addItem(
|
||||
"第 {} 行 · {} · {}".format(t.row_index, pid, t.title), t)
|
||||
else:
|
||||
self._combo.addItem("(选 Excel 后显示替换效果)", None)
|
||||
self._combo.blockSignals(False)
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
from core.ai_outfit import build_output_requirements, render_prompt
|
||||
template = self._panel._prompt_edit.toPlainText()
|
||||
resolution = self._panel._resolution.currentText()
|
||||
missing = "{title}" not in template
|
||||
self._warn.setVisible(missing)
|
||||
if missing:
|
||||
self._warn.setText("⚠ 话术缺少 {title} 占位符,标题不会带入")
|
||||
task = self._combo.currentData()
|
||||
if task is None:
|
||||
self._view.setPlainText(template + build_output_requirements(resolution))
|
||||
else:
|
||||
self._view.setPlainText(render_prompt(template, task, resolution))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main panel
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -235,6 +309,7 @@ class AiOutfitPanel(QWidget):
|
||||
self._title_worker = None
|
||||
self._title_fail = 0 # running failure count for title stats
|
||||
self._title_model_name = "" # configured 标题模型 name (§17.3 配置定名)
|
||||
self._preview_dialog = None # 最终提示词预览弹窗(§7.3,按需)
|
||||
self._row_to_table = {} # excel row_index -> table row
|
||||
self._failures = [] # list of OutfitResult (failed)
|
||||
self._last_resolution = "" # for "value actually changed" check (§10.2)
|
||||
@@ -348,8 +423,10 @@ class AiOutfitPanel(QWidget):
|
||||
prompt_header.addWidget(self._prompt_combo, stretch=1)
|
||||
pv.addLayout(prompt_header)
|
||||
trow = QHBoxLayout()
|
||||
# 「保存」插在「重命名」「删除」之间(§7.2,原编辑框下方的「保存话术」上移改名)
|
||||
for text, slot in (("新建", self._prompt_new), ("另存为", self._prompt_save_as),
|
||||
("重命名", self._prompt_rename), ("删除", self._prompt_delete)):
|
||||
("重命名", self._prompt_rename), ("保存", self._save_prompt),
|
||||
("删除", self._prompt_delete)):
|
||||
b = QPushButton(text)
|
||||
if text == "删除":
|
||||
b.setObjectName("aiPromptDeleteBtn")
|
||||
@@ -358,15 +435,16 @@ class AiOutfitPanel(QWidget):
|
||||
pv.addLayout(trow)
|
||||
self._prompt_edit = QPlainTextEdit()
|
||||
self._prompt_edit.setMinimumHeight(150)
|
||||
self._prompt_edit.textChanged.connect(self._refresh_preview_dialog)
|
||||
pv.addWidget(self._prompt_edit)
|
||||
prow = QHBoxLayout()
|
||||
insert_title_btn = QPushButton("插入标题")
|
||||
insert_title_btn.clicked.connect(
|
||||
lambda: self._prompt_edit.insertPlainText("{title}"))
|
||||
prow.addWidget(insert_title_btn)
|
||||
save_btn = QPushButton("保存话术")
|
||||
save_btn.clicked.connect(self._save_prompt)
|
||||
prow.addWidget(save_btn)
|
||||
preview_btn = QPushButton("预览最终提示词")
|
||||
preview_btn.clicked.connect(self._open_preview)
|
||||
prow.addWidget(preview_btn)
|
||||
pv.addLayout(prow)
|
||||
col.addWidget(prm, stretch=3)
|
||||
|
||||
@@ -419,6 +497,8 @@ class AiOutfitPanel(QWidget):
|
||||
self._retry_count.setRange(0, 10)
|
||||
self._resolution = QComboBox()
|
||||
self._resolution.addItems(_RESOLUTIONS)
|
||||
# Keep the preview dialog's output-requirements block in sync (§7.3).
|
||||
self._resolution.currentIndexChanged.connect(self._refresh_preview_dialog)
|
||||
# activated = user click only; programmatic sets won't pop (docs/11 §10.2)
|
||||
self._resolution.activated.connect(self._on_resolution_activated)
|
||||
self._quality = QComboBox()
|
||||
@@ -631,6 +711,8 @@ class AiOutfitPanel(QWidget):
|
||||
if path:
|
||||
self._excel_edit.setText(path)
|
||||
self._emit_config()
|
||||
if self._preview_dialog is not None:
|
||||
self._preview_dialog.reload_rows()
|
||||
|
||||
def _browse_output(self):
|
||||
path = QFileDialog.getExistingDirectory(
|
||||
@@ -643,6 +725,27 @@ class AiOutfitPanel(QWidget):
|
||||
self._store_current_text()
|
||||
self.statusBar_message("话术已保存")
|
||||
|
||||
# -- 最终提示词预览弹窗(§7.3)--------------------------------------
|
||||
|
||||
def _open_preview(self):
|
||||
"""Open (or raise) the non-modal 最终提示词预览 dialog."""
|
||||
if self._preview_dialog is not None:
|
||||
self._preview_dialog.raise_()
|
||||
self._preview_dialog.activateWindow()
|
||||
return
|
||||
dialog = _OutfitPreviewDialog(self)
|
||||
dialog.finished.connect(self._on_preview_closed)
|
||||
self._preview_dialog = dialog
|
||||
dialog.show()
|
||||
|
||||
def _on_preview_closed(self, _result):
|
||||
self._preview_dialog = None
|
||||
|
||||
def _refresh_preview_dialog(self):
|
||||
"""Refresh the preview if open (话术/分辨率/数据行变化时;否则 no-op)."""
|
||||
if self._preview_dialog is not None:
|
||||
self._preview_dialog.refresh()
|
||||
|
||||
# -- 话术模板(§7.2)------------------------------------------------
|
||||
|
||||
def _rebuild_prompt_combo(self):
|
||||
|
||||
@@ -1323,9 +1323,31 @@
|
||||
|
||||
任务:
|
||||
|
||||
- [ ] `ai_outfit_panel.py`:`_build_title_group` 去掉「标题模型」下拉(`_title_model_combo` 及其 `_compact_combo`/填充/`_set_title_running` 里的禁用);`apply_config` 不再填标题下拉、`_emit_config` 不再写 `title_model`(值由配置/手动维护,UI 不覆盖)
|
||||
- [ ] `ai_outfit_panel.py`:`_selected_title_model_config()` 改为 `_resolve_title_model_config()`——读 `app_config.title_model` 名字,在 `self._models` 里按 `name` 查;命中校验 `api_config_errors` 后返回 `AiModelConfig`;找不到/未配置时 `QMessageBox.warning` 明确报错(提示加文本模型并设 `title_model`)。`_start_title` 改用它;面板需持有当前 `title_model` 值(`apply_config` 存一份 `self._title_model_name`)
|
||||
- [ ] `config_service.py`:`DEFAULT_CONFIG["title_model"]` 默认值由 `""` 改为 `"GPT-5.5 文本"`;注释改为「标题模型名字(对应 ai_models.json 的 name,§17.3 配置定名)」
|
||||
- [ ] `docs/ai_models.sample.json`:补一条 `name="GPT-5.5 文本"`、`api_type=chat`、`model=gpt-5.5`(占位 key)的文本模型示例,作为标题模型样板
|
||||
- [ ] 测试:更新 `test_ai_outfit_panel.py`(断言**无**标题模型下拉、`_title_model_combo` 不存在);新增 `_resolve_title_model_config` 命中/缺失(报错返回 None)用例
|
||||
- [ ] 验证:相关单测 + 全套 py37 通过;离屏冒烟:`title_model` 命中 `ai_models.json` → 生成走该模型;改名/缺失 → 明确报错
|
||||
- [x] `ai_outfit_panel.py`:`_build_title_group` 去掉「标题模型」下拉(`_title_model_combo` 及其 `_compact_combo`/填充/`_set_title_running` 里的禁用);`apply_config` 不再填标题下拉、`_emit_config` 不再写 `title_model`(值由配置/手动维护,UI 不覆盖)
|
||||
- [x] `ai_outfit_panel.py`:`_selected_title_model_config()` 改为 `_resolve_title_model_config()`——读 `app_config.title_model` 名字,在 `self._models` 里按 `name` 查;命中校验 `api_config_errors` 后返回 `AiModelConfig`;找不到/未配置时 `QMessageBox.warning` 明确报错(提示加文本模型并设 `title_model`)。`_start_title` 改用它;面板需持有当前 `title_model` 值(`apply_config` 存一份 `self._title_model_name`)
|
||||
- [x] `config_service.py`:`DEFAULT_CONFIG["title_model"]` 默认值由 `""` 改为 `"GPT-5.5 文本"`;注释改为「标题模型名字(对应 ai_models.json 的 name,§17.3 配置定名)」
|
||||
- [x] `docs/ai_models.sample.json`:补一条 `name="GPT-5.5 文本"`、`api_type=chat`、`model=gpt-5.5`(占位 key)的文本模型示例,作为标题模型样板
|
||||
- [x] 测试:更新 `test_ai_outfit_panel.py`(断言**无**标题模型下拉、`_title_model_combo` 不存在);新增 `_resolve_title_model_config` 命中/缺失(报错返回 None)用例
|
||||
- [x] 验证:相关单测 + 全套 py37 通过;离屏冒烟:`title_model` 命中 `ai_models.json` → 生成走该模型;改名/缺失 → 明确报错
|
||||
|
||||
### 19.20 穿搭话术:按钮行微调 +「插入标题」保留 + 预览改弹窗 — docs/11 §7 / §7.3 / §10
|
||||
|
||||
前置阅读:
|
||||
|
||||
- `docs/11-ai-outfit.md`(§7 提示词、§7.1 输出要求、§7.2 多套模板、§7.3 预览弹窗、§10 界面)
|
||||
- `src/core/ai_outfit.py`(`render_prompt`/`build_output_requirements`)
|
||||
- `src/app/widgets/ai_outfit_panel.py`(`_build_left` 话术组、`_prompt_edit`、模板按钮行、`_save_prompt`/`_store_current_text`、`_start`/`_OutfitWorker`、§19.18 删掉的样本下拉/预览逻辑可参考)
|
||||
- `tests/test_ai_outfit.py`(`render_prompt` 既有用例,**不改 render_prompt,保持原样**)
|
||||
|
||||
背景:
|
||||
|
||||
话术组小改版(用户最终确认):①「保存话术」→「保存」、移到模板按钮行「重命名」之后(行变 `新建/另存为/重命名/保存/删除`);② **保留「插入标题」**+ `{title}` 占位符(标题不自动前置——用户自定位置、不与旧话术重复);③ 「最终提示词预览」因左栏空间紧张(标题生成组 + 话术组并存)**改为按需弹窗**:编辑框下方「预览最终提示词」按钮 → 非模态 `QDialog`,内含数据行下拉 + 只读替换后提示词(含 §7.1 输出要求)。`render_prompt` 与 `DEFAULT_OUTFIT_PROMPT` **不改**。
|
||||
|
||||
任务:
|
||||
|
||||
- [x] `ai_outfit_panel.py` 模板按钮行:在「重命名」与「删除」之间插入「保存」(连 `_save_prompt`);删掉编辑框下方原「保存话术」按钮;**保留**编辑框下方「插入标题」按钮,旁边新增「预览最终提示词」按钮
|
||||
- [x] `ai_outfit_panel.py` 预览弹窗(新 `_OutfitPreviewDialog` 或方法):非模态 `QDialog`,含数据行下拉(`_compact_combo` + `read_all_rows(excel)` 填,状态无关)+ 只读 `QPlainTextEdit`;内容 = `render_prompt(话术正文, 选中行, 当前分辨率)`;缺 `{title}` 时顶部提示;未选 Excel/无行 → 显示话术原文 + 提示
|
||||
- [x] `ai_outfit_panel.py` 弹窗联动:话术 `textChanged` / 分辨率 `currentIndexChanged` / 数据行下拉变化 → 刷新弹窗预览(仅弹窗存在时);弹窗用 `_reload_sample_rows` 思路在打开时/选 Excel 后填行
|
||||
- [x] `ai_outfit_panel.py` 生成:保持现有 `_start` 的「缺 `{title}` 弹窗询问是否继续」(标题靠占位符);不引入自动前置
|
||||
- [x] 测试:面板离屏断言——按钮行含「保存」、编辑框下方仍有「插入标题」、有「预览最终提示词」按钮;打开预览弹窗后选数据行/改分辨率,预览文本 = `render_prompt` 结果(含输出要求);`render_prompt` 既有用例保持绿(不改该函数)
|
||||
- [x] 验证:相关单测 + 全套 py37 通过;离屏冒烟:点「预览最终提示词」弹窗 → 选数据行标题替换、切分辨率输出要求刷新;左栏不因预览常驻而需要滚动
|
||||
@@ -97,6 +97,47 @@ class TestAiOutfitPanelDefaults(unittest.TestCase):
|
||||
|
||||
self.assertEqual(panel._title_prompt_edit.toPlainText(), DEFAULT_TITLE_PROMPT)
|
||||
|
||||
def test_prompt_buttons_save_row_insert_title_and_preview(self):
|
||||
"""§19.20: 按钮行含「保存」;编辑框下方保留「插入标题」+「预览最终提示词」。"""
|
||||
from PySide6.QtWidgets import QPushButton
|
||||
|
||||
panel = self._panel()
|
||||
panel.apply_config(dict(DEFAULT_CONFIG))
|
||||
|
||||
texts = [b.text() for b in panel.findChildren(QPushButton)]
|
||||
self.assertIn("保存", texts)
|
||||
self.assertIn("插入标题", texts)
|
||||
self.assertIn("预览最终提示词", texts)
|
||||
self.assertNotIn("保存话术", texts) # 旧按钮已改名上移
|
||||
|
||||
def test_preview_dialog_renders_selected_row(self):
|
||||
"""§7.3: 预览弹窗按选中数据行替换 {title} 并附加输出要求。"""
|
||||
from openpyxl import Workbook
|
||||
from app.widgets.ai_outfit_panel import _OutfitPreviewDialog
|
||||
|
||||
xlsx = self.tmp / "rows.xlsx"
|
||||
wb = Workbook(); ws = wb.active
|
||||
ws.append(["标题", "货号", "原始图片路径", "结果", "状态", "原因"])
|
||||
ws.append(["纯棉短袖", "TY001", r"D:\img\a.png", "", "", ""])
|
||||
wb.save(str(xlsx)); wb.close()
|
||||
|
||||
panel = self._panel()
|
||||
panel.apply_config(dict(DEFAULT_CONFIG))
|
||||
panel._excel_edit.setText(str(xlsx))
|
||||
panel._prompt_edit.setPlainText("为 {title} 生成")
|
||||
panel._set_combo(panel._resolution, "1K")
|
||||
|
||||
dialog = _OutfitPreviewDialog(panel) # __init__ fills rows + refresh
|
||||
# Row dropdown picked up the valid row; pick it.
|
||||
self.assertEqual(dialog._combo.count(), 1)
|
||||
dialog._combo.setCurrentIndex(0)
|
||||
|
||||
text = dialog._view.toPlainText()
|
||||
self.assertTrue(text.startswith("为 纯棉短袖 生成"))
|
||||
self.assertIn("批量生成输出要求", text)
|
||||
self.assertIn("参考解析度:1K", text)
|
||||
dialog.deleteLater()
|
||||
|
||||
def test_find_title_model_resolves_by_config_name(self):
|
||||
"""title_model 名字命中 ai_models.json → 返回 AiModelConfig。"""
|
||||
from services.ai_image_service import AiModelConfig
|
||||
|
||||
Reference in New Issue
Block a user