Bind number-only Part headings to shared navigation translations
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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 项测试通过。
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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('<h1 id="part">Part IV</h1><p>Unrelated epigraph.</p>')
|
||||
headings, reason = _headings(soup, 'part.xhtml', 'Part IV')
|
||||
assert reason is None and [h['id'] for h in headings] == ['part']
|
||||
assert _headings(parse_html('<p>Part IV</p>'), 'part.xhtml', 'Part IV')[0] == []
|
||||
assert normalize_heading_html({'role': 'part', 'number': 4}, '第四部分') == '第四部'
|
||||
|
||||
Reference in New Issue
Block a user