Coverage for src\genesis\writer\language.py: 100%
51 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
1"""输出语言确定性检测与强制(步骤 A)。
3设计要点:
4- 日文标题多为纯汉字(如「DB設計」无假名),仅凭标题无法判定期望语言;
5 故 resolve_expected_language 采用两级推导:显式 > 标题假名 > 规则文档主导脚本。
6- 检测仅基于「是否含日文假名」:CJK 汉字零假名视为中文(日文不可能不含假名地
7 使用汉字),反之中日混排含假名判日文。这是确定可机器验证的唯一稳健信号。
8- find_language_violations 检正文类块(paragraph/note/list 的 text)与表格 caption
9 (caption 为生成正文需跟随输出语言);heading 跟随模板、table 的 rows/headers 照抄源
10 Excel 原文,不检(design.md §7.2 内容准确性/可追溯性)。
11- 短文本(<12 字)不误杀(如专有术语),阈值见 MIN_VIOLATION_LEN。
12"""
13from __future__ import annotations
15from genesis.writer.models import ContentBlock
17# 日文假名 Unicode 区间
18_HIRAGANA = (0x3040, 0x309F)
19_KATAKANA = (0x30A0, 0x30FF)
20# 中日韩统一表意文字(CJK 汉字)
21_CJK = (0x4E00, 0x9FFF)
23# 受检的正文块类型(heading/table 不检)
24_CHECKED_BLOCK_TYPES = {"paragraph", "note", "list"}
25# 触发违规判定的最小正文长度(防短术语误杀)
26MIN_VIOLATION_LEN = 12
29def _in_range(ch: str, lo: int, hi: int) -> bool:
30 cp = ord(ch)
31 return lo <= cp <= hi
34def has_kana(text: str) -> bool:
35 """文本是否含日文假名(平假名/片假名)。"""
36 return any(_in_range(c, *_HIRAGANA) or _in_range(c, *_KATAKANA) for c in text)
39def has_cjk(text: str) -> bool:
40 """文本是否含 CJK 汉字。"""
41 return any(_in_range(c, *_CJK) for c in text)
44def detect_script(text: str) -> str | None:
45 """检测文本主导自然语言。
47 含假名 → "ja";含 CJK 汉字但零假名 → "zh";二者皆非(纯 ASCII 等)→ None。
48 """
49 if not text:
50 return None
51 if has_kana(text):
52 return "ja"
53 if has_cjk(text):
54 return "zh"
55 return None
58def resolve_expected_language(
59 explicit: str,
60 title: str = "",
61 fallback_texts: tuple[str, ...] | list[str] = (),
62) -> str:
63 """推导本章期望输出语言(单一事实来源,A 的重试校验与 C 的 QA 维度共用)。
65 - explicit 为 "zh"/"ja" → 直接采用(用户显式选择优先)
66 - 否则看标题是否含假名(仅假名可可靠判为日文;纯汉字标题对中/日均可能,不可信)
67 - 否则看 fallback_texts(如影响调查书/章节数据,通常日文)的主导脚本
68 - 均无法推导 → 返回 ""(不可验证,交由上层按 unverifiable 处理)
69 """
70 if explicit in ("zh", "ja"):
71 return explicit
72 # 标题仅当含假名时可靠指示日文;纯汉字/ASCII 标题跳过,改看 fallback
73 if has_kana(title or ""):
74 return "ja"
75 for text in fallback_texts:
76 s = detect_script(text or "")
77 if s:
78 return s
79 return ""
82def find_language_violations(blocks: list[ContentBlock], expected_language: str) -> list[str]:
83 """返回违规正文块文本片段(期望语言非空时才有意义)。
85 违规判定(对正文类块与表格 caption 一致):
86 - 期望 "ja":含 CJK 汉字且零假名(即纯中文)且长度 ≥ 阈值
87 - 期望 "zh":含日文假名
88 受检范围:
89 - paragraph/note/list 的 text(正文)
90 - table 的 caption(生成正文,需跟随输出语言)
91 不检:heading(跟随模板)、table 的 rows/headers(照抄源 Excel 原文,design §7.2)。
92 """
93 if expected_language not in ("zh", "ja"):
94 return []
95 violations: list[str] = []
96 for b in blocks:
97 if b.type == "table":
98 texts = [b.caption or ""] # 仅 caption;rows/headers 照抄源不检
99 elif b.type in _CHECKED_BLOCK_TYPES:
100 texts = [b.text or ""]
101 else:
102 continue
103 for text in texts:
104 if len(text) < MIN_VIOLATION_LEN:
105 continue
106 if expected_language == "ja":
107 if has_cjk(text) and not has_kana(text):
108 violations.append(text)
109 else: # zh
110 if has_kana(text):
111 violations.append(text)
112 return violations