feat(translation): accept/reject gate on Interpreter's detection confidence (#1376)
Interpreter now returns a real per-detection confidence (source stays "auto"), so curator can reject the mis-detections it was blindly storing — e.g. a short English title mis-labelled as German and rewritten into the archive. The gate consumes ONLY Interpreter's own reported detection — curator does no language detection of its own (Scribe rule 133): a field is stored when the engine actually translated it AND either the detected language is CJK (script-detected, reliably high — ja/ko/zh trusted outright, incl. pure-kanji Japanese that lands as zh ~0.75) or the reported confidence clears a latin-script floor (_MIN_LATIN_CONFIDENCE = 0.90). A latin detection below the floor keeps the original and marks the post handled; a missing confidence fails open. The client already sent source="auto" and parsed confidence, so this is purely the gate + tests. Tests: pinned interpreter-client test now asserts source stays "auto"; new pure-unit gate tests (CJK trusted / latin floor / case-insensitive / fail-open) in the fast lane; end-to-end reject-low-latin, accept-high-latin, accept-low-cjk sweeps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CgZP9v2otxVJymiYsnVuMy
This commit is contained in:
@@ -6,6 +6,11 @@ viewing is instant and re-runs are cache-fast. Sync (Celery workers are sync),
|
||||
same pattern as the other backfill sweeps. No-op unless translation is enabled,
|
||||
a base URL is set, and the service is healthy.
|
||||
|
||||
Curator does NO language detection of its own: each field's translation is
|
||||
accepted or rejected purely on Interpreter's reported engine + detected language
|
||||
+ confidence (see ``_accept``) — a short English title Interpreter mis-labels as
|
||||
a European language scores below the latin-script floor and is kept as-is.
|
||||
|
||||
Two entry points:
|
||||
- ``translate_posts`` — the periodic untranslated sweep (every 8h;
|
||||
translated_source_lang IS NULL only), one bounded chunk per fire. The Settings
|
||||
@@ -45,6 +50,18 @@ _RETRANSLATE_COUNTDOWN = 5
|
||||
_INTERRUPT_BACKOFF = 60
|
||||
_INTERRUPT_BACKOFF_MAX = 900
|
||||
|
||||
# --- Translation acceptance gate (Scribe rule 133; contract note #1347) -------
|
||||
# Curator does NO language detection of its own — it accepts or rejects a
|
||||
# translation purely on Interpreter's reported detection. CJK is script-detected
|
||||
# (kana/hangul/kanji) and reliably high-confidence, so ja/ko/zh are trusted
|
||||
# outright (pure-kanji Japanese can even come back labelled zh ~0.75, still
|
||||
# correctly translated). Latin-script detection is a real statistical
|
||||
# probability, so a short English title Interpreter mis-labels as a European
|
||||
# language scores low; require it to clear this floor, else keep the original.
|
||||
# Tune the floor against real titles via the Settings "Test translation" box.
|
||||
_CJK_LANGS = frozenset({"ja", "ko", "zh"})
|
||||
_MIN_LATIN_CONFIDENCE = 0.90
|
||||
|
||||
|
||||
def _interrupt_backoff(retry_after) -> int:
|
||||
"""Seconds to wait before resuming after an Interpreter interruption: the
|
||||
@@ -272,18 +289,39 @@ def _summary(status: str, translated: int, scanned: int) -> str:
|
||||
return f"translated={translated} scanned={scanned}"
|
||||
|
||||
|
||||
def _accept(language: str, confidence) -> bool:
|
||||
"""Should curator store Interpreter's translation, or keep the original?
|
||||
Consumes ONLY Interpreter's own detection (curator does none, Scribe rule
|
||||
133): CJK languages are trusted regardless of the reported number; a
|
||||
latin-script detection must clear ``_MIN_LATIN_CONFIDENCE``. A missing
|
||||
confidence fails OPEN (accept), so a service that omits the field never causes
|
||||
translations to be silently dropped."""
|
||||
if (language or "").split("-", 1)[0].lower() in _CJK_LANGS:
|
||||
return True
|
||||
if confidence is None:
|
||||
return True
|
||||
return confidence >= _MIN_LATIN_CONFIDENCE
|
||||
|
||||
|
||||
def _translate_field(text: str, base_url: str, target: str):
|
||||
"""Translate ONE field independently. Returns (translated, source_lang,
|
||||
engine_version), or (None, None, None) when there's nothing to store — empty
|
||||
text, already the target language, or a passthrough (engine "none"). Per-field
|
||||
(not aggregate first-item) detection so a non-English description still gets
|
||||
translated when the title is already English (mixed-language posts)."""
|
||||
text, already the target language, a passthrough (engine "none"), or a
|
||||
latin-script detection the acceptance gate rejects as too low-confidence (kept
|
||||
as the original). Per-field (not aggregate first-item) detection so a
|
||||
non-English description still gets translated when the title is already
|
||||
English (mixed-language posts)."""
|
||||
if not text:
|
||||
return None, None, None
|
||||
res = ic.translate([text], base_url=base_url, target=target)
|
||||
detected = res["detected_lang"] or target
|
||||
if detected == target or res["engine"] == "none":
|
||||
return None, None, None
|
||||
# Trust only Interpreter's own detection (Scribe rule 133): keep the original
|
||||
# when a latin-script detection doesn't clear the confidence floor, so a short
|
||||
# English title mis-labelled as e.g. German isn't rewritten into the archive.
|
||||
if not _accept(detected, res.get("detected_confidence")):
|
||||
return None, None, None
|
||||
return res["translations"][0], detected, res["engine_version"]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user