Bring your own voice
What this is for
Keep your own TTS or speech-to-speech provider. Stream or play audio into
experience.speech; Liforma owns playback timing and avatar mouth animation.
Two speech paths
| API | Capability | Audio source |
|---|---|---|
speech.speak | textSpeech | Liforma TTS |
speech.play / createUtterance | externalSpeechAudio | Your PCM, encoded bytes, or CORS-open URL |
Unsupported operations throw UnsupportedSpeechOperationError immediately. Mint the
session with externalSpeechAudio (and usually speechAnimation) when the
host owns the voice.
One-shot PCM
await experience.speech.play({
audio: {
data: pcmS16leBytes,
format: { encoding: 'pcm_s16le', sampleRate: 24_000, channels: 1 }
},
// Optional but recommended when you have the spoken text — usually improves lipsync.
transcript: agentReplyText,
queue: 'append' // append | replace-active | replace-all
});Encoded file or CDN URL
Decode happens in the player (Web Audio). Api/models never fetch or transcode
arbitrary media URLs. URL playback requires CORS that allows
player.liforma.ai.
// Encoded bytes — decoded in the player (not on api.liforma.ai)
await experience.speech.play({
audio: { data: mp3Bytes, encoding: 'audio/mpeg' },
queue: 'append'
});
// Or a CORS-open URL fetched + decoded inside the player iframe
await experience.speech.play({
audio: { url: 'https://cdn.example.com/line.mp3' },
queue: 'append'
});Live streaming
Use createUtterance when your vendor emits many PCM chunks per agent turn. Each
provider guide maps real vendor events onto write / close /
interrupt — start with
ElevenLabs,
OpenAI,
Google,
Deepgram, or
LiveKit.
Lipsync quality floors
| Path | Audio | Transcript | Mouth quality |
|---|---|---|---|
PCM helpers (createUtterance) | Correct sample rate locked before write | Yes (force-align) | Highest — prefer this when the vendor exposes text |
| PCM without transcript | Correct sample rate | No | Good — free CTC / energy; less precise phone timing |
Track helpers (createUtterance({ track })) | Sample-rate hint (e.g. LiveKit 48 kHz) | Yes when available (e.g. LiveKit lk.transcription) | High with transcript; baseline free CTC without — prefer
|
Lipsync behaviour
Audio starts after a short jitter buffer (~80–250 ms adaptive from chunk arrival variance; see
BYO_PLAYBACK_GUIDANCEon@liforma/client/byo) — it never waits on cloud speech-animation.- Mouth starts with an energy fallback, then crossfades to tracked phone/JALI results.
Tracked animation uses limited-context HTTP windows to
POST /v1/speech-animation/windows(player-internal).When the spoken text is available, pass it as
transcriptonspeech.play,createUtterance,setTranscript, and/orclose. Audio alone works; including the text usually improves mouth timing and shapes.Custom bridges should chunk PCM around 20–200 ms (
BYO_PLAYBACK_GUIDANCE.RECOMMENDED_CHUNK_MS_*) and never exceedMAX_PCM_CHUNK_BYTES(64 KiB) per write.
Mic ownership
PCM helpers that capture audio (openai, deepgram,
google) share the same options: captureMic (default
true) and optional mediaStream. Keep Liforma
speechInputMode=“off” so only one stack owns the mic. On unexpected
disconnect, helpers cancel the active utterance so the player does not keep an orphaned
turn.
Provider guides
ElevenLabs Agents —
connectElevenLabsAgent(examples: copyhelloByo.ts)OpenAI —
connectOpenAiRealtime/ WebRTC / classic TTS (examples:helloByo.ts)Google —
connectGeminiLive/ Cloud TTS (examples:helloByo.ts)Deepgram —
connectDeepgramAgent(examples:helloByo.ts)LiveKit —
connectLiveKitAgent/createUtterance({ track })+ transcript (examples:helloByo.ts)Other / files — generic turn map, URLs, encoded bytes
- Migrate from ElevenLabs Conversational AI