Download docs/api.md from samuli/juurio: direct link, hf CLI and curl.
- Browser
- Download file 19.1 kB
-
https://huggingface.co/samuli/juurio/resolve/main/docs/api.md
- Command line
-
hf download hf://samuli/juurio/docs/api.md
-
curl -L -o api.md https://huggingface.co/samuli/juurio/resolve/main/docs/api.md
juurio — API & CLI contract
Open-vocabulary Finnish morphological transforms over raw text. You hand it a sentence and an
operation (past, passive, plural, …); it parses, transforms the finite main clause, and
returns the result or an explicit abstention. The differentiator is OOV/loan words: words the
exception lexicon does not cover (striimata, tubettaja) are inflected by small neural realizers.
Architecture (the "split thesis")
Three components, each owning what it is best at:
| Concern | Component |
|---|---|
| structure / scope (which clause, which subject/object) | neural UD dependency parser (Giella/Stanza-free) |
| idiosyncrasy-free morphology (known words) | net + lexicon exception table (byte-exact to the Giella FST) |
| open-vocabulary morphology (OOV/loan words) | tiny lemma-anchored neural realizers |
The serving runtime needs only torch/numpy/transformers — no Stanza, no Giella FST, no Voikko (those are used only to build the models and data). The rule engine owns all non-local structure (agreement propagation, object case, subject drop); the neural realizers are called per token, only when the lexicon has no entry for the cell.
Scope boundary (a feature, not a limitation)
Two entry points with deliberately different promises:
transform(op, text)— finite-clause transforms that preserve (or deliberately rewrite) the clause's grammar: negation, tense/mood, passive, imperative, question, number agreement, possessive on the subject head. Output is a grammatical sentence or an explicit abstain.realize_np(np, …)— phrase-level nominal morphology: inflect an NP into a Case/Number/Px(+ clitic). This is where arbitrary case lives, because case is syntactically governed — making it a clause op would produce ungrammatical sentences (Talo on iso→Talossa on iso).
Anything semantic — e.g. existential/locative framing (Taloissani on koira), which introduces
and reorders arguments — is out of scope for both; it would be a separate future generator op,
not a morphological transform. Keeping these apart is what lets each give a clean guarantee.
Python API
from juurio import Transformer, transform, SUPPORTED_OPS, OP_CATALOG, REASONS, ABSTAIN_REASONS
t = Transformer() # loads parser + models once (heavy; reuse it)
r = t.transform("past", "Koira haukkuu pihalla.")
r.ok # True
r.text # 'Koira haukkui pihalla.'
r.reason # 'ok'
# one-shot convenience (reuses a process-wide Transformer):
transform("plural", "Striimaaja nukkuu.").text # 'Striimaajat nukkuvat.' (OOV noun, neural)
import juurio is cheap; torch + the neural parser/realizers load lazily on the first
Transformer() / transform() (no Stanza/Giella at serving time).
Transformer.transform(op, text, lemma=None, sent_index=0) -> Result
- op — one of
SUPPORTED_OPS(elseValueError). - text — raw text; a leading dialogue dash/quote is handled and re-prepended.
- lemma — override the parser's lemma for the main verb. Use when a loan stem is mis-lemmatized
(
striimaa→ parser saysstriä); a confidence warning is raised automatically otherwise. - sent_index — which parsed sentence to operate on (default 0).
Result
| field | meaning |
|---|---|
text |
transformed sentence, or None if abstained |
ok |
True iff a form was produced |
reason |
one of REASONS ("ok" on success, else in ABSTAIN_REASONS) |
trace |
provenance dict (see below) — juurio transform --trace prints it |
warnings |
low-confidence-lemma notes + neural realization notices (model guess, unverified against the lexicon) |
trace fields: op, root_verb, parser_lemma, lemma_source (override/parser); and, when
relevant, neural_oov (forms the realizer produced — model guesses), oov_unrealizable /
oov_unverified (words it could not/did-not-trust). realize_np traces util, case, number,
px, clitic, head. Inspect on the CLI with --trace, or as JSON with --json.
Performance
Models load once per process; the first call also loads the neural parser (the cold cost).
Indicative numbers (juurio bench, CPU parser + GPU realizer):
| time | |
|---|---|
Transformer() construct |
~2 s |
first transform (cold — parser load) |
~9 s |
transform (warm) |
~70 ms |
realize_np (warm) |
~30 ms |
Reuse one Transformer (or the module-level transform/realize_np, which cache a singleton).
Scope contract
transform operates on a single finite Act/Ind main clause (≈≤12 words, one finite verb).
Anything else abstains (ok=False) — it never guesses. Reason codes:
| reason | meaning |
|---|---|
ok |
a transformation was produced |
parse_empty |
the parser returned no usable sentence at sent_index |
no_clause |
no finite Act/Ind main clause (subordinate-only, fragment, or already-negated) |
out_of_scope |
exceeds scope limits (too long / >1 finite verb / coordination) |
op_not_applicable |
op does not apply to this clause's base state (e.g. past on a past clause) |
oov_unrealizable |
an open-class word needed inflecting but neither the lexicon nor the neural realizer could produce the target form |
oov_unverified |
the neural realizer produced a candidate for an OOV word but it failed validation (stem-inconsistent or near-tie) and was suppressed, not emitted |
conflicting_features |
inflect got two valid but contradictory inputs (spec mon gen + number="sing"; proper=True + a degree; like= + an explicit case/number/degree; a single-word control on a phrase) |
unanalyzable_model |
inflect(like=…) could not analyze the model word into a nominal case/number cell to copy |
Neural OOV is surfaced, never silent. When a form is produced by the neural realizer (not the
lexicon), it appears in trace["neural_unverified"] and a warnings entry. Treat it as a model guess.
Operations
SUPPORTED_OPS is the flat tuple; OP_CATALOG groups them with descriptions. Summary:
- finite verb —
neg,past,pres,cond,perfect,potential,passive - compound tenses (analytic olla auxiliary + the perfect participle) —
pluperfect(olin sanonut),cond_perfect(olisin sanonut),pot_perfect(lienen sanonut); OOV verbs reuse the trainedperfectparticiple + the lexicon'sollaauxiliary - composites (applied atomically) —
neg+past,neg+cond,neg+perfect,passive+past,passive+cond,passive+perfect,neg+passive,neg+passive+past,neg+passive+cond,neg+passive+perfect - clause —
quest(polar question) - imperative paradigm (drop subject, total object → Nom; negative forces partitive; trailing
punctuation preserved) —
imperative(Sg2, OOV-capable),imperative_pl2/sg3/pl1/pl3, and the negativeälä-seriesneg_imperative(Sg2, OOV-capable),neg_imperative_pl2/sg3/pl1/pl3. Scope: applies only when the command's addressee is in the clause — a subjectless/already-imperative source (osta talo.) or a 2nd-person subject (sinä/te); a 3rd/1st-person subject abstains (out_of_scope), since turning "the man buys a house" into a command would invent an addressee. - number (re-agrees subject NP, modifiers, finite verb, copula predicate) —
plural,singular - degree (predicate adjective) —
comp,super - enclitics (on the finite verb) —
clitic_kin,clitic_kaan,clitic_han,clitic_pa - possessive suffix on the subject head, preserving its case/number —
poss_1sg,poss_2sg,poss_3,poss_1pl,poss_2pl(talo→taloni,talossa→talossani; OOVstriimaaja→striimaajani)
juurio list-ops prints the live catalog.
Combined nominal morphology
Nominal ops compose with +: a +-join of nominal atoms (plural/singular, poss_*,
clitic_*) is applied to the subject head as one realization with the final
Number+Case+Px, then the clitic is attached — deterministic surface order stem+case+number+Px+clitic.
This is correct-by-construction (you cannot re-pluralize the already-realized string taloni).
Number changes also re-agree modifiers and the finite verb.
poss_1sg+clitic_kin Koira haukkuu pihalla. -> Koiranikin haukkuu pihalla.
plural+poss_1sg Koira haukkuu pihalla. -> Koirani haukkuvat pihalla.
plural+poss_1sg+clitic_kin Koira haukkuu pihalla. -> Koiranikin haukkuvat pihalla.
plural+poss_1sg Striimaaja nukkuu. -> Striimaajani nukkuvat. (OOV head, neural)
Each dimension may appear at most once. +-composed verb ops (neg+passive+past) are separate,
pre-registered atoms in the catalog. Mixed verb+nominal composition is not supported.
Phrase-level nominal morphology: realize_np
Finnish case is syntactically governed, so arbitrary case is not a clause transform (a
subject-case change would be ungrammatical). Instead it is a phrase utility, separate from
transform, which keeps grammaticality guarantees:
from juurio import realize_np
realize_np("talo", case="Ine", number="Plur", px="1Sg").text # 'taloissani'
realize_np("iso talo", case="Ine", number="Plur").text # 'isoissa taloissa'
realize_np("Helsinki", case="Ine").text # 'Helsingissä'
realize_np("striimaaja", case="Ade", number="Plur", px="1Sg", clitic="kin").text # 'striimaajillanikin' (OOV)
realize_np(np, case, number, px, clitic) inflects an NP — a bare lemma or a short adj* noun
phrase — into the target Case/Number with an optional possessive and enclitic, agreeing modifiers,
and per-token lexicon→neural fallback (surface order stem+case+number+Px+clitic). It returns a Result
(ok with the NP, or an abstain reason). This is the showcase for open-vocabulary nominal
morphology; transform is for grammaticality-preserving clause operations. Sentence-level locatives
(Taloissani on koira) are a separate future generator op, not a case transform.
Phrase-level verb morphology: realize_verbform
The non-finite counterpart of realize_np: inflect a VERB into a participle or infinitive.
The four participles inflect like adjectives in case+number; the 2nd/3rd infinitives are case-marked.
A second open-vocabulary surface — the lexicon handles known verbs, a small verbform realizer
inflects OOV/loan verbs (striimata), flagged unverified.
from juurio import realize_verbform # form ∈ {va, nut, tava, tu, inf2, inf2_pass, inf3, ma}
realize_verbform("lukea", form="va").text # 'lukeva' (present active participle)
realize_verbform("lukea", form="va", case="Ine").text # 'lukevassa'
realize_verbform("lukea", form="tu").text # 'luettu' (past passive participle)
realize_verbform("mennä", form="inf3", case="Ade").text # 'menemällä' (3rd inf adessive)
realize_verbform("mennä", form="inf3", case="Ine").text # 'menemässä'
realize_verbform("tehdä", form="ma", case="Ine").text # 'tekemässä' (agent participle)
realize_verbform("tehdä", form="inf2_pass").text # 'tehtäessä' (passive 2nd inf, temporal)
realize_verbform("kuulla", form="tu", case="Par", px="3").text # 'kuultuansa' (temporal: 'after hearing')
realize_verbform("striimata", form="va").text # 'striimaava' (OOV verb, neural)
va/nut/tava/tu = present/past active/passive participle; inf2/inf3 = 2nd/3rd infinitive;
inf2_pass = passive 2nd infinitive (-ttaessa temporal, "while X is being done"; inessive-only,
case-invariant); ma = agent participle (-ma/-mä, "the X that someone does", inflects in case/number).
px (one of None/1Sg/2Sg/3/1Pl/2Pl) attaches a possessive suffix — participles inflect
like nominals, so tu+Par+px realizes the temporal construction of anteriority (kuultuaan /
the -nsa variant kuultuansa = "after hearing"). For OOV verbs the bare participle comes from the
verbform realizer and the case+Px from the noun realizer (both neural stages flagged unverified).
Returns a Result (ok with the form, or oov_unrealizable/oov_unverified when the morphology
engine cannot produce/trust it). Like realize_np, this is a phrase utility, not a clause transform.
Friendly inflection: inflect
The ergonomic single-word front door over realize_np — human-readable case/number/degree names, a
proper-noun toggle, and inflect-by-example. Returns a Result (never a bare string: abstention
stays first-class).
from juurio import inflect
inflect("talo", "gen").text # 'talon' (alias: gen/genitive/genetiivi)
inflect("talo", "mon gen").text # 'talojen' (combined spec, taivutin-style)
inflect("talo", case="inessive", number="plural").text # 'taloissa'
inflect("talo", px="1sg").text # 'taloni'
inflect("kaunis", "gen", degree="comp").text # 'kauniimman' (degree => adjective path)
inflect("meri", "gen").text # 'meren' (common noun)
inflect("Meri", "gen", proper=True).text # proper-noun reading (≠ 'meren')
inflect("makaronilaatikko", like="talojen").text # 'makaronilaatikkojen' (copy a model word's cell)
inflect("iso talo", "mon gen").text # 'isojen talojen' (phrase -> realize_np)
inflect(word, case="nom", number="sing", *, degree="pos", px="none", clitic=None, proper=None, pos=None, like=None):
- case — a case alias, or a combined spec (
"mon gen","plural genitive comparative"). Accepts English abbrev/full and Finnish (gen/genitive/genetiivi,mon/monikko). - number / degree — aliases too; an explicit value that contradicts the combined spec abstains
(
conflicting_features). - px / clitic — possessive suffix (
1sg…/None) and enclitic, as inrealize_np.px=Noneis accepted (= no suffix). Rejected on a single adjective target. - proper —
True/Falseforces PROPN/NOUN;None(default) infers PROPN from a leading capital. - pos — explicit
'noun'/'proper'/'adjective'(wins over inference). PRON/NUM are not yet a first-classpos. A degree implies the adjective path whenposis unset. - like — a model word; its analyzed case (+number, +degree if adjectival) is copied onto
word. Mutually exclusive withcase/number/degree; an unanalyzable model abstains (unanalyzable_model).
An unknown alias raises ValueError (a typo fails fast); a contradiction abstains
(conflicting_features). A single token is realized parser-free (so proper/pos are honored);
a whitespace phrase delegates to realize_np (which applies px/clitic to the head but rejects the
single-word controls proper/pos/degree). An empty word raises ValueError.
Analysis: analyze / lemmatize
The inverse of generation (what a generate-only inflector cannot do): the serving neural parser exposed directly.
from juurio import analyze, lemmatize, TokenAnalysis
sents = analyze("Koirat haukkuvat pihalla.") # list[list[TokenAnalysis]] (one list per sentence)
tok = sents[0][0]
tok.lemma, tok.upos, tok.feats # 'koira', 'NOUN', {'Case': 'Nom', 'Number': 'Plur'}
tok.asdict() # plain dict (id/form/lemma/upos/head/deprel/feats)
lemmatize("Koirat haukkuvat pihalla.") # ['koira', 'haukkua', 'piha', '.'] (flat)
TokenAnalysis is a frozen record (id, form, lemma, upos, head — 1-based, 0=root —,
deprel, feats as a read-only UD mapping). analyze keeps sentence boundaries; lemmatize
returns a flat lemma list.
CLI
Each library function has a like-named command (the CLI is a thin wrapper over the same
Transformer); realize_np/realize_verbform keep the hyphenated spelling.
# one command per library function
juurio transform <op> "<text>" [--lemma L] [--trace] [--json] # transform()
juurio inflect "<word>" [spec…] [--case C] [--number N] [--degree D] [--px P] [--clitic K] [--pos P] [--like W] [--proper] [--json] # inflect()
juurio analyze "<text>" [--json] # analyze()
juurio lemmatize "<text>" [--json] # lemmatize()
juurio realize-np "<np>" [--case Ine] [--number Plur] [--px 1Sg] [--clitic kin] [--json] # realize_np()
juurio realize-verbform "<verb>" [--form va|nut|tava|tu|inf2|inf2_pass|inf3|ma] [--case Ine] [--number Plur] [--px 3] # realize_verbform()
# infrastructure (no library counterpart)
juurio list-ops
juurio batch <op> [--lemma L] [--json] < lines # one op for every stdin line
juurio batch [--json] < tsv # per-line "op<TAB>text[<TAB>lemma]"
juurio demo # curated tour (incl. OOV) — also a smoke test
juurio repl # interactive shell; model loaded once
inflect accepts a human case/number/degree phrase positionally (juurio inflect talo mon gen
→ talojen, juurio inflect kaunis gen --degree comp), or as --case/--number/--degree flags;
a positional phrase wins over the flags. --proper/--no-proper force/forbid a proper-noun reading.
juurio repl is a read-eval loop over every feature (model loaded once): inflect talo mon gen,
inflect makaronilaatikko like=talojen, transform passive Mies osti talon., analyze …,
lemmatize …, np …, verb …, ops, help. Options are key=value (px=1sg proper=true pos=adjective clitic=kin like=talojen).
Also python -m juurio …. Without --json, prints the result (warnings appended after ⚠) and
sends abstentions to stderr as [abstain: <reason>] <input>. With --json, one JSON object per
line (text/ok/reason/warnings/trace). Exit status: 0 if every transform produced a form,
2 if any abstained — so scripts can branch on it.
$ juurio transform plural "Striimaaja nukkuu."
Striimaajat nukkuvat. ⚠ neural realization (unverified by FST): ['striimaaja->Striimaajat (NOUN)']
Quality
The package is designed to abstain rather than guess: out-of-scope inputs return an explicit
abstention (ok=False) with a reason code instead of a speculative form, and neural OOV realizations
are always surfaced (in trace["neural_unverified"] and warnings) rather than emitted silently. Known
words are inflected to be byte-exact to the reference FST (through the exception lexicon); OOV/loan words are inflected by the neural
realizers, with stem-inconsistent or near-tie candidates suppressed (oov_unverified) rather than
returned.
Run juurio demo for a curated tour (including OOV cases) that doubles as a smoke test.