diff --git a/CHANGELOG.md b/CHANGELOG.md index a200c7a..8fc5d54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Add `--complete-book` to include notes, bibliography, index, copyright and non-spine text documents. - Add explicit configuration/credential file options and a Zenmux Sonnet 5.5 configuration without credentials. - Checkpoint whole-book terminology scans and record conflicting model proposals while retaining established translations. +- Bind number-only Part headings such as House of Huawei's “Part I” and normalize “第一部分” to “第一部”. ## 2026-10-08 — clickable Part navigation diff --git a/doc/QA_2026-10-08.md b/doc/QA_2026-10-08.md index 9de3542..5c78e05 100644 --- a/doc/QA_2026-10-08.md +++ b/doc/QA_2026-10-08.md @@ -67,3 +67,5 @@ OCR、扫描页图文提取、版面中间结构与可点击目录的后续大 50 项测试通过,覆盖中文/双语部号、完整模式提取非 spine 尾注、Zenmux 凭据优先级,以及术语冲突记录、扫描中断和续扫。默认模式继续保留原有跳过策略;完整模式将尾注、参考文献、索引和版权页等文字纳入翻译。 真实 Sonnet 5.5 全书运行中发现,单批术语返回同一别名的不同译名会中止全书。现在自动扫描逐条校验建议,保留已确定译名并记录冲突,每批原子保存结果及进度。手工术语表的冲突仍严格报错。两本书的实际运行结果将在完成后单独记录;本节不代表已完成全书译文验收。 + +追加验证:两本书完整模式共 6,782 个单元,原文格式往返失败 0、新增结构问题 0。《House of Huawei》的 Part I/II/III 只有编号,没有副标题,原绑定算法去掉编号后得到空标题,导致未绑定;现允许明确的 h1–h6 纯编号标题匹配,同文字的普通段落仍不匹配。目录和正文复用同一译文,并将“第一部分”规范为“第一部”。累计 51 项测试通过。 diff --git a/src/preprocessing/navigation.py b/src/preprocessing/navigation.py index 19b1c2a..d0bff71 100644 --- a/src/preprocessing/navigation.py +++ b/src/preprocessing/navigation.py @@ -115,7 +115,10 @@ def _headings(soup, target, label): group = blocks[start:end] if any(len(plain(t.get_text())) > 300 for t in group): break combined = ' '.join(t.get_text(' ', strip=True) for t in group) - if title_identity(combined) == identity and identity: + number_only = (not identity and len(group) == 1 and group[0].name in HEADINGS + and number_info(label)[0] is not None + and number_info(combined) == number_info(label)) + if title_identity(combined) == identity and (identity or number_only): matches.append(group) # A number leaf and the subtitle alone can both match after stripping a # numeric prefix. Prefer the complete adjacent title group. @@ -225,11 +228,11 @@ def normalize_heading_html(record, html): and re.fullmatch(r'[章部篇][::]?',plain(str(nodes[2])))): nodes[1].replace_with(chinese_number(record['number'])) nodes[2].replace_with('章' if record['role']=='chapter' else '部') - elif re.fullmatch(r'(?:\d+|[IVXLCDM]+|第[零一二三四五六七八九十百千万\d]+[章部篇])',compact,re.I): + elif re.fullmatch(r'(?:\d+|[IVXLCDM]+|第[零一二三四五六七八九十百千万\d]+(?:章|部分?|篇))',compact,re.I): node.replace_with(prefix) if len(nodes)>1 and plain(str(nodes[1])) in {':',':'}: nodes[1].replace_with('') elif re.match(r'^第[零一二三四五六七八九十百千万\d]+[章部篇]',compact): - node.replace_with(re.sub(r'^第[零一二三四五六七八九十百千万\d]+[章部篇]',prefix,compact)) + node.replace_with(re.sub(r'^第[零一二三四五六七八九十百千万\d]+(?:章|部分?|篇)',prefix,compact)) return root.decode_contents() diff --git a/tests/test_navigation.py b/tests/test_navigation.py index ecb4840..87db863 100644 --- a/tests/test_navigation.py +++ b/tests/test_navigation.py @@ -265,3 +265,13 @@ def test_complete_book_includes_backmatter_and_non_spine_documents(tmp_path): assert any(e.file_path == 'Text/footnotes.xhtml' and 'Note text' in e.original_text for e in complete) assert FineGrainedExtractor(complete_book=True)._classify_document('index.xhtml') == 'core' assert FineGrainedExtractor(complete_book=True)._classify_document('nav.xhtml') == 'toc' + + +def test_number_only_part_heading_is_bound_without_matching_prose(): + from src.common.html import parse_html + from src.preprocessing.navigation import _headings, normalize_heading_html + soup = parse_html('

Part IV

Unrelated epigraph.

') + headings, reason = _headings(soup, 'part.xhtml', 'Part IV') + assert reason is None and [h['id'] for h in headings] == ['part'] + assert _headings(parse_html('

Part IV

'), 'part.xhtml', 'Part IV')[0] == [] + assert normalize_heading_html({'role': 'part', 'number': 4}, '第四部分') == '第四部'