Mova0.6 pre-release⌕
☰
home / docs / tools / Nightingale — mova-nightingale

Nightingale — mova-nightingale

🐦 A Nightingale is one of Mova's animals (Rust on Mova): one per language, each with its own Sol terminal. It keeps the language's naturalness model — how writers of the language and of each register actually say things — and answers, for a word or phrase of a translation, how natural it is against its alternatives: is the anglicism the usual word here, or a needless loan; is the native coinage the usual word, or a purist oddity. Neither every loan nor full purism is good language; the counts of real text decide, and a soft hand list of loans weighs in as a hint. See the animals.

Under the Eel

The Nightingales sit under the Eel and help it form language models on the CPU. The Eel orchestrates the learning rounds (GPU parser rounds, autoencoder, cross-encoder, translation rounds tr-<src>-<tgt>); a Nightingale is a CPU worker beside it — nice, bounded memory, never the GPU lease:

  1. its work loop counts new text of its language into parts, freezes a dev sample once, and when enough new text is counted leaves a handoff: HANDOFF/gen-<n>-<lineage>/ (model.ngl, meta.json, deltas.tsv, train.ids, dev.ids) with a READY marker written last;
  2. the Eel's lm-<lang> round calls mova-nightingale round, which measures the newest ready handoff against the installed generation on the frozen dev sample (held-out bits per token) and writes the Eel's native round report; the Eel installs the candidate only when the bits fall (eel-lm.example.json);
  3. the Eel's translation rounds read the installed model (<state_dir>/current/) for the naturalness of candidate translations (judge::judge, judge::substitution), and the English service answers from it.

The Nightingale never installs its own model and never holds the GPU. Its Sol answers only bounded questions the counts cannot decide, and its answers are review-only proposals.

The model

