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

1"""输出语言确定性检测与强制(步骤 A)。 

2 

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 

14 

15from genesis.writer.models import ContentBlock 

16 

17# 日文假名 Unicode 区间 

18_HIRAGANA = (0x3040, 0x309F) 

19_KATAKANA = (0x30A0, 0x30FF) 

20# 中日韩统一表意文字(CJK 汉字) 

21_CJK = (0x4E00, 0x9FFF) 

22 

23# 受检的正文块类型(heading/table 不检) 

24_CHECKED_BLOCK_TYPES = {"paragraph", "note", "list"} 

25# 触发违规判定的最小正文长度(防短术语误杀) 

26MIN_VIOLATION_LEN = 12 

27 

28 

29def _in_range(ch: str, lo: int, hi: int) -> bool: 

30 cp = ord(ch) 

31 return lo <= cp <= hi 

32 

33 

34def has_kana(text: str) -> bool: 

35 """文本是否含日文假名(平假名/片假名)。""" 

36 return any(_in_range(c, *_HIRAGANA) or _in_range(c, *_KATAKANA) for c in text) 

37 

38 

39def has_cjk(text: str) -> bool: 

40 """文本是否含 CJK 汉字。""" 

41 return any(_in_range(c, *_CJK) for c in text) 

42 

43 

44def detect_script(text: str) -> str | None: 

45 """检测文本主导自然语言。 

46 

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 

56 

57 

58def resolve_expected_language( 

59 explicit: str, 

60 title: str = "", 

61 fallback_texts: tuple[str, ...] | list[str] = (), 

62) -> str: 

63 """推导本章期望输出语言(单一事实来源,A 的重试校验与 C 的 QA 维度共用)。 

64 

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 "" 

80 

81 

82def find_language_violations(blocks: list[ContentBlock], expected_language: str) -> list[str]: 

83 """返回违规正文块文本片段(期望语言非空时才有意义)。 

84 

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