“Inference in the browser” usually means shipping a neural network to WebGPU, and the honest answer to “should we” is usually “not yet.” But the question underneath it is broader and already answered across the Hraness products: when does a model run beside the app instead of behind an API? A sound bank is a model of an instrument. An interpreter executing a bounded program is a model runner. A procedural audio graph is a generative model with no weights at all. Each of our products runs real inference-shaped work in the tab today, and the rules that govern it are the same ones that will govern a weight file when one is worth shipping: what fits, what is cached, and what it costs.
what fits in a tab
The first bound is input size, and every local model in the family has one written down. Platonik’s lab runs deterministic experiments as bounded JSON programs through fixed interpreters: the courier model caps input at 16 KiB, rejects extra fields, and runs a program of 1–24 rules, each with up to eight conditions, over a deterministic 15×11 maze for at most 256 ticks. The signal model caps JSON at 32 KiB, nodes at 128, output bindings at 32, and stimulus phases at 32. Soundfish’s portable composition document is capped at 24,493 canonical CBOR bytes. These are not preferences; they are the size contract that makes “runs in the tab” a guarantee instead of a hope.
The second bound is on what a model decodes into. Soundfish’s General MIDI renderer keeps the base bank resident and fetches optional drum-kit and override layers only for the programs a document uses, because the alternative is visible in one number: the Grand Piano spans 84 samples, roughly 130 MB decoded, across velocity layers a given track probably never touches. Fetching models by the slice the document needs, rather than the whole catalog, is what keeps a “local” model from being a download that never finishes.
The third answer to “what fits” is: sometimes nothing has to. Sleepyland synthesizes brown, pink, and white noise procedurally and models its surf engine from stochastic wave parameters, so the product ships zero audio assets. When the model is small enough to express as math, the storage question disappears entirely.
what is cached
Local inference only feels local if the model survives a reload, so caching is engineered rather than assumed. Soundfish stores fetched bank blobs in Cache Storage keyed by content SHA-256 (__jungle_content_sha256 on the request), verifies the integrity of every cached or fetched full blob before it reaches the worklet, and treats a missing or failing cache as a typed fallback to the network rather than an exception. The same content-addressed path serves every kit file: the cache can never hand back the wrong bank, because the wrong bank hashes wrong.
Documents get the same treatment at a different tier. Arrangement snapshots live in an IndexedDB store keyed by transport digest (jungle.soundfish.songs), a session handoff rides in session storage (jungle.soundfish.loop-entry.v1), and a prompt-free recovery journal keeps one latest snapshot in local storage plus a byte-capped IndexedDB journal so a crash mid-edit is a restore, not a loss. Nothing about the layout requires the network to explain itself. The pattern to copy is the layering: durable store first, content-verified cache second, network third, and each tier degrades to the next without changing the result.
what it costs
The cost discipline is a ledger, not a feeling. In Platonik’s signal model, every node evaluation, wire read, stimulus read, state write, and output read costs one modeled work unit, and the run’s ceilings are named constants rather than slider settings:
maxTicks: 256,
maxFuel: 2_000_000,
maxBuildSteps: 1024,
The assembler’s build steps cap at 1,024 with node material at 128; the courier model keeps its own explicit ledger where failed actions still cost work. A runaway program does not hang the tab; it exhausts its allowance and stops, deterministically, at a number the reader can see.
Audio carries a different currency: render quanta. A lazy in-worklet sample decode costs about twenty milliseconds inside one roughly three-millisecond quantum, which is why primeProgram decodes in a Worker and transfers PCM instead of letting the render thread fault. And Soundfish’s capture verifier runs the production graph under a blocking 6× CPU throttle and demands no silent gap of 40 ms or more and zero counted dropouts, so the cost model is enforced against a deliberately bad device, not a good one. The common shape is that every cost is a number with a bound, measured against a declared worst case rather than a typical one.
measure this browser
The most transferable habit in the family is that the products measure the machine in front of them rather than trusting a published number. Platonik’s lab has a literal “measure this browser” action: it runs 64 bounded 256-tick courier experiments using the program on the bench, yields between runs, supports cancellation, and reports observed modeled work per elapsed second for that browser. The docs are careful about what the number means: it estimates the toy’s own work and cannot predict Rust throughput, and leaving the view cancels the sample rather than reporting a partial one.
That is the right posture for any tab-local model. Spec-sheet throughput belongs to someone else’s hardware; the only number that matters for a local run is the one measured on the device that will do the work, under the budget the run will actually be held to.
the page displays, the tab computes
There is also a division of labor worth stealing outright. Platonik’s small lab models execute in the tab through fixed interpreters, while its larger Rust experiments are published as committed receipts the browser displays rather than re-executes: the recorded bridge pages show the map, memory, attempted actions, and work from a real Rust receipt, and the receipt carries evidence_hash and result_hash digests a verify command can recompute. The interpreter runs in the tab when you are exploring; the published artifact is a hash-pinned observation of a run that already happened.
Slopcamera draws the same line between authority and derivative: native sources and project decisions stay authoritative, rendered frames, diagrams, and videos are derivatives, and every operation carries a receipt naming its inputs and outputs. The tab (or the local renderer on the same machine) does the compute; the artifact keeps the provenance. Inference you cannot re-run or receipt is inference you cannot trust, and both products treat the receipt as part of the output rather than as logging.
when the tab is the wrong place
The boundary case is as instructive as the local wins. Slopcamera keeps ordinary editing and rendering entirely local on the user’s machine, and keeps model-backed generation behind an explicit line: optional work goes through the caller’s own Vercel AI Gateway access, uploading named local media requires a matching explicit acknowledgement, the unacknowledged offer expires after ten minutes, and the product’s own website never accepts a Gateway credential. Native Python authoring is trusted current-user code requiring separate authorization, not a sandbox pretending to be one.
The lesson generalizes past models. Local execution is chosen when the input is bounded, the result is deterministic or cacheable, and the user gains privacy, offline behavior, or zero marginal cost. It is refused when the work needs resources the tab cannot promise, when custody of the user’s media would silently change hands, or when a remote provider would do the same job with a clearer consent story. The tab is a real runtime with a real budget, and the products that run models there successfully are the ones that wrote the budget down first, enforced it in code, and measured the device in front of them rather than the one in the spec sheet.
sources
- platonik:
docs/observatory.mdfor the interpreter bounds, work ledgers, and the measure-this-browser harness;lib/observatory/for the caps (maxTicks: 256,maxFuel: 2_000_000,maxBuildSteps: 1024, 16 KiB and 32 KiB JSON limits). - sound.fish:
lib/synth/soundfont-cache.tsfor the content-keyed Cache Storage layer,lib/synth/renderer.tsfor per-program fetching andprimeProgram, andscripts/audio/verify-studio-capture.tsfor the throttled no-gap capture check. - slopcamera: the README and
src/studio/AGENTS.mdfor the acknowledged upload boundary and the trusted-native-code contract. - sleepyland: procedural noise and surf models with no shipped audio assets.