公開前の確認用サイト
日常訳NICHIJOYAKUEnglish ↗

OPEN SOURCE

software/tests/test_iteration1_quality.py

このサイトで読み、必要なファイルを保存できます。

日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。

← オープンソースの案内へ

software/tests/test_iteration1_quality.py424 行このファイルを保存 ↓

ZIPに含まれるものと同じ公開ソースです。行番号から、その行へのリンクを保存できます。

ファイルの確認情報SHA-256 7f1a5e175c0c5016386b39fce6ee2e65cf00a2ce4171c5601c020d8c1fae3439
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:14    def test_empty(self) -> None:15        assert _sanitize_translation_output("") == ""16        assert _sanitize_translation_output("   \n ") == ""1718    def test_plain_japanese_passthrough(self) -> None:19        text = "初めに言葉があった。言葉は神と共にあった。"20        assert _sanitize_translation_output(text) == text2122    def test_self_correction_keeps_last_japanese_block(self) -> None:23        raw = (24            "この命に命があった。\n\n"25            "Wait—let me provide the correct translation.\n\n"26            "この方の内に命があった。そしてこの命は人々の光であった。"27        )28        assert (29            _sanitize_translation_output(raw)30            == "この方の内に命があった。そしてこの命は人々の光であった。"31        )3233    def test_strips_code_fences(self) -> None:34        raw = "```\n光は闇の中で輝いている。\n```"35        assert _sanitize_translation_output(raw) == "光は闇の中で輝いている。"3637    def test_strips_translation_label(self) -> None:38        assert _sanitize_translation_output("翻訳: 神は言われた。") == "神は言われた。"3940    def test_all_latin_returns_original(self) -> None:41        raw = "I cannot translate this verse."42        assert _sanitize_translation_output(raw) == raw434445class TestKougoReference:46    def _verse(self, **kwargs) -> Verse:47        return Verse(48            reference=VerseReference(book="john", chapter=1, verse=1), **kwargs49        )5051    def test_prompt_labels_kougo_correctly(self) -> None:52        builder = PromptBuilder(GlossaryManager())53        verse = self._verse(japanese_kougo="初めに言があった。")54        section = builder._format_reference_translations(verse)55        assert "口語訳 (1955): 初めに言があった。" in section56        assert "大正改訳" not in section5758    def test_best_text_prefers_kougo_over_taisho(self) -> None:59        builder = PromptBuilder(GlossaryManager())60        verse = self._verse(japanese_kougo="口語", japanese_taisho="大正")61        assert builder._get_best_text(verse) == "口語"626364class TestHebrewGlossaryMatching:65    def test_normalize_strips_points_and_separators(self) -> None:66        assert _normalize_hebrew("בְּ/רֵאשִׁ֖ית") == "בראשית"6768    def test_pointed_text_matches_unpointed_term(self) -> None:69        gm = GlossaryManager(70            [71                EnhancedGlossaryTerm(72                    original="אלהים",73                    transliteration="elohim",74                    japanese="神",75                    english="God",76                )77            ]78        )79        terms = gm.find_terms_in_text("בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת")80        assert [t.original for t in terms] == ["אלהים"]8182    def test_capitalized_greek_inflection_matches(self) -> None:83        gm = GlossaryManager(84            [85                EnhancedGlossaryTerm(86                    original="μακάριος",87                    transliteration="makarios",88                    japanese="幸い",89                    english="blessed",90                    inflected_forms=["μακάριοι"],91                )92            ]93        )94        terms = gm.find_terms_in_text("Μακάριοι οἱ πτωχοὶ")95        assert [t.original for t in terms] == ["μακάριος"]969798class TestEvaluatorIteration2:99    def test_prompt_includes_hebrew_when_no_greek(self) -> None:100        from src.evaluation.evaluator import build_evaluation_prompt101102        prompt = build_evaluation_prompt(103            translation="初めに神は天と地を創造された。",104            greek_text=None,105            kjv_text="In the beginning God created...",106            reference_japanese=None,107            hebrew_text="בראשית ברא אלהים",108        )109        assert "ORIGINAL TEXT (Hebrew):" in prompt110        assert "בראשית" in prompt111112    def test_prompt_includes_surrounding_context(self) -> None:113        from src.evaluation.evaluator import build_evaluation_prompt114115        prompt = build_evaluation_prompt(116            translation="テスト",117            greek_text="λόγος",118            kjv_text=None,119            reference_japanese=None,120            surrounding_context="[before 1] 初めに言があった。",121        )122        assert "SURROUNDING PASSAGE" in prompt123        assert "初めに言があった" in prompt124125    def test_glossary_guidance_lists_entries(self) -> None:126        from src.evaluation.evaluator import build_evaluation_prompt127128        prompt = build_evaluation_prompt(129            translation="テスト",130            greek_text="βασιλεία",131            kjv_text=None,132            reference_japanese=None,133            glossary_terms=["βασιλεία → 御国 (in Matthew = 天の国)"],134        )135        assert "GLOSSARY GUIDANCE" in prompt136        assert "- βασιλεία → 御国 (in Matthew = 天の国)" in prompt137138    def test_build_verse_prompt_formats_terms_with_notes(self, tmp_path) -> None:139        from src.evaluation.evaluator import TranslationEvaluator140        from src.models.glossary import EnhancedGlossaryTerm141        from src.models.verse import TranslationContext, Verse, VerseReference142143        ref = VerseReference(book="matthew", chapter=5, verse=3)144        target = Verse(reference=ref, greek_text="Μακάριοι", english_kjv="Blessed")145        ctx = TranslationContext(146            target_verse=target,147            preceding_verses=[148                Verse(149                    reference=VerseReference(book="matthew", chapter=5, verse=2),150                    japanese_kougo="そして口を開いて教えられた。",151                )152            ],153            following_verses=[],154            glossary_terms=[155                EnhancedGlossaryTerm(156                    original="μακάριος",157                    transliteration="makarios",158                    japanese="幸い",159                    english="blessed",160                    notes="Beatitude formula",161                )162            ],163        )164        evaluator = TranslationEvaluator(data_dir=tmp_path)165        prompt = evaluator.build_verse_prompt("幸いである", ctx)166        assert "μακάριος → 幸い (Beatitude formula)" in prompt167        assert "そして口を開いて教えられた。" in prompt168        assert "[before 2 —" in prompt169170    def test_surrounding_context_none_when_no_neighbors(self) -> None:171        from src.evaluation.evaluator import TranslationEvaluator172        from src.models.verse import TranslationContext, Verse, VerseReference173174        ref = VerseReference(book="john", chapter=1, verse=1)175        ctx = TranslationContext(176            target_verse=Verse(reference=ref, greek_text="λόγος"),177            preceding_verses=[],178            following_verses=[],179            glossary_terms=[],180        )181        evaluator = TranslationEvaluator(data_dir="data")182        assert evaluator._format_surrounding_context(ctx) is None183184185class TestSeamContinuity:186    def test_chunk_prompt_includes_preceding_translations(self) -> None:187        from src.context.glossary import GlossaryManager188        from src.models.pericope import ChunkTranslationContext, Pericope189        from src.models.verse import Verse, VerseReference190        from src.pipeline.prompt import PromptBuilder191192        pericope = Pericope(193            id="john-1-19-test",194            title="Testimony",195            start_chapter=1,196            start_verse=19,197            end_chapter=1,198            end_verse=20,199            verses=["1:19", "1:20"],200        )201        ctx = ChunkTranslationContext(202            pericope=pericope,203            book="john",204            verses=[205                Verse(206                    reference=VerseReference(book="john", chapter=1, verse=19),207                    greek_text="Καὶ αὕτη ἐστὶν ἡ μαρτυρία",208                )209            ],210            preceding_verses=[],211            following_verses=[],212            glossary_terms=[],213        )214        builder = PromptBuilder(GlossaryManager())215        prompt = builder.build_chunk_prompt(216            ctx,217            preceding_translations=[218                (219                    VerseReference(book="john", chapter=1, verse=18),220                    "これまで神を見た者は一人もいない。",221                )222            ],223        )224        assert "PRECEDING TEXT AS ALREADY TRANSLATED" in prompt225        assert "[1:18] これまで神を見た者は一人もいない。" in prompt226227    def test_chunk_prompt_no_seam_section_without_translations(self) -> None:228        from src.context.glossary import GlossaryManager229        from src.models.pericope import ChunkTranslationContext, Pericope230        from src.models.verse import Verse, VerseReference231        from src.pipeline.prompt import PromptBuilder232233        pericope = Pericope(234            id="p1",235            title=None,236            start_chapter=1,237            start_verse=1,238            end_chapter=1,239            end_verse=1,240            verses=["1:1"],241        )242        ctx = ChunkTranslationContext(243            pericope=pericope,244            book="john",245            verses=[246                Verse(247                    reference=VerseReference(book="john", chapter=1, verse=1),248                    greek_text="Ἐν ἀρχῇ ἦν ὁ λόγος",249                )250            ],251            preceding_verses=[],252            following_verses=[],253            glossary_terms=[],254        )255        builder = PromptBuilder(GlossaryManager())256        prompt = builder.build_chunk_prompt(ctx)257        assert "PRECEDING TEXT AS ALREADY TRANSLATED" not in prompt258259260class TestWebNativeStyle:261    def test_web_native_readability_prompt(self) -> None:262        from src.models.style import (263            READABILITY_PROMPTS,264            ReadabilityLevel,265            TranslationStyle,266            build_system_prompt,267        )268269        assert ReadabilityLevel("web_native") is ReadabilityLevel.WEB_NATIVE270        block = READABILITY_PROMPTS[ReadabilityLevel.WEB_NATIVE]271        assert "online" in block272        assert "NEVER trade meaning for smoothness" in block273274        style = TranslationStyle(readability_level=ReadabilityLevel.WEB_NATIVE)275        prompt = build_system_prompt(style)276        assert "Web-native" in prompt277278    def test_polite_formality_prompt(self) -> None:279        from src.models.style import (280            FORMALITY_PROMPTS,281            FormalityLevel,282            TranslationStyle,283            build_system_prompt,284        )285286        assert FormalityLevel("polite") is FormalityLevel.POLITE287        style = TranslationStyle(formality_level=FormalityLevel.POLITE)288        prompt = build_system_prompt(style)289        assert "です/ます" in prompt290291    def test_style_roundtrip_with_new_values(self) -> None:292        from src.models.style import FormalityLevel, ReadabilityLevel, TranslationStyle293294        style = TranslationStyle(295            readability_level=ReadabilityLevel.WEB_NATIVE,296            formality_level=FormalityLevel.POLITE,297        )298        again = TranslationStyle.from_dict(style.to_dict())299        assert again.readability_level is ReadabilityLevel.WEB_NATIVE300        assert again.formality_level is FormalityLevel.POLITE301302    def test_evaluator_criteria_web_native_audience(self) -> None:303        from src.evaluation.evaluator import _build_style_criteria304305        text = _build_style_criteria(306            {"readability_level": "web_native", "formality_level": "polite"}307        )308        assert "20s" in text309        assert "です/ます" in text310311312class TestMorphologyExposure:313    def test_decode_greek_participle(self) -> None:314        from src.pipeline.morphology import decode_greek315316        note = decode_greek("δικαιούμενοι", "δικαιόω", "V-", "-PPPNPM-")317        assert note == (318            "δικαιούμενοι (lemma δικαιόω): present passive participle"319            " nominative plural"320        )321322    def test_decode_greek_non_verb_returns_none(self) -> None:323        from src.pipeline.morphology import decode_greek324325        assert decode_greek("χάριτι", "χάρις", "N-", "----DSF-") is None326327    def test_decode_hebrew_wayyiqtol_and_suffix(self) -> None:328        from src.pipeline.morphology import decode_hebrew329330        assert decode_hebrew("וַיֹּאמֶר", "HVqw3ms") == (331            "וַיֹּאמֶר: qal wayyiqtol (narrative past) 3ms"332        )333        assert "pronominal suffix 1cs" in decode_hebrew("רֹעִי", "HVqrmsc/Sp1cs")334335    def test_selection_prioritizes_marked_forms_and_caps(self) -> None:336        from src.pipeline.morphology import select_morphology_notes337338        words = [339            {"word": f"w{i}", "lemma": "λύω", "pos": "V-", "parsing": "3AAI-S--"}340            for i in range(8)341        ] + [{"word": "part", "lemma": "λύω", "pos": "V-", "parsing": "-PPPNPM-"}]342        notes = select_morphology_notes(words, "greek", limit=3)343        assert len(notes) == 3344        assert notes[0].startswith("part")  # participle outranks plain aorists345346    def test_prompt_flag_disables_morphology(self, monkeypatch) -> None:347        import src.pipeline.prompt as prompt_mod348        from src.models.verse import Verse, VerseReference349350        verse = Verse(351            reference=VerseReference(book="romans", chapter=3, verse=24),352            greek_text="δικαιούμενοι",353            greek_words=[354                {"word": "δικαιούμενοι", "lemma": "δικαιόω", "pos": "V-", "parsing": "-PPPNPM-"}355            ],356        )357        monkeypatch.setattr(prompt_mod, "MORPHOLOGY_ENABLED", True)358        assert prompt_mod._morphology_notes_for(verse)359        monkeypatch.setattr(prompt_mod, "MORPHOLOGY_ENABLED", False)360        assert prompt_mod._morphology_notes_for(verse) == []361362    def test_decode_greek_finite_verb_and_short_parsing(self) -> None:363        from src.pipeline.morphology import decode_greek364365        note = decode_greek("ἦν", "εἰμί", "V-", "3IAI-S--")366        assert note == "ἦν (lemma εἰμί): imperfect active indicative 3sg"367        assert decode_greek("x", "y", "V-", "abc") is None368        assert decode_greek("x", "y", "V-", "--------") is None369370    def test_decode_hebrew_edge_cases(self) -> None:371        from src.pipeline.morphology import decode_hebrew372373        assert decode_hebrew("שָׁלוֹם", "HNcmsa") is None  # noun374        assert decode_hebrew("x", "HV") is None  # too short375        assert decode_hebrew("x", "HVxz") is None  # unknown stem+conj376        # imperative prioritized377        from src.pipeline.morphology import _hebrew_priority378379        assert _hebrew_priority("HVqv2ms") == 0380        assert _hebrew_priority("HVqp3ms") == 1381        assert _hebrew_priority("HNcmsa") == 1382383    def test_select_notes_empty_and_hebrew(self) -> None:384        from src.pipeline.morphology import select_morphology_notes385386        assert select_morphology_notes(None, "greek") == []387        assert select_morphology_notes([], "hebrew") == []388        notes = select_morphology_notes(389            [390                {"word": "וַיֹּאמֶר", "morph": "HVqw3ms"},391                {"word": "אֱלֹהִים", "morph": "HNcmpa"},392            ],393            "hebrew",394        )395        assert notes == ["וַיֹּאמֶר: qal wayyiqtol (narrative past) 3ms"]396397    def test_chunk_prompt_includes_morphology(self, monkeypatch) -> None:398        import src.pipeline.prompt as prompt_mod399400        monkeypatch.setattr(prompt_mod, "MORPHOLOGY_ENABLED", True)401        from src.context.glossary import GlossaryManager402        from src.models.pericope import ChunkTranslationContext, Pericope403        from src.models.verse import Verse, VerseReference404        from src.pipeline.prompt import PromptBuilder405406        pericope = Pericope(407            id="p-m", title=None, start_chapter=1, start_verse=1,408            end_chapter=1, end_verse=1, verses=["1:1"],409        )410        ctx = ChunkTranslationContext(411            pericope=pericope, book="genesis",412            verses=[413                Verse(414                    reference=VerseReference(book="genesis", chapter=1, verse=1),415                    hebrew_text="בָּרָא",416                    hebrew_words=[{"word": "בָּרָא", "morph": "HVqp3ms"}],417                )418            ],419            preceding_verses=[], following_verses=[], glossary_terms=[],420        )421        prompt = PromptBuilder(GlossaryManager()).build_chunk_prompt(ctx)422        assert "MORPHOLOGY" in prompt423        assert "[1:1] בָּרָא: qal perfect (qatal) 3ms" in prompt424