fix(ai-outfit): 标题生成对「标题(A)空」的印花表不再误报无行 (§19.27)

catch-22:「生成标题」用 read_all_rows 取行,而它要求 标题(A)+原始图片路径(C)
都非空;但标题生成的目的就是填 A。印花导出表若 A 空(被清空/早期导出),
所有行被跳过 → 误报「该表没有可处理的行」。实测 output/20260623_094529.xlsx:
A 全空、C 是印花子目录。

- excel_service.read_all_rows 增 require_title=True 开关:False 时只在
  原始图片路径(C) 空时跳过、允许 标题(A) 空;默认 True 行为不变(预览/
  无待处理统计/图片生成 load_outfit_tasks 仍要求 A)
- ai_outfit_panel._TitleWorker.run 改调 read_all_rows(..., require_title=False)

测试:A 空+C 有的行 require_title=False 收录、默认跳过;C 空两种都跳过。
全套 py37 通过(test_config_service 的 packaging 模板失败属并行 §19.13,无关)。
离屏冒烟:真表 20260623_094529.xlsx 默认 0 行→require_title=False 加载 6 行;
worker 把 6 条标题按序回填 A、C(印花目录)不动。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 17:35:13 +08:00
co-authored by Claude Opus 4.8
parent be1eb9ee36
commit a9360f9421
5 changed files with 59 additions and 8 deletions
+3 -1
View File
@@ -429,7 +429,9 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
> **§17.7 改版**:早期为「逐行看图、各生成 1 条」(每行一次请求、带图)。现改为 > **§17.7 改版**:早期为「逐行看图、各生成 1 条」(每行一次请求、带图)。现改为
> **一次请求、纯提示词(不传图)、生成多条、按序回填**(用户在提示词里自写数量)。 > **一次请求、纯提示词(不传图)、生成多条、按序回填**(用户在提示词里自写数量)。
- **行来源**:复用 `excel_service.read_all_rows(excel)`(状态无关)——拿到全部有效行(仅为「填到哪些行 + 行号」),**覆盖式**写 A,不看 E 列状态、不引入新列。 - **行来源(§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 条…标题之间用逗号分隔,不要表格/换行/序号」)。 - **一次请求、纯提示词**:把用户标题提示词**原样**(不带图、不替换 `{title}`)发一次给文本模型;提示词里由用户自写数量并**约定标题之间用逗号隔开**(如「生成 6 条…标题之间用逗号分隔,不要表格/换行/序号」)。
- **按逗号拆分多条**:把响应文本按**逗号**(半角 `,` / 全角 `,`,并兼容换行作兜底分隔)拆成多条,每条清洗(去首尾空白、去行首序号/符号 `1. / - / ①`、去首尾引号、丢空)→ 得到标题列表。**不再按行/不解析 Markdown 表格**——靠提示词约定逗号分隔(§19.23)。 - **按逗号拆分多条**:把响应文本按**逗号**(半角 `,` / 全角 `,`,并兼容换行作兜底分隔)拆成多条,每条清洗(去首尾空白、去行首序号/符号 `1. / - / ①`、去首尾引号、丢空)→ 得到标题列表。**不再按行/不解析 Markdown 表格**——靠提示词约定逗号分隔(§19.23)。
- **按序回填**:第 i 条 → 第 i 行 A(`write_title_result(excel, row_index, title)`),逐条写、刷新中栏「处理明细」表该行「标题」列。 - **按序回填**:第 i 条 → 第 i 行 A(`write_title_result(excel, row_index, title)`),逐条写、刷新中栏「处理明细」表该行「标题」列。
+3 -1
View File
@@ -175,7 +175,9 @@ class _TitleWorker(QObject):
try: try:
ensure_excel_writable(self._excel_path) ensure_excel_writable(self._excel_path)
rows = read_all_rows(self._excel_path) # 标题生成的行来源只要求 原始图片路径(C) 非空,标题(A) 可空——A 正是
# 要填的列;否则 A 空的印花表会被误判为无行(§19.27)。
rows = read_all_rows(self._excel_path, require_title=False)
except Exception as exc: # noqa: BLE001 - report to UI except Exception as exc: # noqa: BLE001 - report to UI
self.failed.emit(str(exc)) self.failed.emit(str(exc))
return return
+9 -5
View File
@@ -115,13 +115,17 @@ def load_outfit_tasks(excel_path, retry_failed=False):
workbook.close() workbook.close()
def read_all_rows(excel_path): def read_all_rows(excel_path, require_title=True):
"""Return an OutfitTask for every valid data row, ignoring E-column status. """Return an OutfitTask for every valid data row, ignoring E-column status.
Unlike load_outfit_tasks (which filters for *processable* rows), this returns Unlike load_outfit_tasks (which filters for *processable* rows), this returns
all rows whose 标题 + 衣服图路径 are filled — including 「完成」/「失败」, and all rows whose 衣服图路径 is filled — including 「完成」/「失败」, and regardless
regardless of 货号 (which is optional, docs/11 §4) — for preview/sample purposes of 货号 (optional, docs/11 §4) — for preview/sample purposes (docs/11 §10.3).
only (docs/11 §10.3). Rows missing title/garment are skipped.
require_title (docs/11 §17.1 / §19.27): when True (default) a row also needs
标题(A) non-empty (preview / image-gen input). 标题生成 passes False because
its very job is to FILL A, so rows with an empty 标题 must NOT be skipped —
only 原始图片路径(C) is required.
""" """
path = Path(excel_path) path = Path(excel_path)
workbook = load_workbook(str(path)) workbook = load_workbook(str(path))
@@ -132,7 +136,7 @@ def read_all_rows(excel_path):
title = sheet.cell(row_index, COL_TITLE).value title = sheet.cell(row_index, COL_TITLE).value
product_id = sheet.cell(row_index, COL_PRODUCT_ID).value product_id = sheet.cell(row_index, COL_PRODUCT_ID).value
garment_path = sheet.cell(row_index, COL_GARMENT_PATH).value garment_path = sheet.cell(row_index, COL_GARMENT_PATH).value
if _is_empty(title) or _is_empty(garment_path): # 货号可空 if _is_empty(garment_path) or (require_title and _is_empty(title)):
continue continue
raw_status = sheet.cell(row_index, COL_STATUS).value raw_status = sheet.cell(row_index, COL_STATUS).value
rows.append( rows.append(
+20
View File
@@ -1475,3 +1475,23 @@
- [x] `ai_outfit_panel.py` `_configure_detail_table_columns`:把「衣服图」(col 3) 初值 150→约 90、「结果 / 原因」(col 5) 初值 220→约 130(仍 `Interactive` 可手拖);「行/货号/状态」固定窄列与「标题」Stretch 不变 → 标题自动更宽 - [x] `ai_outfit_panel.py` `_configure_detail_table_columns`:把「衣服图」(col 3) 初值 150→约 90、「结果 / 原因」(col 5) 初值 220→约 130(仍 `Interactive` 可手拖);「行/货号/状态」固定窄列与「标题」Stretch 不变 → 标题自动更宽
- [x] `test_ai_outfit_panel.py`:在既有列宽用例补断言——`columnWidth(3) <= 90`、`columnWidth(5) <= 130`(标题仍 Stretch、短列仍 Fixed 不变) - [x] `test_ai_outfit_panel.py`:在既有列宽用例补断言——`columnWidth(3) <= 90`、`columnWidth(5) <= 130`(标题仍 Stretch、短列仍 Fixed 不变)
- [x] 验证:`test_ai_outfit_panel.py` + 全套 py37 通过(`test_config_service` 的 packaging 模板失败属并行 §19.13,无关);离屏断言新列宽 - [x] 验证:`test_ai_outfit_panel.py` + 全套 py37 通过(`test_config_service` 的 packaging 模板失败属并行 §19.13,无关);离屏断言新列宽
### 19.27 修复:标题生成对「标题(A) 空」的印花表误报无行 — docs/11 §17.1
前置阅读:
- `docs/11-ai-outfit.md`(§17.1 行来源)
- `src/services/excel_service.py`(`read_all_rows` 有效性判定 `_is_empty(title) or _is_empty(garment_path)`)
- `src/app/widgets/ai_outfit_panel.py`(`_TitleWorker.run` 调 `read_all_rows`、`_on_title_finished` 的「该表没有可处理的行」)
- `tests/test_excel_service.py`(`read_all_rows` 用例)
背景(catch-22):
「生成标题」用 `read_all_rows` 取行,而它要求 标题(A)+原始图片路径(C) 都非空;但标题生成的目的就是**填 A**。印花导出表若 A 为空(被清空/早期导出),6 行全被跳过 → 误报「该表没有可处理的行」。实测 `output/20260623_094529.xlsx`:A 全空、C 是印花子目录。修复:标题生成的行来源只要求 C 非空、A 可空。
任务:
- [x] `excel_service.py`:`read_all_rows(excel_path, require_title=True)` 加开关——`require_title=False` 时只在 `_is_empty(garment_path)` 跳过(允许 A 空);默认 `True` 行为不变。更新 docstring
- [x] `ai_outfit_panel.py`:`_TitleWorker.run` 改调 `read_all_rows(self._excel_path, require_title=False)`;`_reload_after_titles` 仍可用默认(重载后 A 已填);图片生成/预览/`_show_no_pending_message` 不动(仍要求 A)
- [x] `tests/test_excel_service.py`:补用例——A 空、C 非空的行:`require_title=False` 收录、默认(True)跳过;既有用例保持绿
- [x] 验证:相关单测 + 全套 py37 通过;离屏冒烟:A 全空、C=印花目录的表,标题生成能加载 N 行(不再「无可处理行」),mock 文本模型 → 按序回填 A
+23
View File
@@ -126,6 +126,29 @@ class TestOutfitExcelService(unittest.TestCase):
self.assertEqual(tasks[0].product_id, "") self.assertEqual(tasks[0].product_id, "")
self.assertEqual([r.row_index for r in read_all_rows(p)], [2]) self.assertEqual([r.row_index for r in read_all_rows(p)], [2])
def test_read_all_rows_require_title_false_keeps_empty_title_rows(self):
"""§19.27: 标题生成的行来源——A 空、C 非空的行 require_title=False 收录,
默认(True)跳过;C 空的行两种都跳过。"""
from services.excel_service import read_all_rows
p = self.tmp / "notitle.xlsx"
wb = Workbook()
ws = wb.active
ws.append(["标题", "商品id", "原始图片路径", "结果", "状态", "原因"])
ws.append([None, None, "D:\\out\\TY030\\", "", "", ""]) # A空、C有(印花表)
ws.append([None, None, "D:\\out\\TY037\\", "", "", ""]) # A空、C有
ws.append([None, None, "", "", "", ""]) # C空 → 两种都跳过
wb.save(str(p))
wb.close()
# 默认要求标题 → A 全空 → 跳过印花行(旧 catch-22 现象)
self.assertEqual([r.row_index for r in read_all_rows(p)], [])
# require_title=False → A 空也收录,只按 C 过滤
rows = read_all_rows(p, require_title=False)
self.assertEqual([r.row_index for r in rows], [2, 3])
self.assertEqual(rows[0].title, "") # A 留空(待生成)
self.assertTrue(rows[0].garment_path.endswith("TY030" + "\\"))
def test_write_title_result_updates_a_only_and_saves(self): def test_write_title_result_updates_a_only_and_saves(self):
"""标题生成回填只改 A 列,不动 D/E/F(docs/11 §17)。""" """标题生成回填只改 A 列,不动 D/E/F(docs/11 §17)。"""
from services.excel_service import write_title_result from services.excel_service import write_title_result