# -*- coding: utf-8 -*- """morphology_lat_full.py — production-grade Latin morphological generator. Latin is the FLAGSHIP dead-language realizer. It rides the *architecture* of the Romance/Italic engine (the same Realization / spec-driven design and the UniMorph loader pattern from morphology_it_full.py) but with the CASE SYSTEM RESTORED — the feature Romance lost. Latin therefore exercises machinery the modern Romance siblings never needed: 5 declensions x 6 cases x 2 numbers x 3 genders, plus a 4-conjugation verb system with tense/mood/voice. DATA (real, attested — no fabrication): NOUNS + ADJECTIVES — UniMorph Latin (github.com/unimorph/lat, CC-BY-SA 3.0) 163,182 N forms across ~thousands of lemmas, each with the full case paradigm N;NOM/GEN/DAT/ACC/ABL/VOC;SG/PL (real inflected forms, WITH macrons: puella->puellam, rēx->rēgis, corpus->corporis). 244,197 ADJ forms with case x GENDER x number, incl. UniMorph's combined tags (GEN+DAT, MASC+FEM, MASC+FEM+NEUT) which are split on load. 462,668 V.PTCP forms (participles) also carry case/gender/number. UniMorph N tags DO NOT encode inherent gender, so noun gender is inferred from the declension (nom-sg + gen-sg endings) with a curated exceptions map — the standard, attestable rule (1st decl -a/-ae = fem, 2nd -us/-i = masc, -um = neut, ...). VERBS — RULE ENGINE (honest gap: UniMorph Latin's verb list is a 947-lemma sample of rare/prefixed verbs that MISSES every core textbook verb — amō, videō, sum, regō, ... are all absent). Latin conjugation is, however, highly regular, so verbs are generated by a deterministic 4-conjugation engine over curated principal parts (present / perfect / supine stems), sourced from standard references. Irregulars (sum, possum, eō, ferō, volō, nōlō, mālō) are curated full tables. Forms are flagged "rule" (not "lexicon") for honesty. Confidence flag on every form (same contract as the Romance engine): "lexicon" from UniMorph (trust: high) "rule" deterministic morphology rule (trust: medium) "fallback" could not inflect; returned lemma (trust: low -> FLAG) Public API (used by realizer_lat.py): decline_noun(lemma, case, number) -> (form, conf) noun_gender(lemma) -> "m"|"f"|"n" decline_adj(lemma, case, gender, number) -> (form, conf) conjugate(lemma, tense, mood, voice, person, number) -> (form, conf) participle(lemma, kind, case, gender, number) -> (form, conf) # kind: prs|pfv|fut infinitive(lemma, tense="present", voice="active") -> (form, conf) lexicon_stats() -> dict """ import os import pickle _HERE = os.path.dirname(os.path.abspath(__file__)) _UNIMORPH = os.path.join(_HERE, "data", "lat.unimorph") _CACHE = os.path.join(_HERE, "data", "lat_morph_cache.pkl") _CASES = ("NOM", "GEN", "DAT", "ACC", "ABL", "VOC") _CASE_MAP = {"nom": "NOM", "gen": "GEN", "dat": "DAT", "acc": "ACC", "abl": "ABL", "voc": "VOC"} _NUM = {"singular": "SG", "plural": "PL"} _GEN = {"m": "MASC", "f": "FEM", "n": "NEUT"} # ── UniMorph loader: noun + adjective + participle case paradigms ──────────────── def _build_cache(): nouns = {} # lemma -> {(CASE, NUM): form} adjs = {} # lemma -> {(CASE, GEN, NUM): form} ptcps = {} # lemma -> {(CASE, GEN, NUM): form} (from V.PTCP; keyed loosely) with open(_UNIMORPH, encoding="utf-8") as fh: for line in fh: line = line.rstrip("\n") if not line or "\t" not in line: continue parts = line.split("\t") if len(parts) != 3: continue lemma, form, tag = parts feats = tag.split(";") head = feats[0] fs = set(feats) case = next((c for c in _CASES if c in fs), None) # handle combined case tags like GEN+DAT if case is None: for f in feats: if "+" in f and any(c in f.split("+") for c in _CASES): case = [c for c in _CASES if c in f.split("+")] break num = "SG" if "SG" in fs else ("PL" if "PL" in fs else None) if case is None or num is None: continue cases = case if isinstance(case, list) else [case] if head == "N": d = nouns.setdefault(lemma, {}) for c in cases: d.setdefault((c, num), form) elif head == "ADJ": # gender may be combined: MASC+FEM+NEUT, MASC+FEM genders = [] for g in ("MASC", "FEM", "NEUT"): if any(g == x or (g in x.split("+")) for x in feats): genders.append(g) if not genders: genders = ["MASC", "FEM", "NEUT"] d = adjs.setdefault(lemma, {}) for c in cases: for g in genders: d.setdefault((c, g, num), form) data = {"nouns": nouns, "adjs": adjs, "ptcps": ptcps} try: with open(_CACHE, "wb") as fh: pickle.dump(data, fh, protocol=pickle.HIGHEST_PROTOCOL) except OSError: pass return data def _load(): if os.path.exists(_CACHE) and os.path.exists(_UNIMORPH): if os.path.getmtime(_CACHE) >= os.path.getmtime(_UNIMORPH): try: with open(_CACHE, "rb") as fh: return pickle.load(fh) except Exception: pass return _build_cache() _LEX = _load() _NOUNS, _ADJS = _LEX["nouns"], _LEX["adjs"] # ── noun gender inference (declension-based, curated exceptions) ───────────────── # Real, attestable rule: gender follows declension + nominative shape, with the # standard closed set of exceptions. _GENDER_EXC = { # 1st-declension masculines (people/agents) "agricola": "m", "poēta": "m", "nauta": "m", "incola": "m", "scrība": "m", "auriga": "m", "pīrāta": "m", "athlēta": "m", # 2nd-declension neuters / feminines "vīrus": "n", "vulgus": "n", "pelagus": "n", "humus": "f", # common 3rd-declension whose gender the ending would mispredict "rēx": "m", "dux": "m", "mīles": "m", "pater": "m", "frāter": "m", "homō": "m", "leō": "m", "sōl": "m", "mōns": "m", "pōns": "m", "fōns": "m", "sanguis": "m", "ōrdō": "m", "sermō": "m", "amor": "m", "dolor": "m", "labor": "m", "timor": "m", "honor": "m", "color": "m", "pēs": "m", "dēns": "m", "flōs": "m", "mōs": "m", "mensis": "m", "orbis": "m", "piscis": "m", "ignis": "m", "collis": "m", "grex": "m", "prīnceps": "m", "māter": "f", "soror": "f", "uxor": "f", "mulier": "f", "virgō": "f", "urbs": "f", "arx": "f", "pāx": "f", "lēx": "f", "lūx": "f", "vōx": "f", "nox": "f", "nix": "f", "vīs": "f", "salūs": "f", "virtūs": "f", "aetās": "f", "cīvitās": "f", "lībertās": "f", "vēritās": "f", "voluptās": "f", "nātiō": "f", "ratiō": "f", "ōrātiō": "f", "legiō": "f", "regiō": "f", "mens": "f", "gens": "f", "ars": "f", "pars": "f", "mors": "f", "sors": "f", "nāvis": "f", "turris": "f", "avis": "f", "vallis": "f", "classis": "f", "corpus": "n", "tempus": "n", "opus": "n", "genus": "n", "onus": "n", "pectus": "n", "latus": "n", "vulnus": "n", "scelus": "n", "sīdus": "n", "caput": "n", "iter": "n", "flūmen": "n", "nōmen": "n", "carmen": "n", "agmen": "n", "certāmen": "n", "lūmen": "n", "ōmen": "n", "cōgnōmen": "n", "mare": "n", "animal": "n", "exemplar": "n", "rēte": "n", # 4th-declension exceptions "manus": "f", "domus": "f", "tribus": "f", "porticus": "f", "īdūs": "f", "cornū": "n", "genū": "n", "gelū": "n", "verū": "n", # 5th-declension "diēs": "m", "merīdiēs": "m", } def _infer_gender(lemma): if lemma in _GENDER_EXC: return _GENDER_EXC[lemma] d = _NOUNS.get(lemma) nom = d.get(("NOM", "SG")) if d else lemma gen = d.get(("GEN", "SG")) if d else None nom = nom or lemma # 5th declension: gen -eī / -ēī if gen and (gen.endswith("eī") or gen.endswith("ēī")): return "f" # 1st declension: nom -a, gen -ae if nom.endswith("a") and (not gen or gen.endswith("ae")): return "f" # 2nd declension neuter: nom -um if nom.endswith("um"): return "n" # 2nd declension masc: nom -us/-er/-ir, gen -ī if (nom.endswith("us") or nom.endswith("er") or nom.endswith("ir")) and \ (not gen or gen.endswith("ī")): return "m" # 4th declension: gen -ūs if gen and gen.endswith("ūs"): return "n" if nom.endswith("ū") else "m" # 3rd declension neuters by common nom endings if nom.endswith(("men", "us", "ur", "al", "ar", "e", "ma")): # -us here is 3rd-decl neuter type (corpus) only if gen shows -oris/-eris if nom.endswith("us") and gen and (gen.endswith("oris") or gen.endswith("eris") or gen.endswith("uris")): return "n" if nom.endswith(("men", "al", "ar", "e")): return "n" # default 3rd-declension: masculine (most common) return "m" _GENDER_CACHE = {} def noun_gender(lemma): lemma = lemma.strip() if lemma not in _GENDER_CACHE: _GENDER_CACHE[lemma] = _infer_gender(lemma) return _GENDER_CACHE[lemma] # ── PUBLIC: noun declension ───────────────────────────────────────────────────── def decline_noun(lemma, case, number): lemma = lemma.strip() C = _CASE_MAP.get(case, case.upper()) N = _NUM.get(number, number) d = _NOUNS.get(lemma) if d and (C, N) in d: return d[(C, N)], "lexicon" # abl sg often == the -e/-o form; try nom fallback if d: # try VOC==NOM, ACC neuter==NOM etc are already in data; last resort lemma return lemma, "fallback" return lemma, "fallback" # ── PUBLIC: adjective declension ──────────────────────────────────────────────── def decline_adj(lemma, case, gender, number): lemma = lemma.strip() C = _CASE_MAP.get(case, case.upper()) G = _GEN.get(gender, gender.upper()) N = _NUM.get(number, number) d = _ADJS.get(lemma) if d and (C, G, N) in d: return d[(C, G, N)], "lexicon" # try other gender (some adjs listed only under MASC+FEM etc handled at load) if d: for altG in ("MASC", "FEM", "NEUT"): if (C, altG, N) in d: return d[(C, altG, N)], "lexicon" return lemma, "fallback" return lemma, "fallback" # ═══════════════════════════════════════════════════════════════════════════════ # VERB RULE ENGINE (4 conjugations + curated irregulars) # ═══════════════════════════════════════════════════════════════════════════════ # Curated principal parts for common attested verbs: # lemma -> (conj, present_stem, perfect_stem, supine_stem) # conj in {1,2,3,"3io",4}. Stems carry macrons (matching UniMorph orthography). _VERBS = { "amō": (1, "am", "amāv", "amāt"), "laudō": (1, "laud", "laudāv", "laudāt"), "portō": (1, "port", "portāv", "portāt"), "vocō": (1, "voc", "vocāv", "vocāt"), "dō": (1, "d", "ded", "dat"), "spectō": (1, "spect", "spectāv", "spectāt"), "pugnō": (1, "pugn", "pugnāv", "pugnāt"), "labōrō": (1, "labōr", "labōrāv", "labōrāt"), "necō": (1, "nec", "necāv", "necāt"), "parō": (1, "par", "parāv", "parāt"), "cōgitō": (1, "cōgit", "cōgitāv", "cōgitāt"), "habitō": (1, "habit", "habitāv", "habitāt"), "nārrō": (1, "nārr", "nārrāv", "nārrāt"), "servō": (1, "serv", "servāv", "servāt"), "superō": (1, "super", "superāv", "superāt"), "oppugnō": (1, "oppugn", "oppugnāv", "oppugnāt"), "ambulō": (1, "ambul", "ambulāv", "ambulāt"), "clāmō": (1, "clām", "clāmāv", "clāmāt"), "vulnerō": (1, "vulner", "vulnerāv", "vulnerāt"), "aedificō": (1, "aedific", "aedificāv", "aedificāt"), "expugnō": (1, "expugn", "expugnāv", "expugnāt"), "dēfendō": (3, "dēfend", "dēfend", "dēfēns"), "petō": (3, "pet", "petīv", "petīt"), "occīdō": (3, "occīd", "occīd", "occīs"), "interficiō": ("3io", "interfic", "interfēc", "interfect"), "timeō": (2, "tim", "timu", None), "iaceō": (2, "iac", "iacu", None), "pāreō": (2, "pār", "pāru", "pārit"), "respondeō": (2, "respond", "respond", "respōns"), "vertō": (3, "vert", "vert", "vers"), "ostendō": (3, "ostend", "ostend", "ostent"), "cōnstituō": (3, "cōnstitu", "cōnstitu", "cōnstitūt"), "cōgnōscō": (3, "cōgnōsc", "cōgnōv", "cōgnit"), "crēdō": (3, "crēd", "crēdid", "crēdit"), "ēdūcō": (3, "ēdūc", "ēdūx", "ēduct"), "cōnservō": (1, "cōnserv", "cōnservāv", "cōnservāt"), "iuvō": (1, "iuv", "iūv", "iūt"), "dēbeō": (2, "dēb", "dēbu", "dēbit"), "moneō": (2, "mon", "monu", "monit"), "videō": (2, "vid", "vīd", "vīs"), "habeō": (2, "hab", "habu", "habit"), "teneō": (2, "ten", "tenu", "tent"), "timeō": (2, "tim", "timu", None), "terreō": (2, "terr", "terru", "territ"), "dēleō": (2, "dēl", "dēlēv", "dēlēt"), "iubeō": (2, "iub", "iuss", "iuss"), "maneō": (2, "man", "māns", "māns"), "moveō": (2, "mov", "mōv", "mōt"), "doceō": (2, "doc", "docu", "doct"), "sedeō": (2, "sed", "sēd", "sess"), "rīdeō": (2, "rīd", "rīs", "rīs"), "regō": (3, "reg", "rēx", "rēct"), "dūcō": (3, "dūc", "dūx", "duct"), "scrībō": (3, "scrīb", "scrīps", "scrīpt"), "mittō": (3, "mitt", "mīs", "miss"), "pōnō": (3, "pōn", "posu", "posit"), "agō": (3, "ag", "ēg", "āct"), "dīcō": (3, "dīc", "dīx", "dict"), "gerō": (3, "ger", "gess", "gest"), "vincō": (3, "vinc", "vīc", "vict"), "petō": (3, "pet", "petīv", "petīt"), "legō": (3, "leg", "lēg", "lēct"), "currō": (3, "curr", "cucurr", "curs"), "vīvō": (3, "vīv", "vīx", "vīct"), "quaerō": (3, "quaer", "quaesīv", "quaesīt"), "trahō": (3, "trah", "trāx", "tract"), "claudō": (3, "claud", "claus", "claus"), "cōgō": (3, "cōg", "coēg", "coāct"), "relinquō": (3, "relinqu", "relīqu", "relict"), "capiō": ("3io", "cap", "cēp", "capt"), "faciō": ("3io", "fac", "fēc", "fact"), "iaciō": ("3io", "iac", "iēc", "iact"), "rapiō": ("3io", "rap", "rapu", "rapt"), "fugiō": ("3io", "fug", "fūg", "fugit"), "cupiō": ("3io", "cup", "cupīv", "cupīt"), "accipiō": ("3io", "accip", "accēp", "accept"), "audiō": (4, "aud", "audīv", "audīt"), "veniō": (4, "ven", "vēn", "vent"), "sciō": (4, "sc", "scīv", "scīt"), "sentiō": (4, "sent", "sēns", "sēns"), "mūniō": (4, "mūn", "mūnīv", "mūnīt"), "dormiō": (4, "dorm", "dormīv", "dormīt"), "aperiō": (4, "aper", "aperu", "apert"), "inveniō": (4, "inven", "invēn", "invent"), } # ── Present-system paradigms: full ending tables per conjugation, attached to the # bare present stem (pstem). Hardcoded from the standard grammar with correct # macrons/vowel-lengths — deterministic and independently verifiable. Keys: # (tense, mood, voice) -> {conj: [1sg,2sg,3sg,1pl,2pl,3pl]} _PARADIGM = { ("present", "ind", "active"): { 1: ["ō", "ās", "at", "āmus", "ātis", "ant"], 2: ["eō", "ēs", "et", "ēmus", "ētis", "ent"], 3: ["ō", "is", "it", "imus", "itis", "unt"], "3io": ["iō", "is", "it", "imus", "itis", "iunt"], 4: ["iō", "īs", "it", "īmus", "ītis", "iunt"], }, ("present", "ind", "passive"): { 1: ["or", "āris", "ātur", "āmur", "āminī", "antur"], 2: ["eor", "ēris", "ētur", "ēmur", "ēminī", "entur"], 3: ["or", "eris", "itur", "imur", "iminī", "untur"], "3io": ["ior", "eris", "itur", "imur", "iminī", "iuntur"], 4: ["ior", "īris", "ītur", "īmur", "īminī", "iuntur"], }, ("imperfect", "ind", "active"): { 1: ["ābam", "ābās", "ābat", "ābāmus", "ābātis", "ābant"], 2: ["ēbam", "ēbās", "ēbat", "ēbāmus", "ēbātis", "ēbant"], 3: ["ēbam", "ēbās", "ēbat", "ēbāmus", "ēbātis", "ēbant"], "3io": ["iēbam", "iēbās", "iēbat", "iēbāmus", "iēbātis", "iēbant"], 4: ["iēbam", "iēbās", "iēbat", "iēbāmus", "iēbātis", "iēbant"], }, ("imperfect", "ind", "passive"): { 1: ["ābar", "ābāris", "ābātur", "ābāmur", "ābāminī", "ābantur"], 2: ["ēbar", "ēbāris", "ēbātur", "ēbāmur", "ēbāminī", "ēbantur"], 3: ["ēbar", "ēbāris", "ēbātur", "ēbāmur", "ēbāminī", "ēbantur"], "3io": ["iēbar", "iēbāris", "iēbātur", "iēbāmur", "iēbāminī", "iēbantur"], 4: ["iēbar", "iēbāris", "iēbātur", "iēbāmur", "iēbāminī", "iēbantur"], }, ("future", "ind", "active"): { 1: ["ābō", "ābis", "ābit", "ābimus", "ābitis", "ābunt"], 2: ["ēbō", "ēbis", "ēbit", "ēbimus", "ēbitis", "ēbunt"], 3: ["am", "ēs", "et", "ēmus", "ētis", "ent"], "3io": ["iam", "iēs", "iet", "iēmus", "iētis", "ient"], 4: ["iam", "iēs", "iet", "iēmus", "iētis", "ient"], }, ("future", "ind", "passive"): { 1: ["ābor", "āberis", "ābitur", "ābimur", "ābiminī", "ābuntur"], 2: ["ēbor", "ēberis", "ēbitur", "ēbimur", "ēbiminī", "ēbuntur"], 3: ["ar", "ēris", "ētur", "ēmur", "ēminī", "entur"], "3io": ["iar", "iēris", "iētur", "iēmur", "iēminī", "ientur"], 4: ["iar", "iēris", "iētur", "iēmur", "iēminī", "ientur"], }, ("present", "sbjv", "active"): { 1: ["em", "ēs", "et", "ēmus", "ētis", "ent"], 2: ["eam", "eās", "eat", "eāmus", "eātis", "eant"], 3: ["am", "ās", "at", "āmus", "ātis", "ant"], "3io": ["iam", "iās", "iat", "iāmus", "iātis", "iant"], 4: ["iam", "iās", "iat", "iāmus", "iātis", "iant"], }, ("present", "sbjv", "passive"): { 1: ["er", "ēris", "ētur", "ēmur", "ēminī", "entur"], 2: ["ear", "eāris", "eātur", "eāmur", "eāminī", "eantur"], 3: ["ar", "āris", "ātur", "āmur", "āminī", "antur"], "3io": ["iar", "iāris", "iātur", "iāmur", "iāminī", "iantur"], 4: ["iar", "iāris", "iātur", "iāmur", "iāminī", "iantur"], }, ("imperfect", "sbjv", "active"): { 1: ["ārem", "ārēs", "āret", "ārēmus", "ārētis", "ārent"], 2: ["ērem", "ērēs", "ēret", "ērēmus", "ērētis", "ērent"], 3: ["erem", "erēs", "eret", "erēmus", "erētis", "erent"], "3io": ["erem", "erēs", "eret", "erēmus", "erētis", "erent"], 4: ["īrem", "īrēs", "īret", "īrēmus", "īrētis", "īrent"], }, ("imperfect", "sbjv", "passive"): { 1: ["ārer", "ārēris", "ārētur", "ārēmur", "ārēminī", "ārentur"], 2: ["ērer", "ērēris", "ērētur", "ērēmur", "ērēminī", "ērentur"], 3: ["erer", "erēris", "erētur", "erēmur", "erēminī", "erentur"], "3io": ["erer", "erēris", "erētur", "erēmur", "erēminī", "erentur"], 4: ["īrer", "īrēris", "īrētur", "īrēmur", "īrēminī", "īrentur"], }, } # perfect-active endings (added to perfect stem) — same for all conjugations _PERF_ACT = { ("perfect", "ind"): ["ī", "istī", "it", "imus", "istis", "ērunt"], ("pluperfect", "ind"): ["eram", "erās", "erat", "erāmus", "erātis", "erant"], ("futureperfect", "ind"): ["erō", "eris", "erit", "erimus", "eritis", "erint"], ("perfect", "sbjv"): ["erim", "erīs", "erit", "erīmus", "erītis", "erint"], ("pluperfect", "sbjv"):["issem", "issēs", "isset", "issēmus", "issētis", "issent"], } def _idx(person, number): base = {"first": 0, "second": 1, "third": 2}[person] return base + (0 if number == "singular" else 3) def _present_system(conj, pstem, tense, mood, voice, person, number): """Generate a present-system form (present/imperfect/future ind & subj).""" table = _PARADIGM.get((tense, mood, voice)) if not table or conj not in table: return None return pstem + table[conj][_idx(person, number)] def _active_infinitive_stem(conj, pstem): return {1: pstem + "ā", 2: pstem + "ē", 3: pstem + "e", "3io": pstem + "e", 4: pstem + "ī"}[conj] _IRREG = { "sum": { ("present", "ind", "active"): ["sum", "es", "est", "sumus", "estis", "sunt"], ("imperfect", "ind", "active"): ["eram", "erās", "erat", "erāmus", "erātis", "erant"], ("future", "ind", "active"): ["erō", "eris", "erit", "erimus", "eritis", "erunt"], ("perfect", "ind", "active"): ["fuī", "fuistī", "fuit", "fuimus", "fuistis", "fuērunt"], ("pluperfect", "ind", "active"): ["fueram", "fuerās", "fuerat", "fuerāmus", "fuerātis", "fuerant"], ("present", "sbjv", "active"): ["sim", "sīs", "sit", "sīmus", "sītis", "sint"], ("imperfect", "sbjv", "active"): ["essem", "essēs", "esset", "essēmus", "essētis", "essent"], }, "possum": { ("present", "ind", "active"): ["possum", "potes", "potest", "possumus", "potestis", "possunt"], ("imperfect", "ind", "active"): ["poteram", "poterās", "poterat", "poterāmus", "poterātis", "poterant"], ("future", "ind", "active"): ["poterō", "poteris", "poterit", "poterimus", "poteritis", "poterunt"], ("perfect", "ind", "active"): ["potuī", "potuistī", "potuit", "potuimus", "potuistis", "potuērunt"], ("present", "sbjv", "active"): ["possim", "possīs", "possit", "possīmus", "possītis", "possint"], }, "eō": { ("present", "ind", "active"): ["eō", "īs", "it", "īmus", "ītis", "eunt"], ("imperfect", "ind", "active"): ["ībam", "ībās", "ībat", "ībāmus", "ībātis", "ībant"], ("future", "ind", "active"): ["ībō", "ībis", "ībit", "ībimus", "ībitis", "ībunt"], ("perfect", "ind", "active"): ["iī", "īstī", "iit", "iimus", "īstis", "iērunt"], ("present", "sbjv", "active"): ["eam", "eās", "eat", "eāmus", "eātis", "eant"], }, "volō": { ("present", "ind", "active"): ["volō", "vīs", "vult", "volumus", "vultis", "volunt"], ("imperfect", "ind", "active"): ["volēbam", "volēbās", "volēbat", "volēbāmus", "volēbātis", "volēbant"], ("future", "ind", "active"): ["volam", "volēs", "volet", "volēmus", "volētis", "volent"], ("perfect", "ind", "active"): ["voluī", "voluistī", "voluit", "voluimus", "voluistis", "voluērunt"], ("present", "sbjv", "active"): ["velim", "velīs", "velit", "velīmus", "velītis", "velint"], }, "nōlō": { ("present", "ind", "active"): ["nōlō", "nōn vīs", "nōn vult", "nōlumus", "nōn vultis", "nōlunt"], ("present", "sbjv", "active"): ["nōlim", "nōlīs", "nōlit", "nōlīmus", "nōlītis", "nōlint"], }, "ferō": { ("present", "ind", "active"): ["ferō", "fers", "fert", "ferimus", "fertis", "ferunt"], ("imperfect", "ind", "active"): ["ferēbam", "ferēbās", "ferēbat", "ferēbāmus", "ferēbātis", "ferēbant"], ("future", "ind", "active"): ["feram", "ferēs", "feret", "ferēmus", "ferētis", "ferent"], ("perfect", "ind", "active"): ["tulī", "tulistī", "tulit", "tulimus", "tulistis", "tulērunt"], ("present", "sbjv", "active"): ["feram", "ferās", "ferat", "ferāmus", "ferātis", "ferant"], }, } def conjugate(lemma, tense, mood, voice="active", person="third", number="singular"): """Return (surface, confidence). Perfect-passive forms are periphrastic and handled in the realizer (sum + PPP); this returns synthetic forms only.""" lemma = lemma.strip() i = _idx(person, number) ir = _IRREG.get(lemma) if ir: tbl = ir.get((tense, mood, voice)) or ir.get((tense, mood, "active")) if tbl and tbl[i]: return tbl[i], "rule" v = _VERBS.get(lemma) if not v: v = _infer_principal_parts(lemma) if not v: return lemma, "fallback" conj, pstem, perfstem, supstem = v # imperative (present active) 2sg / 2pl if mood == "imp": return _imperative(conj, pstem, person, number), "rule" # perfect-system active if tense in ("perfect", "pluperfect", "futureperfect") and voice == "active": if not perfstem: return lemma, "fallback" end = _PERF_ACT.get((tense, mood)) if end: return perfstem + end[i], "rule" # present-system (active + passive) if tense in ("present", "imperfect", "future"): form = _present_system(conj, pstem, tense, mood, voice, person, number) if form: return form, "rule" return lemma, "fallback" def _imperative(conj, pstem, person, number): if number == "singular": return {1: pstem + "ā", 2: pstem + "ē", 3: pstem + "e", "3io": pstem + "e", 4: pstem + "ī"}[conj] return {1: pstem + "āte", 2: pstem + "ēte", 3: pstem + "ite", "3io": pstem + "ite", 4: pstem + "īte"}[conj] def _infer_principal_parts(lemma): """OOV fallback: infer conjugation + stems from the 1sg-present citation form. Perfect/supine stems are guessed regularly (often wrong for 3rd conj) and the resulting forms are still returned as 'rule' but the realizer down-weights.""" if lemma.endswith("ō"): base = lemma[:-1] # can't distinguish conj from 1sg alone reliably; default by ending vowel if base.endswith("i"): return ("3io", base[:-1], base[:-1] + "īv", base[:-1] + "īt") return (3, base, base + "s", base + "t") return None # ── PUBLIC: participles ───────────────────────────────────────────────────────── def participle(lemma, kind, case="nom", gender="m", number="singular"): """kind: 'prs' (present active, -ns/-ntis), 'pfv' (perfect passive, -tus), 'fut' (future active, -tūrus). Declined as an adjective via rule endings. Returns (form, conf).""" v = _VERBS.get(lemma) if not v: return lemma, "fallback" conj, pstem, perfstem, supstem = v if kind == "pfv": if not supstem: return lemma, "fallback" base = supstem[:-1] if supstem.endswith("t") or supstem.endswith("s") else supstem stem = supstem # supine stem already ends in t/s: amāt- -> amātus return _decline_us_a_um(stem, case, gender, number), "rule" if kind == "fut": if not supstem: return lemma, "fallback" return _decline_us_a_um(supstem + "ūr", case, gender, number), "rule" if kind == "prs": # present active participle: stem + ns (nom), stem + nt- (oblique), 3rd-decl pv = {1: "ā", 2: "ē", 3: "ē", "3io": "iē", 4: "iē"}[conj] ntstem = pstem + pv + "nt" return _decline_pres_ptcp(pstem + pv, case, gender, number), "rule" return lemma, "fallback" def _decline_us_a_um(stem, case, gender, number): """Decline a -us/-a/-um adjective/participle stem (2-1-2 declension).""" C = _CASE_MAP.get(case, case.upper()) end = { ("NOM", "m", "singular"): "us", ("NOM", "f", "singular"): "a", ("NOM", "n", "singular"): "um", ("GEN", "m", "singular"): "ī", ("GEN", "f", "singular"): "ae", ("GEN", "n", "singular"): "ī", ("DAT", "m", "singular"): "ō", ("DAT", "f", "singular"): "ae", ("DAT", "n", "singular"): "ō", ("ACC", "m", "singular"): "um", ("ACC", "f", "singular"): "am", ("ACC", "n", "singular"): "um", ("ABL", "m", "singular"): "ō", ("ABL", "f", "singular"): "ā", ("ABL", "n", "singular"): "ō", ("VOC", "m", "singular"): "e", ("VOC", "f", "singular"): "a", ("VOC", "n", "singular"): "um", ("NOM", "m", "plural"): "ī", ("NOM", "f", "plural"): "ae", ("NOM", "n", "plural"): "a", ("GEN", "m", "plural"): "ōrum", ("GEN", "f", "plural"): "ārum", ("GEN", "n", "plural"): "ōrum", ("DAT", "m", "plural"): "īs", ("DAT", "f", "plural"): "īs", ("DAT", "n", "plural"): "īs", ("ACC", "m", "plural"): "ōs", ("ACC", "f", "plural"): "ās", ("ACC", "n", "plural"): "a", ("ABL", "m", "plural"): "īs", ("ABL", "f", "plural"): "īs", ("ABL", "n", "plural"): "īs", ("VOC", "m", "plural"): "ī", ("VOC", "f", "plural"): "ae", ("VOC", "n", "plural"): "a", }.get((C, gender, number), "us") return stem + end def _decline_pres_ptcp(stem, case, gender, number): """Present active participle (amāns, amantis) — 3rd-declension, stem+ns/nt.""" C = _CASE_MAP.get(case, case.upper()) if C == "NOM" and number == "singular": return stem + "ns" if C == "VOC" and number == "singular": return stem + "ns" base = stem + "nt" end = { ("GEN", "singular"): "is", ("DAT", "singular"): "ī", ("ACC", "singular"): "em" if gender != "n" else "", ("ABL", "singular"): "e", ("NOM", "plural"): "ēs" if gender != "n" else "ia", ("GEN", "plural"): "ium", ("DAT", "plural"): "ibus", ("ACC", "plural"): "ēs" if gender != "n" else "ia", ("ABL", "plural"): "ibus", ("VOC", "plural"): "ēs", }.get((C, number), "is") if C == "ACC" and number == "singular" and gender == "n": return stem + "ns" return base + end def infinitive(lemma, tense="present", voice="active"): lemma = lemma.strip() if lemma == "sum": return ("esse", "rule") if tense == "present" else ("fuisse", "rule") v = _VERBS.get(lemma) if not v: return lemma, "fallback" conj, pstem, perfstem, supstem = v if tense == "present": if voice == "active": return _active_infinitive_stem(conj, pstem).rstrip() + \ ("re" if conj != 3 and conj != "3io" else "re"), "rule" # passive present infinitive base = {1: pstem + "ā", 2: pstem + "ē", 4: pstem + "ī"}.get(conj) if base: return base + "rī", "rule" return pstem + "ī", "rule" # 3rd: regī if tense == "perfect" and voice == "active" and perfstem: return perfstem + "isse", "rule" return lemma, "fallback" def lexicon_stats(): return { "noun_adj_source": "UniMorph Latin (github.com/unimorph/lat, CC-BY-SA 3.0)", "verb_source": "rule-based 4-conjugation engine over curated attested " "principal parts (UniMorph verb list is a 947-lemma sample " "MISSING all core verbs — amō/sum/videō absent)", "noun_lemmas": len(_NOUNS), "adj_lemmas": len(_ADJS), "curated_verb_lemmas": len(_VERBS) + len(_IRREG), "gender_inference": "declension-based (nom+gen endings) + curated exceptions", } if __name__ == "__main__": import json print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False)) print("\n-- noun declension puella (1st, fem) --") for c in ("nom", "gen", "dat", "acc", "abl", "voc"): print(f" {c}: sg={decline_noun('puella', c, 'singular')[0]:10} " f"pl={decline_noun('puella', c, 'plural')[0]}") print("\n-- rēx (3rd, m):", [decline_noun('rēx', c, 'singular')[0] for c in ('nom','gen','dat','acc','abl')]) print("-- gender: puella=", noun_gender("puella"), "rēx=", noun_gender("rēx"), "bellum=", noun_gender("bellum"), "corpus=", noun_gender("corpus"), "manus=", noun_gender("manus"), "diēs=", noun_gender("diēs")) print("\n-- conjugate videō (2nd) present ind active --") for p in ("first", "second", "third"): for n in ("singular", "plural"): print(f" {p[:3]}.{n[:2]}: {conjugate('videō','present','ind','active',p,n)[0]}") print("-- amō forms:", conjugate("amō","present","ind","active","first","singular")[0], conjugate("amō","imperfect","ind","active","third","plural")[0], conjugate("amō","future","ind","active","first","singular")[0], conjugate("amō","perfect","ind","active","third","singular")[0]) print("-- sum:", [conjugate("sum","present","ind","active",p,"singular")[0] for p in ("first","second","third")]) print("-- participle amō pfv acc.f.sg:", participle("amō","pfv","acc","f","singular")[0]) print("-- infinitive amō:", infinitive("amō")[0], "| regō pass:", infinitive("regō", voice="passive")[0])