The production directory oldbox calls "deal-hunter-es" declares target_language "es-ES", its glossary says Spanish, and its 32-chapter run finished - and all 3,038 translated paragraphs are in Chinese. It is a second Chinese draft filed under a Spanish name. The cause is in the pipeline: the drafter's instructions are hard-coded to Simplified Chinese in three places (SYSTEM_DRAFTER, draft_prompt, SYSTEM_CRITIC), so the glossary's target language was ignored. No non-Chinese translation has ever been produced by that pipeline. The danger at this end is that the rows would be stored under "es", the review lane would look populated, and a reviewer would approve Chinese as Spanish. So the ingest now checks the script of the delivered text against the declared language, and refuses the delivery before writing a single row. The check is about the script, not the language: it cannot tell Spanish from Portuguese, but it can tell Chinese from Spanish, which is the mistake that actually happens. Japanese needs its kana and Korean its Hangul, and a Chinese delivery carrying kana is a Japanese translation filed in the wrong lane. An unknown language is not checked - there is no expectation to check it against. Proven on the real data: the es delivery is refused with "the text is mostly Han characters, so it is not Spanish", and the 4,300-row Chinese delivery still imports unchanged. 19 new tests. Suite: 909 passed, 8 failed (the same 8 pre-existing failures).
206 lines
6.6 KiB
Python
206 lines
6.6 KiB
Python
# Copyright 2026 Umbra Tools (umbratools.com)
|
||
# SPDX-License-Identifier: Apache-2.0
|
||
|
||
"""Tests for the script check on a translation delivery (spec 31 section 8).
|
||
|
||
A delivery that claims one language and arrives written in another is not a
|
||
translation of that language. It is the "declared value disagrees with the
|
||
measured value" defect class, and it is checked at the boundary: once the rows
|
||
are stored, the review lane looks populated and the mistake is invisible.
|
||
|
||
The check is about the *script*, not the language. It cannot tell Spanish
|
||
from Portuguese. It can tell Chinese from Spanish, which is the mistake that
|
||
actually happens: a pipeline configured for one language that silently
|
||
produces another.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sqlite3
|
||
|
||
import pytest
|
||
|
||
from umbraaudio import languages as lang
|
||
from umbraaudio import translations as tr
|
||
|
||
CHINESE = "欢迎收听《Deal Hunter》有声书。我是旁白,将为您介绍几位您将听到的声音。"
|
||
SPANISH = "Bienvenido al audiolibro Deal Hunter. Soy tu narrador."
|
||
RUSSIAN = "Добро пожаловать в аудиокнигу Deal Hunter. Я ваш рассказчик."
|
||
JAPANESE = "『Deal Hunter』的有声書へようこそ。私はナレーターです。"
|
||
KOREAN = "Deal Hunter 오디오북에 오신 것을 환영합니다. 저는 내레이터입니다."
|
||
|
||
|
||
# --- the registry's own answers --------------------------------------------
|
||
|
||
|
||
def test_chinese_is_not_spanish() -> None:
|
||
reason = lang.script_mismatch("es", CHINESE)
|
||
assert reason is not None
|
||
assert "Han" in reason
|
||
assert "Spanish" in reason
|
||
|
||
|
||
def test_chinese_is_chinese() -> None:
|
||
assert lang.script_mismatch("zh", CHINESE) is None
|
||
|
||
|
||
def test_spanish_is_spanish() -> None:
|
||
assert lang.script_mismatch("es", SPANISH) is None
|
||
|
||
|
||
def test_russian_is_russian() -> None:
|
||
assert lang.script_mismatch("ru", RUSSIAN) is None
|
||
|
||
|
||
def test_latin_text_is_not_russian() -> None:
|
||
reason = lang.script_mismatch("ru", SPANISH)
|
||
assert reason is not None
|
||
assert "Latin" in reason
|
||
assert "Russian" in reason
|
||
|
||
|
||
def test_japanese_needs_kana() -> None:
|
||
assert lang.script_mismatch("ja", JAPANESE) is None
|
||
|
||
|
||
def test_chinese_text_is_not_japanese() -> None:
|
||
"""Kanji are Han, so Chinese reads as Japanese without the kana."""
|
||
reason = lang.script_mismatch("ja", CHINESE)
|
||
assert reason is not None
|
||
assert "kana" in reason
|
||
|
||
|
||
def test_japanese_text_is_not_chinese() -> None:
|
||
"""Kana in a Chinese delivery means the target is the wrong language."""
|
||
reason = lang.script_mismatch("zh", JAPANESE)
|
||
assert reason is not None
|
||
assert "Japanese" in reason or "kana" in reason
|
||
|
||
|
||
def test_korean_is_hangul() -> None:
|
||
assert lang.script_mismatch("ko", KOREAN) is None
|
||
|
||
|
||
def test_chinese_text_is_not_korean() -> None:
|
||
reason = lang.script_mismatch("ko", CHINESE)
|
||
assert reason is not None
|
||
|
||
|
||
def test_names_kept_in_english_do_not_break_the_check() -> None:
|
||
"""A Chinese paragraph keeps Deal Hunter and Umbra Audio in English."""
|
||
text = CHINESE + " Deal Hunter 是船名,Umbra Audio 是出品方。"
|
||
assert lang.script_mismatch("zh", text) is None
|
||
|
||
|
||
def test_an_unknown_language_is_not_checked() -> None:
|
||
"""There is no script expectation for a language we do not know."""
|
||
assert lang.script_mismatch("xx", CHINESE) is None
|
||
|
||
|
||
def test_empty_text_is_not_checked() -> None:
|
||
assert lang.script_mismatch("es", "") is None
|
||
assert lang.script_mismatch("es", "1234 -- ...") is None
|
||
|
||
|
||
# --- the gate at the ingest boundary ----------------------------------------
|
||
|
||
|
||
def _seed(conn: sqlite3.Connection) -> None:
|
||
conn.executescript(
|
||
"""
|
||
CREATE TABLE project (id INTEGER PRIMARY KEY, name TEXT);
|
||
CREATE TABLE chapters (id INTEGER PRIMARY KEY, chapter_index INTEGER);
|
||
CREATE TABLE utterances (
|
||
id INTEGER PRIMARY KEY,
|
||
chapter_id INTEGER,
|
||
utterance_index INTEGER,
|
||
speaker_name TEXT,
|
||
text TEXT,
|
||
context_before TEXT,
|
||
context_after TEXT,
|
||
language_code TEXT,
|
||
language_origin TEXT
|
||
);
|
||
INSERT INTO project (id, name) VALUES (1, 'Test');
|
||
INSERT INTO utterances (id, chapter_id, utterance_index, speaker_name, text) VALUES
|
||
(1, 1, 0, 'narrator', 'The ship landed.'),
|
||
(2, 1, 1, 'Kainda', 'Welcome to the ship.');
|
||
"""
|
||
)
|
||
conn.commit()
|
||
|
||
|
||
@pytest.fixture
|
||
def conn() -> sqlite3.Connection:
|
||
c = sqlite3.connect(":memory:")
|
||
c.row_factory = sqlite3.Row
|
||
_seed(c)
|
||
tr.ensure_tables(c)
|
||
yield c
|
||
c.close()
|
||
|
||
|
||
def test_a_mismatched_delivery_is_refused_and_writes_nothing(conn: sqlite3.Connection) -> None:
|
||
"""The Chinese text filed under Spanish must not reach the database."""
|
||
report = tr.import_translations(
|
||
conn,
|
||
1,
|
||
{
|
||
"target_language": "es",
|
||
"utterances": [
|
||
{"utterance_id": 1, "text": CHINESE},
|
||
{"utterance_id": 2, "text": "我是旁白。"},
|
||
],
|
||
},
|
||
)
|
||
assert report.accepted == 0
|
||
assert report.problems
|
||
assert "not Spanish" in report.problems[0]
|
||
assert conn.execute("SELECT COUNT(*) FROM utterance_translations").fetchone()[0] == 0
|
||
|
||
|
||
def test_a_matching_delivery_is_imported(conn: sqlite3.Connection) -> None:
|
||
report = tr.import_translations(
|
||
conn,
|
||
1,
|
||
{
|
||
"target_language": "es",
|
||
"utterances": [
|
||
{"utterance_id": 1, "text": SPANISH},
|
||
{"utterance_id": 2, "text": "Bienvenido a la nave."},
|
||
],
|
||
},
|
||
)
|
||
assert report.accepted == 2
|
||
assert report.problems == []
|
||
assert conn.execute("SELECT COUNT(*) FROM utterance_translations").fetchone()[0] == 2
|
||
|
||
|
||
def test_a_locale_code_is_checked_against_its_base_language(conn: sqlite3.Connection) -> None:
|
||
"""``es-ES`` is still Spanish; ``zh-Hans`` is still Chinese."""
|
||
report = tr.import_translations(
|
||
conn,
|
||
1,
|
||
{"target_language": "es-ES", "utterances": [{"utterance_id": 1, "text": CHINESE}]},
|
||
)
|
||
assert report.accepted == 0
|
||
assert report.problems
|
||
|
||
|
||
def test_a_collapse_orphan_is_not_mistaken_for_a_mismatch(conn: sqlite3.Connection) -> None:
|
||
"""A row with no text is a gap, not evidence of the wrong language."""
|
||
report = tr.import_translations(
|
||
conn,
|
||
1,
|
||
{
|
||
"target_language": "es",
|
||
"utterances": [
|
||
{"utterance_id": 1, "text": SPANISH},
|
||
{"utterance_id": 2, "voiced": False, "reason": "absorbed into 1"},
|
||
],
|
||
},
|
||
)
|
||
assert report.accepted == 2
|
||
assert report.not_voiced == 1
|
||
assert report.problems == []
|