Files
UmbraTrans/tools/translation/tests/test_translation.py
umbra audio 3e0568b9d1 Generate backend endpoints for language-project workflow
Add REST endpoints and frontend wiring for per-language project actions:
- POST /api/workspace/{project}/translation/{lang}/queue — queue translation
- POST /api/workspace/{project}/audio/{lang}/queue — queue audio generation
- GET/POST /api/workspace/{project}/format/{lang} — format metadata (EPUB/PDF)
- GET /api/workspace/{project}/review/{lang}/chapters/{chapter} — utterance review
- GET/POST /api/workspace/{project}/front-matter/{lang} — front matter editor
- GET /api/workspace/{project}/chapters/{lang} — chapter list
- GET /api/workspace/{project}/chapter-content/{lang}/{chapter} — chapter text

Frontend fixes:
- Review page shows per-language text (English source, Chinese translation, etc.)
- Non-translated languages show 'Queue translation' button only
- Speaker data from English source matched by paragraph ID
- UmbraAudio column layout: Speaker, Variation, Text, Actions
- Chapter tree in left sidebar: Front matter, Back matter, Body
- Audio Format label at top of sidebar
- Pandoc formatting toolbar for front matter text editing
2026-10-25 11:04:53 -07:00

138 lines
5.0 KiB
Python

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
"""Unit tests for the translation cleanup and QA helpers.
The pipeline calls an LLM, so these tests deliberately cover only the
deterministic parts: prompt-leak removal, quote normalisation, English
residue detection, JSON extraction and the mechanical QA checks.
"""
from __future__ import annotations
import sys
import unittest
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent))
from llm import parse_json # noqa: E402
from pipeline import latin_residue, sanitize # noqa: E402
from qa import check_chapter # noqa: E402
class SanitizeTests(unittest.TestCase):
def test_strips_dialogue_label(self) -> None:
self.assertEqual(sanitize("(对话;凯因达)“我在游艇上。”"), "“我在游艇上。”")
def test_strips_narration_label(self) -> None:
self.assertEqual(sanitize("(旁白)凯因达登上了游艇。"), "凯因达登上了游艇。")
def test_strips_paragraph_id(self) -> None:
self.assertEqual(sanitize("[ch-6-p0000] “你好。”"), "“你好。”")
def test_title_and_brand(self) -> None:
self.assertEqual(sanitize("《交易猎人》"), "《Deal Hunter》")
self.assertEqual(sanitize("由乌姆布拉音频系统制作"), "由Umbra Audio制作")
def test_missing_hour(self) -> None:
self.assertEqual(sanitize("她在小时前登船。"), "她一小时前登船。")
def test_straight_quotes_become_chinese(self) -> None:
self.assertEqual(sanitize('"你好",她说。'), "“你好”,她说。")
def test_unmatched_closing_quote_removed(self) -> None:
self.assertEqual(sanitize("凯因达登上了游艇。”"), "凯因达登上了游艇。")
def test_unmatched_opening_quote_closed(self) -> None:
self.assertEqual(sanitize("“你好"), "“你好”")
def test_ellipsis_and_dash(self) -> None:
self.assertEqual(sanitize("等等... 然后--"), "等等…… 然后——")
class ResidueTests(unittest.TestCase):
def test_lowercase_english_is_flagged(self) -> None:
common, proper = latin_residue("她被 escorted 到了舱室。")
self.assertEqual(common, ["escorted"])
self.assertEqual(proper, [])
def test_allowed_names_are_ignored(self) -> None:
common, proper = latin_residue("Deal Hunter 和 Hunter 在 Umbra Audio。")
self.assertEqual(common, [])
self.assertEqual(proper, [])
def test_unknown_capitalised_name_is_reported(self) -> None:
common, proper = latin_residue("这是 Foo 的船。")
self.assertEqual(common, [])
self.assertEqual(proper, ["Foo"])
class JsonTests(unittest.TestCase):
def test_plain_json(self) -> None:
self.assertEqual(parse_json('{"a": 1}'), {"a": 1})
def test_fenced_json(self) -> None:
self.assertEqual(parse_json('```json\n{"a": 1}\n```'), {"a": 1})
def test_json_with_prose(self) -> None:
self.assertEqual(parse_json('结果如下:\n{"a": 1}\n完毕。'), {"a": 1})
class QaTests(unittest.TestCase):
def test_missing_paragraph_is_block(self) -> None:
chapter = {
"id": "ch-x",
"index": 1,
"title": "Test",
"paragraphs": [
{"id": "ch-x-p0000", "type": "narration", "text": "One."},
{"id": "ch-x-p0001", "type": "narration", "text": "Two."},
],
}
data = {"paragraphs": {"ch-x-p0000": "第一段。"}}
issues, metrics = check_chapter(chapter, data)
kinds = {i["kind"] for i in issues}
self.assertIn("missing", kinds)
self.assertEqual(metrics["translated"], 1)
def test_metadata_leak_and_residue(self) -> None:
chapter = {
"id": "ch-x",
"index": 1,
"title": "Test",
"paragraphs": [
{"id": "ch-x-p0000", "type": "narration", "text": "One."},
{"id": "ch-x-p0001", "type": "narration", "text": "The Albert system is far."},
],
}
data = {
"paragraphs": {
"ch-x-p0000": "(旁白)第一段 escorted。",
"ch-x-p0001": "阿尔伯特系统很远。",
}
}
issues, _ = check_chapter(chapter, data)
kinds = {i["kind"] for i in issues}
self.assertIn("metadata_leak", kinds)
self.assertIn("english_residue", kinds)
self.assertIn("system_mistranslated", kinds)
def test_ship_systems_are_not_star_systems(self) -> None:
chapter = {
"id": "ch-x",
"index": 1,
"title": "Test",
"paragraphs": [
{"id": "ch-x-p0000", "type": "narration", "text": "Keep away from the ship systems."},
],
}
data = {"paragraphs": {"ch-x-p0000": "远离飞船系统。"}}
issues, _ = check_chapter(chapter, data)
self.assertNotIn("system_mistranslated", {i["kind"] for i in issues})
if __name__ == "__main__":
unittest.main()