A 0.6B-parameter language model runs in your browser tab, on your hardware. You can hold a conversation with it, and every message you send also searches 123,287 photographs that a 27B-parameter model encoded months ago on a datacenter GPU.
The large model is never downloaded and never runs. Its work is finished and sitting in a file: what you download is its output, about a kilobyte per photograph, not the weights that produced it. The small model reads what the large one wrote, which is the whole of it.
One model, two jobs. Your message is read twice by the same frozen weights: once inside a chat template to write the reply, and once bare, so a 2.1 MB linear head can turn that hidden state into the vector that searches.
Two reads rather than one, and that is forced rather than lazy. The head is fitted on plain text, and handing it the chat-formatted prompt instead moves the median rank of the right photograph from 187 to 41,014 out of 123,287, with a cosine of 0.10 between the two vectors. A linear read-out is fitted to one distribution of hidden states and does not survive being handed another. The same lesson turns up again in the runtime anchor.
Cached image embeddings are ordinary and language models in a browser are not new. The read-out is neither: text encoded by the small model lands in the same space as images encoded by a model forty-five times its size, because both sides are linear read-outs fitted on frozen backbones.
No inference server, no API key, no telemetry. Eight megabytes of static files, computed entirely on your device. It runs on the CPU through WebAssembly, so it needs no WebGPU and works where GPU-backed engines cannot start, and there is nothing to install because it is a static page rather than an app. A phone can run it from a link and keep working with the network off.
| model (Qwen3-0.6B, Q4_0) | 382 MB, fetched once from the Hub, cached |
| gallery (123,287 × 1024, int8) | 130 MB, cached |
| engine, head and anchor | 7.8 MB |
The browser ends up storing 513 MB, measured rather than estimated. The page asks for persistent storage before it starts and tells you the answer.
Verified on desktop Chrome, desktop Safari and iOS Safari. iPhones needed a real fix rather than a smaller payload: the model is parsed straight out of a Blob instead of being staged in WebAssembly memory first. Staging meant the 382 MB file existed twice while it was converted into tensors, and wasm memory grows but never shrinks, so that peak was permanent rather than momentary. iOS killed the tab every time.
Ask something with a scene in it and the scores spread out. Ask the model about itself and you will keep seeing the same photographs, because a question with no visual content projects to near the middle of the gallery, where the closest images are simply the ones nearest the average of all of them.
The line is not abstract against concrete, it is whether the words name anything physical. Semiosis inside your frozen state is as abstract as a sentence gets and it retrieves bodies curled inside suitcases and drawers, because frozen and inside are things a photograph can be.
We looked at what sits at the centre: of the eight images nearest it, seven are people and five are single portraits. Ask the model what it is thinking about and it will say something about the people it has met. Two unrelated systems, the same fallback.
The strength slider adds a direction to your query in read-out space, calibrated on retention, the share of your own results that survive. Push it far enough and class purity looks superb while retention collapses to 0.01, which means the axis has replaced your query. The default sits at 0.5 for that reason.
This steers the query only. The reply is produced by the unmodified model.
Every figure here is backed by a committed artifact in
space-bacon/SRT under artifacts/nla/q4/.
The read-out geometry is a public Rust crate with no model dependency, compiled
to wasm32-unknown-unknown and verified against the Python path that produced
the published numbers.
Known boundary: the shared space does not represent word order. On 141 minimal pairs, a woman walking a horse against a horse walking a woman, accuracy is 0.468 against a chance of 0.5. It reads the words, not their arrangement.
9 commits
A 0.6B-parameter language model runs in your browser tab, on your hardware. You can hold a conversation with it, and every message you send also searches 123,287 photographs that a 27B-parameter model encoded months ago on a datacenter GPU.
The large model is never downloaded and never runs. Its work is finished and sitting in a file: what you download is its output, about a kilobyte per photograph, not the weights that produced it. The small model reads what the large one wrote, which is the whole of it.
One model, two jobs. Your message is read twice by the same frozen weights: once inside a chat template to write the reply, and once bare, so a 2.1 MB linear head can turn that hidden state into the vector that searches.
Two reads rather than one, and that is forced rather than lazy. The head is fitted on plain text, and handing it the chat-formatted prompt instead moves the median rank of the right photograph from 187 to 41,014 out of 123,287, with a cosine of 0.10 between the two vectors. A linear read-out is fitted to one distribution of hidden states and does not survive being handed another. The same lesson turns up again in the runtime anchor.
Cached image embeddings are ordinary and language models in a browser are not new. The read-out is neither: text encoded by the small model lands in the same space as images encoded by a model forty-five times its size, because both sides are linear read-outs fitted on frozen backbones.
No inference server, no API key, no telemetry. Eight megabytes of static files, computed entirely on your device. It runs on the CPU through WebAssembly, so it needs no WebGPU and works where GPU-backed engines cannot start, and there is nothing to install because it is a static page rather than an app. A phone can run it from a link and keep working with the network off.
| model (Qwen3-0.6B, Q4_0) | 382 MB, fetched once from the Hub, cached |
| gallery (123,287 × 1024, int8) | 130 MB, cached |
| engine, head and anchor | 7.8 MB |
The browser ends up storing 513 MB, measured rather than estimated. The page asks for persistent storage before it starts and tells you the answer.
Verified on desktop Chrome, desktop Safari and iOS Safari. iPhones needed a real fix rather than a smaller payload: the model is parsed straight out of a Blob instead of being staged in WebAssembly memory first. Staging meant the 382 MB file existed twice while it was converted into tensors, and wasm memory grows but never shrinks, so that peak was permanent rather than momentary. iOS killed the tab every time.
Ask something with a scene in it and the scores spread out. Ask the model about itself and you will keep seeing the same photographs, because a question with no visual content projects to near the middle of the gallery, where the closest images are simply the ones nearest the average of all of them.
The line is not abstract against concrete, it is whether the words name anything physical. Semiosis inside your frozen state is as abstract as a sentence gets and it retrieves bodies curled inside suitcases and drawers, because frozen and inside are things a photograph can be.
We looked at what sits at the centre: of the eight images nearest it, seven are people and five are single portraits. Ask the model what it is thinking about and it will say something about the people it has met. Two unrelated systems, the same fallback.
The strength slider adds a direction to your query in read-out space, calibrated on retention, the share of your own results that survive. Push it far enough and class purity looks superb while retention collapses to 0.01, which means the axis has replaced your query. The default sits at 0.5 for that reason.
This steers the query only. The reply is produced by the unmodified model.
Every figure here is backed by a committed artifact in
space-bacon/SRT under artifacts/nla/q4/.
The read-out geometry is a public Rust crate with no model dependency, compiled
to wasm32-unknown-unknown and verified against the Python path that produced
the published numbers.
Known boundary: the shared space does not represent word order. On 141 minimal pairs, a woman walking a horse against a horse walking a woman, accuracy is 0.468 against a chance of 0.5. It reads the words, not their arrangement.
9 commits