Reading one hand
Episode 07 · 2 Nov 2026

Iteration 3, value by value

A personal model at 6.8% error, a third fewer than before. Behind it: a base that starts 16 points better, a plan chosen by 120 searched trials, and the lucky run that changed how we choose.

TL;DR

Win and failure

The question Iteration 3 changed the synthetic data, the GPU, the writer's lines and the personalising plan. What did each value rest on, and what did it buy?

Iteration 3 rebuilt the fake handwriting the model learns from, and the base model got much better at reading the writer before seeing a single line of theirs. Then a search on a rented GPU picked how to fit it to the writer, and taught us something on the way: the winning run was partly luck, so a plan is now judged by five runs, not one. The result reads the writer's test lines with a third fewer mistakes than our best model so far.

  • Personal v3.1-stable reads the writer's 24 test lines at 6.8% error, from 10.3% for v2.1-personal: 41 wrong letters of 600 instead of 62, from fewer of the writer's lines (68, not 85), within the 50 ms speed budget.
  • Base v3, with nothing of the writer's in it, reads them at 27.2%, down from 43.3%. Five data changes at once, so we know the data helped, not which change did.
  • Two searches, 120 trials on a rented GPU for $1.36, picked how to personalise. Freezing any layer of the base always hurt.
  • The first winner was a lucky run: made again, its 10.9% became 15.6%. Over five seeds the steadier plan wins, 11.4% against 12.8% for the old plan.
  • Decoder settings tuned on the dev lines won there and lost on the test lines, so the defaults stay. The whole iteration cost about $17.
The lesson

Judge a training plan by its mean over several seeds, not by its best run: on a small dev set, the winner of a search is partly luck.

Why read on The same plan on the same lines scored 10.9%, then 15.6%. The five-seed chart that settled which plan to keep, and why the best trial of a search is the one to trust least.

1 The question

Iteration 3 rebuilt the base model's data and searched for how to personalise it. Every value had to rest on a measurement.

Iteration 3 has two halves. The first rebuilds the base model The general reader, trained on lots of synthetic handwriting before it ever sees yours. Changing it: A better base gives a better personal model: it starts lower and learns your hand faster. Alone it can still read your writing badly. Here: From v1 on, trained on a cloud GPU from a recipe; v2's base alone gets 43.6% of your letters wrong, 13.1% once personalised.: the general reader, trained on 300,000 lines of synthetic data Training data written by code instead of collected from people: here, handwriting fonts turned into pen paths with timing. Changing it: It's unlimited and licence-clean, but if it doesn't look like real writing the model learns the wrong thing: the synthetic-to-real gap. Here: 300,000 lines for v2, from 134 fonts under open licences (OFL or Apache). before it sees a single line of the writer's. The second fits it to the writer: fine-tuning Continuing to train an already-trained model for a short while on new data: here, your own lines. Changing it: It adapts a general model to your hand from very little data. Too long, or with too high a learning rate, and it memorises your lines or forgets general writing. Here: 40 varied copies of each of your 25 lines, plus 2,000 synthetic lines replayed. It took v2 from 43.6% to 13.1% errors..

Each half is a list of values: how many words, which GPU, how many epochs, how big a step. Every value here was set one of three ways, and the episode says which:

  • Measured: a small experiment or a count decided it before the big run.
  • Searched: a tool tried many values on held-out lines and kept the best.
  • Kept: the model's size, its training settings and the decoder are v2's, so the change can be read against v2.
Personal v3.1-stable, test lines
6.8%
from 10.3% (v2.1-personal)
Base v3, before personalising
27.2%
from 43.3% (base v2)
Personalising trials searched
120
$1.36 on a rented GPU
All of iteration 3
≈ $17
base $15.06, personalising $1.99

2 Five changes to what the base reads

v3's synthetic ink writes more like the writer and covers what the writer writes. Each change answers a measurement.

Episode 4 found that the old synthetic ink didn't write like a hand: it lifted the pen too often and drew jagged paths. A closer look at what the writer actually writes found more gaps. v3 fixes five things at once:

