sachinkesiraju/jax-realtime

Full‑duplex realtime voice assistant in your browser with jax‑js

9

stars

34

commits

TypeScript

primary language

Aug 19, 2026

updated

jax-realtime.netlify.app

README

jax-realtime

A real-time, full-duplex voice assistant that runs entirely in your browser on WebGPU, built with jax-js.

Every stage (speech → ASR → LLM → TTS → speech, plus optional vision) runs locally in the tab; nothing is sent to a server.

jax-realtime: the idle orb, ready to talk

It's inspired by the Thinking Machines interaction model and GPT-Live, rebuilt as a small-model cascade that fits in a browser tab. The goal is a conversation that feels live: you can interrupt it mid-sentence, pause mid-thought without losing your turn, and it keeps searching in the background while you talk.

StageModelRuns on
Ear (ASR)Whisper base.en (int8, dequantized to fp16)WebGPU via jax-js
Turn-taking (VAD)Silero VAD v5, ported to TypeScriptCPU (~2 ms / 32 ms frame)
Brain (LLM)SmolLM2-360M-Instruct (fp16)WebGPU via jax-js
Voice (TTS)Kyutai Pocket TTS + Mimi codec (fp16)WebGPU via jax-js
Eye (vision)D-FINE small (COCO-80)WebGPU via @jax-js/onnx

Everything shares the single WebGPU device. The streaming ASR lane is paused while the assistant speaks so it doesn't contend with TTS for the GPU; barge-in is therefore energy-based (see below), and captions resume the moment the assistant stops.

Interaction

  • Full-duplex micro-turns: a ~150 ms tick loop drives a deterministic, priority-ordered policy: adaptive barge-in (talk over the assistant, including its tool narrations, and the audio cuts in ~300 ms; the threshold auto-calibrates to the echo floor of each reply), adaptive endpointing, and time-awareness timers. A watchdog force-recovers the session if a reply ever stalls, so it can't wedge.
  • Continuation-merge: if the endpoint fires on a mid-thought pause and you resume speaking before the reply's first audio, the unheard reply is aborted and both halves are answered as one turn ("append, don't restart").
  • Learned turn signal: a pure-TypeScript port of Silero VAD v5 scores P(speech) every 32 ms on the CPU and drives speech onset, silence tracking, and the phantom-turn guard: keyboard noise and ambient swells never even latch an utterance, near-silence never reaches Whisper (so no hallucinated "thank you"s), and quiet speech still passes. A repetition-degeneracy gate drops decoder loops on top.
  • Eye (vision): enabled by default for webcam context, with a pre-load toggle to skip its 42 MB model, camera access, and GPU residency. D-FINE runs low-priority object detection (it yields the GPU to audio), smooths the person count, and answers "what do you see?" / "how many people?" / "tell me about the person" directly from the measurements. Proactive interjections (stepped away, phone spotted, slouching) are best-effort rule heuristics. The webcam shows as a corner PiP with detection boxes.
  • Typed conversation memory: bounded facts the user explicitly states (name, trip, pet, favorite, plans, relationships) are retained and injected only when relevant; exact recall bypasses small-model guessing.
  • Two-tier tools: factual asks are delegated so the small on-device model isn't left guessing: weather ("what's the weather in Tokyo" → open-meteo, in °F/mph), facts ("who is Ada Lovelace" → Wikipedia), plus instant offline calculator and clock/date. Web lookups speak a holding line and fetch in the background, then answer on the next silence and render a card; the card clears when the conversation moves on, and the spoken answer can be interrupted like any reply.

Performance (all on jax-js / WebGPU)

