Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Halfword

Word prediction for AAC, optimised for how long it takes to say something — not for how many keystrokes it saves.

Halfword sits on top of whatever access method already works for someone — a switch, eye gaze, a scanning grid, a touchscreen — and decides what to put on screen so that saying the thing takes less time. It does not implement access methods and it does not measure people.

Try it in your browser · Model · Benchmark dataset

pip install -e .
halfword simulate --method scanning --rate 1.2     # the bundled corpus, offline
halfword bench --sweep --curve --out benchmarks/run.json
halfword figures --bench benchmarks/run.json --out figures/

Everything above runs with no downloads and no network. That is deliberate: a claim about a device that works offline should be checkable offline.


If you use AAC, or support someone who does — we need you, and we know it

This was built without AAC users in the room. That is a real limitation, not a modest disclaimer. Every default here comes from the research literature and from device documentation. None of it comes from people who use these interfaces to say things to other people, every day, all day.

There is a name for this problem in the AAC field. Blasko, Light, McNaughton, Williams & Zimmerman put it in a title: "Nothing about AAC users without AAC users: a call for meaningful inclusion in research, technology development, and professional training" (AAC, 2025). We would rather be corrected early than cited politely.

What would help most, roughly in order:

  1. Are the defaults wrong? halfword costs --method scanning prints every timing parameter with its source. If a scan interval, dwell time or error rate does not match what you actually use, that is the single most valuable thing you could tell us.
  2. Is "seconds to utterance" even the right target? It is not obviously right. Light & McNaughton's work on communicative competence argues that rate is one thing among several, and being understood, being able to repair a misunderstanding, and not being exhausted at the end may all matter more. If optimising seconds would make something else worse, we want to hear which thing.
  3. Would a shorter candidate list actually feel better? Our result says show fewer candidates. That is a claim about mechanical scan cost. It might feel worse — fewer chances to spot the word, more spelling. We do not know, and the simulation cannot tell us.
  4. What is missing entirely? We model composing one sentence from scratch. Not stored phrases, not repair, not the partner finishing your sentence for you, not deciding to say something shorter because the long thing would take four minutes.

Where to reach us: open an issue, or the Community tab on the Space or the model. If you would rather not write in public, say so in an issue and we will find another way.

Organisations doing this properly, if you are looking for people rather than for us — all links checked: ISAAC and its national chapters, including Communication Matters (UK), which runs mentoring and Communication Clubs; CommunicationFIRST, led by people with speech disabilities; AACcessible (US non-profit); Ace Centre (UK), whose switch-scanning frequency analysis is open source.


The claim, positioned honestly

What is already known, and is not ours. That keystroke savings and communication rate come apart is old news in AAC. Koester & Levine modelled text-entry speed with word prediction in 1994 and found in 1996 that prediction could make people slower. Higginbotham (1992) evaluated keystroke savings across five devices. Trnka et al. (2007–2009) measured both and reported seconds-per-keystroke rising from 2.28 s to 2.92 s as prediction got stronger. Quinn & Zhai (2016) found the same trade in mainstream touch typing. Closest of all, Cai et al. (2024, Nature Communications) ran a field study with two eye-gaze users with ALS in which keystroke savings moved sharply while entry rate did not — pricing an option selection at ~12.7 s against inter-keystroke intervals of 3.5–5.0 s.

We do not claim that. If you want the finding "keystroke savings is not time", cite them.

What is different here. Two things.

First, the mechanism is not slowness. The intuitive story — slower access methods diverge more — is wrong, and we can show it is wrong. Trnka et al. slowed participants to 5–8 wpm with a 1.5 s per-key delay, squarely AAC-like, and the keystroke-savings-optimal system still won on time by a wide margin. Slowness alone does not produce the effect.

What produces it is position costing something. Under row-column scanning the tenth candidate is nine scan steps deeper than the first, and every candidate row lengthens the sweep to every remaining letter. Under dwell-based gaze the tenth candidate is one saccade away, exactly like the first — and gaze is slow.

