WikifitaGitHub live67e8de5
projeto · claude_desktop/claude-desktop-voice-protocol

Claude Desktop Voice Protocol — WebSocket STT/TTS

Voice protocol extracted via runtime introspection (debugger spy + harness)

Baixar raw

Claude Desktop Voice Protocol — WebSocket STT/TTS

Voice protocol of Claude Desktop 1.14271.0, extracted via runtime introspection (debugger spy + harness) on 2026-06-23/24.

Actual URL

wss://claude.ai/api/ws/voice/organizations/{org_uuid}/chat_conversations/{chat_uuid}

Query Parameters

ParameterValueDescription
input_encodingopusInput codec (OPUS, not PCM!)
input_sample_rate16000Microphone sample rate
input_channels1Mono
output_formatpcm_16000PCM 16kHz for TTS
languageen-USLanguage
timezoneAmerica/Sao_PauloClient timezone
voicebutteryTTS voice profile
tts_speed1.00TTS speed
server_interrupt_enabledtrueUser can interrupt
client_aectrueAcoustic Echo Cancellation
client_platformdesktop_appIdentification

Headers

Connection: Upgrade
Sec-WebSocket-Key: ...
anthropic-client-app: com.anthropic.claudefordesktop
anthropic-client-os-platform: macOS

Response: 101 Switching Protocols, Cloudflare (CF-RAY, Server: cloudflare).

Audio

DirectionCodecFrame Size
Client → Server (input)OPUS~428 bytes
Server → Client (output)PCM 16kHz mono 16-bit~428 bytes (~22ms)

Voice Mode Flow (complete)

Captured on 2026-06-24, 3656 frames. Session: chat_conversations/c84a1375-9ca6-4045-b2dd-ecf97f0274dc.

═══ 1. Connection ═══
WebSocket created → handshake → 101 Switching Protocols

═══ 2. Initialization ═══
S → session_server_initialized
S → message_sse: conversation_ready

═══ 3. STT (Speech-to-Text) ═══
S → transcription_start
C → OPUS frames (continuous)
S → transcript_interim: "However,"
S → transcript_interim: "However, there"
S → transcript_interim: "However, darkened his smile."
C → user_input_end
S → transcription_start           ← may repeat if user keeps speaking
S → transcript_interim: "..."
C → user_input_end
S → transcript_interim: "However, darkened the smile of ranger."
C → user_input_end

═══ 4. Message commit ═══
S → message_complete: texts=['However, darkened the smile of ranger.']

═══ 5. Claude response (streaming) ═══
S → message_sse: message_start
S → message_sse: content_block_start
S → message_sse: content_block_delta ×4
S → message_sse: content_block_stop
S → message_sse: message_delta
S → message_sse: message_stop

═══ 6. TTS (Text-to-Speech) ═══
S → playback_start (server_vad_to_tts_start_ms: 3136)
S → tts_word: "I'm" pts=6465
S → [30× PCM frames 428B]
S → tts_word: " not" pts=6697
S → tts_word: " quite" pts=6860
S → tts_word: " sure" pts=7080
S → [30× PCM frames 428B]
S → tts_word: " what" pts=7266
... (interleaved: tts_word + PCM frames)
S → tts_word: " Aleffita." pts=8183
... (continues until sentence completes)
S → SpeechComplete

═══ 7. Disconnection ═══
WebSocket closed

All Events

Client → Server

TypeCaseDescription
keep_alivesnake_casePing every 4s
CloseStreamCamelCaseClose stream (Press+Hold)
transcription_startsnake_caseStarted speaking
user_input_endsnake_caseStopped speaking
tools_registersnake_caseRegister tools {"tools":[]}
(binary OPUS)Compressed audio ~428B

Server → Client

TypeCaseDescriptionPayload
session_server_initializedsnake_caseSession OK{}
message_ssesnake_caseConversation SSE{"event":{"type":"message_start",...}}
transcription_startsnake_caseTranscription start{}
transcript_interimsnake_casePartial transcription{"text":"...","utterance_seq":0}
TranscriptTextCamelCaseFinal transcription (Press+Hold){"data":"..."}
TranscriptEndpointCamelCaseTranscription end{}
user_input_endsnake_caseEcho{}
message_startsnake_caseMsg streaming start{}
message_deltasnake_caseContent chunk{"data":"..."}
message_stopsnake_caseMsg streaming end{}
message_completesnake_caseComplete msg{"data":{"content":[...]}}
playback_startsnake_caseTTS start{"server_vad_to_tts_start_ms":N,"type":"playback_start"}
tts_wordsnake_caseTTS word{"text":"word","pts_ms":N}
tts_segment_endsnake_caseTTS segment end{}
playback_endsnake_casePlayback end{}
SpeechCompleteCamelCaseAll speech completed{}
errorsnake_caseError{"data":{"error_code":"overloaded",...}}
(binary PCM)TTS audio ~428B

TTS Structure

TTS is interleaved: each tts_word is followed by ~30 PCM frames of ~428 bytes. The pts_ms (presentation timestamp) indicates the exact moment of each word.

tts_word: "I'm" pts=6465   → 30× PCM 428B
tts_word: "not" pts=6697   → more PCM frames
tts_word: "quite" pts=6860 → ...

Implementation Notes

  1. Input is OPUS: Requires OPUS codec (libopus). MiMo ASR accepts WAV/MP3/FLAC — gateway needs to decode OPUS → WAV
  2. TTS is PCM: MiMo TTS returns MP3 — gateway needs to convert MP3 → PCM 16kHz
  3. Per-conversation: URL contains organizations/{org} and chat_conversations/{chat} — state is per conversation
  4. Streaming required: transcript_interim during speech, not just at the end
  5. Cloudflare: Traffic passes through CF — may require specific headers
  6. keep_alive: snake_case, every 4s
  7. CloseStream: CamelCase (Press+Hold)
  8. Mixed casing: Legacy events in CamelCase, new ones in snake_case
  9. server_interrupt_enabled: Client can interrupt TTS with new speech
  10. client_aec: Echo cancellation active — audio quality matters

Captures

FileFramesModeTranscription
voice-session-2026-06-24.json3656Voice Mode + TTS"However, darkened the smile of ranger."
voice-captures.json (23/06 15:33)95STT Press+Hold"Melifon Cinco Mata Muntaja in Chile"
voice-captures.json (23/06 13:56)554Voice Modeerror: overloaded

References

  • ~/.bezetacil/scripts/001-debugger-spy.js — Spy v4 (captures WS metadata + frames)
  • ~/.claude/wikifita/bezetacil-toolset.md — ASAR pipeline + harness
  • ~/.claude/infra/bezetacil/ — Toolset scripts