The turn-latency floor is set by the single GPU, so the work went into cutting GPU cost per token/frame rather than overlapping stages (which a single device can't do; see docs/BENCHMARKS.md for the full map-reduce campaign log, including the negative results):

  • Fused decode: the LLM decode step is fused from dozens of per-layer jit dispatches into one, and Pocket TTS from ~11 into two, cutting the command-buffer submit overhead that dominated per-step cost (~22% each).
  • GPU top-k sampling: the LLM samples from a device-side top-64 (one small readback) instead of transferring the full vocab logits every token, folded into the fused step's single dispatch.
  • Stable prefill shapes: every turn has a different prompt length, which otherwise forces jax-js to compile new traces mid-conversation. Prompts use 256-token buckets, the common buckets are warmed during loading, and the KV cache has one fixed capacity, so long conversations never hit a multi-second recompile stall mid-turn.
  • Faster confidence-aware ASR: timestamp-gate candidate reductions are reused for confidence scoring instead of scanning the vocabulary again. ASR runs 5–7% faster while preserving all 21 paired clean/quiet/distorted transcripts; low-confidence failures request a repeat before invoking the LLM.
  • Bounded history window: the prompt is capped at 8 messages so it never leaves the warm prefill buckets: first-token latency stays flat (~500–610 ms) across long sessions instead of doubling around turn 6, and the worst turn of a 12-turn session sits within ~150 ms of the median.
  • Deterministic memory fast paths: exact recall and bounded trip, pet, and activity follow-ups can answer in a few milliseconds without model generation.
  • Smaller download: Whisper ships a per-row int8 build (73 MB instead of 144 MB), while the TTS checkpoint omits 35 MB of audio-encoder weights never used for synthesis. The SmolLM2 brain stays full fp16 for conversation quality.

Runtime behaviour is tunable at src/tunables.ts (read live, so A/B experiments don't need a rebuild).

Hard-won details

  • The assistant used to interrupt itself. The barge-in threshold calibrated its echo floor during the reply's first ticks, which fall in the silent LLM/TTS latency gap, so the floor was ambient-level and the assistant's own playback echo tripped it. Calibration now only counts ticks where the TTS output analyser confirms audio is actually playing.
  • Whisper invents "Thank you." from silence. Every Whisper size does this (silent-outro captions in its training data), and it decodes with high confidence, so no model swap or confidence gate catches it. The fix is signal-side: unvoiced audio never reaches the decoder, on every endpoint path, including barge-in continuations, which originally skipped the guard.
  • A mid-sentence TTS cut sounds like a full stop. Each chunk is synthesized as a complete utterance, so flushing at a bare word boundary produces sentence-final falling intonation plus a pause in the middle of your sentence. Chunks now only split at real punctuation: a measured latency cost, paid for prosody.

Run it

npm install
npm run dev

Open http://localhost:5173 in a WebGPU-capable browser (Chrome/Edge on desktop, Safari 26+). Click Load models: ~1.0 GB on first run, cached in OPFS afterwards. The Eye is on by default and requests camera access; uncheck it before loading to skip it. Then press the orb once and just talk, hands-free. Press it again to end.

Weights come from Hugging Face: SmolLM2 at full fp16 for conversation quality, Whisper as a per-row int8 build dequantized at load (it transcribes the bench suite identically to fp16).

How it works

The pipeline stages, from microphone to speaker:

PathWhat's there
src/mic.ts16 kHz PCM capture via an AudioWorklet.
src/asr/Whisper encoder/decoder and features. streaming.ts transcribes live (LocalAgreement-2: words lock in once two passes agree) and filters out the assistant's own voice. vad.ts is the Silero VAD v5 port.
src/llm/smollm.tsSmolLM2-360M forward pass with a KV cache: one fused GPU dispatch per token, bucket-padded prefill so jit traces are reused across turns.
src/memory.tsExtraction and deterministic recall of facts the user shared.
src/tts/Pocket TTS flow-matching LM + the Mimi codec on jax-js, streamed to an AudioContext player. src/sentence-split.ts chunks reply text at clause boundaries for synthesis.
src/vision/D-FINE detector on @jax-js/onnx, webcam VisionSession, COCO labels, box-dedupe and person-count smoothing.
src/tools/tools.tsKeyless intent detection → weather / Wikipedia / calc / clock.

License

MIT. Model inference code is adapted from the jax-js repository by Eric Zhang (MIT licensed); model weights remain under their respective licenses.

Contributors

sachinkesiraju/jax-realtime

Full‑duplex realtime voice assistant in your browser with jax‑js

9

stars

34

commits

TypeScript

primary language

Aug 19, 2026

updated

jax-realtime.netlify.app

README

jax-realtime

A real-time, full-duplex voice assistant that runs entirely in your browser on WebGPU, built with jax-js.

Every stage (speech → ASR → LLM → TTS → speech, plus optional vision) runs locally in the tab; nothing is sent to a server.

jax-realtime: the idle orb, ready to talk

It's inspired by the Thinking Machines interaction model and GPT-Live, rebuilt as a small-model cascade that fits in a browser tab. The goal is a conversation that feels live: you can interrupt it mid-sentence, pause mid-thought without losing your turn, and it keeps searching in the background while you talk.

StageModelRuns on
Ear (ASR)Whisper base.en (int8, dequantized to fp16)WebGPU via jax-js
Turn-taking (VAD)Silero VAD v5, ported to TypeScriptCPU (~2 ms / 32 ms frame)
Brain (LLM)SmolLM2-360M-Instruct (fp16)WebGPU via jax-js
Voice (TTS)Kyutai Pocket TTS + Mimi codec (fp16)WebGPU via jax-js
Eye (vision)D-FINE small (COCO-80)WebGPU via @jax-js/onnx

Everything shares the single WebGPU device. The streaming ASR lane is paused while the assistant speaks so it doesn't contend with TTS for the GPU; barge-in is therefore energy-based (see below), and captions resume the moment the assistant stops.

Interaction

  • Full-duplex micro-turns: a ~150 ms tick loop drives a deterministic, priority-ordered policy: adaptive barge-in (talk over the assistant, including its tool narrations, and the audio cuts in ~300 ms; the threshold auto-calibrates to the echo floor of each reply), adaptive endpointing, and time-awareness timers. A watchdog force-recovers the session if a reply ever stalls, so it can't wedge.
  • Continuation-merge: if the endpoint fires on a mid-thought pause and you resume speaking before the reply's first audio, the unheard reply is aborted and both halves are answered as one turn ("append, don't restart").
  • Learned turn signal: a pure-TypeScript port of Silero VAD v5 scores P(speech) every 32 ms on the CPU and drives speech onset, silence tracking, and the phantom-turn guard: keyboard noise and ambient swells never even latch an utterance, near-silence never reaches Whisper (so no hallucinated "thank you"s), and quiet speech still passes. A repetition-degeneracy gate drops decoder loops on top.
  • Eye (vision): enabled by default for webcam context, with a pre-load toggle to skip its 42 MB model, camera access, and GPU residency. D-FINE runs low-priority object detection (it yields the GPU to audio), smooths the person count, and answers "what do you see?" / "how many people?" / "tell me about the person" directly from the measurements. Proactive interjections (stepped away, phone spotted, slouching) are best-effort rule heuristics. The webcam shows as a corner PiP with detection boxes.
  • Typed conversation memory: bounded facts the user explicitly states (name, trip, pet, favorite, plans, relationships) are retained and injected only when relevant; exact recall bypasses small-model guessing.
  • Two-tier tools: factual asks are delegated so the small on-device model isn't left guessing: weather ("what's the weather in Tokyo" → open-meteo, in °F/mph), facts ("who is Ada Lovelace" → Wikipedia), plus instant offline calculator and clock/date. Web lookups speak a holding line and fetch in the background, then answer on the next silence and render a card; the card clears when the conversation moves on, and the spoken answer can be interrupted like any reply.

Performance (all on jax-js / WebGPU)

The turn-latency floor is set by the single GPU, so the work went into cutting GPU cost per token/frame rather than overlapping stages (which a single device can't do; see docs/BENCHMARKS.md for the full map-reduce campaign log, including the negative results):

  • Fused decode: the LLM decode step is fused from dozens of per-layer jit dispatches into one, and Pocket TTS from ~11 into two, cutting the command-buffer submit overhead that dominated per-step cost (~22% each).
  • GPU top-k sampling: the LLM samples from a device-side top-64 (one small readback) instead of transferring the full vocab logits every token, folded into the fused step's single dispatch.
  • Stable prefill shapes: every turn has a different prompt length, which otherwise forces jax-js to compile new traces mid-conversation. Prompts use 256-token buckets, the common buckets are warmed during loading, and the KV cache has one fixed capacity, so long conversations never hit a multi-second recompile stall mid-turn.
  • Faster confidence-aware ASR: timestamp-gate candidate reductions are reused for confidence scoring instead of scanning the vocabulary again. ASR runs 5–7% faster while preserving all 21 paired clean/quiet/distorted transcripts; low-confidence failures request a repeat before invoking the LLM.
  • Bounded history window: the prompt is capped at 8 messages so it never leaves the warm prefill buckets: first-token latency stays flat (~500–610 ms) across long sessions instead of doubling around turn 6, and the worst turn of a 12-turn session sits within ~150 ms of the median.
  • Deterministic memory fast paths: exact recall and bounded trip, pet, and activity follow-ups can answer in a few milliseconds without model generation.
  • Smaller download: Whisper ships a per-row int8 build (73 MB instead of 144 MB), while the TTS checkpoint omits 35 MB of audio-encoder weights never used for synthesis. The SmolLM2 brain stays full fp16 for conversation quality.

Runtime behaviour is tunable at src/tunables.ts (read live, so A/B experiments don't need a rebuild).

Hard-won details

  • The assistant used to interrupt itself. The barge-in threshold calibrated its echo floor during the reply's first ticks, which fall in the silent LLM/TTS latency gap, so the floor was ambient-level and the assistant's own playback echo tripped it. Calibration now only counts ticks where the TTS output analyser confirms audio is actually playing.
  • Whisper invents "Thank you." from silence. Every Whisper size does this (silent-outro captions in its training data), and it decodes with high confidence, so no model swap or confidence gate catches it. The fix is signal-side: unvoiced audio never reaches the decoder, on every endpoint path, including barge-in continuations, which originally skipped the guard.
  • A mid-sentence TTS cut sounds like a full stop. Each chunk is synthesized as a complete utterance, so flushing at a bare word boundary produces sentence-final falling intonation plus a pause in the middle of your sentence. Chunks now only split at real punctuation: a measured latency cost, paid for prosody.

Run it

npm install
npm run dev

Open http://localhost:5173 in a WebGPU-capable browser (Chrome/Edge on desktop, Safari 26+). Click Load models: ~1.0 GB on first run, cached in OPFS afterwards. The Eye is on by default and requests camera access; uncheck it before loading to skip it. Then press the orb once and just talk, hands-free. Press it again to end.

Weights come from Hugging Face: SmolLM2 at full fp16 for conversation quality, Whisper as a per-row int8 build dequantized at load (it transcribes the bench suite identically to fp16).

How it works

The pipeline stages, from microphone to speaker:

PathWhat's there
src/mic.ts16 kHz PCM capture via an AudioWorklet.
src/asr/Whisper encoder/decoder and features. streaming.ts transcribes live (LocalAgreement-2: words lock in once two passes agree) and filters out the assistant's own voice. vad.ts is the Silero VAD v5 port.
src/llm/smollm.tsSmolLM2-360M forward pass with a KV cache: one fused GPU dispatch per token, bucket-padded prefill so jit traces are reused across turns.
src/memory.tsExtraction and deterministic recall of facts the user shared.
src/tts/Pocket TTS flow-matching LM + the Mimi codec on jax-js, streamed to an AudioContext player. src/sentence-split.ts chunks reply text at clause boundaries for synthesis.
src/vision/D-FINE detector on @jax-js/onnx, webcam VisionSession, COCO labels, box-dedupe and person-count smoothing.
src/tools/tools.tsKeyless intent detection → weather / Wikipedia / calc / clock.

License

MIT. Model inference code is adapted from the jax-js repository by Eric Zhang (MIT licensed); model weights remain under their respective licenses.

Contributors

Languages

TypeScript

96.6%

CSS

3.3%