Merge pull request 'feat(translation): per-field language detection for mixed-language posts' (#205) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 36s
CI / integration (push) Successful in 3m48s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 8s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 8s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 36s
CI / integration (push) Successful in 3m48s
This commit was merged in pull request #205.
This commit is contained in:
@@ -250,29 +250,39 @@ def _summary(status: str, translated: int, scanned: int) -> str:
|
|||||||
return f"translated={translated} scanned={scanned}"
|
return f"translated={translated} scanned={scanned}"
|
||||||
|
|
||||||
|
|
||||||
|
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)."""
|
||||||
|
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
|
||||||
|
return res["translations"][0], detected, res["engine_version"]
|
||||||
|
|
||||||
|
|
||||||
def _translate_one(session, post, base_url: str, target: str) -> int:
|
def _translate_one(session, post, base_url: str, target: str) -> int:
|
||||||
"""Translate one post's title/description in place. Returns 1 if it stored a
|
"""Translate a post's title/description in place, each field independently.
|
||||||
translation, 0 for passthrough/empty (which still marks the post handled)."""
|
Returns 1 if it stored at least one translation, 0 when the post is all
|
||||||
|
passthrough/empty (still marks it handled so the sweep won't revisit it)."""
|
||||||
title = (post.post_title or "").strip()
|
title = (post.post_title or "").strip()
|
||||||
desc = (html_to_plain(post.description) if post.description else "") or ""
|
desc = (html_to_plain(post.description) if post.description else "") or ""
|
||||||
desc = desc.strip()
|
desc = desc.strip()
|
||||||
fields: list[tuple[str, str]] = []
|
title_tr, title_lang, title_ev = _translate_field(title, base_url, target)
|
||||||
if title:
|
desc_tr, desc_lang, desc_ev = _translate_field(desc, base_url, target)
|
||||||
fields.append(("title", title))
|
if title_tr is None and desc_tr is None:
|
||||||
if desc:
|
# Nothing non-target (or nothing to translate) → handled, store nothing.
|
||||||
fields.append(("desc", desc))
|
post.translated_source_lang = target
|
||||||
if not fields:
|
|
||||||
post.translated_source_lang = target # nothing to translate → handled
|
|
||||||
return 0
|
return 0
|
||||||
res = ic.translate([t for _, t in fields], base_url=base_url, target=target)
|
post.post_title_translated = title_tr
|
||||||
detected = res["detected_lang"] or target
|
post.description_translated = desc_tr
|
||||||
if detected == target or res["engine"] == "none":
|
# Source lang = whichever field was actually non-target (for a mixed post,
|
||||||
post.translated_source_lang = target # already target language → handled
|
# the translated field's language, not the already-English one).
|
||||||
return 0
|
post.translated_source_lang = title_lang or desc_lang
|
||||||
by_field = {f: res["translations"][i] for i, (f, _) in enumerate(fields)}
|
post.translation_engine_version = title_ev or desc_ev
|
||||||
post.post_title_translated = by_field.get("title")
|
|
||||||
post.description_translated = by_field.get("desc")
|
|
||||||
post.translated_source_lang = detected
|
|
||||||
post.translation_engine_version = res["engine_version"]
|
|
||||||
post.translated_at = datetime.now(UTC)
|
post.translated_at = datetime.now(UTC)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -115,6 +115,35 @@ def test_translate_posts_passthrough_english_marks_handled(db_sync, monkeypatch)
|
|||||||
assert p.post_title_translated is None # ...but nothing to show
|
assert p.post_title_translated is None # ...but nothing to show
|
||||||
|
|
||||||
|
|
||||||
|
def test_translate_posts_mixed_language_translates_nonenglish_field(db_sync, monkeypatch):
|
||||||
|
# English title + Japanese description: the description is still translated
|
||||||
|
# even though the title is already English (per-field detection, not the old
|
||||||
|
# aggregate first-item bail). Source lang comes from the translated field.
|
||||||
|
_patch(monkeypatch, db_sync)
|
||||||
|
monkeypatch.setattr("backend.app.tasks.translation.ic.health", lambda *a, **k: True)
|
||||||
|
|
||||||
|
def fake_translate(texts, **k):
|
||||||
|
t = texts[0]
|
||||||
|
if t.isascii(): # already English → passthrough
|
||||||
|
return {"translations": [t], "detected_lang": "en",
|
||||||
|
"engine": "none", "engine_version": None}
|
||||||
|
return {"translations": [f"EN:{t}"], "detected_lang": "ja",
|
||||||
|
"engine": "llm", "engine_version": "v9"}
|
||||||
|
|
||||||
|
monkeypatch.setattr("backend.app.tasks.translation.ic.translate", fake_translate)
|
||||||
|
a = _artist(db_sync)
|
||||||
|
p = _post(db_sync, a.id, title="hello", desc="ねこ")
|
||||||
|
_enable(db_sync)
|
||||||
|
db_sync.commit()
|
||||||
|
|
||||||
|
assert "translated=1" in translate_posts()
|
||||||
|
db_sync.refresh(p)
|
||||||
|
assert p.post_title_translated is None # English title untouched
|
||||||
|
assert p.description_translated == "EN:ねこ" # Japanese desc translated
|
||||||
|
assert p.translated_source_lang == "ja" # from the translated field
|
||||||
|
assert p.translation_engine_version == "v9"
|
||||||
|
|
||||||
|
|
||||||
def test_translate_posts_disabled_is_noop(db_sync, monkeypatch):
|
def test_translate_posts_disabled_is_noop(db_sync, monkeypatch):
|
||||||
_patch(monkeypatch, db_sync)
|
_patch(monkeypatch, db_sync)
|
||||||
a = _artist(db_sync)
|
a = _artist(db_sync)
|
||||||
|
|||||||
Reference in New Issue
Block a user