OPEN SOURCE
software/src/context/glossary.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1"""Glossary management for consistent theological term translation."""23from __future__ import annotations45import re6from typing import TYPE_CHECKING, cast78from src.models.glossary import EnhancedGlossaryTerm, GlossaryTerm, TheologicalDomain910if TYPE_CHECKING:11pass1213_HEBREW_MARKS = re.compile(r"[֑-ׇ/]")141516def _normalize_hebrew(text: str) -> str:17"""Strip Hebrew pointing/cantillation and morpheme separators."""18return _HEBREW_MARKS.sub("", text)192021class GlossaryManager:22"""Manages theological term glossary for consistent translations.2324Supports morphological matching through lemmas and inflected forms.25"""2627def __init__(self, terms: list[GlossaryTerm] | None = None) -> None:28"""Initialize the glossary manager.2930Args:31terms: Initial list of glossary terms.32"""33self._terms: list[GlossaryTerm] = terms or []34self._by_original: dict[str, GlossaryTerm] = {}35self._by_transliteration: dict[str, GlossaryTerm] = {}36self._by_domain: dict[TheologicalDomain, list[GlossaryTerm]] = {37domain: [] for domain in TheologicalDomain38}39# Morphological indexes40self._by_lemma: dict[str, GlossaryTerm] = {}41self._by_strongs: dict[str, GlossaryTerm] = {}42self._all_forms: dict[str, GlossaryTerm] = {} # All matchable forms43self._match_pattern: re.Pattern | None = None # Compiled regex for fast matching44self._rebuild_indexes()4546def _rebuild_indexes(self) -> None:47"""Rebuild lookup indexes including morphological indexes."""48self._by_original = {term.original: term for term in self._terms}49self._by_transliteration = {50term.transliteration.lower(): term for term in self._terms51}52# Rebuild domain index53self._by_domain = {domain: [] for domain in TheologicalDomain}54for term in self._terms:55self._by_domain[term.domain].append(term)5657# Rebuild morphological indexes58self._by_lemma = {}59self._by_strongs = {}60self._all_forms = {}6162for term in self._terms:63# Index by lemma64if term.lemma:65self._by_lemma[term.lemma] = term6667# Index by Strong's number68if term.strongs_number:69self._by_strongs[term.strongs_number.upper()] = term7071# Index all matchable forms72for form in term.get_all_matchable_forms():73self._all_forms[form.lower()] = term7475# Build compiled regex pattern for fast matching76self._build_match_pattern()7778def _build_match_pattern(self) -> None:79"""Build compiled regex pattern for matching all glossary forms."""80if not self._all_forms:81self._match_pattern = None82return8384# Sort forms by length (longest first) to match longest forms first85sorted_forms = cast(list[str], sorted(self._all_forms.keys(), key=len, reverse=True))8687# Escape special regex characters and join with |88escaped_forms = [re.escape(form) for form in sorted_forms]89pattern_str = "|".join(escaped_forms)9091# Compile with Unicode support; IGNORECASE so verse-initial92# capitalized Greek forms (Μακάριοι) still match.93self._match_pattern = re.compile(pattern_str, re.UNICODE | re.IGNORECASE)9495def add_term(self, term: GlossaryTerm) -> None:96"""Add a term to the glossary."""97self._terms.append(term)98self._by_original[term.original] = term99self._by_transliteration[term.transliteration.lower()] = term100self._by_domain[term.domain].append(term)101102# Update morphological indexes103if term.lemma:104self._by_lemma[term.lemma] = term105if term.strongs_number:106self._by_strongs[term.strongs_number.upper()] = term107for form in term.get_all_matchable_forms():108self._all_forms[form.lower()] = term109110# Rebuild match pattern (could be optimized for single additions)111self._build_match_pattern()112113def get_by_original(self, original: str) -> GlossaryTerm | None:114"""Look up a term by its original Greek/Hebrew form."""115return self._by_original.get(original)116117def get_by_transliteration(self, translit: str) -> GlossaryTerm | None:118"""Look up a term by its transliteration."""119return self._by_transliteration.get(translit.lower())120121def get_by_lemma(self, lemma: str) -> GlossaryTerm | None:122"""Look up a term by its lemma (dictionary form).123124Args:125lemma: The lemma to look up.126127Returns:128The GlossaryTerm if found, None otherwise.129"""130return self._by_lemma.get(lemma)131132def get_by_strongs(self, strongs: str) -> GlossaryTerm | None:133"""Look up a term by Strong's Concordance number.134135Args:136strongs: Strong's number (e.g., 'G3056').137138Returns:139The GlossaryTerm if found, None otherwise.140"""141return self._by_strongs.get(strongs.upper())142143def get_by_any_form(self, form: str) -> GlossaryTerm | None:144"""Look up a term by any of its matchable forms.145146Searches original, lemma, and all inflected forms.147148Args:149form: The form to look up.150151Returns:152The GlossaryTerm if found, None otherwise.153"""154return self._all_forms.get(form.lower())155156def get_by_domain(self, domain: TheologicalDomain) -> list[GlossaryTerm]:157"""Get all terms in a specific theological domain.158159Args:160domain: The theological domain to filter by.161162Returns:163List of terms in that domain.164"""165return list(self._by_domain.get(domain, []))166167def get_related_terms(self, term: GlossaryTerm) -> list[GlossaryTerm]:168"""Get all related terms for a given term.169170Args:171term: The term to find related terms for.172173Returns:174List of related GlossaryTerm objects.175"""176related = []177for original in term.related_terms:178related_term = self.get_by_original(original)179if related_term:180related.append(related_term)181return related182183def find_terms_in_text(self, text: str) -> list[GlossaryTerm]:184"""Find all glossary terms present in a text.185186Uses efficient compiled regex matching with morphological awareness.187Matches original forms, lemmas, and all known inflected forms.188189Args:190text: Source text (Greek, Hebrew, or transliteration).191192Returns:193List of unique matching glossary terms.194"""195if not self._match_pattern or not text:196return []197198found_terms: dict[str, GlossaryTerm] = {} # Use dict to deduplicate199200# Match against the raw text and, for Hebrew, against a normalized201# copy with vowel points/cantillation (U+0591-U+05C7) and MorphHB202# "/" morpheme separators removed — glossary entries store unpointed203# consonantal forms, which never match pointed text directly.204candidates = [text]205normalized = _normalize_hebrew(text)206if normalized != text:207candidates.append(normalized)208209for candidate in candidates:210for match in self._match_pattern.finditer(candidate):211matched_form = match.group(0)212term = self._all_forms.get(matched_form.lower())213if term and term.original not in found_terms:214found_terms[term.original] = term215216# Also check transliterations (case-insensitive)217text_lower = text.lower()218for term in self._terms:219if term.original not in found_terms:220if term.transliteration.lower() in text_lower:221found_terms[term.original] = term222223return list(found_terms.values())224225def find_terms_in_text_with_positions(226self, text: str227) -> list[tuple[GlossaryTerm, int, int]]:228"""Find all glossary terms in text with their positions.229230Useful for highlighting or annotating text with glossary matches.231232Args:233text: Source text (Greek, Hebrew, or transliteration).234235Returns:236List of tuples (term, start_pos, end_pos) for each match.237"""238if not self._match_pattern or not text:239return []240241matches = []242for match in self._match_pattern.finditer(text):243matched_form = match.group(0)244term = self._all_forms.get(matched_form.lower())245if term:246matches.append((term, match.start(), match.end()))247248return matches249250def get_all_terms(self) -> list[GlossaryTerm]:251"""Get all glossary terms."""252return list(self._terms)253254def format_for_prompt(255self, terms: list[GlossaryTerm], include_enhanced: bool = True256) -> str:257"""Format glossary terms for inclusion in a translation prompt.258259Args:260terms: List of terms to format.261include_enhanced: Whether to include enhanced metadata.262263Returns:264Formatted string for prompt inclusion.265"""266if not terms:267return "No specific glossary terms apply to this verse."268269lines = []270for term in terms:271# Core information272line = f"- {term.original} ({term.transliteration}) → {term.japanese}"273if term.notes:274line += f" [{term.notes}]"275lines.append(line)276277# Enhanced metadata (if available and requested)278if include_enhanced and isinstance(term, EnhancedGlossaryTerm):279if term.semantic_range:280semantic_str = ", ".join(term.semantic_range)281lines.append(f" Semantic range: {semantic_str}")282if term.avoid_translations:283avoid_str = ", ".join(term.avoid_translations)284lines.append(f" Avoid: {avoid_str}")285if term.definition:286lines.append(f" Definition: {term.definition}")287288return "\n".join(lines)289290def get_domain_summary(self) -> dict[str, int]:291"""Get a summary of term counts by domain.292293Returns:294Dictionary mapping domain names to term counts.295"""296return {297domain.value: len(terms) for domain, terms in self._by_domain.items()298}299300def __len__(self) -> int:301return len(self._terms)302