Realtime voice for Claude Code: a LiveKit room you join from the browser, speech-to-text (Underdog Bark 3.0 Clean), and text-to-speech (Paradee). Runs as an MCP server through npx -y github:AnEntrypoint/ccv.
Add the server to Claude Code (.mcp.json in the project, or claude mcp add ccv -- npx -y github:AnEntrypoint/ccv).
Start Claude Code with the channel flag, so speech arrives in the session on its own:
npx -y --package github:AnEntrypoint/ccv ccv-claude
That runs claude --dangerously-load-development-channels server:ccv, passing through any arguments. It exists because the name after server: is matched exactly and a stray character in a hand-typed flag silently matches no server; the equivalent by hand is:
claude --dangerously-load-development-channels server:ccv
The name after server: must match the MCP server name exactly (ccv). A stray trailing character — including the carriage return a paste can carry — silently matches no server, the flag is ignored, and no speech is ever delivered. Custom channels are not on the approved allowlist, so --channels alone is not enough and the startup confirmation dialog has to be accepted. Nothing in the voice stack depends on this flag: a session that receives nothing can call voice_listen, which blocks until the user speaks.
Then from the session:
voice_setup: installs LiveKit and downloads the models in the background (about 1 GB once).voice_start: starts LiveKit (dev mode) and the voice host. Open the printed page (http://127.0.0.1:8800/) and click join.voice_heardlists what was said;voice_listenwaits for the next thing said;voice_speakreplies. Long replies stream sentence by sentence, andvoice_interruptcuts off the one playing.voice_statusshows setup progress and recent logs;voice_stopshuts the stack down.
Every utterance is also appended to <CCV_HOME>/heard.jsonl as one JSON object per line
(at, from, text). Clients that do not deliver MCP channel notifications can follow that file instead.
When a spoken reply finishes playing, the server sends the same channel notification with event: done.
Talk over a reply, or press Interrupt on the page, and it stops. The page decides this locally: it measures the microphone and the reply it is playing on the same audio clock, silences playback the moment the user wins, and posts /interrupt to the host, which drops the queued audio and returns the in-flight voice_speak early. Deciding it in the browser is what makes it reliable — the mic there has already been echo-cancelled by the browser, and the decision needs no network round trip. The gate is measured, not assumed: the first 350 ms of each reply set the level the mic hears while the user is silent (near silence on a headset, closer to our own level on a speaker), and the user has to beat that by 9 dB.
The host keeps its own energy-based barge-in as a fallback for clients that cannot decide locally. It is deliberately hard to trip, because a microphone that hears the speaker would otherwise cancel every reply the moment it starts. A cancelled voice_speak call also stops its own audio.
.env in the current directory is read at start. Process environment variables win over it.
| Variable | Default | Purpose |
|---|---|---|
HF_TOKEN |
none | Faster Hugging Face downloads (sent as an Authorization header) |
LIVEKIT_URL |
started locally | Use an existing LiveKit server instead of starting one |
LIVEKIT_API_KEY / LIVEKIT_API_SECRET |
devkey / secret |
Keys for the room |
LK_ROOM |
lk-voice |
Room name |
CCV_PORT |
8800 |
Voice page and host port (127.0.0.1) |
CCV_HOME |
./.ccv |
Where binaries and models live |
END_SILENCE_MS |
800 |
Quiet time that ends an utterance (debounce only, nothing is force-sent) |
MIN_SPEECH_MS |
250 |
Shorter bursts (clicks, knocks) are dropped |
BARGE_DBFS / BARGE_MS |
-30 / 250 |
Host fallback barge-in: level and duration that interrupt playback |
ECHO_MARGIN_DB |
8 |
Host fallback barge-in: dB the mic must beat our own playback by, so a mic that hears the speaker cannot cancel every reply |
- LiveKit: Linux amd64/arm64 and Windows amd64/arm64 use the release archive, checked against its SHA-256 checksums. macOS uses
brew install livekit, with Docker (livekit/livekit-server) as a fallback. - Speech runs in Node with onnxruntime-node and an espeak-ng WebAssembly build. No Python and no native build step.
- STT:
velddev/Underdog-Bark-3.0-Clean-sherpa-onnx(int8 encoder, decoder, joiner, tokens). - TTS:
sahilmahendrakar/Paradee-8M-v1.1(onnx/paradee_int8.onnx,config.json).
Verified locally: models download with resume and checksum, LiveKit installs and passes its checksum, MCP tools list and start/stop, the host joins the room and serves the page, and a live conversation across two browser clients — long-form speech, sentence-by-sentence streaming, and barge-in interrupting a reply. Channel delivery into the session is the client's to grant: the flag above, the accepted dialog, and a session that runs long enough to register the handler.