Two comparisons from our own benchmark make the dissociation concrete:

  • Slowest ≠ most divergent. A single switch on a linear sweep at a 2.35 s interval is the slowest configuration we simulate: 574 s per utterance. It diverges by 6.6%. Row-column scanning at a 0.5 s interval is seven times faster — 79 s per utterance — and diverges by 14.7%, more than twice as much.
  • Slow and positionless is not divergent at all. Gaze at a 1.2 s dwell needs 20 s per utterance, two and a half times slower than touch at its worst setting. It diverges by 0.01%; touch diverges by 1.0%.

We make it measurable. The positional cost ratio ζ — how much dearer the deepest candidate slot is than the first, per keystroke — predicts the divergence across all twenty configurations with R² = 0.90. Entry speed manages R² = 0.46 (0.54 in log wpm), and with the wrong sign in places.

Second, the list length becomes a computed quantity. Fixed, hand-chosen candidate-list lengths are standard practice: Lesher & Higginbotham use a fixed 7, MacKenzie (2012) models 6. We could not find prior work that makes the list length a decision variable in a time objective, solved per keystroke. That is a real but narrow gap, and one paper we could not obtain — Higginbotham, Lesher & Moulton, "Techniques for augmenting scanning communication" (AAC 14(2), 1998), which evaluates adding word lists to a scanning matrix scored in switch activations — is the most likely place it already exists. If you have it, please tell us what it says.


Results

Conditions AE on the bundled corpus, four access methods, each swept across its own published rate range. Full table: halfword bench --sweep.

scanning (1.2 s) switch, linear (1.2 s) gaze (0.6 s dwell) touch
A no prediction 264 s 591 s 20.9 s 11.5 s
B frequency word list 225 s 356 s 14.0 s 8.0 s
C small LM, top-5 192 s 307 s 12.3 s 6.6 s
D Halfword (cost-aware) 178 s 297 s 11.9 s 6.5 s
E D + on-device personalisation 145 s 238 s 10.2 s 5.4 s
— keystroke-optimal, billed in seconds 211 s 318 s 11.9 s 6.6 s

Personalisation (D → E) is worth 14–20% across every access method, and every adapter passed the general-ability check.

Metric (3), the headline. D versus the keystroke-optimal policy. Two corpora, because they say different things and both are worth seeing:

access method ζ list tax at 8 seconds saved (bundled) seconds saved (public corpora)
row-column scanning +0.40 +108% 15.7% 16.6%
single switch, linear +0.23 +40% 6.5% 6.4%
touch −0.12 +12% 0.9% −1.2%
eye gaze −0.01 +0.6% 0.01% 0.00%

Gaze is the control condition and it is the important row: slow, and it shows nothing. If the gap were about speed it would be large there.

Two honest caveats on that table, both from running it on real corpora rather than the bundled one:

  • On touch, Halfword is slightly worse — about 1% slower than just showing everything. Where position is nearly free there is nothing to optimise, and the decoder's one-step approximation costs a little. The method is for scanning-like access; on direct selection it is a wash, and here a marginally losing one. That is the honest boundary of the claim.
  • On real text it does trade some keystroke savings for time. On the bundled corpus the two policies achieved identical keystroke savings (43.4%). On public conversational corpora, scanning at 1.2 s gives up about four points of keystroke savings — 34.8% against 39.2% — to gain 16.6% of the time. So the headline is not "the same savings for free"; it is "seconds are what matter, and buying them with a few points of a proxy metric is the right trade". If you are reporting keystroke savings, Halfword will look slightly worse. That is the point.

divergence

personalisation

Full per-configuration results, including the rate sweeps, are in the benchmark dataset.

Metric (5), latency. The n-gram backend runs at 0.16 ms median per selection (0.43 ms p95). The 101 M-parameter Llama-architecture model predicts in 43 ms median on Metal and 137 ms on CPU — against a 600 ms gaze dwell and a 1200 ms scan step. Across the whole benchmark, zero seconds of model latency were charged to the person.

Exports are measured by loading and running the artefact, not by trusting that it was written. At the full 101 M size, one forward pass (batch 1, sequence 16):

format size median max error vs PyTorch notes
PyTorch eager, CPU 27.5 ms baseline
ONNX Runtime 405 MB 12.9 ms 4.8e-06 2.1× faster than eager; dynamic batch and sequence verified
Core ML, fp16 202 MB 27.9 ms 2.9e-02 half the size; fp16 error is expected
GGUF (llama arch) 453 MB 129 tensors, 27 KV keys, reads back under gguf.GGUFReader