What base v3 learns from, against base v2, and the measurement behind each change.
Changev2v3Why
How font ink moves1.8 pen lifts, a jagged path, written fast0.96 pen lifts, 10.1 points and 0.67 s per letterThe writer's own rates, measured on their recorded lines (episode 4).
Hand-like wordsnonea quarter of the lines built from InkSight wordsFont letters are traced skeletons; InkSight gives the strokes a pen would draw.
Characters written116 of 177all 177, with amounts, units, n°, checkboxes, more math61 characters never appeared, among them $ # & @ ; … β Σ ± ∞. The writer uses 17 of them.
Vocabulary5,000 words per language20,000Covers 88.5% of the words the writer writes, up from 74.9%.
Share of math15% of lines25%The writer's own share. Math was the worst-read kind of line: 23.5% error.
What InkSight is, and why its words aren't joined

InkSight is an open model (Apache-2.0 weights) that looks at a picture of a word and writes down the pen strokes a hand would use to draw it. We give it our own words rendered in our own open fonts, so no one's real handwriting goes in.

Its words lift the pen about 1.2 times per letter, where traced font ink lifts it 1.8 times. Those lifts are learned from real pens: dots on i's, crossed t's. So they are kept as they are, while font ink is joined down to the writer's 0.96.

Before paying for the GPU, a sample of 3,000 v3 lines was checked against the writer's ink. v2's lines had raised five warnings in that check, on pen lifts, points and time per letter, a rare capital and characters never seen. v3's raised none.

3 How many words to ask InkSight for: 3,000, not 6,000

The first guess counted words. The model reads letters, and by that measure 3,000 words already show almost everything.

The first plan was 6,000 words, picked by eye from how much of the texts' words a bank covers. But the recogniser reads letters, not words. A word missing from the bank is still written, in font ink.

What InkSight adds is how a hand forms and joins letters. So the measure that matters is how many letters and letter pairs the model sees in InkSight style, and that saturates early:

90%92%94%96%98%100%letter pairs, as written, that the bank shows in InkSight style1,000 words92.3%2,000 words95.6%3,000 words97.2%4,000 words97.8%6,000 words98.7%8,000 words99.2%10,000 words99.9%
Share of letter pairs written in InkSight style, by the number of words in the bank
RowValueInterval
1,000 words92.3%
2,000 words95.6%
3,000 words97.2%
4,000 words97.8%
6,000 words98.7%
8,000 words99.2%
10,000 words99.9%
Measured on 100,000 sampled v3 texts, math aside. At 3,000 words the bank already shows 99.7% of the letters written. Doubling it to 6,000 adds 1.5 points of letter pairs, for at least twice the price.

And each extra word costs more than the one before. InkSight writes one point at a time, a batch of words finishes only when its longest word does, and rarer words are longer.

Images derendered
5,774
3,000 words in 2 fonts each
Faithful
97%
5,621 kept; 2,935 of 3,000 words
Cost
$13.29
4 h 31 min; $11 planned

A word counts as faithful when its strokes trace at least 80% of the letters' centre lines and at least 80% of the strokes stay on the ink. Midway through, the bill looked headed for $21 to $26; later batches held shorter words, and it ended at $13.29.

4 Which GPU: the cheapest epoch, not the fastest card

A 16-minute benchmark priced one epoch on six GPUs. The model is too small to fill any of them, so the mid-range card won.

Left to itself, the cloud provider (Modal) takes whichever of three GPU A graphics card: thousands of small cores doing arithmetic in parallel, which is what training mostly is. Changing it: Training is many times faster than on a CPU; rented by the second, so time is money. Here: A Modal T4 at about $0.59 an hour: v2's base cost $1.42. is free first. So before training, the same model, batch size and number format ran on six of them, and each was priced per epoch, including the processor cores and memory the provider also bills.

$0.00$0.05$0.10$0.15$0.20$0.25dollars per epoch of 300,000 lines, lower is betterA10G$0.093L40S$0.108L4$0.117A100 40 GB$0.159T4$0.163H100$0.206
Cost of one training epoch of base v3 on six GPUs
RowValueInterval
A10G$0.093
L40S$0.108
L4$0.117
A100 40 GB$0.159
T4$0.163
H100$0.206
Peak GPU memory was 1.7 GB on every card: a 3 × 192 network at 64 lines a batch can't keep a big GPU busy, so the fastest card is only 28% quicker, and the H100 costs more than twice as much per epoch.
The benchmark, 1 Nov 2026 ($0.38 in all).
GPULines per secondMinutes per epochDollars per epoch
A10G1,5213.2$0.093
L40S1,9412.5$0.108
L49894.9$0.117
A100 40 GB1,3813.5$0.159
T46048.0$0.163
H1001,8132.7$0.206

