OPEN SOURCE
software/tests/test_iteration1_quality.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1"""Tests for iteration-1 quality fixes: output sanitation, kougo labeling,2Hebrew-aware glossary matching."""34from __future__ import annotations56from src.context.glossary import GlossaryManager, _normalize_hebrew7from src.models.glossary import EnhancedGlossaryTerm8from src.models.verse import Verse, VerseReference9from src.pipeline.prompt import PromptBuilder10from src.pipeline.translator import _sanitize_translation_output111213class TestSanitizeTranslationOutput:14def test_empty(self) -> None:15assert _sanitize_translation_output("") == ""16assert _sanitize_translation_output(" \n ") == ""1718def test_plain_japanese_passthrough(self) -> None:19text = "初めに言葉があった。言葉は神と共にあった。"20assert _sanitize_translation_output(text) == text2122def test_self_correction_keeps_last_japanese_block(self) -> None:23raw = (24"この命に命があった。\n\n"25"Wait—let me provide the correct translation.\n\n"26"この方の内に命があった。そしてこの命は人々の光であった。"27)28assert (29_sanitize_translation_output(raw)30== "この方の内に命があった。そしてこの命は人々の光であった。"31)3233def test_strips_code_fences(self) -> None:34raw = "```\n光は闇の中で輝いている。\n```"35assert _sanitize_translation_output(raw) == "光は闇の中で輝いている。"3637def test_strips_translation_label(self) -> None:38assert _sanitize_translation_output("翻訳: 神は言われた。") == "神は言われた。"3940def test_all_latin_returns_original(self) -> None:41raw = "I cannot translate this verse."42assert _sanitize_translation_output(raw) == raw434445class TestKougoReference:46def _verse(self, **kwargs) -> Verse:47return Verse(48reference=VerseReference(book="john", chapter=1, verse=1), **kwargs49)5051def test_prompt_labels_kougo_correctly(self) -> None:52builder = PromptBuilder(GlossaryManager())53verse = self._verse(japanese_kougo="初めに言があった。")54section = builder._format_reference_translations(verse)55assert "口語訳 (1955): 初めに言があった。" in section56assert "大正改訳" not in section5758def test_best_text_prefers_kougo_over_taisho(self) -> None:59builder = PromptBuilder(GlossaryManager())60verse = self._verse(japanese_kougo="口語", japanese_taisho="大正")61assert builder._get_best_text(verse) == "口語"626364class TestHebrewGlossaryMatching:65def test_normalize_strips_points_and_separators(self) -> None:66assert _normalize_hebrew("בְּ/רֵאשִׁ֖ית") == "בראשית"6768def test_pointed_text_matches_unpointed_term(self) -> None:69gm = GlossaryManager(70[71EnhancedGlossaryTerm(72original="אלהים",73transliteration="elohim",74japanese="神",75english="God",76)77]78)79terms = gm.find_terms_in_text("בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת")80assert [t.original for t in terms] == ["אלהים"]8182def test_capitalized_greek_inflection_matches(self) -> None:83gm = GlossaryManager(84[85EnhancedGlossaryTerm(86original="μακάριος",87transliteration="makarios",88japanese="幸い",89english="blessed",90inflected_forms=["μακάριοι"],91)92]93)94terms = gm.find_terms_in_text("Μακάριοι οἱ πτωχοὶ")95assert [t.original for t in terms] == ["μακάριος"]969798class TestEvaluatorIteration2:99def test_prompt_includes_hebrew_when_no_greek(self) -> None:100from src.evaluation.evaluator import build_evaluation_prompt101102prompt = build_evaluation_prompt(103translation="初めに神は天と地を創造された。",104greek_text=None,105kjv_text="In the beginning God created...",106reference_japanese=None,107hebrew_text="בראשית ברא אלהים",108)109assert "ORIGINAL TEXT (Hebrew):" in prompt110assert "בראשית" in prompt111112def test_prompt_includes_surrounding_context(self) -> None:113from src.evaluation.evaluator import build_evaluation_prompt114115prompt = build_evaluation_prompt(116translation="テスト",117greek_text="λόγος",118kjv_text=None,119reference_japanese=None,120surrounding_context="[before 1] 初めに言があった。",121)122assert "SURROUNDING PASSAGE" in prompt123assert "初めに言があった" in prompt124125def test_glossary_guidance_lists_entries(self) -> None:126from src.evaluation.evaluator import build_evaluation_prompt127128prompt = build_evaluation_prompt(129translation="テスト",130greek_text="βασιλεία",131kjv_text=None,132reference_japanese=None,133glossary_terms=["βασιλεία → 御国 (in Matthew = 天の国)"],134)135assert "GLOSSARY GUIDANCE" in prompt136assert "- βασιλεία → 御国 (in Matthew = 天の国)" in prompt137138def test_build_verse_prompt_formats_terms_with_notes(self, tmp_path) -> None:139from src.evaluation.evaluator import TranslationEvaluator140from src.models.glossary import EnhancedGlossaryTerm141from src.models.verse import TranslationContext, Verse, VerseReference142143ref = VerseReference(book="matthew", chapter=5, verse=3)144target = Verse(reference=ref, greek_text="Μακάριοι", english_kjv="Blessed")145ctx = TranslationContext(146target_verse=target,147preceding_verses=[148Verse(149reference=VerseReference(book="matthew", chapter=5, verse=2),150japanese_kougo="そして口を開いて教えられた。",151)152],153following_verses=[],154glossary_terms=[155EnhancedGlossaryTerm(156original="μακάριος",157transliteration="makarios",158japanese="幸い",159english="blessed",160notes="Beatitude formula",161)162],163)164evaluator = TranslationEvaluator(data_dir=tmp_path)165prompt = evaluator.build_verse_prompt("幸いである", ctx)166assert "μακάριος → 幸い (Beatitude formula)" in prompt167assert "そして口を開いて教えられた。" in prompt168assert "[before 2 —" in prompt169170def test_surrounding_context_none_when_no_neighbors(self) -> None:171from src.evaluation.evaluator import TranslationEvaluator172from src.models.verse import TranslationContext, Verse, VerseReference173174ref = VerseReference(book="john", chapter=1, verse=1)175ctx = TranslationContext(176target_verse=Verse(reference=ref, greek_text="λόγος"),177preceding_verses=[],178following_verses=[],179glossary_terms=[],180)181evaluator = TranslationEvaluator(data_dir="data")182assert evaluator._format_surrounding_context(ctx) is None183184185class TestSeamContinuity:186def test_chunk_prompt_includes_preceding_translations(self) -> None:187from src.context.glossary import GlossaryManager188from src.models.pericope import ChunkTranslationContext, Pericope189from src.models.verse import Verse, VerseReference190from src.pipeline.prompt import PromptBuilder191192pericope = Pericope(193id="john-1-19-test",194title="Testimony",195start_chapter=1,196start_verse=19,197end_chapter=1,198end_verse=20,199verses=["1:19", "1:20"],200)201ctx = ChunkTranslationContext(202pericope=pericope,203book="john",204verses=[205Verse(206reference=VerseReference(book="john", chapter=1, verse=19),207greek_text="Καὶ αὕτη ἐστὶν ἡ μαρτυρία",208)209],210preceding_verses=[],211following_verses=[],212glossary_terms=[],213)214builder = PromptBuilder(GlossaryManager())215prompt = builder.build_chunk_prompt(216ctx,217preceding_translations=[218(219VerseReference(book="john", chapter=1, verse=18),220"これまで神を見た者は一人もいない。",221)222],223)224assert "PRECEDING TEXT AS ALREADY TRANSLATED" in prompt225assert "[1:18] これまで神を見た者は一人もいない。" in prompt226227def test_chunk_prompt_no_seam_section_without_translations(self) -> None:228from src.context.glossary import GlossaryManager229from src.models.pericope import ChunkTranslationContext, Pericope230from src.models.verse import Verse, VerseReference231from src.pipeline.prompt import PromptBuilder232233pericope = Pericope(234id="p1",235title=None,236start_chapter=1,237start_verse=1,238end_chapter=1,239end_verse=1,240verses=["1:1"],241)242ctx = ChunkTranslationContext(243pericope=pericope,244book="john",245verses=[246Verse(247reference=VerseReference(book="john", chapter=1, verse=1),248greek_text="Ἐν ἀρχῇ ἦν ὁ λόγος",249)250],251preceding_verses=[],252following_verses=[],253glossary_terms=[],254)255builder = PromptBuilder(GlossaryManager())256prompt = builder.build_chunk_prompt(ctx)257assert "PRECEDING TEXT AS ALREADY TRANSLATED" not in prompt258259260class TestWebNativeStyle:261def test_web_native_readability_prompt(self) -> None:262from src.models.style import (263READABILITY_PROMPTS,264ReadabilityLevel,265TranslationStyle,266build_system_prompt,267)268269assert ReadabilityLevel("web_native") is ReadabilityLevel.WEB_NATIVE270block = READABILITY_PROMPTS[ReadabilityLevel.WEB_NATIVE]271assert "online" in block272assert "NEVER trade meaning for smoothness" in block273274style = TranslationStyle(readability_level=ReadabilityLevel.WEB_NATIVE)275prompt = build_system_prompt(style)276assert "Web-native" in prompt277278def test_polite_formality_prompt(self) -> None:279from src.models.style import (280FORMALITY_PROMPTS,281FormalityLevel,282TranslationStyle,283build_system_prompt,284)285286assert FormalityLevel("polite") is FormalityLevel.POLITE287style = TranslationStyle(formality_level=FormalityLevel.POLITE)288prompt = build_system_prompt(style)289assert "です/ます" in prompt290291def test_style_roundtrip_with_new_values(self) -> None:292from src.models.style import FormalityLevel, ReadabilityLevel, TranslationStyle293294style = TranslationStyle(295readability_level=ReadabilityLevel.WEB_NATIVE,296formality_level=FormalityLevel.POLITE,297)298again = TranslationStyle.from_dict(style.to_dict())299assert again.readability_level is ReadabilityLevel.WEB_NATIVE300assert again.formality_level is FormalityLevel.POLITE301302def test_evaluator_criteria_web_native_audience(self) -> None:303from src.evaluation.evaluator import _build_style_criteria304305text = _build_style_criteria(306{"readability_level": "web_native", "formality_level": "polite"}307)308assert "20s" in text309assert "です/ます" in text310311312class TestMorphologyExposure:313def test_decode_greek_participle(self) -> None:314from src.pipeline.morphology import decode_greek315316note = decode_greek("δικαιούμενοι", "δικαιόω", "V-", "-PPPNPM-")317assert note == (318"δικαιούμενοι (lemma δικαιόω): present passive participle"319" nominative plural"320)321322def test_decode_greek_non_verb_returns_none(self) -> None:323from src.pipeline.morphology import decode_greek324325assert decode_greek("χάριτι", "χάρις", "N-", "----DSF-") is None326327def test_decode_hebrew_wayyiqtol_and_suffix(self) -> None:328from src.pipeline.morphology import decode_hebrew329330assert decode_hebrew("וַיֹּאמֶר", "HVqw3ms") == (331"וַיֹּאמֶר: qal wayyiqtol (narrative past) 3ms"332)333assert "pronominal suffix 1cs" in decode_hebrew("רֹעִי", "HVqrmsc/Sp1cs")334335def test_selection_prioritizes_marked_forms_and_caps(self) -> None:336from src.pipeline.morphology import select_morphology_notes337338words = [339{"word": f"w{i}", "lemma": "λύω", "pos": "V-", "parsing": "3AAI-S--"}340for i in range(8)341] + [{"word": "part", "lemma": "λύω", "pos": "V-", "parsing": "-PPPNPM-"}]342notes = select_morphology_notes(words, "greek", limit=3)343assert len(notes) == 3344assert notes[0].startswith("part") # participle outranks plain aorists345346def test_prompt_flag_disables_morphology(self, monkeypatch) -> None:347import src.pipeline.prompt as prompt_mod348from src.models.verse import Verse, VerseReference349350verse = Verse(351reference=VerseReference(book="romans", chapter=3, verse=24),352greek_text="δικαιούμενοι",353greek_words=[354{"word": "δικαιούμενοι", "lemma": "δικαιόω", "pos": "V-", "parsing": "-PPPNPM-"}355],356)357monkeypatch.setattr(prompt_mod, "MORPHOLOGY_ENABLED", True)358assert prompt_mod._morphology_notes_for(verse)359monkeypatch.setattr(prompt_mod, "MORPHOLOGY_ENABLED", False)360assert prompt_mod._morphology_notes_for(verse) == []361362def test_decode_greek_finite_verb_and_short_parsing(self) -> None:363from src.pipeline.morphology import decode_greek364365note = decode_greek("ἦν", "εἰμί", "V-", "3IAI-S--")366assert note == "ἦν (lemma εἰμί): imperfect active indicative 3sg"367assert decode_greek("x", "y", "V-", "abc") is None368assert decode_greek("x", "y", "V-", "--------") is None369370def test_decode_hebrew_edge_cases(self) -> None:371from src.pipeline.morphology import decode_hebrew372373assert decode_hebrew("שָׁלוֹם", "HNcmsa") is None # noun374assert decode_hebrew("x", "HV") is None # too short375assert decode_hebrew("x", "HVxz") is None # unknown stem+conj376# imperative prioritized377from src.pipeline.morphology import _hebrew_priority378379assert _hebrew_priority("HVqv2ms") == 0380assert _hebrew_priority("HVqp3ms") == 1381assert _hebrew_priority("HNcmsa") == 1382383def test_select_notes_empty_and_hebrew(self) -> None:384from src.pipeline.morphology import select_morphology_notes385386assert select_morphology_notes(None, "greek") == []387assert select_morphology_notes([], "hebrew") == []388notes = select_morphology_notes(389[390{"word": "וַיֹּאמֶר", "morph": "HVqw3ms"},391{"word": "אֱלֹהִים", "morph": "HNcmpa"},392],393"hebrew",394)395assert notes == ["וַיֹּאמֶר: qal wayyiqtol (narrative past) 3ms"]396397def test_chunk_prompt_includes_morphology(self, monkeypatch) -> None:398import src.pipeline.prompt as prompt_mod399400monkeypatch.setattr(prompt_mod, "MORPHOLOGY_ENABLED", True)401from src.context.glossary import GlossaryManager402from src.models.pericope import ChunkTranslationContext, Pericope403from src.models.verse import Verse, VerseReference404from src.pipeline.prompt import PromptBuilder405406pericope = Pericope(407id="p-m", title=None, start_chapter=1, start_verse=1,408end_chapter=1, end_verse=1, verses=["1:1"],409)410ctx = ChunkTranslationContext(411pericope=pericope, book="genesis",412verses=[413Verse(414reference=VerseReference(book="genesis", chapter=1, verse=1),415hebrew_text="בָּרָא",416hebrew_words=[{"word": "בָּרָא", "morph": "HVqp3ms"}],417)418],419preceding_verses=[], following_verses=[], glossary_terms=[],420)421prompt = PromptBuilder(GlossaryManager()).build_chunk_prompt(ctx)422assert "MORPHOLOGY" in prompt423assert "[1:1] בָּרָא: qal perfect (qatal) 3ms" in prompt424