Skip to main content
The pipecat-facemode package is a small Pipecat FrameProcessor that taps TTSAudioRawFrame, sends the PCM audio to FaceMode over the canonical WebSocket protocol, and converts the avatar video and audio tracks in your LiveKit room back into Pipecat output frames. It does not replace a Pipecat transport.

Pipeline placement

Place FaceModeVideoService after your TTS service and before the output transport:
Do not put another audio output before this service. By default the processor consumes TTSAudioRawFrame so the source TTS audio and the avatar’s returned audio are not both played.

Install

The package supports Pipecat 1.7 through the current 1.x line, LiveKit RTC 1.x, aiohttp 3.x, websockets 14 through 16, and NumPy 2.x. The websockets>=14 requirement matters: the service uses the additional_headers handshake API and never falls back to an unaffinitized connection.

Quickstart

This example uses Pipecat’s LiveKit transport. Because FaceMode publishes the avatar video and audio tracks directly into the room, the transport’s own audio and video outputs are disabled.

Constructor options

FaceModeService and FaceModeProcessor are exported aliases for FaceModeVideoService. Useful members: service.session_id, service.is_started, and service.avatar_ready.

How audio flows

  • Each TTSAudioRawFrame is sent to FaceMode as binary little-endian signed 16-bit PCM, with no JSON or base64 wrapper. start_utterance precedes the first chunk of an utterance.
  • end_utterance is sent on a TTS stop frame or after utterance_idle_timeout seconds of silence. cancel_utterance is sent on Pipecat interruption frames.
  • The avatar’s LiveKit audio track is pushed downstream as OutputAudioRawFrame; the avatar video track becomes an output image frame.
  • The negotiated sample rate must be an integer between 8000 and 48000 Hz (preferred: 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000) and channels must be 1 or 2. A mid-session format change raises FaceModeProtocolError.

Token grants

Keep all LiveKit and FaceMode credentials on the server. Two LiveKit connections are involved, and production deployments should use separate participant identities and tokens for each:
  • room.token identifies the avatar participant and must grant room_join for the exact room name, can_publish (for the avatar video and audio tracks), and can_subscribe.
  • livekit_subscriber_token identifies this Pipecat process and must grant room_join for the same room and can_subscribe. Grant publish only if the application needs it.
A token with publish and subscribe grants can be reused for both connections, but do not share a participant identity between them: a LiveKit room permits only one participant per identity. Example server-side token generation with livekit-api:
Token generation belongs on your backend only. The LiveKit API secret must never be sent to a client.

Waiting for avatar media

await service.wait_for_avatar() resolves when FaceMode reports audio_ready or the first avatar video frame arrives. It raises FaceModeNotReadyError after the timeout (30 seconds by default; pass timeout=None to wait indefinitely). Call it before announcing that avatar media is available to the user.

Automatic reconnect

If the WebSocket drops after the session is fully started, the service reconnects without a pipeline-visible failure:
  1. It calls POST /api/sessions/{id}/reconnect with the original room object (plus the persisted inputProvider when configured). Every response mints a fresh one-time wsToken; tokens are never reused.
  2. It opens a new socket with the facemode.<newToken> subprotocol, resends the same start negotiation, and waits for started before audio resumes. An explicitly open utterance is re-declared on the new socket.
  3. At most 2 attempts run with bounded backoff; recovery is single-flight. Sequence numbers keep increasing and already-sent audio is never replayed. Sends that arrive during the gap wait for the new socket instead of failing.
No reconnect is attempted on intentional shutdown (EndFrame, CancelFrame, stop, cleanup), on server-terminal states (session_ending, ended, fatal error messages), or before the session is fully started. When the budget is exhausted the failure surfaces as a FaceModeProtocolError.

Lifecycle and failure notes

  • EndFrame triggers a graceful shutdown: the service sends end_session and waits up to 3 seconds for ended before closing the socket. CancelFrame and cleanup() skip the graceful message.
  • ended is a WebSocket protocol acknowledgement, not proof that backend session cleanup is complete. Callers that need backend cleanup confirmation should poll GET /api/sessions/{id} until the session reaches a terminal state.
  • The ingestion-ready poll budget and the WebSocket open budget are each 240 seconds.
  • LiveKit tracks published before the subscriber connects are discovered at startup; later track_subscribed events are handled automatically. The local subscriber’s own track is always ignored, and remote tracks are accepted only from avatar_participant_identity.
  • Typed FaceMode exceptions never include API keys or token values.

Error types

All errors derive from FaceModeError (a RuntimeError):