The provider's default tries an L4 first: 26% dearer and 53% slower per epoch than the A10G. The batch size and number format stayed as the recipe says, since they change what the model learns, not just how fast.

5 The training run: 30 minutes, 87 cents

12 epochs finished on the A10G in half an hour, ended by the plan rather than a clock.

The 300,000 lines were made on 60 processor machines at once, checked against the writer's ink, and only then was the GPU started. Episode 3 had found a clock that cut runs short, so the time limit was raised from 150 to 220 minutes: the 12 epoch One full pass over every training line. Changing it: More epochs keep lowering the training error until it levels off. Past that point they cost time for nothing, or the model starts memorising the training lines. The best epoch is the one that scores best on held-out lines, not the last one. Here: v1's base ran 29 epochs of about 97,000 lines (about 1.5 minutes each on a T4); v2's ran 12 epochs of 290,000 lines (about 12 minutes each). Personalising stops after 6 epochs or 10 minutes. end training, not the clock.

Training
30 min
12 of 12 epochs, about 2,000 lines/s
GPU machine
$0.87
$1.39 with the 60 machines making lines
Data check
0 errors
2 small warnings, run kept going
0.0%2.0%4.0%6.0%8.0%123456789101112epochCER on held-out synthetic linesbase v3 · 1.4%
From 8.4% after one epoch to 1.4% after twelve, still falling slowly. These are synthetic lines, harder than v2's (rare symbols, more math), so they don't compare with v2's curve, and 1.4% here is far from the writer's real ink: the synthetic-to-real gap The difference between how well a model reads the synthetic data it trained on and how well it reads real writing. Changing it: A model can score almost perfectly on synthetic lines and still read real ink badly, so only a test on real writing counts. Closing it means making the synthetic data look more real. Here: v1's base: 0.1% errors on synthetic lines, 58.3% on yours..
The two warnings, and why they didn't stop the run

36 lines too narrow per letter, of 300,000: short math like "n < ∞" in narrow fonts. Negligible.

Three fonts with four times a typical share. Only 6 of the 134 fonts draw all of β γ Σ ∑ ± ∞, so every line carrying them has to go to those few. That is the cost of writing every character, not a fault in the data; drawing rare symbols in more fonts is noted for v4.

6 Win: the base reads the writer 16 points better

Before seeing any of the writer's lines, base v3 gets 27.2% of their letters wrong, against 43.3% for base v2.

Both bases were read on the same 24 test lines (600 characters) with the same decoder. Neither has seen any of the writer's ink.

