Skip to content

ElevenLabs

contrib ยท v0.1.0

ElevenLabs behind the voice contracts: realtime speech-to-text (Scribe) and streaming text-to-speech. Install both, or one part with --only stt or --only tts. The API key stays on the server.

Install copit add @chanx-kit/elevenlabs
Import from elevenlabs
Requires kits audio-stream-in, audio-stream-out
Parts stt (transcriber.py), tts (synthesizer.py): all by default, or copit add @chanx-kit/elevenlabs --only <part>
Contract transcriber@1
UI kits transcriber
Contract synthesizer@1
UI kits player
Python packages chanx>=2.10.2,<3, websockets>=13
Keywords audio, speech-to-text, text-to-speech, elevenlabs, provider
Source kits/elevenlabs

ElevenLabs: realtime speech-to-text (Scribe) and streaming text-to-speech, each behind its contract, so any transcriber or player UI works with it and switching provider is one class.

copit add @chanx-kit/elevenlabs                 # both parts
copit add @chanx-kit/elevenlabs --only stt      # speech-to-text only
copit add @chanx-kit/elevenlabs --only tts      # text-to-speech only
Part Module Contract Needs
stt transcriber.py transcriber@1 audio-stream-in
tts synthesizer.py synthesizer@1 audio-stream-out

Leaving a part out also leaves out the base kit only it needs. Import from the part's module (from .ws_kits.elevenlabs.transcriber import ...), since the other may not be there. Picking parts needs copit 0.9; older copit installs both.

Set ELEVENLABS_API_KEY in the server's environment; the browser never sees it.

Speech to text

Live speech-to-text with ElevenLabs Scribe realtime behind the transcriber contract: any transcriber UI works with it, and switching from Deepgram is one class.

from .ws_kits.elevenlabs.transcriber import ElevenLabsTranscriberTopic

class MicTopic(ElevenLabsTranscriberTopic):
    options = {"vad_silence_threshold_secs": "0.8"}

Set ELEVENLABS_API_KEY on the server. Audio goes to Scribe as base64 PCM at the negotiated rate (pcm_24000 by default); commit_strategy=vad lets Scribe end utterances on silence.

Scribe transcriber@1
first partial_transcript of an utterance speech_started
partial_transcript transcript_partial
committed_transcript transcript_final, then utterance_end
empty chunk with commit on audio_end last words flushed, then the stream closes

Scribe sends no speech-started event, so the adapter reports speech starting at an utterance's first partial transcript. That is a fraction of a second later than a dedicated voice activity event, which is enough for the voice agent's barge-in.

Verified against the live service (2026-09-27, free tier): partials growing into one committed transcript, with speech_started from the first partial.

Text to speech

Streaming text-to-speech with ElevenLabs behind the synthesizer contract, so any player UI works with it and you can switch providers by changing one class.

from .ws_kits.elevenlabs.synthesizer import ElevenLabsSynthesizerTopic

class ReplyVoiceTopic(ElevenLabsSynthesizerTopic):
    default_voice = "EXAVITQu4vr4xnSDxMaL"   # Sarah, premade
    model_id = "eleven_flash_v2_5"

await ReplyVoiceTopic.speak(session_id, "Hello from the server.")

Set ELEVENLABS_API_KEY in the server's environment; the browser never sees it. Audio is requested as raw PCM (pcm_24000 by default, matching output_format), so nothing is decoded on the way: ElevenLabs' base64 audio becomes bytes, re-cut into whole frames and streamed to every listener. A client may pick a voice per speak.

The adapter speaks the stream-input WebSocket with websockets directly, no SDK: an opening message with voice_settings, the text with flush, then empty text to end, reading audio until isFinal.

Verified against the live service (2026-09-27, free tier): first audio after about 0.4 s, and it speaks into the sandbox's player and voice agent in a browser. Use a premade voice on the free tier: others answer payment_required, which the adapter reports as provider_unavailable with that hint.