a816b119e7
Backfill ELP vocabulary from FULL lexicons (UniMorph + kaikki.org Wiktionary, real gender/inflections) for 8 languages, 812,894 entries total, in the proven seed-fn format matching the 18 ancient vocabularies: es 72,032 | fr 130,517 | de 144,692 | la 22,590 | it 193,675 | pt 115,772 | ro 86,504 | ca 47,112 4 of these (es fr de la) backfill ELP languages that had morphology but no vocabulary; it/pt/ro/ca are new Romance (need morphology-*.el ports next). Adds lang_profile_* for all 8 + reproducible generators under tests/lang-gen. Vocab is runtime seed data (not in build manifest, like the 18 ancients); seed-fn format validated to compile to C via elc.
667 lines
33 KiB
Python
667 lines
33 KiB
Python
# -*- 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])
|