OPEN SOURCE
software/scripts/report_terminology.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1"""Cross-book terminology consistency report.23For every translated verse of a book, finds which glossary terms occur in the4source text and whether the translation uses the glossary's Japanese rendering5(or a discouraged one). Aggregates per-term so drift is visible at a glance —6e.g. λόγος rendered 言葉 in twelve verses but 言 in two.78Purely mechanical (no LLM calls). Usage:910uv run python scripts/report_terminology.py <book> [<book> ...]11"""1213from __future__ import annotations1415import json16import sys17from pathlib import Path1819sys.path.insert(0, str(Path(__file__).parent.parent))2021from src.context.glossary import GlossaryManager # noqa: E40222from src.data.loader import DataLoader # noqa: E40223from src.models.verse import VerseReference # noqa: E402242526_VERB_ENDINGS = "るすうくぐつぬぶむ"272829def _rendering_present(japanese: str, text: str) -> bool:30"""Check a glossary rendering against text, tolerating verb conjugation.3132義と認める won't literally appear in 義と認められる and 罪を犯す won't33appear in 罪を犯し — for renderings ending in a dictionary-form verb34ending, also accept the stem (rendering minus the final kana).35"""36if japanese in text:37return True38if len(japanese) > 2 and japanese[-1] in _VERB_ENDINGS and japanese[:-1] in text:39return True40return False414243def _accepted_renderings(term) -> list[str]:44"""All acceptable renderings: the primary plus semantic_range alternates."""45return [term.japanese] + [46alt for alt in getattr(term, "semantic_range", []) if alt47]484950def report_book(book: str, data_dir: Path) -> int:51"""Print the terminology report for one book. Returns issue count."""52loader = DataLoader(data_dir)53glossary = GlossaryManager(loader.load_glossary())5455output_dir = data_dir / "output" / book56if not output_dir.exists():57print(f"[{book}] no translations found")58return 05960# term.original -> {"used": [refs], "missing": [refs], "avoided": [(ref, bad)]}61stats: dict[str, dict] = {}6263for chapter_file in sorted(output_dir.glob("chapter_*.json")):64if chapter_file.stem.endswith("_eval"):65continue66data = json.loads(chapter_file.read_text(encoding="utf-8"))67chapter = data["chapter"]68for verse_num, vdata in data["verses"].items():69text = vdata.get("text", "")70if not text:71continue72ref = VerseReference(book=book, chapter=chapter, verse=int(verse_num))73source_verse = loader.load_verse(ref)74source = source_verse.greek_text or source_verse.hebrew_text75if not source:76continue77for term in glossary.find_terms_in_text(source):78entry = stats.setdefault(79term.original, {"used": [], "missing": [], "avoided": []}80)81label = f"{chapter}:{verse_num}"82for bad in term.avoid_translations:83if bad and bad in text:84entry["avoided"].append((label, bad))85if any(86_rendering_present(r, text) for r in _accepted_renderings(term)87):88entry["used"].append(label)89else:90entry["missing"].append(label)9192issues = 093print(f"\n=== Terminology report: {book} ===")94for original, entry in sorted(stats.items()):95used, missing, avoided = entry["used"], entry["missing"], entry["avoided"]96term = glossary.get_by_original(original)97flag = ""98if avoided:99flag = " << DISCOURAGED RENDERING USED"100issues += len(avoided)101elif used and missing:102flag = " << INCONSISTENT"103issues += len(missing)104elif missing and not used:105# Never rendered with the glossary term — may be inflection or106# legitimate paraphrase; informational only.107flag = " (glossary rendering not found — check manually)"108print(109f" {original} → {term.japanese if term else '?'}: "110f"used {len(used)}, without {len(missing)}{flag}"111)112for label in entry["missing"][:5]:113if used: # only detail actual inconsistencies114print(f" without glossary rendering: {label}")115for label, bad in avoided[:5]:116print(f" uses discouraged '{bad}': {label}")117118if not stats:119print(" (no glossary terms matched)")120return issues121122123def main() -> int:124books = sys.argv[1:]125if not books:126print(__doc__)127return 2128data_dir = Path(__file__).parent.parent / "data"129total = 0130for book in books:131total += report_book(book, data_dir)132print(f"\nTotal potential issues: {total}")133return 0134135136if __name__ == "__main__":137main()138