Three things in export_backends.py are load-bearing and easy to get wrong silently, so they are documented where they live: the ONNX example batch must be ≥ 2 or torch.export specialises the batch axis to 1 with no warning; the Q/K weights must be permuted for GGUF or llama.cpp's RoPE quietly disagrees with Hugging Face's (argmax agreement drops from 100% to 96.9%); and Core ML must be verified with CPU_ONLY, because at 0.1 B parameters ComputeUnit.ALL aborts the process inside Metal — a native assertion no except can catch. A toy model reproduces none of these, which is why the export path is exercised at full size.


The neural model loses to the 4-gram, and the card says so

The published 0.1 B model is a reference implementation, not a good predictor. On 8 000 held-out utterances it never saw:

next word word completion (2 letters typed) latency
4-gram fallback, 15 MB 59.2% 90.2% 0.7 ms
neural, 101 M 9.2% 63.9% 197 ms

It saw about 9 M tokens in 100 minutes on a laptop GPU — three orders of magnitude short of a small LM that is actually good. It has learned morphology (plplease, wiwith) and little about what people say next, which is what that token budget buys.

What is correct and tested is everything around it: the sizing, the three exports and their gotchas, prefix-constrained decoding with token healing, and the KV-cache reuse that keeps a prediction inside a scan step. Swap in better weights and nothing else changes. Every benchmark number in this README uses the n-gram, so none of them depends on the neural model being good.

The four claims, and where each is checked

claim substantiated by test
time-to-utterance — seconds and keystrokes are different objectives costs.py, decoder.py, simulate.py, evaluate.py test_decoder.py::test_decoder_is_exactly_optimal checks the decoder against brute-force enumeration on every access method; test_costs.py checks every simulated entry rate against published ranges
on-device personalisation — adapting helps without breaking general ability personalize.py test_personalize.py — adapts on a speaker's past, measures on their future, and separately checks the regression guard can reject
on-device only — nothing derived from a person's words leaves personalize.py, hub.py test_no_egress.py — static audit of the import closure, live socket sabotage, and a full adaptation run with the network broken
low latency — predictions land before the person could act predictor.py, export_backends.py test_latency.py — asserts against the access method's own first-action window, not an arbitrary millisecond count
python -m pytest              # 60 tests, offline, ~13 s

Every function docstring names which of the four claims it bears on.


How the decoder works

At each selection it chooses the candidate list that minimises expected seconds:

J = Σ_j q_j · (c_pos(j) − L_j · κ_cont)  +  (1 − Σ_j q_j) · (κ_next(N) − κ_cont)

Each candidate is charged what it costs to reach (c_pos) and credited the characters it enters (L_j). The term that punishes long lists is κ_next(N), the cost of the next keyboard selection on the surface as displayed — proposing an eleventh candidate makes every letter dearer in the branch where none of them was right.

Three things worth knowing:

  • It is solved exactly, not greedily. The rearrangement inequality fixes the ordering within any chosen set, and a dynamic program then finds the optimal set in O(M·N). Sweeping N gives the global optimum in microseconds. test_decoder.py verifies this against brute force.
  • The expected remaining length of the utterance cancels out of the argmin. The decoder never guesses how long your sentence will be — which removes the obvious place a fudge factor would hide.
  • Probabilities have to be calibrated, or none of it works. A model's raw score is not "the probability this candidate is what you mean". Fed raw scores, the decoder concluded prediction is always right, which makes long lists free and collapses the whole trade-off. calibrate.py fits a logistic model on "is this the first correct candidate", so the residual mass is a real "you will have to spell another character".

Publishing

Nothing uploads by accident. hub.py is one of only two modules permitted to touch the network — a rule enforced by test_no_egress.py — repositories are created private by default, and every payload is scanned for anything resembling personal adaptation data before it moves.

python scripts/train.py --out runs/halfword-100m --steps 4000
python scripts/export.py --model runs/halfword-100m --out exports/    # Core ML, ONNX, GGUF
python scripts/build_bench.py --out benchmarks/hf --push you/halfword-bench

