Elamur: **Mova** · [Parse](https://mova.elamur.ai/free/en/parse/) · [Dictionary](https://md.dictionary.elamur.ai/free/en/) · [Translate](https://md.translate.elamur.ai/free/en/) · [Research](https://md.research.elamur.ai/free/en/) · [MCP](https://mcp.elamur.ai/) · [Groupants](https://md.groupants.elamur.ai/free/en/)

[Mova](https://md.mova.elamur.ai/free/en/) · [this page in HTML](https://mova.elamur.ai/free/en/docs/tools/nightingale/)

[home](https://md.mova.elamur.ai/free/en/) / [docs](https://md.mova.elamur.ai/free/en/docs/) / [tools](https://md.mova.elamur.ai/free/en/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](https://md.mova.elamur.ai/free/en/docs/repo-docs/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`](https://md.mova.elamur.ai/free/en/docs/tools/treestore/), the target side of a parallel file parsed by the language's own annotator, or CoNLL-U files) is counted into:

| kind | what | examples kept |
| --- | --- | --- |
| `f` | forms, lowercased (`<num>` for a token with a digit) | 3 sentence ids |
| `l` | `lemma\|UPOS` | 3 |
| `x` | lemmas the parser tagged `X` or `Foreign=Yes` | 3 |
| `b` | form bigrams with `<s>`, `</s>` | 1 |
| `t` | form trigrams (pruned at 3 by default) | 1 |
| `d`, `e` | dependency-local patterns `head\|UPOS rel dep\|UPOS` and the same from the dependent: the events of [`wordgraph::extract`](https://md.mova.elamur.ai/free/en/docs/wordgraph/) (the Elephant's rules), read back as strings | 1 |
| `y` | forms 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:

| frame | examples |
| --- | --- |
| `natural` | Is "e-mail" natural in French? How natural is "deadline" in parliamentary French? Is "workflow" an anglicism? |
| `use` | What do French writers use for "deadline"? How do you say "deadline" in Canadian French? |
| `compare` | Which 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`):

| key | value |
| --- | --- |
| `executable`, `executable_sha256` | the installed `mova-nightingale`, pinned |
| `args` | `["round", "<the language's nightingale.json>"]` |
| `state_dir` | e.g. `<eel state>/lm-<lang>` (outside `run_root`); the Nightingale's `installed` points at its `current` |
| `gate`, `metric`, `better` | `install`, `heldout_bits_per_token`, `lower` |
| `min_gain` | e.g. 0.001 bits per token |
| `files`, `primary` | `["model.ngl", "meta.json", "deltas.tsv"]`, `model.ngl` |
| `threads`, `nice`, `timeout_seconds` | 2, 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):

- **a shuffled corpus loses the naturalness signal**: the same text with its forms permuted across each unit (every form count kept) scores 4.52 bits per token on the dev sample against 1.53 for the real text, and «strong tea»'s share against «powerful tea» falls from > 0.95 to < 0.8;
- **a listed loan is penalised but retrievable**: «deadline» against «due date» is `natural` unlisted and not `natural` listed `prefer-native`; its weighted share is lower, its raw share, counts and examples are unchanged;
- **a label at random carries no variety**: one text split into two labels at random learns 0 deltas, while real varieties learn theirs («ain't» natural in en/song and not in the standard; «color» natural in en-US; both in the composed en-US/song that no text carries).

## 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).

| step | wall | peak RAM |
| --- | --- | --- |
| count shard 00000 alone (6.2 M tokens; 4.57 M records without trigrams) | 149 s | 1.15 GB (2.45 GB before the counter kept example ids as numbers; the same part bytes) |
| count 10 shards on 3 threads, with trigrams | 408 s | 3.05 GB |
| merge into the model (8.44 M records, 93 MB) | 69 s | 0.03 GB |
| handoff (merge, load, deltas) | 100 s | 2.88 GB |
| load and measure the dev sample (8 255 sentences) | 17 s | 2.90 GB |

- Held-out bits per token on the frozen dev sample: empty model 39.4 (a byte alphabet), one shard 8.48, ten shards 8.01; the **shuffled control** (shard 00000 with its forms permuted across the shard) 10.53 against 8.48 real.
- Two builds from the same parts wrote the same model bytes (body SHA-256 `d8d70790…`), and the bigram-only and trigram builds measure the same 8.0123 (the language model does not read trigrams).
- English checks (candidate against alternative, counts, verdict): «strong tea» 5 / «powerful tea» 0 natural; «heavy rain» 72 / «strong rain» 3 natural; «high temperature» 125 / «tall temperature» 0; «fast food» 83 / «quick food» 0; «deeply rooted» 39 / «strongly rooted» 4; «pay attention» 23 / «give attention» 2; by dependency patterns «make a decision» 513 / «do a decision» 0, «take a photograph» 51 / «make a photograph» 10, «commit a crime» 139 / «make a crime» 36 — all natural, and every reversed pair rare or marked («make a crime» marked, share 0.21). The bigram chain had put «make a decision» at 0.6 expected counts (unknown); the dependency method is what makes three-word phrases decidable. Loans: «worldview» 115 / «weltanschauung» 3, «zeitgeist» natural.
- English answers: «In my English counts «make a decision» is much more natural than «do a decision»: 513 times against never.» «For «weltanschauung», my English texts have «worldview» (115 times) and «world view» (22 times); «weltanschauung» itself 3 times. My hand list prefers the native words (academic); it is a hint, not a ban.»

**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):

| loan | count | alternative | standard | parl. | conv. | signals |
| --- | --- | --- | --- | --- | --- | --- |
| e-mail | 5 | courriel 18, courrier électronique 28 | rare 0.08 | rare | rare | english-dictionary |
| mail | 22 | courriel 18 | marked 0.25 | marked | marked | english-dictionary |
| deadline | 2 | délai 301, échéance 72, date limite 21 | rare 0.00 | rare | rare | orthography, english-dictionary |
| week-end | 122 | fin de semaine 14 | natural 0.89 | natural | natural | — |
| parking | 51 | stationnement 65, parc de stationnement 5 | natural 0.42 | natural | natural | orthography |
| software | 11 | logiciel 90 | rare 0.06 | rare | rare | orthography, english-dictionary |
| hardware | 2 | matériel 458 | rare | rare | rare | foreign-tag, english-dictionary |
| feedback | 0 | retour 1 391 | rare | rare | rare | orthography, english-dictionary |
| meeting | 60 | réunion 1 017, rencontre 697 | rare 0.02 | rare | rare | orthography, english-dictionary |
| challenge | 57 | défi 383 | rare 0.10 | rare | rare | english-dictionary |
| leader | 319 | dirigeant 255, meneur 42 | marked 0.39 | natural | marked | orthography, english-dictionary |
| manager | 71 | directeur 882, responsable 895 | rare 0.03 | rare | rare | english-dictionary |
| start-up | 23 | jeune pousse 4 | natural 0.77 | marked | natural | — |
| marketing | 68 | mercatique 2 | natural 0.96 | natural | natural | orthography, english-dictionary |
| best-seller | 6 | succès de librairie 0 | natural 0.93 | natural | natural | — |
| timing | 7 | calendrier 497 | rare 0.01 | rare | rare | orthography, english-dictionary |
| sponsor | 12 | parrain 28, commanditaire 23 | marked 0.15 | marked | marked | english-dictionary |
| coach | 7 | entraîneur 1 060 | rare 0.01 | marked | rare | english-dictionary |
| business | 159 | affaires 2 404 | rare 0.04 | rare | rare | orthography, foreign-tag, english-dictionary, rising |
| smartphone | 11 | téléphone intelligent 0 | natural 0.92 | natural | natural | — |
| hashtag | 10 | mot-dièse 0 | natural 0.91 | natural | natural | orthography |
| fake news | 4 | infox 0 | marked 0.18 | marked | marked | orthography, english-dictionary |
| data | 51 | données 1 590 | rare 0.02 | rare | rare | orthography, foreign-tag, english-dictionary |
| cool | 33 | sympa 6 | natural 0.54 | natural | rare | english-dictionary |
| newsletter, brainstorming, checklist | 0 | 0 | unknown | unknown | unknown | english-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

- The standard is the pooled text: a language whose text is mostly Wikipedia has an encyclopedic standard, and the other registers are deltas from it. Deltas are learned for forms and bigrams only (not lemmas or dependency patterns); their composition is log-linear and unnormalized (shares cancel it; per-million counts in a variety are estimates).
- Pruning drops records under the minimum count per part merge (not per part); a record kept in one register and pruned in all others is kept only where it reached the minimum.
- A phrase of three or more words is a chain estimate from bigrams. Tokenization is the language annotator's: a hyphenated loan («e-mail», «week-end») may be one token or three, and the hand list's forms are tokenized the same way.
- Parallel text is a translation: its target side is translationese, so the French model from Europarl or Tatoeba is a stand-in until the French Wikipedia pass fills its store, and the scan judges a translation with a model that partly contains translations. The scan assumes English is the source side.
- `chosen.json` counts co-occurrence in aligned sentences, not word alignment.
- The loan signals are evidence for a question, not judgements; their precision is unmeasured until reviewed answers exist. The trend signal needs a chronological source (Wikipedia gives none).
- The model is held in memory by the English service (a tier-1 slice: see Measurements); a whole-Wikipedia model needs higher minimum counts or a memory-mapped index, not implemented.

## Sources

- Hermann Ney, Ute Essen, Reinhard Kneser 1994, «On structuring probabilistic dependences in stochastic language modelling» (Computer Speech and Language 8): interpolated absolute discounting, the bigram model and its back-off weight (`src/model.rs`).
- Stanley F. Chen, Joshua Goodman 1999, «An empirical study of smoothing techniques for language modeling»: the comparison that places absolute discounting and its fixed D (`src/model.rs`).
- dunning-1993 I1: the 2×2 log-likelihood ratio G² that keeps a variety's delta only where it is significant (`src/delta.rs`); Carlo E. Bonferroni 1936 for the multiple-testing bar over the records tested.
- The dependency events are the Elephant's (`wordgraph::extract`; its README cites schuster-2016 for the conjunct propagation).
- [← previous Nautilus — mova-nautilus](https://md.mova.elamur.ai/free/en/docs/tools/nautilus/)
- [next → ocrfix](https://md.mova.elamur.ai/free/en/docs/tools/ocrfix/)
- [↑ back to tools](https://md.mova.elamur.ai/free/en/docs/tools/)
