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:
@@ -429,7 +429,9 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
|
||||
> **§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 条…标题之间用逗号分隔,不要表格/换行/序号」)。
|
||||
- **按逗号拆分多条**:把响应文本按**逗号**(半角 `,` / 全角 `,`,并兼容换行作兜底分隔)拆成多条,每条清洗(去首尾空白、去行首序号/符号 `1. / - / ①`、去首尾引号、丢空)→ 得到标题列表。**不再按行/不解析 Markdown 表格**——靠提示词约定逗号分隔(§19.23)。
|
||||
- **按序回填**:第 i 条 → 第 i 行 A(`write_title_result(excel, row_index, title)`),逐条写、刷新中栏「处理明细」表该行「标题」列。
|
||||
|
||||
@@ -175,7 +175,9 @@ class _TitleWorker(QObject):
|
||||
|
||||
try:
|
||||
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
|
||||
self.failed.emit(str(exc))
|
||||
return
|
||||
|
||||
@@ -115,13 +115,17 @@ def load_outfit_tasks(excel_path, retry_failed=False):
|
||||
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.
|
||||
|
||||
Unlike load_outfit_tasks (which filters for *processable* rows), this returns
|
||||
all rows whose 标题 + 衣服图路径 are filled — including 「完成」/「失败」, and
|
||||
regardless of 货号 (which is optional, docs/11 §4) — for preview/sample purposes
|
||||
only (docs/11 §10.3). Rows missing title/garment are skipped.
|
||||
all rows whose 衣服图路径 is filled — including 「完成」/「失败」, and regardless
|
||||
of 货号 (optional, docs/11 §4) — for preview/sample purposes (docs/11 §10.3).
|
||||
|
||||
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)
|
||||
workbook = load_workbook(str(path))
|
||||
@@ -132,7 +136,7 @@ def read_all_rows(excel_path):
|
||||
title = sheet.cell(row_index, COL_TITLE).value
|
||||
product_id = sheet.cell(row_index, COL_PRODUCT_ID).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
|
||||
raw_status = sheet.cell(row_index, COL_STATUS).value
|
||||
rows.append(
|
||||
|
||||
@@ -1474,4 +1474,24 @@
|
||||
|
||||
- [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` + 全套 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
|
||||
@@ -126,6 +126,29 @@ class TestOutfitExcelService(unittest.TestCase):
|
||||
self.assertEqual(tasks[0].product_id, "")
|
||||
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):
|
||||
"""标题生成回填只改 A 列,不动 D/E/F(docs/11 §17)。"""
|
||||
from services.excel_service import write_title_result
|
||||
|
||||
Reference in New Issue
Block a user