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.
589 lines
24 KiB
Python
589 lines
24 KiB
Python
"""morphology_it_full.py — production-grade Italian morphological generator.
|
||
|
||
NOT a toy. Backed by three real, Wiktionary-lineage lexical sources:
|
||
|
||
VERBS
|
||
UniMorph Italian (github.com/unimorph/ita, CC-BY-SA 3.0)
|
||
10,009 verb lemmas × full paradigm, CLEAN orthography (no stress marks):
|
||
indicative present / imperfetto (PST;IPFV) / passato remoto (PST;PFV) /
|
||
futuro, condizionale (COND),
|
||
congiuntivo presente (SBJV;PRS) / imperfetto (SBJV;PST),
|
||
affirmative imperative, infinitive, gerundio (V.CVB;PRS),
|
||
past participle (masc-sg; fem/plural derived by vowel rule).
|
||
it_irreg_verbs.json — 66 high-frequency verbs UniMorph MISSES
|
||
(essere, avere, potere, uscire, tenere, prendere, piacere, …), extracted
|
||
from kaikki.org Italian, filtered to standard forms, and DE-STRESSED to
|
||
real orthography (kaikki marks tonic stress everywhere: pàrlo->parlo,
|
||
avùto->avuto; final legit accents kept: sarò, è). Built by build_it_irreg.py.
|
||
This layer takes priority — it supplies the two auxiliaries essere/avere,
|
||
which the whole passato-prossimo / essere-agreement system depends on.
|
||
|
||
NOUNS + ADJECTIVES — kaikki.org Italian (Wiktionary extract, CC-BY-SA 3.0)
|
||
noun lemmas WITH inherent gender (head-template arg) + real (often irregular)
|
||
plural — uomo->uomini, uovo->uova, dito->dita, città invariant — resolved
|
||
PER LEMMA, never guessed.
|
||
adjective lemmas with real feminine + masc/fem plural (italiano->italiana/
|
||
italiani/italiane, felice->felici invariant).
|
||
|
||
Fallbacks (degrade, never crash, on OOV input):
|
||
verbs : rule generator for regular -are/-ere/-ire (with -care/-gare h-insertion
|
||
and -ciare/-giare/-iare i-drop spelling rules)
|
||
nouns : gender heuristic (endings) + rule pluralization (ambiguous -co/-go FLAGGED)
|
||
adjs : -o/-a/-e gender rule + rule pluralization
|
||
|
||
Confidence flag on every form:
|
||
"lexicon" from UniMorph / kaikki-irregular / kaikki noun-adj (trust: high)
|
||
"rule" deterministic rule (trust: medium)
|
||
"fallback" could not inflect; returned lemma / ambiguous (trust: low -> FLAG)
|
||
|
||
Public API (used by realizer_it.py):
|
||
conjugate(lemma, mood, tense, person, number) -> (form, conf)
|
||
participle(lemma, gender="m", number="singular") -> (form, conf)
|
||
gerund(lemma) -> (form, conf)
|
||
noun_gender(lemma) -> "m"|"f"
|
||
inflect_noun(lemma, number, gender=None) -> (form, conf)
|
||
inflect_adj(lemma, gender, number) -> (form, conf)
|
||
lexicon_stats() -> dict
|
||
"""
|
||
import json
|
||
import os
|
||
import pickle
|
||
|
||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||
_UNIMORPH = os.path.join(_HERE, "data", "ita.unimorph")
|
||
_IRREG = os.path.join(_HERE, "data", "it_irreg_verbs.json")
|
||
_KAIKKI = os.path.join(_HERE, "data", "kaikki_it.jsonl")
|
||
_CACHE = os.path.join(_HERE, "data", "it_morph_cache.pkl")
|
||
|
||
# ── (mood, tense) -> UniMorph feature set that must ALL be present ────────────────
|
||
_VERB_KEYMAP = {
|
||
("ind", "present"): {"IND", "PRS"},
|
||
("ind", "imperfect"): {"IND", "PST", "IPFV"},
|
||
("ind", "passato_remoto"): {"IND", "PST", "PFV"},
|
||
("ind", "future"): {"IND", "FUT"},
|
||
("ind", "conditional"): {"COND"},
|
||
("sbjv", "present"): {"SBJV", "PRS"},
|
||
("sbjv", "imperfect"): {"SBJV", "PST"},
|
||
("imp", "affirmative"): {"POS", "IMP"},
|
||
}
|
||
_PERSON = {"first": "1", "second": "2", "third": "3"}
|
||
_NUMBER = {"singular": "SG", "plural": "PL"}
|
||
|
||
|
||
def _feat_set(tag):
|
||
return set(tag.split(";"))
|
||
|
||
|
||
# ── build verb lexicon from UniMorph ─────────────────────────────────────────────
|
||
def _build_verbs():
|
||
verbs = {} # (lemma, "mood|tense|person|number") -> form
|
||
part = {} # lemma -> masc-sg past participle
|
||
ger = {} # lemma -> gerundio
|
||
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
|
||
f = _feat_set(tag)
|
||
head = tag.split(";")[0]
|
||
|
||
if head == "V.PTCP":
|
||
if "PST" in f:
|
||
part.setdefault(lemma, form)
|
||
continue
|
||
if head == "V.CVB": # gerundio (converb, present)
|
||
if "PRS" in f:
|
||
ger.setdefault(lemma, form)
|
||
continue
|
||
if head != "V":
|
||
continue
|
||
|
||
person = next((p for p in ("1", "2", "3") if p in f), None)
|
||
number = "SG" if "SG" in f else ("PL" if "PL" in f else None)
|
||
if person is None or number is None:
|
||
continue
|
||
for (mood, tense), req in _VERB_KEYMAP.items():
|
||
# exact-set discipline: PST;PFV must not match PST;IPFV, etc.
|
||
if not req <= f:
|
||
continue
|
||
# guard IND;PST ambiguity: require the specific aspect feature
|
||
if tense == "imperfect" and "PFV" in f:
|
||
continue
|
||
if tense == "passato_remoto" and "IPFV" in f:
|
||
continue
|
||
# COND must not also be a subjunctive/imperative slot
|
||
verbs.setdefault((lemma, f"{mood}|{tense}|{person}|{number}"), form)
|
||
break
|
||
return verbs, part, ger
|
||
|
||
|
||
# ── kaikki nouns + adjectives ────────────────────────────────────────────────────
|
||
_EXCL_FORM_TAGS = {"alternative", "archaic", "obsolete", "dialectal", "regional",
|
||
"diminutive", "augmentative", "pejorative", "comparative",
|
||
"superlative", "misspelling", "rare", "informal", "literary",
|
||
"poetic", "error-unrecognized-form", "apocopic", "obsolete",
|
||
"construed", "collective"}
|
||
|
||
|
||
def _kaikki_gender(arg):
|
||
if not arg:
|
||
return None
|
||
a = str(arg).lower()
|
||
if a.startswith("f"):
|
||
return "f"
|
||
if a.startswith("m"):
|
||
return "m"
|
||
return None
|
||
|
||
|
||
def _build_nouns_adjs():
|
||
nouns = {} # lemma -> {"g","SG","PL"}
|
||
adjs = {} # lemma -> {("m","SG"),("f","SG"),("m","PL"),("f","PL")}
|
||
with open(_KAIKKI, encoding="utf-8") as fh:
|
||
for line in fh:
|
||
try:
|
||
d = json.loads(line)
|
||
except Exception:
|
||
continue
|
||
pos = d.get("pos")
|
||
word = d.get("word", "")
|
||
if not word or " " in word:
|
||
continue
|
||
forms = d.get("forms", []) or []
|
||
|
||
if pos == "noun":
|
||
ht = d.get("head_templates") or []
|
||
g = None
|
||
if ht:
|
||
g = _kaikki_gender((ht[0].get("args") or {}).get("1"))
|
||
if g is None:
|
||
tags = d.get("tags") or []
|
||
if "feminine" in tags:
|
||
g = "f"
|
||
elif "masculine" in tags:
|
||
g = "m"
|
||
pl = None
|
||
for x in forms:
|
||
t = set(x.get("tags") or [])
|
||
if "plural" in t and not (t & _EXCL_FORM_TAGS):
|
||
fm = x.get("form")
|
||
if fm and " " not in fm and fm != "#":
|
||
pl = fm
|
||
break
|
||
if word not in nouns:
|
||
nouns[word] = {"g": g, "SG": word, "PL": pl}
|
||
else:
|
||
cur = nouns[word]
|
||
if cur.get("g") is None and g:
|
||
cur["g"] = g
|
||
if not cur.get("PL") and pl:
|
||
cur["PL"] = pl
|
||
|
||
elif pos == "adj":
|
||
d0 = adjs.setdefault(word, {})
|
||
d0.setdefault(("m", "SG"), word)
|
||
for x in forms:
|
||
t = set(x.get("tags") or [])
|
||
fm = x.get("form")
|
||
if not fm or " " in fm or (t & _EXCL_FORM_TAGS):
|
||
continue
|
||
if "feminine" in t and "plural" in t:
|
||
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
|
||
elif "masculine" in t and "plural" in t:
|
||
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
|
||
elif "feminine" in t:
|
||
d0[("f", "SG")] = d0.get(("f", "SG")) or fm
|
||
elif "plural" in t: # invariant-gender adj (felice -> felici)
|
||
d0[("m", "PL")] = d0.get(("m", "PL")) or fm
|
||
d0[("f", "PL")] = d0.get(("f", "PL")) or fm
|
||
return nouns, adjs
|
||
|
||
|
||
def _build_cache():
|
||
verbs, part, ger = _build_verbs()
|
||
nouns, adjs = _build_nouns_adjs()
|
||
with open(_IRREG, encoding="utf-8") as fh:
|
||
irreg = json.load(fh)
|
||
data = {"verbs": verbs, "part": part, "ger": ger,
|
||
"nouns": nouns, "adjs": adjs, "irreg": irreg}
|
||
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):
|
||
srcs = [_UNIMORPH, _KAIKKI, _IRREG]
|
||
newest = max(os.path.getmtime(s) for s in srcs if os.path.exists(s))
|
||
if os.path.getmtime(_CACHE) >= newest:
|
||
try:
|
||
with open(_CACHE, "rb") as fh:
|
||
return pickle.load(fh)
|
||
except Exception:
|
||
pass
|
||
return _build_cache()
|
||
|
||
|
||
_LEX = _load()
|
||
_VERBS, _PART, _GER, _NOUNS, _ADJS, _IRREGV = (
|
||
_LEX["verbs"], _LEX["part"], _LEX["ger"], _LEX["nouns"], _LEX["adjs"],
|
||
_LEX["irreg"])
|
||
|
||
|
||
# ── regular-ending rule fallback ─────────────────────────────────────────────────
|
||
def _vclass(lemma):
|
||
if lemma.endswith("are"):
|
||
return "are"
|
||
if lemma.endswith("ere"):
|
||
return "ere"
|
||
if lemma.endswith("ire"):
|
||
return "ire"
|
||
return None
|
||
|
||
|
||
# endings [1sg,2sg,3sg,1pl,2pl,3pl]
|
||
_REG = {
|
||
("ind", "present", "are"): ["o", "i", "a", "iamo", "ate", "ano"],
|
||
("ind", "present", "ere"): ["o", "i", "e", "iamo", "ete", "ono"],
|
||
("ind", "present", "ire"): ["o", "i", "e", "iamo", "ite", "ono"],
|
||
("ind", "imperfect", "are"): ["avo", "avi", "ava", "avamo", "avate", "avano"],
|
||
("ind", "imperfect", "ere"): ["evo", "evi", "eva", "evamo", "evate", "evano"],
|
||
("ind", "imperfect", "ire"): ["ivo", "ivi", "iva", "ivamo", "ivate", "ivano"],
|
||
("ind", "passato_remoto", "are"): ["ai", "asti", "ò", "ammo", "aste", "arono"],
|
||
("ind", "passato_remoto", "ere"): ["ei", "esti", "é", "emmo", "este", "erono"],
|
||
("ind", "passato_remoto", "ire"): ["ii", "isti", "ì", "immo", "iste", "irono"],
|
||
("sbjv", "present", "are"): ["i", "i", "i", "iamo", "iate", "ino"],
|
||
("sbjv", "present", "ere"): ["a", "a", "a", "iamo", "iate", "ano"],
|
||
("sbjv", "present", "ire"): ["a", "a", "a", "iamo", "iate", "ano"],
|
||
("sbjv", "imperfect", "are"): ["assi", "assi", "asse", "assimo", "aste", "assero"],
|
||
("sbjv", "imperfect", "ere"): ["essi", "essi", "esse", "essimo", "este", "essero"],
|
||
("sbjv", "imperfect", "ire"): ["issi", "issi", "isse", "issimo", "iste", "issero"],
|
||
# imperative: 2sg,3sg(Lei),1pl,2pl,3pl (1sg has none)
|
||
("imp", "affirmative", "are"): [None, "a", "i", "iamo", "ate", "ino"],
|
||
("imp", "affirmative", "ere"): [None, "i", "a", "iamo", "ete", "ano"],
|
||
("imp", "affirmative", "ire"): [None, "i", "a", "iamo", "ite", "ano"],
|
||
}
|
||
# future / conditional attach to a stem = infinitive minus final -e, with
|
||
# -are -> -er (parlare->parler-), -ere/-ire keep (credere->creder-, dormir-)
|
||
_FUT = ["ò", "ai", "à", "emo", "ete", "anno"]
|
||
_COND = ["ei", "esti", "ebbe", "emmo", "este", "ebbero"]
|
||
|
||
|
||
def _slot_idx(person, number):
|
||
base = {"first": 0, "second": 1, "third": 2}[person]
|
||
return base + (0 if number == "singular" else 3)
|
||
|
||
|
||
def _fut_stem(lemma, vc):
|
||
body = lemma[:-3] # drop are/ere/ire
|
||
if vc == "are":
|
||
return body + "er"
|
||
return body + vc[0] + "r" # ere->er? no: keep vowel: creder-, dormir-
|
||
# NOTE corrected below
|
||
|
||
|
||
def _apply_are_spelling(stem, ending):
|
||
"""-care/-gare insert h before front endings; -ciare/-giare/-sciare/-iare drop i."""
|
||
front = ending[:1] in ("i", "e")
|
||
if stem.endswith(("c", "g")) and front:
|
||
return stem + "h" + ending
|
||
if stem.endswith(("ci", "gi", "sci")) and ending[:1] == "i":
|
||
return stem[:-1] + ending # mangi+iamo -> mangiamo
|
||
if stem.endswith("i") and ending[:1] == "i":
|
||
return stem[:-1] + ending # studi+iamo -> studiamo
|
||
return stem + ending
|
||
|
||
|
||
def _rule_conjugate(lemma, mood, tense, person, number):
|
||
vc = _vclass(lemma)
|
||
if vc is None:
|
||
return None
|
||
body = lemma[:-3]
|
||
i = _slot_idx(person, number)
|
||
if mood == "ind" and tense in ("future", "conditional"):
|
||
stem = body + "er" if vc == "are" else body + vc[0] + "r"
|
||
# ere: creder-, ire: dormir- -> body + 'e'/'i' + 'r'
|
||
if vc == "ere":
|
||
stem = body + "er"
|
||
elif vc == "ire":
|
||
stem = body + "ir"
|
||
end = (_FUT if tense == "future" else _COND)[i]
|
||
# spelling: -care/-gare -> cherò/gherò ; -ciare/-giare -> cerò/gerò
|
||
if vc == "are":
|
||
if body.endswith(("c", "g")):
|
||
stem = body + "her"
|
||
elif body.endswith(("ci", "gi", "sci")):
|
||
stem = body[:-1] + "er"
|
||
elif body.endswith("i"):
|
||
stem = body[:-1] + "er"
|
||
return stem + end
|
||
table = _REG.get((mood, tense, vc))
|
||
if not table:
|
||
return None
|
||
end = table[i]
|
||
if end is None:
|
||
return None
|
||
if vc == "are":
|
||
return _apply_are_spelling(body, end)
|
||
# -ere/-ire: guard against double-i (dormi+iamo -> dormiamo)
|
||
if body.endswith("i") and end[:1] == "i":
|
||
return body[:-1] + end
|
||
return body + end
|
||
|
||
|
||
# ── PUBLIC: verb conjugation ─────────────────────────────────────────────────────
|
||
def conjugate(lemma, mood, tense, person, number):
|
||
"""Return (surface, confidence). mood in ind|sbjv|imp; tense per _VERB_KEYMAP."""
|
||
lemma = lemma.strip().lower()
|
||
key = f"{mood}|{tense}|{_PERSON.get(person,'?')}|{number}"
|
||
ir = _IRREGV.get(lemma)
|
||
if ir and key in ir:
|
||
return ir[key], "lexicon"
|
||
p, n = _PERSON.get(person), _NUMBER.get(number)
|
||
if p and n:
|
||
form = _VERBS.get((lemma, f"{mood}|{tense}|{p}|{n}"))
|
||
if form:
|
||
return form, "lexicon"
|
||
r = _rule_conjugate(lemma, mood, tense, person, number)
|
||
if r:
|
||
return r, "rule"
|
||
return lemma, "fallback"
|
||
|
||
|
||
# ── PUBLIC: participle + gerund ──────────────────────────────────────────────────
|
||
def _participle_msg(lemma):
|
||
"""Return (masc-sg participle, source) or (None, None)."""
|
||
ir = _IRREGV.get(lemma)
|
||
if ir and "part" in ir:
|
||
return ir["part"], "lexicon"
|
||
if lemma in _PART:
|
||
return _PART[lemma], "lexicon"
|
||
return None, None
|
||
|
||
|
||
def participle(lemma, gender="m", number="singular"):
|
||
"""Past participle with gender/number agreement (for essere-perfect & passives).
|
||
UniMorph/irregular give masc-sg; fem/plural derived by final-vowel swap
|
||
(-o -> -a/-i/-e), valid for regular -ato/-uto/-ito AND irregulars
|
||
(preso->presa/presi/prese, aperto->aperta/aperti/aperte, morto->morta/...)."""
|
||
lemma = lemma.strip().lower()
|
||
g = "f" if gender == "f" else "m"
|
||
num = "SG" if number == "singular" else "PL"
|
||
msg, src = _participle_msg(lemma)
|
||
conf = "lexicon"
|
||
if msg is None:
|
||
vc = _vclass(lemma)
|
||
if vc == "are":
|
||
msg = lemma[:-3] + "ato"
|
||
elif vc == "ere":
|
||
msg = lemma[:-3] + "uto"
|
||
elif vc == "ire":
|
||
msg = lemma[:-3] + "ito"
|
||
else:
|
||
return lemma, "fallback"
|
||
conf = "rule"
|
||
# agreement: only -o participles inflect for gender+number
|
||
if msg.endswith("o"):
|
||
stem = msg[:-1]
|
||
suf = {"m|SG": "o", "f|SG": "a", "m|PL": "i", "f|PL": "e"}[f"{g}|{num}"]
|
||
return stem + suf, conf
|
||
return msg, conf # non -o participle: leave as-is (rare)
|
||
|
||
|
||
def gerund(lemma):
|
||
lemma = lemma.strip().lower()
|
||
ir = _IRREGV.get(lemma)
|
||
if ir and "ger" in ir:
|
||
return ir["ger"], "lexicon"
|
||
if lemma in _GER:
|
||
return _GER[lemma], "lexicon"
|
||
vc = _vclass(lemma)
|
||
if vc == "are":
|
||
return lemma[:-3] + "ando", "rule"
|
||
if vc in ("ere", "ire"):
|
||
return lemma[:-3] + "endo", "rule"
|
||
return lemma, "fallback"
|
||
|
||
|
||
# ── PUBLIC: noun gender + number ─────────────────────────────────────────────────
|
||
_FEM_SUF = ("zione", "sione", "gione", "tà", "tù", "trice", "aggine", "udine",
|
||
"igine", "ie", "essa", "izia", "ezza")
|
||
_MASC_SUF = ("ore", "ame", "iere", "ale", "ile")
|
||
|
||
|
||
def _gender_heuristic(noun):
|
||
for suf in _FEM_SUF:
|
||
if noun.endswith(suf):
|
||
return "f"
|
||
for suf in _MASC_SUF:
|
||
if noun.endswith(suf):
|
||
return "m"
|
||
if noun.endswith("o"):
|
||
return "m"
|
||
if noun.endswith("a"):
|
||
return "f"
|
||
if noun.endswith("à") or noun.endswith("ù"):
|
||
return "f"
|
||
return "m" # -e and consonant-final loanwords default masculine
|
||
|
||
|
||
def noun_gender(lemma):
|
||
lemma = lemma.strip().lower()
|
||
d = _NOUNS.get(lemma)
|
||
if d and d.get("g") in ("m", "f"):
|
||
return d["g"]
|
||
return _gender_heuristic(lemma)
|
||
|
||
|
||
def _rule_plural(noun, gender):
|
||
"""Deterministic Italian pluralization. Returns (form, ok); ok=False FLAGS an
|
||
ambiguous case the lexicon would normally resolve (-co/-go palatalization)."""
|
||
if not noun:
|
||
return noun, True
|
||
# invariant: accented final vowel, consonant-final, monosyllable, -i final
|
||
if noun[-1:] in ("à", "è", "é", "ì", "í", "ò", "ó", "ù", "ú"):
|
||
return noun, True
|
||
if noun[-1:] not in ("a", "e", "o", "i", "u"):
|
||
return noun, True # consonant-final loanword: invariant
|
||
if noun.endswith("i"):
|
||
return noun, True # e.g. crisi, analisi: invariant
|
||
if noun.endswith("io"):
|
||
return noun[:-2] + "i", True # figlio->figli (unstressed i)
|
||
if noun.endswith("cia") or noun.endswith("gia"):
|
||
# vowel before cia/gia -> -cie/-gie ; consonant -> -ce/-ge (approx)
|
||
return noun[:-2] + "e", True # arancia->arance (majority)
|
||
if noun.endswith("ca"):
|
||
return noun[:-2] + "che", True # amica->amiche
|
||
if noun.endswith("ga"):
|
||
return noun[:-2] + "ghe", True
|
||
if noun.endswith("co"):
|
||
return noun[:-2] + "chi", False # AMBIGUOUS (amico->amici) -> flag
|
||
if noun.endswith("go"):
|
||
return noun[:-2] + "ghi", False # AMBIGUOUS (psicologo->psicologi)
|
||
if noun.endswith("a"):
|
||
return noun[:-1] + "e", True # casa->case (m -a: -i, but rare)
|
||
if noun.endswith("o"):
|
||
return noun[:-1] + "i", True # libro->libri
|
||
if noun.endswith("e"):
|
||
return noun[:-1] + "i", True # cane->cani, chiave->chiavi
|
||
return noun, True
|
||
|
||
|
||
def inflect_noun(lemma, number, gender=None):
|
||
lemma = lemma.strip().lower()
|
||
d = _NOUNS.get(lemma)
|
||
if number == "singular":
|
||
return (d["SG"] if d and d.get("SG") else lemma), ("lexicon" if d else "rule")
|
||
if d and d.get("PL"):
|
||
return d["PL"], "lexicon"
|
||
g = gender or noun_gender(lemma)
|
||
form, ok = _rule_plural(lemma, g)
|
||
return form, ("rule" if ok else "fallback")
|
||
|
||
|
||
# adjectives whose kaikki entries are unreliable (messy inflection templates):
|
||
# supply audited regular agreement forms (prenominal apocope handled in realizer).
|
||
_ADJ_FIX = {
|
||
"bello": {("m", "SG"): "bello", ("f", "SG"): "bella",
|
||
("m", "PL"): "belli", ("f", "PL"): "belle"},
|
||
"quello": {("m", "SG"): "quello", ("f", "SG"): "quella",
|
||
("m", "PL"): "quelli", ("f", "PL"): "quelle"},
|
||
}
|
||
|
||
|
||
# ── PUBLIC: adjective agreement ──────────────────────────────────────────────────
|
||
def inflect_adj(lemma, gender, number):
|
||
lemma = lemma.strip().lower()
|
||
g = "f" if gender == "f" else "m"
|
||
num = "SG" if number == "singular" else "PL"
|
||
fix = _ADJ_FIX.get(lemma)
|
||
if fix and (g, num) in fix:
|
||
return fix[(g, num)], "lexicon"
|
||
d = _ADJS.get(lemma)
|
||
if d:
|
||
form = d.get((g, num))
|
||
if form:
|
||
return form, "lexicon"
|
||
sg = d.get((g, "SG")) or d.get(("m", "SG")) or lemma
|
||
if num == "PL":
|
||
pl, ok = _rule_plural(sg, g)
|
||
return pl, ("rule" if ok else "fallback")
|
||
return sg, "lexicon"
|
||
# rule fallback
|
||
a = lemma
|
||
if a.endswith("o"): # -o/-a/-i/-e class
|
||
base = a[:-1]
|
||
suf = {"m|SG": "o", "f|SG": "a", "m|PL": "i", "f|PL": "e"}[f"{g}|{num}"]
|
||
return base + suf, "rule"
|
||
if a.endswith("e"): # felice-class: SG invariant, PL -i
|
||
if num == "PL":
|
||
return a[:-1] + "i", "rule"
|
||
return a, "rule"
|
||
if num == "PL":
|
||
p, ok = _rule_plural(a, g)
|
||
return p, ("rule" if ok else "fallback")
|
||
return a, "rule"
|
||
|
||
|
||
def lexicon_stats():
|
||
return {
|
||
"verb_source": "UniMorph Italian (github.com/unimorph/ita) + kaikki.org "
|
||
"irregulars (de-stressed)",
|
||
"noun_adj_source": "kaikki.org Italian (Wiktionary extract)",
|
||
"license": "CC-BY-SA 3.0 (Wiktionary/UniMorph lineage)",
|
||
"unimorph_verb_forms": len(_VERBS),
|
||
"unimorph_verb_lemmas": len({k[0] for k in _VERBS}),
|
||
"irregular_verb_lemmas": len(_IRREGV),
|
||
"participle_lemmas": len(_PART),
|
||
"gerund_lemmas": len(_GER),
|
||
"noun_lemmas": len(_NOUNS),
|
||
"adj_lemmas": len(_ADJS),
|
||
}
|
||
|
||
|
||
if __name__ == "__main__":
|
||
print(json.dumps(lexicon_stats(), indent=2, ensure_ascii=False))
|
||
tests = [
|
||
("parlare", "ind", "present", "first", "singular", "parlo"),
|
||
("essere", "ind", "present", "third", "singular", "è"),
|
||
("avere", "ind", "present", "first", "singular", "ho"),
|
||
("mangiare", "ind", "present", "second", "singular", "mangi"),
|
||
("finire", "ind", "present", "first", "singular", "finisco"),
|
||
("andare", "ind", "present", "third", "plural", "vanno"),
|
||
("fare", "ind", "future", "first", "singular", "farò"),
|
||
("potere", "sbjv", "present", "third", "singular", "possa"),
|
||
("prendere", "ind", "passato_remoto", "first", "singular", "presi"),
|
||
("cercare", "ind", "present", "second", "singular", "cerchi"),
|
||
("dormire", "ind", "present", "third", "plural", "dormono"),
|
||
("credere", "ind", "future", "first", "singular", "crederò"),
|
||
]
|
||
ok = 0
|
||
for lemma, mood, tense, per, num, exp in tests:
|
||
got, conf = conjugate(lemma, mood, tense, per, num)
|
||
flag = "OK " if got == exp else "XX "
|
||
ok += got == exp
|
||
print(f" {flag}{lemma:9} {mood}/{tense:14} {per[:3]}.{num[:2]} -> {got:12} ({conf}) exp={exp}")
|
||
print(f"verb tests {ok}/{len(tests)}")
|
||
print(" gender: casa=", noun_gender("casa"), "problema=", noun_gender("problema"),
|
||
"mano=", noun_gender("mano"), "città=", noun_gender("città"),
|
||
"cane=", noun_gender("cane"))
|
||
print(" plural: uomo->", inflect_noun("uomo", "plural"),
|
||
"| uovo->", inflect_noun("uovo", "plural"),
|
||
"| città->", inflect_noun("città", "plural"),
|
||
"| amico->", inflect_noun("amico", "plural"),
|
||
"| casa->", inflect_noun("casa", "plural"))
|
||
print(" adj: italiano/f/pl->", inflect_adj("italiano", "f", "plural"),
|
||
"| felice/m/pl->", inflect_adj("felice", "m", "plural"),
|
||
"| bello/f/sg->", inflect_adj("bello", "f", "singular"))
|
||
print(" part: aprire/f/sg->", participle("aprire", "f", "singular"),
|
||
"| prendere/m/pl->", participle("prendere", "m", "plural"),
|
||
"| andare/f/sg->", participle("andare", "f", "singular"))
|
||
print(" ger: fare->", gerund("fare"), "| parlare->", gerund("parlare"))
|