Limitations, stated rather than buried

  • The seconds are simulated. No one has been timed using this. The simulator prices mechanical scan, dwell and pointing cost under stated assumptions; it is a lower bound on cost, not a prediction of anyone's experience.
  • We do not model the cost of reading a list. This is the biggest omission and it cuts both ways. Every study above attributes the keystroke-savings/time gap largely to attention: shifting focus to the list, reading it, deciding, and the wasted look when the word is not there. Including it would push the optimal list length lower, so our "show fewer" conclusion is conservative. But it would also change which candidates to cut, and it means we cannot represent Trnka et al.'s finding that people learn when to look — that they are already running their own cost-aware policy our model assumes away.
  • No AAC user data, anywhere. There is, as far as we know, no public openly-licensed corpus of real AAC communication, and this project is not going to create one. We evaluate on public conversational text. The register is not the same.
  • Pseudo-speakers are a construction. The personalisation numbers show the machinery works. They are not an effect size anyone should expect.
  • The single-switch linear-sweep condition is deliberately extreme. It is a real configuration, and it is the slowest thing in the benchmark. Note it diverges less than row-column scanning despite being slower — because its keyboard is already so expensive that candidate rows barely change the ratio. That is the mechanism doing what we said it would.
  • A note on the numbers throughout. Every duration is a property of an interface configuration. A 2 s scan interval means this interface costs more per selection and nothing whatsoever about who is using it. People choose slower settings for good reasons — fatigue, spasticity, a switch that needs a firm press, wanting fewer errors — and the language layer's job is to adapt to that, not the other way round.

Data and licences

Code is Apache-2.0. Corpora carry their own licences, recorded individually with a verification date in halfword/data/corpus_sources.json and reproduced in the dataset card. build_bench.py refuses any source missing a licence or a verification date.

source licence commercial
halfword-seed (bundled, hand-authored — not AAC user data) CC0-1.0 yes
allenai/soda CC BY 4.0 yes
google/Synthetic-Persona-Chat CC BY 4.0 yes
pfb30/multi_woz_v22 Apache-2.0 yes
Helsinki-NLP/tatoeba CC BY 2.0 FR yes
li2017dailydialog/daily_dialog CC BY-NC-SA 4.0 no — excluded unless --allow-noncommercial, and it makes anything trained on it non-commercial too

Cost-model parameters come from the sources below. Provenance for each is in docs/research/, and halfword costs --method <m> prints it. Parameters are labelled verified (we read the source), likely (consistent across sources) or inferred (our modelling choice) — test_costs.py fails if any parameter lacks a citation or a confidence label.

  • Simpson, Koester & LoPresti (2006). Selecting an appropriate scan rate: the .65 rule. RESNA. — response latency, press jitter, the .65 rule
  • Mankowski, Simpson & Koester (2013). Validating a model of row-column scanning. Disabil Rehabil Assist Technol 8(3), 321–329. doi — scan rates, error rates, recovery delay
  • Koester & Simpson (2019). Effectiveness and usability of Scanning Wizard software. Disabil Rehabil Assist Technol 14(2), 161–171. — per-subject scan intervals
  • Majaranta, Ahola & Špakov (2009). Fast gaze typing with an adjustable dwell time. CHI. doi — dwell times, entry rates, error rates
  • Carpenter (1988). Movements of the Eyes. — saccadic main sequence
  • MacKenzie (1992). Fitts' law as a research and design tool in HCI. HCI 7, 91–139. — touch parameters
  • Bonaker, Nel, Vertanen & Broderick (2022). A performance evaluation of Nomon. CHI. doi — switch-user reaction times

Prior art for the claim itself: Koester & Levine (1994, 1996, 1998); Higginbotham (1992); Higginbotham, Lesher & Moulton (1998); Trnka et al. (2007, 2008, 2009); MacKenzie (2012); Quinn & Zhai (2016); Cai et al. (2022, 2024); Blasko et al. (2025); Light & McNaughton (2014).

We fetched and read the sources marked verified in docs/research/. Several of the AAC-journal papers above are paywalled and we worked from abstracts, secondary descriptions or hosted PDFs; those are marked likely rather than verified. If we have mischaracterised your work, please open an issue — we would rather fix it than defend it.

About

AAC word prediction optimised for time to utterance, not keystrokes saved. The divergence is driven by positional cost, not by how slow the access method is.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages