ngocdang83 commited on
Commit
d15ad15
·
verified ·
1 Parent(s): a3a6c30

sync honorific_normalize.py (Codex harden honorific+V9 guards)

Browse files
Files changed (1) hide show
  1. src/honorific_normalize.py +46 -7
src/honorific_normalize.py CHANGED
@@ -22,10 +22,14 @@ import re
22
  import unicodedata
23
  from collections import Counter
24
 
 
 
25
  HONORIFIC_OFF = "off"
26
  HONORIFIC_SAFE = "safe"
27
  HONORIFIC_STRICT = "xianxia_strict"
28
  HONORIFIC_MODES = {HONORIFIC_OFF, HONORIFIC_SAFE, HONORIFIC_STRICT}
 
 
29
 
30
  # ---------------------------------------------------------------------------
31
  # Bảng tra: ZH-term → {hv (đích Hán-Việt), drift (biến thể VI cần thay), tier}
@@ -106,6 +110,13 @@ _TARGET_LONG_PHRASES = tuple(
106
  reverse=True,
107
  )
108
  )
 
 
 
 
 
 
 
109
  _GENERIC_KINSHIP_SINGLE_DRIFTS = {"anh", "chị", "em"}
110
 
111
  WUXIA_SIGNALS = [
@@ -166,7 +177,7 @@ def _match_case(new: str, old: str) -> str:
166
 
167
 
168
  def _target_spans(text: str, phrase: str) -> list[tuple[int, int]]:
169
- pat = r"(?<![A-Za-zÀ-ỹ])" + re.escape(phrase) + r"(?![A-Za-zÀ-ỹ])"
170
  return [(m.start(), m.end()) for m in re.finditer(pat, text, flags=re.IGNORECASE)]
171
 
172
 
@@ -177,7 +188,7 @@ def _inside_any_span(start: int, end: int, spans: list[tuple[int, int]]) -> bool
177
  def _protected_target_spans(vi: str, current_variant: str) -> list[tuple[int, int]]:
178
  """Các phrase dài không được để drift một từ ăn vào giữa."""
179
  spans: list[tuple[int, int]] = []
180
- for phrase in _TARGET_LONG_PHRASES:
181
  if phrase == current_variant:
182
  continue
183
  if f" {current_variant.casefold()}" not in f" {phrase.casefold()}":
@@ -190,11 +201,34 @@ def _has_pronoun_competition(variant: str, source_mentions: list[str]) -> bool:
190
  variant = variant.casefold()
191
  if variant == "anh":
192
  return "他" in source_mentions
 
 
193
  if variant == "em":
194
  return "你" in source_mentions
195
  return False
196
 
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  def _replace_one_drift(
199
  vi: str,
200
  variants: list[str],
@@ -202,7 +236,7 @@ def _replace_one_drift(
202
  *,
203
  tier: str,
204
  source_mentions: list[str],
205
- ) -> str:
206
  """Thay một mention theo thứ tự nguồn.
207
 
208
  Nếu chỉ còn drift một từ mơ hồ (`anh/chị/em`) trong dòng có đại từ nguồn
@@ -213,7 +247,7 @@ def _replace_one_drift(
213
  if tier.startswith("kinship") and v.casefold() in _GENERIC_KINSHIP_SINGLE_DRIFTS:
214
  if _has_pronoun_competition(v, source_mentions):
215
  continue
216
- pat = r"(?<![A-Za-zÀ-ỹ])(" + re.escape(v) + r")(?![A-Za-zÀ-ỹ])"
217
  protected_spans = _protected_target_spans(vi, v)
218
 
219
  def repl(m):
@@ -226,7 +260,7 @@ def _replace_one_drift(
226
  vi = re.sub(pat, repl, vi, flags=re.IGNORECASE)
227
  if done:
228
  break
229
- return vi
230
 
231
 
232
  def _skip(tier: str, classical: bool, apply_kinship: bool, apply_pronouns: bool,
@@ -281,13 +315,17 @@ def normalize_honorifics(zh: str, vi: str, mode: str | None = None,
281
 
282
  if (not apply_kinship and not apply_pronouns) or not vi or not zh:
283
  return vi
 
284
  vi = unicodedata.normalize("NFC", vi)
285
- zh = unicodedata.normalize("NFC", zh)
286
  mentions = longest_match_mentions(zh)
287
  if not mentions:
288
  return vi
289
  classical = bool(classical_context) if classical_context is not None else is_classical(zh)
 
290
  for term in mentions:
 
 
291
  entry = HONORIFIC_MAP[term]
292
  hv = entry.get("hv")
293
  if hv is None:
@@ -298,13 +336,14 @@ def normalize_honorifics(zh: str, vi: str, mode: str | None = None,
298
  drift = entry.get("drift", [])
299
  if not drift:
300
  continue
301
- vi = _replace_one_drift(
302
  vi,
303
  drift,
304
  hv,
305
  tier=entry["tier"],
306
  source_mentions=mentions,
307
  )
 
308
  return vi
309
 
310
 
 
22
  import unicodedata
23
  from collections import Counter
24
 
25
+ from text_preprocess import NORMALIZE_T2S, normalize_chinese_text
26
+
27
  HONORIFIC_OFF = "off"
28
  HONORIFIC_SAFE = "safe"
29
  HONORIFIC_STRICT = "xianxia_strict"
30
  HONORIFIC_MODES = {HONORIFIC_OFF, HONORIFIC_SAFE, HONORIFIC_STRICT}
31
+ KINSHIP_MODES = {"always", "classical_only"}
32
+ _BOUNDARY_CHARS = r"\wÀ-ỹ"
33
 
34
  # ---------------------------------------------------------------------------
35
  # Bảng tra: ZH-term → {hv (đích Hán-Việt), drift (biến thể VI cần thay), tier}
 
110
  reverse=True,
111
  )
112
  )
113
+ _RECOGNIZE_ONLY_TARGET_PHRASES = (
114
+ "anh ấy", "anh ta", "chị ấy", "chị ta", "em ấy", "em ta",
115
+ "các anh", "các chị", "các em",
116
+ )
117
+ _PROTECTED_TARGET_PHRASES = tuple(
118
+ sorted(set(_TARGET_LONG_PHRASES) | set(_RECOGNIZE_ONLY_TARGET_PHRASES), key=len, reverse=True)
119
+ )
120
  _GENERIC_KINSHIP_SINGLE_DRIFTS = {"anh", "chị", "em"}
121
 
122
  WUXIA_SIGNALS = [
 
177
 
178
 
179
  def _target_spans(text: str, phrase: str) -> list[tuple[int, int]]:
180
+ pat = rf"(?<![{_BOUNDARY_CHARS}])" + re.escape(phrase) + rf"(?![{_BOUNDARY_CHARS}])"
181
  return [(m.start(), m.end()) for m in re.finditer(pat, text, flags=re.IGNORECASE)]
182
 
183
 
 
188
  def _protected_target_spans(vi: str, current_variant: str) -> list[tuple[int, int]]:
189
  """Các phrase dài không được để drift một từ ăn vào giữa."""
190
  spans: list[tuple[int, int]] = []
191
+ for phrase in _PROTECTED_TARGET_PHRASES:
192
  if phrase == current_variant:
193
  continue
194
  if f" {current_variant.casefold()}" not in f" {phrase.casefold()}":
 
201
  variant = variant.casefold()
202
  if variant == "anh":
203
  return "他" in source_mentions
204
+ if variant == "chị":
205
+ return "她" in source_mentions
206
  if variant == "em":
207
  return "你" in source_mentions
208
  return False
209
 
210
 
211
+ def _remaining_rewrite_budgets(source_mentions: list[str], vi: str) -> Counter[str]:
212
+ """Số mention nguồn còn được rewrite sau khi target đã có canonical.
213
+
214
+ Nếu `哥哥` đã được dịch thành `ca ca`, mention đó đã được tiêu thụ và không
215
+ được tiếp tục lấy một `anh` khác trong cùng câu để đổi thêm.
216
+ """
217
+ budgets: Counter[str] = Counter()
218
+ for term, source_count in Counter(source_mentions).items():
219
+ entry = HONORIFIC_MAP.get(term) or {}
220
+ hv = entry.get("hv")
221
+ if not hv:
222
+ continue
223
+ budgets[term] = max(0, source_count - len(_target_spans(vi, hv)))
224
+ return budgets
225
+
226
+
227
+ def _normalize_kinship_mode(kinship_mode: str | None) -> str:
228
+ mode = (kinship_mode or "always").strip().lower()
229
+ return mode if mode in KINSHIP_MODES else "classical_only"
230
+
231
+
232
  def _replace_one_drift(
233
  vi: str,
234
  variants: list[str],
 
236
  *,
237
  tier: str,
238
  source_mentions: list[str],
239
+ ) -> tuple[str, int]:
240
  """Thay một mention theo thứ tự nguồn.
241
 
242
  Nếu chỉ còn drift một từ mơ hồ (`anh/chị/em`) trong dòng có đại từ nguồn
 
247
  if tier.startswith("kinship") and v.casefold() in _GENERIC_KINSHIP_SINGLE_DRIFTS:
248
  if _has_pronoun_competition(v, source_mentions):
249
  continue
250
+ pat = rf"(?<![{_BOUNDARY_CHARS}])(" + re.escape(v) + rf")(?![{_BOUNDARY_CHARS}])"
251
  protected_spans = _protected_target_spans(vi, v)
252
 
253
  def repl(m):
 
260
  vi = re.sub(pat, repl, vi, flags=re.IGNORECASE)
261
  if done:
262
  break
263
+ return vi, done
264
 
265
 
266
  def _skip(tier: str, classical: bool, apply_kinship: bool, apply_pronouns: bool,
 
315
 
316
  if (not apply_kinship and not apply_pronouns) or not vi or not zh:
317
  return vi
318
+ kinship_mode = _normalize_kinship_mode(kinship_mode)
319
  vi = unicodedata.normalize("NFC", vi)
320
+ zh = unicodedata.normalize("NFC", normalize_chinese_text(zh, NORMALIZE_T2S))
321
  mentions = longest_match_mentions(zh)
322
  if not mentions:
323
  return vi
324
  classical = bool(classical_context) if classical_context is not None else is_classical(zh)
325
+ rewrite_budgets = _remaining_rewrite_budgets(mentions, vi)
326
  for term in mentions:
327
+ if rewrite_budgets[term] <= 0:
328
+ continue
329
  entry = HONORIFIC_MAP[term]
330
  hv = entry.get("hv")
331
  if hv is None:
 
336
  drift = entry.get("drift", [])
337
  if not drift:
338
  continue
339
+ vi, changed = _replace_one_drift(
340
  vi,
341
  drift,
342
  hv,
343
  tier=entry["tier"],
344
  source_mentions=mentions,
345
  )
346
+ rewrite_budgets[term] -= changed
347
  return vi
348
 
349