Every pitch deck wants voice AI in the cloud: upload audio, wait for a GPU farm, download a result, pray the latency doesn't kill the bit. We wanted character voices on a keyed CB transmit — which means the conversion has to happen in the gap between "PTT down" and "audio hits the channel." That gap is not generous. It is also not negotiable.

The browser is a weird place to run ML. It's a sandbox with opinions. Memory ceilings. Threads that aren't quite threads. WASM helps, but WASM is not a magic "run PyTorch here" button — it's a contract you negotiate with the main thread, workers, and whatever the user's phone had for lunch. We pushed inference off the UI path early. If the model hiccups, the channel UI should still feel alive even if this particular transmission needs a beat to render.

Model size is the silent product decision. A heavier model might sound richer in a demo video and explode on a three-year-old Android tab. We bias toward models that fit the PWA budget: load once, warm up honestly, degrade gracefully when the device says no. Users forgive "this voice preset isn't available on your phone." They do not forgive "the entire app crashed mid-key."

Privacy is the part we don't shut up about. Voice conversion on-device means the raw mic path doesn't need a detour through our servers to sound like a puppet, a robot, or whatever chaos the channel culture demands. That's not anti-cloud dogma — it's matching architecture to use case. CB radio is intimate, ephemeral, and often weird on purpose. Shipping voice to a third party by default would feel wrong even if it were cheap.

We're still exploring smarter caching, faster warm starts, and better fallbacks when WASM SIMD isn't in the cards. But the core bet holds: a PWA can carry serious on-device ML if you treat latency, memory, and failure like radio constraints — not like ML benchmark constraints.