Records (src/count.rs). Every sentence of a source (CoNLL-U trees of a treestore, the target side of a parallel file parsed by the language's own annotator, or CoNLL-U files) is counted into:

kindwhatexamples kept
fforms, lowercased (<num> for a token with a digit)3 sentence ids
llemma|UPOS3
xlemmas the parser tagged X or Foreign=Yes3
bform bigrams with <s>, </s>1
tform trigrams (pruned at 3 by default)1
d, edependency-local patterns head|UPOS rel dep|UPOS and the same from the dependent: the events of wordgraph::extract (the Elephant's rules), read back as strings1
yforms per time bucket 0–9 of a chronological source—

Each count is kept under its source's register label lang[-dialect]/style[+style] (src/register.rs): «fr/encyclopedic», «fr-CA/parliamentary», «en-US/song+colloquial». A document is dev when its key hashes to 0 modulo dev_modulo; dev documents are never counted. One unit gives one part; parts merge into a model by a streaming k-way merge (one record per part in memory) with minimum counts per kind; the same parts give the same model bytes in any order (the lineage is the hash of the sorted part hashes and the pruning).

Language model (src/model.rs): interpolated absolute discounting over form bigrams (D = 0.75) backed off to a unigram that leaves the discounted and pruned mass to a character-bigram spelling model, so every string has a probability (Ney, Essen and Kneser 1994; Chen and Goodman 1999). The held-out measure is the mean −log2 P per token (and </s>) of the frozen dev sentences — the gate of the Eel's round.

Varieties as deltas (src/delta.rs). One standard per language — the pooled counts of all its text — plus small learned deltas per facet of the labels (a dialect modifier, each style): for a facet F and a form or bigram w, δ_F(w) = log2((c_F + ½)/N_F) − log2((c_R + ½)/N_R) against the rest of the text, kept only with enough evidence, |δ| ≥ 1 bit and Dunning's G² (dunning-1993 I1, the 2×2 log-likelihood ratio) at least 10.83 and at least the Bonferroni bar 2·ln(n / 0.001) for the n records tested in the facet and kind (without that bar a random split of one text into two labels kept chance trigram deltas: the control was red), at most 200 000 per facet and kind, for forms, bigrams and trigrams. A variety is read on the standard's scale as c_std(w) · 2^(Σ δ_F(w)) over its facets, so labels compose: «fr-CA/colloquial» uses the CA and colloquial deltas even when no text has that exact label, and a word can be natural in a variety and rare in the standard («courriel» in fr-CA, «ain't» in songs). A facet the model has no text for adds nothing and is said so. A dialect that is near a language of its own (Arabic or Chinese varieties) is configured as its own language code — its own Nightingale and standard — with style deltas over it; facets are plain strings, so other levels can be added. The deltas are learned at every handoff and bound to their model by its lineage.

Judgements (src/judge.rs). The evidence of a phrase is its form count (one form), bigram count (two), trigram count (three) or the chain estimate (more), in a variety shifted by its deltas, with counts per register, per million, examples and the PMI of two forms. Against alternatives the verdict is by add-½ shares: natural (≥ 0.4), marked, rare (< 0.1), or unknown below 5 counts in all — the counts cannot decide, a question for the Sol. When the language's parser is at hand and a compared phrase has three or more forms, every compared phrase is counted by its dependency patterns instead (each by its weakest pattern: obj(make, decision) against obj(do, decision)), provided every one has a pattern. The substitution test compares the bits of a sentence with each alternative put in.

Loan signals — etymology-free, none decides alone; two make a loan candidate: orthography (the character-trigram orthography models' log-likelihood ratio per character, the contrast language's over the target's, above 0.3 bits), foreign-tag (the parser tags the lemma X/Foreign=Yes in ≥ 20 % of its uses), english-dictionary (a lemma of Mova's English dictionary at least 4 times as frequent per million in the English model), rising (twice the relative frequency in the late half of a chronological source, at least 20 counts).

The hand list (seeds/<lang>/loans.tsv, ours): loanword, native alternatives, register note, hint, note. A soft list: prefer-native multiplies the loan's count by the penalty (0.5) in the shares, either by its square root, established and prefer-loan leave the loan alone (prefer-loan weighs the coinages down instead). A listed loan is penalised, never removed: its counts, examples and alternatives are always answered.

In English

serve answers English on a private socket. The question reader (src/ask.rs, over Mova's own parse of the question, as the Elephant's) takes quoted words and three frames:

frameexamples
naturalIs "e-mail" natural in French? How natural is "deadline" in parliamentary French? Is "workflow" an anglicism?
useWhat do French writers use for "deadline"? How do you say "deadline" in Canadian French?
compareWhich is more natural in English, "strong tea" or "powerful tea"?

A style or dialect word on the language («parliamentary», «everyday», «Canadian», «British») selects the variety. A question about another language points to that language's Nightingale. What the reader does not take goes to the deputy (deputy.example.json, handler nightingale: status, proposals; both only observe).

Answers on the probes are in Measurements.

The work loop

work (one per language; STOP in the work folder ends it; an error stops it, nothing is retried). One step: count up to units_per_step new units on threads threads; freeze the dev sample once it holds dev_tokens; hand off a new generation when the new train tokens reach build_share of the newest; scan the next scan_per_step rows of each configured parallel file; ask the Sol one question when its pacing passed. The scan flags loan-use (a listed loan whose weighted share in the register is below 0.4, with the substitution test), purist (a listed native word whose share is below 0.1 while the loan has the evidence and is not prefer-native) and loan-candidate (an unlisted word copied verbatim from the English side, not a proper noun, seen at least twice in the language, not next to another copied word under 50 per million — that is a code-switched fragment —, with two loan signals; queued for the Sol). It counts what translators chose for each listed loan whose English word is on the source side. Proposals (WORK/proposals/) are review-only: proposals.jsonl, loans.proposed.tsv (in the hand list's columns) and manual.proposed.md (notes addressed to the configured translation manuals). One flag per (kind, word, register); the Sol is never asked a question twice.

The Sol (src/consult.rs; sol.example.json, terminal.example.json): its own mova-terminal held by crab-tmux-worker, purpose naturalness, the rules of the Elephant's consultants: no question without operator_word, only its own terminal, journaled before and after, never replayed; at most 10 words a question, under 24 000 bytes; the answer is taken only when every item has exactly one line N<TAB>verdict<TAB>alternatives<TAB>note with a verdict of loan-ok, prefer-native, either, not-a-loan, unsure.

Commands

mova-nightingale count CONFIG [N]                      count up to N new units of the sources
mova-nightingale count-units CONFIG UNIT…              count the named units
mova-nightingale count-into CONFIG PARTS N [SEED]      count into another parts folder (SEED: the shuffled control)
mova-nightingale build PARTS LANG OUT                  merge parts into a model
mova-nightingale dev CONFIG | handoff CONFIG           freeze the dev sample / build a handoff now
mova-nightingale measure MODEL_DIR DEV_TSV             held-out bits per token
mova-nightingale judge CONFIG [--label L] PHRASE [ALT…] | signals CONFIG WORD | status CONFIG
mova-nightingale ask | serve | client                  English (as the Elephant's)
mova-nightingale work CONFIG [SOL|- [STEPS|- [SECS]]]  the work loop
mova-nightingale round CONFIG --round K --out OUT --threads T [--current DIR] [--base DIR]   the Eel's round child

Build and test:

cargo build --release --manifest-path tools/nightingale/Cargo.toml --offline
MOVA_OCTOPUS_TEST_MODEL=$MOVA_DATA/…/en.bin cargo test --release --manifest-path tools/nightingale/Cargo.toml --offline

The Eel's lm-<lang> round (for the Eel's schedule)

A gated native round of the Eel's contract (tools/eel, native.rs: eel_native.<kind>, gate install); the kind name lm-<lang> (lm-en, lm-fr, …) has to be added to the Eel's KINDS and check, which is the Eel's change, not this crate's. Configuration (eel-lm.example.json):

keyvalue
executable, executable_sha256the installed mova-nightingale, pinned
args["round", "<the language's nightingale.json>"]
state_dire.g. <eel state>/lm-<lang> (outside run_root); the Nightingale's installed points at its current
gate, metric, betterinstall, heldout_bits_per_token, lower
min_gaine.g. 0.001 bits per token
files, primary["model.ngl", "meta.json", "deltas.tsv"], model.ngl
threads, nice, timeout_seconds2, 10, 7200

The child writes OUT/round.json (metric, dev_sha256 — the same bytes every round, the Eel pins it at its first round —, current.value and current.model_sha256 of the installed model.ngl or none, candidate.value, summary with generation, lineages, tokens, records and the gain), OUT/train.ids and OUT/dev.ids (disjoint by construction) and OUT/candidate/. With no handoff newer than the installed lineage the round builds one from the counted parts; with no new parts the candidate equals the installed model and measures the same, so the Eel installs nothing. Rounds take a new handoff as it appears: the work loop builds them beside the Eel, the round only measures and copies (seconds to minutes; a build in the round is the fallback).

Translation rounds (tr-<src>-<tgt>) read <state_dir>/current/ with nightingale::model::Model::load and judge with nightingale::judge::{judge, substitution} in the target's register label.

Tests

tests/model.rs (no model needed): counting, the dev split, two handoffs (more text, fewer dev bits), the Eel's round against nothing installed and with the installed lineage (no gain), determinism (two countings give the same parts; reversed merge order gives the same model bytes), refusals (a tampered part, a part of another language). tests/english.rs (needs the model): the three frames, the other-language pointer, the deputy's status, a negation and «Delete the model.» refused; the work loop flags a listed loan, a purist choice and an unlisted copy, asks its Sol once, keeps the answer as review-only proposals in the hand list's columns, leaves the seed list and the model unchanged, and asks nothing without the operator's word. Unit tests: labels and facets, the hand list, consultant rules and strict answers, quotations, the example configurations (no machine paths).

Negative controls (2026-11-06, each must show red when its signal is gone):

Measurements (2026-11-06)

A shared box (load 20–30 on 20 cores), heavy steps under tools/jobs, nice -n 10. Probes, not the deployment.

English — 10 tier-1 shards of the Wikipedia tree store (the first 10 of 102; 4 913 train and 87 dev articles, 47.3 M train tokens, 1.92 M sentences; the whole-Wikipedia v5 store had no finished file yet).

stepwallpeak RAM
count shard 00000 alone (6.2 M tokens; 4.57 M records without trigrams)149 s1.15 GB (2.45 GB before the counter kept example ids as numbers; the same part bytes)
count 10 shards on 3 threads, with trigrams408 s3.05 GB
merge into the model (8.44 M records, 93 MB)69 s0.03 GB
handoff (merge, load, deltas)100 s2.88 GB
load and measure the dev sample (8 255 sentences)17 s2.90 GB

French — the French Wikipedia pass's finished test store (top 1 000 articles, 8.66 M tokens, fr/encyclopedic), 2 Tatoeba units (0.32 M, fr/conversational), 6 Europarl units of 20 000 rows spread over its time (3.43 M, fr/parliamentary, chronological): 12.4 M train tokens; counting 498 s at 0.97 GB (fr.bin parses the parallel sides); 2.54 M records; dev bits per token 7.99 (two builds equal). Deltas: 101 780 (4 % of the records; 219 216 before the Bonferroni bar), over three styles. The whole French pass (frwiki-all-v1) had just started; its finished files will replace the test store.

Verdicts of the hand list's loans (33 entries; standard / parliamentary / conversational; weighted share in the standard; the first listed alternative with its count; signals fired):

loancountalternativestandardparl.conv.signals
e-mail5courriel 18, courrier électronique 28rare 0.08rarerareenglish-dictionary
mail22courriel 18marked 0.25markedmarkedenglish-dictionary
deadline2délai 301, échéance 72, date limite 21rare 0.00rarerareorthography, english-dictionary
week-end122fin de semaine 14natural 0.89naturalnatural—
parking51stationnement 65, parc de stationnement 5natural 0.42naturalnaturalorthography
software11logiciel 90rare 0.06rarerareorthography, english-dictionary
hardware2matériel 458rarerarerareforeign-tag, english-dictionary
feedback0retour 1 391rarerarerareorthography, english-dictionary
meeting60réunion 1 017, rencontre 697rare 0.02rarerareorthography, english-dictionary
challenge57défi 383rare 0.10rarerareenglish-dictionary
leader319dirigeant 255, meneur 42marked 0.39naturalmarkedorthography, english-dictionary
manager71directeur 882, responsable 895rare 0.03rarerareenglish-dictionary
start-up23jeune pousse 4natural 0.77markednatural—
marketing68mercatique 2natural 0.96naturalnaturalorthography, english-dictionary
best-seller6succès de librairie 0natural 0.93naturalnatural—
timing7calendrier 497rare 0.01rarerareorthography, english-dictionary
sponsor12parrain 28, commanditaire 23marked 0.15markedmarkedenglish-dictionary
coach7entraîneur 1 060rare 0.01markedrareenglish-dictionary
business159affaires 2 404rare 0.04rarerareorthography, foreign-tag, english-dictionary, rising
smartphone11téléphone intelligent 0natural 0.92naturalnatural—
hashtag10mot-dièse 0natural 0.91naturalnaturalorthography
fake news4infox 0marked 0.18markedmarkedorthography, english-dictionary
data51données 1 590rare 0.02rarerareorthography, foreign-tag, english-dictionary
cool33sympa 6natural 0.54naturalrareenglish-dictionary
newsletter, brainstorming, checklist00unknownunknownunknownenglish-dictionary (questions for the Sol)

The established loans of the list come out natural (week-end, parking, marketing, best-seller, smartphone, hashtag) and the coinages beside them rare; the jargon loans rare against their native words (deadline, meeting, software, timing, data); «leader» natural only in parliamentary French. The counts of polysemous alternatives («affaires», «retour», «calendrier») include their other senses, so these shares overstate the native word.

Scan of 20 000 Europarl pairs (56 s, 1.65 GB): 4 loan-use flags («timing», «mail», «business», «look») with manual notes, translators' choices for the listed loans (English «deadline»: «délai» 8, «échéance» 4, «date limite» 1, another word 4; «leader»: «leader» 102, «dirigeant» 3; «challenge»: «défi» 56), and loan-candidate flags queued for the Sol: 84 with the first signals (mostly cognates such as «public», «long», «moral»), 68 with the character-trigram orthography and the 4× English ratio, 26 with the fragment rule. Of these 26, by an unreviewed reading, about 9 are English loans in French use (test, football, screening, leadership, benchmark, clearing, scotch, round, task), about 10 French words or cognates (technologies, local, corpus, uranium, change, pôle, capture…) and the rest code-switched fragments or acronyms (shall, with, last, MEDA): the signals choose questions, they do not judge.

Limits

Sources

This page in Markdown → https://md.mova.elamur.ai/free/en/docs/tools/nightingale/