0%10%20%30%40%50%60%character error rate on the test lines, lower is betterAll 24 lines, v243.3%All 24 lines, v327.2%French, v247.6%French, v322.7%English, v240.2%English, v327.8%Mixed, v238.0%Mixed, v326.5%Math, v249.4%Math, v336.5%
Base v2 and base v3 on the writer's 24 test lines, overall with 95% bootstrap intervals and per language, before any personalising
RowValueInterval
All 24 lines, v243.3%38% to 49%
All 24 lines, v327.2%21% to 33%
French, v247.6%
French, v322.7%
English, v240.2%
English, v327.8%
Mixed, v238.0%
Mixed, v326.5%
Math, v249.4%
Math, v336.5%
Grey is base v2, blue base v3; whiskers are 95% bootstrap intervals over the 24 lines (a language has 4 to 7 lines, too few for its own). Every kind of line improves; French most, from 47.6% to 22.7%. Math stays the hardest.
Base against base on the 24 test lines (1 Nov 2026).
MeasureBase v2Base v3Change, points
CER Character error rate: substituted, deleted and inserted letters, divided by the number of letters in the right text. Changing it: The main score here; lower is better. It weighs every letter the same, and it can go over 100% when a reading has many extra letters. Here: The goal is 15% or less. v2 personalised: 13.1%.43.3%27.2%−16.2
Without the language model (greedy decoding Reading the model's output by taking the top guess at every point, then applying CTC's rules. Changing it: Fast and simple, but it can't use spelling knowledge, so it produces things like "1%e" for "the". Here: Greedy CER is the network on its own: 18.4% for v2 personalised, against 13.1% with the beam search.)48.7%32.0%−16.7
WER Word error rate: the same as CER, counted in whole words. Changing it: Harsher than CER: one wrong letter makes the whole word wrong. Closer to how a reader judges a note. Here: v2 personalised: 40.2%, against 13.1% CER.83.6%65.7%−17.9
French (7 lines)47.6%22.7%−24.9
English (7 lines)40.2%27.8%−12.4
Mixed (4 lines)38.0%26.5%−11.6
Math (6 lines)49.4%36.5%−12.9
Time within which 95% of lines are read49.6 ms51.0 ms+1.4 ms

In letters: 260 wrong of 600 for base v2, 163 for base v3. Paired line by line, the step is −16.2 points (−21.8 to −10.6): base v3 is better on 20 lines and worse on 3. The gain shows with and without the language model, so it is the network reading better, not the decoder guessing better.

Base v3 alone now reads the writer about as well as v1 did after personalising (24.5%). Its worst lines are still symbols and units: "60 km/h × 1,5 h = 90 km", "x² − 9 = (x − 3)(x + 3)". Speed is unchanged: the same 3 × 192 network, and 1.4 ms is about the size of this laptop's run-to-run noise.

Predicted before the run

Episode 4, for a base trained on ink that writes like the writer: base CER under 30% (from 43%), personalised CER falls with it, and the data check shows no drift warning.

Observed

Base v3: 27.2%, under 30%. Personalised v3.1-stable: 6.8%, from 10.3% (section 13). The data check gave no warning on pen lifts, points or pace; its one width warning was on 36 narrow math lines of 300,000. v3 is that base plus four other changes, so this tests the bet, not the ink fix alone. Prediction held

7 Tried and not kept: a word language model

A word-level language model was built and measured. It gained 3 or 4 letters of 600, inside the noise, so it stays out.

The decoder already uses a language model A model of which letters tend to follow which, learned from plain text. Changing it: It fixes readings the network finds ambiguous. Weighted too heavily, it overrules what you actually wrote, such as rare words and names. Here: A character model (each letter given the 4 before it) from 120,000 Tatoeba sentences per language, weighted 0.5. over letters (which letters tend to follow which) and a bonus for the writer's own words. A model over whole words would prefer "meeting" to "meeling".

Built and measured
  • A word model of the 20,000 most used words and 100,000 word pairs, about 0.5 MB a language.
  • Its weight chosen on held-out lines, then the test lines read once.
  • Best result: 0.4 points better, 3 or 4 characters of 600, for about 1 MB more to download.
Why it can't help yet
  • The errors left are symbols, units and the writer's own words (names, jargon, "rdv"). A dictionary of common words knows none of them.
  • It's worth measuring again with 100 or more test lines, or once the network makes fewer errors of its own.

Two ready-made tools were also set aside: KenLM and pyctcdecode can't run in the browser, where the model reads. Instead, a brute-force check scores every possible reading of small made-up inputs and makes sure the beam search finds the best one, in 130 cases. It found readings that could never be produced still taking up room; removing them saved 1 to 2% of decoding time.

8 Personalising: where to start, and which lines to learn from

Start from base v3, learn from both recording rounds at once, and keep a fifth of the lines aside to choose settings with.

Three models could have been the starting point:

The candidates, scored on the same 24 test lines. Only one has never seen the writer's training lines.
Start fromTest CERWhy, or why not
v0.2 local base32.8%A small 1 × 128 network, 12 times fewer weights. It caps what can be learned, and it already saw every training line.
v2.1-personal10.3%Today's best, but built on v2's data, and fitted on all 85 training lines: none left to choose settings with.
Base v327.2%Picked. 16 points better than base v2 as a start, the same size and speed as v2.1-personal, and it has never seen a line of the writer's.

The last point matters most. To choose settings honestly, some of the writer's lines have to be ones the model hasn't learned from.

The writer has recorded two rounds: 25 training lines and 15 test lines, then 60 and 24. Personal v3 learns from both at once: 85 training lines, one copy of each (one round had been uploaded twice).

Lines it learns from
68
the fit
Dev lines
17
441 characters, to choose settings
Test lines
24
read once, at the very end
Three piles of lines, and why each stays in its pile

The fit is what the model trains on. Its score there is flattered: it has seen the answers.

dev set A second set of held-out lines, for choosing settings and the best epoch. Changing it: It keeps the test set untouched, so the final score isn't flattered by choices made while looking at it. Here: A fifth of the writer's training lines, held out: the decoder and the personalising settings are tuned on them, never on the test lines. are a fifth of each language's training lines, picked the same way every run. They are never trained on, so they say how a choice of settings does on new ink. Settings are chosen by them.

test set Examples kept aside and never trained on, used only to score the model. Changing it: It's the only honest measure of how a model does on new writing. Too small and the score is noisy; used for tuning, and it stops being honest. Here: 15 of your English lines today; 30 planned, in the same language mix as your notes. judge once. Choosing anything by them, even which of two models to keep, would let their luck into the score.

10 A best at the edge of the map: search wider

The first winner sat at the limit of two ranges, so a second search widened them. Its best landed in the middle.

The first search's best used 2 epochs, the fewest it allowed, and a learning rate 95% of the way to its ceiling. A best at the edge of a range says the real best may lie past it.

So a second search ran with room on both sides: down to 1 epoch, and learning rates up to 0.01. It was a study of its own, because the search method learns only from trials that shared the same ranges.

The two searches (60 trials each, none cut by time) and their bests, on the dev lines.
Settingv3's planSearch 1: shortSearch 2: long
Dev CER13.4%10.9%10.4%
Learning rate0.0010.00420.0017
Copies per line401970
Synthetic lines replayed2,0001,370361
Epochs628
Timing hidden20%1%14%
Layers frozen000
Search time and cost–19 min, $0.5231 min, $0.84

The second best was not past the old edges but in the middle of the new ranges: 8 epochs at a moderate rate, with its runners-up close by (11.3% to 12.0%). Two different recipes, one short and hot, one long and gentle, 2.5 and 3 points better than v3's plan.

What mattered most: the share of the spread in dev scores each setting explains, by Optuna's importance measure.
SettingSearch 1Search 2
Copies per line46%16%
Epochs21%15%
Learning rate20%34%
Timing hidden9%4%
Synthetic lines replayed3%25%
Layers frozen0%6%

The ranking changed between the searches. Importance belongs to the ranges searched as much as to the setting: widen the learning rate's range, and it has more room to make a difference.

The two bests differ by 2 letters of 441, inside the noise. A rule was written before measuring: within 3 letters, keep the plan that catastrophic forgetting When a model trained on new data loses what it knew before. Changing it: A personalised model could read your hand and nothing else. Mixing old data back in (replay) and a small learning rate prevent it. Here: Checked every personalisation: error on synthetic lines stays flat. less of what the base knew.

Measured on 2,000 synthetic lines neither had seen, base v3 read 1.4%, the short plan 3.6% and the long plan 4.2%. The short plan won the tie, and became the recipe for personal v3.1.

11 Failure: the 10.9% that didn't come back

Made again from its recipe, the short plan scored 15.6%. One run per plan can't rank plans.

Personal v3.1 was made from its recipe: same plan, same lines, same dev lines. It scored 15.6% on the dev lines, not 10.9%: 21 letters worse, with nothing changed.

Training on a GPU isn't exactly repeatable. The loss this model learns from adds up many small numbers in parallel, and the order changes from run to run. Rounding differs in the last digit, and training amplifies it from the first epoch. A short plan at a high learning rate amplifies it most.

Why the best of 120 trials is the one to trust least

Every trial's score is part plan, part luck. A search keeps the lowest score, so it favours plans that are good, and also runs that were lucky.

The more trials, and the noisier each one, the more the winner owes to luck: the winner's curse. On 17 dev lines, where a letter is 0.2%, there is plenty of noise to be lucky with.

So a search's best is a shortlist, not a verdict. The fix is to run the finalists again, with different seed The starting number for every random choice: which font, which word, how much slant. Changing it: Same recipe and same seed give the same data, so a version can be rebuilt. Different seeds show how much a result is luck. Here: Pinned in each recipe., and compare what they do on average.

12 Five seeds each: judge a plan by its mean and its spread

Over five runs each, the long plan wins: 11.4% on average, against 12.8% for v3's plan and 13.9% for the short one.

A seed now picks the varied copies and seeds training, so a plan can be run several times on purpose. The two finalists and v3's own plan, as the reference, were each trained with four more seeds, one call at a time.

8%10%12%14%16%18%CER on the 17 dev lines: mean, and lowest to highest runLong plan (5 runs)11.4%v3's plan (4 runs)12.8%Short plan (6 runs)13.9%
Dev error rate of three personalising plans over several runs: the mean, with the lowest and highest run
RowValueInterval
Long plan (5 runs)11.4%10% to 13%
v3's plan (4 runs)12.8%12% to 13%
Short plan (6 runs)13.9%11% to 16%
The dot is the mean, the whisker runs from the luckiest run to the unluckiest. The short plan's whisker spans 21 letters: its 10.9% was its best day.
Every run, dev CER (17 lines, 441 characters). v3's plan is averaged over seeds 1 to 4; its seed-0 run, trial 0 of the search, read 13.4%.
PlanRuns, by seedMeanSpread (sd)
Long plan0: 10.4% · 1: 10.9% · 2: 10.7% · 3: 12.2% · 4: 12.7%11.4%1.0 points
v3's plan1: 12.0% · 2: 13.4% · 3: 12.9% · 4: 12.9%12.8%0.6 points
Short plan0: 10.9% · 0, again: 15.6% · 1: 15.6% · 2: 14.3% · 3: 12.9% · 4: 14.3%13.9%1.8 points

The long plan is ahead of v3's plan by 1.4 points on average, and of the short plan by 2.6. Measured against the spread, those gaps are 2.7 and 2.9 times their own uncertainty: unlikely to be luck, though five runs is still a small sample.

Mean, spread and t, in plain words

The mean is what a plan does on a typical run. The spread (standard deviation) is how far one run usually lands from it: about 1 point for the long plan, almost 2 for the short one.

To compare two means, divide the gap by how uncertain it is, which depends on both spreads and how many runs there were (Welch's t). Near 1, the gap could easily be luck; past 2, it rarely is.

v3's own plan is the steadiest of the three (spread 0.6), just not the best. A plan that is reliably good can beat one that is sometimes brilliant.

The long plan, with seed 0, became the release v3.1-stable (every run of it, epoch by epoch, is in episode 8). Made from its recipe, it read the dev lines at 10.43%, exactly its earlier run: this time the run repeated.

The v3.1-stable recipe, value by value.
Valuev3's planv3.1-stableSet by
Starting pointbase v3base v3a test score, and no lines seen
Lines learned fromone roundboth rounds: 68, 17 held outmore of the writer's lines is the largest lever
Learning rate0.0010.0017two searches, five seeds
Copies per line4070two searches, five seeds
Synthetic lines replayed2,000361two searches, five seeds
Epochs68two searches, five seeds
Timing hidden20%14%two searches, five seeds
Layers frozen00every frozen layer hurt
Time limit10 min60 min, never reachedthe epochs end training (about a minute)
Seed–0the plan's best of five
Personalising calls to Modal
20
searches, finalists, repeats, releases
All of them
$1.99
one model: about 3 cents
On this laptop
12 h +
for one thorough search

13 Win: 6.8% on the test lines

Personal v3.1-stable gets 41 of 600 test letters wrong, a third fewer than v2.1-personal, from fewer of the writer's lines, and inside the speed budget.

The last steps kept their order: tune the decoder on the dev lines (section 14), then read the 24 test lines once. They are the same lines v2.1-personal was judged on.

Character error rate
6.8%
95% interval 4.6% to 9.2%
Word error rate
23.6%
from 32.9% (v2.1-personal)
95% of lines read within
45.1 ms
budget 50 ms: passed
0%10%20%30%40%50%60%CER on the 24 test lines, lower is better15%8% goalv2 base43.3%v3 base27.2%v2.1-personal10.3%v3.1-stable6.8%
The two bases and the two best personal models on the 24 test lines, with 95% bootstrap intervals, the 15% target and the 8% goal
RowValueInterval
v2 base43.3%38% to 49%
v3 base27.2%21% to 33%
v2.1-personal10.3%7% to 14%
v3.1-stable6.8%5% to 9%
Blue: iteration 3's models. Personalising took base v3 from 27.2% to 6.8%, and v3.1-stable reads 41 letters wrong where v2.1-personal read 62.

Paired line by line against v2.1-personal, the step is −3.5 points (−6.0 to −1.3). v3.1-stable is better on 13 lines, worse on 3, and the interval stays clear of zero.

Per language on the 24 test lines. The last column is v3.1-stable read by the network alone, without the language model.
LanguageLinesv2.1-personalv3.1-stableNo language model
French79.7%5.9%8.6%
English79.1%5.7%7.7%
Mixed49.9%8.3%9.1%
Math615.3%9.4%8.2%

Every language improves. Math reads better without the language model (8.2% against 9.4%): that model learned which letters follow which from ordinary text, so on a formula it pushes the reading toward words. A math-aware language model, or none for math lines, is a cheap thing to try next.

On all 39 test lines of both rounds (1,036 characters), v3.1-stable reads 8.6% (5.3% to 13.4%), with a word error rate of 26.4%. The first round's 15 English lines are the harder ones: English reads 9.5% with them, against 5.7% without.

The model is exported for the browser (8.6 MB, the same reading as the training code on 10 of 10 check lines) and is now the one the app reads with.

14 Not kept: decoder settings tuned on the dev lines

Tuning the decoder won 4 letters on the dev lines and lost 6 on the test lines, so the defaults stay.

As in episode 5, the decoder's settings were searched on the dev lines: 100 trials, keeping the fastest settings within half a point of the most accurate. The rule set beforehand: keep tuned settings only if they also win on the test lines.

v3.1-stable read with the default and the tuned settings.
SettingsDev linesTest lines95% of lines within, three alternating runs
Default (kept)10.2%6.8%47.3 ms · 45.6 ms · 46.1 ms
Tuned9.3%7.8%52.2 ms · 41.7 ms · 41.7 ms

The tuned settings gained 4 letters on the 441 dev characters and lost 6 on the 600 test characters. A gain of four letters is inside the noise of 17 lines, and settings chosen on those lines fit their quirks. The defaults stay, and they also stayed within the 50 ms budget on every timing run, where the tuned ones went over once.

15 What carries over

Seven habits for anyone choosing values for a small model on little data.

Beyond this project
  • Measure what the model actually learns from. Here, letters and letter pairs, not words: by that measure, half the planned word bank bought almost all of the benefit.
  • Benchmark before renting. For a small model the fastest GPU is rarely the cheapest epoch, and a default "whatever is free" choice can cost a quarter more for half the speed.
  • Know how big a difference is in units you can picture. One letter of 441 is 0.2%: two settings half a point apart are two letters apart.
  • When a search's best sits at the edge of a range, widen the range before believing it.
  • Treat a search's winner as a shortlist. Rerun the finalists with several seeds and pick by the mean and the spread, not by the best run.
  • A gain on a small dev set is a hypothesis. Four letters better on the dev lines turned into six worse on the test lines.
  • Write each decision down as a recipe, with its reasons and every run behind it. A recipe is never edited once it has made a model: when the luck in v3.1 showed, a new recipe replaced it.

Previous · Episode 05 · 1 Nov 2026

Sharpening the tools before iteration 3

Eleven experiments are waiting. Before paying for any of them: tune what's free, record everything, search instead of guess, and make each run cheaper.

Method

Next · Episode 08 · 2 Nov 2026

One recipe, 136 training runs

How personal v3.1-stable got its six settings: two searches on a rented GPU, a lucky winner caught, five seeds per finalist, and $1.99 for a model that reads the writer at 6.8% error.

Method