"""Selective morphology notes for translation prompts. The MorphGNT (Greek) and OSHB/MorphHB (Hebrew) sources carry word-level parse codes. Dumping them all would be noise; verbs are where parsing actually disambiguates the Japanese rendering (tense/aspect, voice, mood, participle constructions, wayyiqtol chains), so we decode verb forms only, prioritize the forms most likely to be mis-rendered, and cap the count. """ from __future__ import annotations _GREEK_TENSE = { "P": "present", "I": "imperfect", "F": "future", "A": "aorist", "X": "perfect", "Y": "pluperfect", } _GREEK_VOICE = {"A": "active", "M": "middle", "P": "passive"} _GREEK_MOOD = { "I": "indicative", "D": "imperative", "S": "subjunctive", "O": "optative", "N": "infinitive", "P": "participle", } _GREEK_CASE = {"N": "nominative", "G": "genitive", "D": "dative", "A": "accusative", "V": "vocative"} _GREEK_NUMBER = {"S": "singular", "P": "plural"} # Non-indicative moods and marked tenses are where renderings go wrong. _GREEK_PRIORITY_MOODS = {"P", "D", "S", "N", "O"} _GREEK_PRIORITY_TENSES = {"I", "X", "Y", "F"} _HEBREW_STEM = { "q": "qal", "N": "niphal", "p": "piel", "P": "pual", "h": "hiphil", "H": "hophal", "t": "hithpael", } _HEBREW_CONJ = { "p": "perfect (qatal)", "q": "sequential perfect (weqatal)", "i": "imperfect (yiqtol)", "w": "wayyiqtol (narrative past)", "h": "cohortative", "j": "jussive", "v": "imperative", "r": "active participle", "s": "passive participle", "a": "infinitive absolute", "c": "infinitive construct", } def decode_greek(word: str, lemma: str, pos: str, parsing: str) -> str | None: """Decode a MorphGNT verb entry to a short human-readable note. MorphGNT ``parsing`` has 8 slots: person, tense, voice, mood, case, number, gender, degree. """ if pos.strip("-") != "V" or len(parsing) < 6: return None person, tense, voice, mood = parsing[0], parsing[1], parsing[2], parsing[3] case, number = parsing[4], parsing[5] bits = [] if tense in _GREEK_TENSE: bits.append(_GREEK_TENSE[tense]) if voice in _GREEK_VOICE: bits.append(_GREEK_VOICE[voice]) if mood in _GREEK_MOOD: bits.append(_GREEK_MOOD[mood]) if mood == "P": # participles: case/number matter for construal if case in _GREEK_CASE: bits.append(_GREEK_CASE[case]) if number in _GREEK_NUMBER: bits.append(_GREEK_NUMBER[number]) elif person in "123" and number in _GREEK_NUMBER: bits.append(f"{person}{'sg' if number == 'S' else 'pl'}") if not bits: return None return f"{word} (lemma {lemma}): {' '.join(bits)}" def _greek_priority(parsing: str) -> int: mood = parsing[3] if len(parsing) > 3 else "-" tense = parsing[1] if len(parsing) > 1 else "-" if mood in _GREEK_PRIORITY_MOODS: return 0 if tense in _GREEK_PRIORITY_TENSES: return 1 return 2 def decode_hebrew(word: str, morph: str) -> str | None: """Decode an OSHB verb entry (e.g. ``HVqw3ms`` or ``HVqrmsc/Sp1cs``).""" segments = morph.split("/") main = next((s.lstrip("H") for s in segments if s.lstrip("H").startswith("V")), None) if main is None or len(main) < 3: return None stem, conj = main[1], main[2] bits = [] if stem in _HEBREW_STEM: bits.append(_HEBREW_STEM[stem]) if conj in _HEBREW_CONJ: bits.append(_HEBREW_CONJ[conj]) if not bits: return None rest = main[3:6] if rest and rest[0] in "123": bits.append(rest) if any(s.startswith("Sp") for s in segments): suffix = next(s for s in segments if s.startswith("Sp")) bits.append(f"+ pronominal suffix {suffix[2:]}") return f"{word}: {' '.join(bits)}" def _hebrew_priority(morph: str) -> int: main = next((s.lstrip("H") for s in morph.split("/") if s.lstrip("H").startswith("V")), "") conj = main[2] if len(main) > 2 else "-" if conj in "rsvhj": # participles, imperatives, volitives return 0 return 1 def select_morphology_notes( words: list[dict] | None, language: str, limit: int = 6 ) -> list[str]: """Select and decode the most disambiguation-relevant verb forms. Args: words: Word entries from the source JSON. language: "greek" or "hebrew". limit: Maximum number of notes. Returns: Decoded notes, highest-priority first, capped at ``limit``. """ if not words: return [] candidates: list[tuple[int, str]] = [] for w in words: if language == "greek": note = decode_greek( w.get("word", ""), w.get("lemma", ""), w.get("pos", ""), w.get("parsing", "") ) if note: candidates.append((_greek_priority(w.get("parsing", "")), note)) else: note = decode_hebrew(w.get("word", ""), w.get("morph", "")) if note: candidates.append((_hebrew_priority(w.get("morph", "")), note)) candidates.sort(key=lambda c: c[0]) return [note for _, note in candidates[:limit]]