NagaYu commited on
Commit
46925f6
·
verified ·
1 Parent(s): 3017552

Halfword 100M: Llama-architecture AAC predictor with Core ML, ONNX and GGUF exports

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ halfword.gguf filter=lfs diff=lfs merge=lfs -text
37
+ ngram.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ library_name: transformers
6
+ pipeline_tag: text-generation
7
+ tags:
8
+ - aac
9
+ - augmentative-and-alternative-communication
10
+ - assistive-technology
11
+ - accessibility
12
+ - text-entry
13
+ - word-prediction
14
+ - on-device
15
+ - eye-gaze
16
+ - switch-scanning
17
+ - coreml
18
+ - onnx
19
+ - gguf
20
+ datasets:
21
+ - allenai/soda
22
+ - google/Synthetic-Persona-Chat
23
+ - pfb30/multi_woz_v22
24
+ - Helsinki-NLP/tatoeba
25
+ ---
26
+
27
+ # Halfword 100M
28
+
29
+ A 0.1 B-parameter Llama-architecture language model for **AAC word prediction**, trained to run on the device and paired with a decoder that optimises **seconds to utterance** rather than keystrokes saved.
30
+
31
+ The model on its own is unremarkable — a small LM trained on public conversation. What it is *for* is the interesting part: it feeds a decoder that knows what a candidate costs to reach on the person's actual access method, and chooses the candidate list accordingly.
32
+
33
+ - **Code, benchmark and figures:** https://github.com/NagaYu/halfword
34
+ - **Interactive demo:** https://huggingface.co/spaces/NagaYu/halfword
35
+ - **Benchmark dataset:** https://huggingface.co/datasets/NagaYu/halfword-bench
36
+
37
+ ## What ships here
38
+
39
+ | file | what it is |
40
+ |---|---|
41
+ | `model.safetensors`, `config.json` | the 101 M-parameter Llama-architecture model |
42
+ | `tokenizer.json` | 16 k byte-level BPE, trained on the same corpus |
43
+ | `halfword.onnx` | ONNX export, dynamic batch and sequence, verified in onnxruntime |
44
+ | `halfword.mlpackage` | Core ML export, float16 |
45
+ | `halfword.gguf` | GGUF under the stock `llama` architecture, for llama.cpp |
46
+ | `ngram.json` | 4-gram fallback predictor — runs with no accelerator and no torch |
47
+ | `calibrator.json` | acceptance calibrator (see below; the decoder does not work without it) |
48
+
49
+ ## Architecture
50
+
51
+ Llama-architecture (RMSNorm, RoPE, SwiGLU, tied embeddings). That is an **export decision, not a preference**: it is what Core ML, ONNX and llama.cpp's GGUF path all already understand, so one definition leaves in three formats without a bespoke converter for each.
52
+
53
+ | | |
54
+ |---|---|
55
+ | parameters | 100.7 M |
56
+ | layers / hidden / heads / KV heads | 14 / 768 / 12 / 4 |
57
+ | intermediate | 2048 |
58
+ | vocabulary | 16 384 |
59
+ | context | 512 |
60
+
61
+ ## Usage
62
+
63
+ ```python
64
+ from halfword.predictor import NeuralPredictor, Context
65
+
66
+ pred = NeuralPredictor.load("NaGaYu/halfword-100m") # local dir or snapshot
67
+ pred.warmup()
68
+ for c in pred.predict(Context(history=("how are you today",), committed="i would like some wa")):
69
+ print(c.insert, round(c.probability, 4), c.kind)
70
+ ```
71
+
72
+ Predictions are **prefix-constrained**: having typed `wa`, every candidate extends `wa`. Tokens that cannot are masked before the softmax is inspected, which both improves the candidates and bounds the work — a word finishes in one or two tokens far more often than a free continuation does.
73
+
74
+ To get the point of the project, use it with the decoder:
75
+
76
+ ```python
77
+ from halfword.costs import CostModel, Surface, load_keyboards
78
+ from halfword.decoder import CostAwareDecoder
79
+ from halfword.calibrate import AcceptanceCalibrator
80
+
81
+ cost = CostModel.from_name("scanning", scan_interval_s=1.2)
82
+ surface = Surface(load_keyboards()["abc_6x6"])
83
+ decoder = CostAwareDecoder(max_candidates=8,
84
+ calibrator=AcceptanceCalibrator.load("calibrator.json"))
85
+ shown = decoder.present(pred.predict(ctx), surface, cost) # may legitimately be 0 candidates
86
+ ```
87
+
88
+ ## How good is it? Worse than the n-gram beside it.
89
+
90
+ Measured on 8 000 utterances the model never saw (rows beyond the training cut of each source), top-5, 120 word boundaries:
91
+
92
+ | | next word, nothing typed | word completion, 2 letters typed | latency |
93
+ |---|---|---|---|
94
+ | `ngram.json` (4-gram, 15 MB) | **59.2%** | **90.2%** | 0.7 ms |
95
+ | this model (101 M) | 9.2% | 63.9% | 197 ms |
96
+
97
+ That is not a typo and it is not a bug hunt left unfinished. **The trivial baseline shipped alongside this model beats it comfortably**, and you should use `ngram.json` unless you have a reason not to.
98
+
99
+ The reason is ordinary: the model saw roughly 9 M tokens of training — about three passes over a 3 M-token corpus, in 100 minutes on a laptop GPU. Small language models that are actually good see three to four orders of magnitude more. This one has learned English morphology (`pl` → `please`, `wi` → `with`, `m` → `much`) but not much about what people say next, which is exactly the shape you would predict from the token budget.
100
+
101
+ **What it is good for.** It is a working reference for the parts that are hard to get right and easy to get silently wrong: the Llama-architecture sizing, the three exports with their gotchas, the prefix-constrained decoding with token healing, and the KV-cache reuse that keeps latency inside a scan step. All of that is correct and tested. Swap in better weights and the rest of the stack does not change.
102
+
103
+ **What would fix it.** More tokens, in this order: a larger and more conversational corpus (the current mix is 40% Tatoeba single sentences, which teach grammar but not dialogue), 10–50× the training steps, and a held-out early-stopping signal instead of a fixed step count. None of that is research; it is compute.
104
+
105
+ ## Latency
106
+ ## The calibrator is not optional
107
+
108
+ The decoder converts probabilities into seconds. A raw model score is not "the probability this candidate is what the person means", and fed raw scores the decoder concludes prediction is *always* right — which makes long candidate lists free and collapses the entire trade-off the project exists to model. `calibrator.json` fits "is this the first correct candidate in the pool", so the residual mass is a genuine "you will have to spell another character". Held-out expected calibration error is reported in `provenance.json`.
109
+
110
+ ## Training data
111
+
112
+ Public, openly-licensed conversational corpora only. Licences were checked individually and are recorded per source with a verification date.
113
+
114
+ | source | licence |
115
+ |---|---|
116
+ | [`allenai/soda`](https://huggingface.co/datasets/allenai/soda) | CC BY 4.0 |
117
+ | [`google/Synthetic-Persona-Chat`](https://huggingface.co/datasets/google/Synthetic-Persona-Chat) | CC BY 4.0 |
118
+ | [`pfb30/multi_woz_v22`](https://huggingface.co/datasets/pfb30/multi_woz_v22) | Apache-2.0 |
119
+ | [`Helsinki-NLP/tatoeba`](https://huggingface.co/datasets/Helsinki-NLP/tatoeba) | CC BY 2.0 FR |
120
+
121
+ **No AAC user data.** There is, as far as we know, no public openly-licensed corpus of real AAC communication, and this project did not create one. The register of open-domain chat is not the register of AAC use, and that gap is a real limitation of everything here.
122
+
123
+ Non-commercial corpora (e.g. DailyDialog, CC BY-NC-SA) are excluded by default. Including them would make this model non-commercial too.
124
+
125
+ ## Limitations
126
+
127
+ - **It loses to a 4-gram.** See the table above. This is a reference implementation with real weights, not a good predictor, and the card says so where you will read it rather than in a footnote.
128
+ - **Small model, narrow corpus.** 0.1 B parameters, 3 M training tokens, 100 minutes. It will not know your family's names, your medications, or your jokes — and on this evidence it does not know much about ordinary conversation either. On-device personalisation addresses the first of those, and never sends anything anywhere.
129
+ - **The seconds are simulated.** Nobody has been timed using this. The cost models price mechanical scan, dwell and pointing time under stated assumptions.
130
+ - **Visual search cost is not modelled.** The AAC literature argues the cost of *reading* a prediction list is a large part of why prediction can slow people down. Ignoring it makes long lists look cheaper than they are, so the reported gaps are a lower bound.
131
+ - **Every duration is a property of an interface, never of a person.** A long scan interval means *this interface costs more per selection* and nothing about who is using it.
132
+
133
+ ## If you use AAC, or support someone who does
134
+
135
+ This was built without AAC users in the room, which is a real limitation and not a small one. Blasko et al. (2025) put the problem in a title: *["Nothing about AAC users without AAC users"](https://doi.org/10.1080/07434618.2025.2514748)*. If the defaults look wrong to you — the scan intervals, the dwell times, the assumption that seconds are the right thing to minimise at all — please open a discussion here or an issue on GitHub. We would rather be corrected than cited.
136
+
137
+ ## Citation
138
+
139
+ The finding that keystroke savings and communication rate come apart is **not ours**. See Koester & Levine (1994, 1996), Higginbotham (1992), Trnka et al. (2007–2009), Quinn & Zhai (2016), and especially Cai et al. ([2024, *Nature Communications*](https://doi.org/10.1038/s41467-024-53873-3)). What is new here is narrower: identifying *positional cost* rather than slowness as the mechanism, and making candidate-list length a computed decision variable in a time objective. Full positioning in the [repository README](https://github.com/NagaYu/halfword).
calibrator.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"weights": [3.5384301057400043, 0.10019243562855197, 0.2035921170264405, 0.11479055941771045, -1.9041843052800418, 0.03140271699474312, -0.6681733880820065, -3.49499685786082, -0.7230667556147661], "features": ["bias", "logp", "margin_from_top", "rank", "log1p_rank", "new_chars", "log1p_new_chars", "is_phrase", "pool_size"]}
config.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "LlamaForCausalLM"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "bos_token_id": 1,
8
+ "dtype": "float32",
9
+ "eos_token_id": 2,
10
+ "head_dim": 64,
11
+ "hidden_act": "silu",
12
+ "hidden_size": 768,
13
+ "initializer_range": 0.02,
14
+ "intermediate_size": 2048,
15
+ "max_position_embeddings": 512,
16
+ "mlp_bias": false,
17
+ "model_type": "llama",
18
+ "num_attention_heads": 12,
19
+ "num_hidden_layers": 14,
20
+ "num_key_value_heads": 4,
21
+ "pad_token_id": 0,
22
+ "pretraining_tp": 1,
23
+ "rms_norm_eps": 1e-05,
24
+ "rope_scaling": null,
25
+ "rope_theta": 10000.0,
26
+ "tie_word_embeddings": true,
27
+ "transformers_version": "4.57.6",
28
+ "use_cache": true,
29
+ "vocab_size": 16384
30
+ }
export_report.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "runs/halfword-100m",
3
+ "parameters": 100685568,
4
+ "torch_cpu_median_ms": 46.064916999966954,
5
+ "exports": [
6
+ {
7
+ "format": "onnx",
8
+ "path": "runs/exports/halfword.onnx",
9
+ "ok": true,
10
+ "bytes": 404918466,
11
+ "latency_ms_median": 15.054291498017847,
12
+ "latency_ms_p90": 16.251625002041692,
13
+ "max_abs_error": 2.956390380859375e-05,
14
+ "notes": "",
15
+ "detail": {
16
+ "dynamic_batch_verified": true
17
+ }
18
+ },
19
+ {
20
+ "format": "coreml",
21
+ "path": "runs/exports/halfword.mlpackage",
22
+ "ok": true,
23
+ "bytes": 201964284,
24
+ "latency_ms_median": 32.850979499926325,
25
+ "latency_ms_p90": 33.795082999859005,
26
+ "max_abs_error": 0.03583550453186035,
27
+ "notes": "",
28
+ "detail": {
29
+ "precision": "fp16",
30
+ "compute_units": "CPU_ONLY"
31
+ }
32
+ },
33
+ {
34
+ "format": "gguf",
35
+ "path": "runs/exports/halfword.gguf",
36
+ "ok": true,
37
+ "bytes": 453451168,
38
+ "latency_ms_median": 0.0,
39
+ "latency_ms_p90": 0.0,
40
+ "max_abs_error": NaN,
41
+ "notes": "",
42
+ "detail": {
43
+ "tensors_written": 129,
44
+ "arch": "llama",
45
+ "kv_keys": 27,
46
+ "tensors_read": 129
47
+ }
48
+ }
49
+ ]
50
+ }
generation_config.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "bos_token_id": 1,
4
+ "eos_token_id": 2,
5
+ "pad_token_id": 0,
6
+ "transformers_version": "4.57.6"
7
+ }
halfword.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:de44722a8edfad750d3703675f292e624d67e7183108667a087e5fa4a21ebf9d
3
+ size 453451168
halfword.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:70fbae6be568ce7adcaeff3d8ae2ae6e23bde9ef94487e73d4467dc6622314c1
3
+ size 270325
halfword.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f82f3cea164447deed6084d4c2b93efe2bcb6f3403f2c80c02b739029e0c33ef
3
+ size 201418688
halfword.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "4DAF3277-28BC-46B2-9652-165CB7080110": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Specification",
7
+ "name": "model.mlmodel",
8
+ "path": "com.apple.CoreML/model.mlmodel"
9
+ },
10
+ "58A426E7-F772-4C0B-9CA1-D20E50BD8627": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Weights",
13
+ "name": "weights",
14
+ "path": "com.apple.CoreML/weights"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "4DAF3277-28BC-46B2-9652-165CB7080110"
18
+ }
halfword.mlpackage/executorch_debug_handle_mapping.json ADDED
The diff for this file is too large to render. See raw diff
 
halfword.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cd2fedfc4db04239d90cf136fb5ac768ff1a674b1f0b9621839214bd368c82a4
3
+ size 404918466
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:600887c9c760a51f4c283434563129f8b636f6218cdb77c1d71cedc330eeeafd
3
+ size 402756480
ngram.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0f9e3f3eb2925d4843366c1012582cdff49e627f55dde19b9eca79b40278fdcb
3
+ size 15180085
provenance.json ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "ngram": {
3
+ "order": 4,
4
+ "pruned_min_count": 2,
5
+ "bytes": 15180085,
6
+ "word_types": 29273
7
+ },
8
+ "calibration": {
9
+ "ece": 0.0041261140127787236,
10
+ "n": 349872,
11
+ "pools": 103977
12
+ },
13
+ "config": "halfword-100m",
14
+ "parameters": 100685568,
15
+ "train_steps": 3000,
16
+ "final_loss": 2.892653913497925,
17
+ "train_tokens": 3014077,
18
+ "batch_size": 16,
19
+ "seq_len": 192,
20
+ "lr": 0.0004,
21
+ "device": "mps",
22
+ "sources": [
23
+ {
24
+ "name": "halfword-seed",
25
+ "license": "CC0-1.0",
26
+ "url": "halfword/data/seed_corpus.json",
27
+ "verified_on": "2026-08-31"
28
+ },
29
+ {
30
+ "name": "allenai/soda",
31
+ "license": "cc-by-4.0",
32
+ "url": "https://huggingface.co/datasets/allenai/soda",
33
+ "verified_on": "2026-08-31"
34
+ },
35
+ {
36
+ "name": "google/Synthetic-Persona-Chat",
37
+ "license": "cc-by-4.0",
38
+ "url": "https://huggingface.co/datasets/google/Synthetic-Persona-Chat",
39
+ "verified_on": "2026-08-31"
40
+ },
41
+ {
42
+ "name": "pfb30/multi_woz_v22",
43
+ "license": "apache-2.0",
44
+ "url": "https://huggingface.co/datasets/pfb30/multi_woz_v22",
45
+ "verified_on": "2026-08-31"
46
+ },
47
+ {
48
+ "name": "Helsinki-NLP/tatoeba",
49
+ "license": "cc-by-2.0",
50
+ "url": "https://huggingface.co/datasets/Helsinki-NLP/tatoeba",
51
+ "verified_on": "2026-08-31"
52
+ }
53
+ ],
54
+ "commercial_use_permitted": true
55
+ }
special_tokens_map.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": "<s>",
3
+ "eos_token": "</s>",
4
+ "pad_token": "<pad>",
5
+ "unk_token": "<unk>"
6
+ }
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "<pad>",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": true
10
+ },
11
+ "1": {
12
+ "content": "<s>",
13
+ "lstrip": false,
14
+ "normalized": false,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "2": {
20
+ "content": "</s>",
21
+ "lstrip": false,
22
+ "normalized": false,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": true
26
+ },
27
+ "3": {
28
+ "content": "<unk>",
29
+ "lstrip": false,
30
+ "normalized": false,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": true
34
+ }
35
+ },
36
+ "bos_token": "<s>",
37
+ "clean_up_tokenization_spaces": false,
38
+ "eos_token": "</s>",
39
+ "extra_special_tokens": {},
40
+ "model_max_length": 1000000000000000019884624838656,
41
+ "pad_token": "<pad>",
42
+ "tokenizer_class": "PreTrainedTokenizerFast",
43
+ "unk_token": "<unk>"
44
+ }