pipecat-facemode
FaceMode avatar output for Pipecat pipelines. The integration taps
TTSAudioRawFrame, sends canonical 16-bit PCM to FaceMode, and converts the
avatar's LiveKit audio and video tracks back into Pipecat output frames.
The package is intentionally a small FrameProcessor. It does not replace a
Pipecat transport. Place it after a TTS service and before the output transport:
transport input -> STT -> LLM -> TTS -> FaceModeVideoService -> transport output
By default, source TTSAudioRawFrame objects are consumed. This prevents the
source TTS audio and the avatar's returned audio from being played twice. Set
forward_tts_audio=True only when the pipeline intentionally needs both.
Install
pip install pipecat-facemode
From this directory:
pip 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.
Basic usage
room.token is the server-side worker token sent to FaceMode. The separate
livekit_subscriber_token is used by this Pipecat process to subscribe to the
room. Use separate participant identities and tokens in production.
import os
from pipecat.pipeline.pipeline import Pipeline
from pipecat.services.cartesia.tts import CartesiaTTSService
from pipecat_facemode import FaceModeVideoService
tts = CartesiaTTSService(
api_key=os.environ["CARTESIA_API_KEY"],
settings=CartesiaTTSService.Settings(
voice="71a7ad14-091c-4e8e-a314-022ece01c121",
),
)
facemode = FaceModeVideoService(
api_key=os.environ["FACEMODE_API_KEY"],
avatar_id=os.environ.get("FACEMODE_AVATAR_ID", ""),
room={
"type": "livekit",
"url": os.environ["LIVEKIT_URL"],
"token": os.environ["LIVEKIT_WORKER_TOKEN"],
},
livekit_subscriber_token=os.environ["LIVEKIT_SUBSCRIBER_TOKEN"],
room_name=os.environ["LIVEKIT_ROOM_NAME"],
# Optional speech input provider persisted by the backend for the session:
# deepgram, gemini, gnani, elevenlabs, openai, cartesia, sarvam, or custom.
input_provider=os.environ.get("FACEMODE_INPUT_PROVIDER") or None,
)
pipeline = Pipeline([
transport.input(),
tts,
facemode,
transport.output(),
])
See examples/basic_pipeline.py for a complete
pipeline using Pipecat's LiveKit transport. The same-room example disables the
transport's audio and video outputs because the FaceMode worker already publishes
both avatar tracks directly into that LiveKit room. It also uses distinct tokens
for the FaceMode worker, Pipecat subscriber, Pipecat transport, and viewer.
FaceMode session and WebSocket protocol
On StartFrame, the service:
-
Connects to the supplied LiveKit room with
livekit-rtcand enables automatic track subscription. -
Calls
POST {api_url}/sessionswith the room object (andinputProviderwheninput_provideris configured). The request never sendswaitForIngestion; current backends reject it as an unknown field:{ "avatarId": "avatar-id", "inputProvider": "deepgram", "room": { "type": "livekit", "url": "wss://project.livekit.cloud", "token": "server-side-token" } }
A
201response returnsingestion.ready: truewith the worker WebSocketurland a one-timewsTokenimmediately. -
When the response has
ingestion.ready: false, pollsGET {api_url}/sessions/{id}using bounded backoff for up to 240 seconds until ready WebSocket credentials are available;FAILEDorENDEDworker states stop the wait immediately. Room credentials from the initial response remain in memory and are not expected in the status response. -
Opens the returned WebSocket with the
facemode.<ws-token>subprotocol (theaivatar.prefix is rejected by current backends), compression disabled, native keepalives disabled, a 240-second open timeout, and optional backend-providedingestion.headersforwarded unchanged. This package requireswebsockets>=14, so it uses theadditional_headersAPI and never falls back to an unaffinitized connection. -
Starts the receive task, sends canonical
startfrom the pipelineStartFrame, and waits for a validatedstartedresponse. -
Starts application keepalives only after negotiation succeeds, preserving the protocol requirement that
startis the first application message.
Call await service.wait_for_avatar() when an application needs to wait for
FaceMode's audio_ready event or the first LiveKit video frame before announcing
that avatar media is available.
The canonical protocol messages are used as follows:
startnegotiatesaudio_encoding: pcm_s16le, sample rate, and channel count.start_utteranceprecedes the first binary audio chunk.TTSAudioRawFrame.audiois sent as binary little-endian signed 16-bit PCM, with no JSON or base64 wrapper.end_utterancefollows a TTS stop frame or a short idle period.cancel_utteranceis sent forStartInterruptionFrame.pingis sent periodically while the session is alive.end_sessionis sent during gracefulEndFrameor pipeline cleanup.
Automatic reconnect
If the canonical WebSocket drops after a session was fully started, the service reconnects without caller-visible pipeline failure:
- It calls
POST {api_url}/sessions/{id}/reconnectwith the originalroomobject (plus the persistedinputProviderwhen configured). Every response mints a fresh one-timewsToken; tokens are never reused. - It opens a new socket with the
facemode.<new-token>subprotocol, resends the samestartnegotiation, and waits forstartedbefore audio resumes. - At most 2 reconnect attempts run with bounded backoff. Recovery is single-flight, so simultaneous close/error notifications share one attempt.
Sequence numbers keep increasing across reconnects, an explicitly open
utterance is re-declared on the new socket, 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 reconnect budget is exhausted, the failure surfaces as a
FaceModeProtocolError that never contains token or key material.
The service calls super().__init__() and super().process_frame(...), and
forwards non-consumed frames with their original FrameDirection. Lifecycle and
interruption frames are forwarded after the FaceMode action completes. Pipecat
1.7 exposes the interruption event as InterruptionFrame; releases that expose
separate StartInterruptionFrame and StopInterruptionFrame classes are handled
without changing the public service API.
LiveKit token grants
Keep all LiveKit and FaceMode credentials on the server. Never put them in browser code or log messages.
The token in room.token must identify the FaceMode worker participant and grant
that participant:
room_join=Truefor the exactLIVEKIT_ROOM_NAME;can_publish=Trueso the avatar can publish its audio and video tracks;can_subscribe=Trueso the worker can join the room's media session; and- a stable, unique participant identity and name if your room policy requires it.
The separate livekit_subscriber_token used by FaceModeVideoService must
identify the Pipecat subscriber and grant:
room_join=Truefor the same room; andcan_subscribe=True.
Grant subscriber publish permission only if the application needs it. A token with publish and subscribe grants can be reused for both connections, but sharing a participant identity between the FaceMode worker and Pipecat subscriber is not recommended because a LiveKit room permits only one participant for an identity.
Example server-side token generation with livekit-api:
from livekit import api
worker_token = (
api.AccessToken()
.with_identity("facemode-avatar")
.with_name("FaceMode Avatar")
.with_grants(
api.VideoGrants(
room_join=True,
room=room_name,
can_publish=True,
can_subscribe=True,
)
)
.to_jwt()
)
subscriber_token = (
api.AccessToken()
.with_identity("pipecat-facemode-subscriber")
.with_name("Pipecat FaceMode Subscriber")
.with_grants(
api.VideoGrants(
room_join=True,
room=room_name,
can_subscribe=True,
)
)
.to_jwt()
)
The sample token code is for a backend only. The LiveKit API secret must never be sent to a client.
Video frame compatibility
Pipecat 1.7 uses OutputImageRawFrame for output video. The package also checks
for OutputVideoRawFrame and falls back to ImageRawFrame, so importing the
package remains safe across Pipecat releases that use different output video
class names. LiveKit video is requested as RGB24 when the SDK exposes that
format, then copied into a NumPy-backed RGB or RGBA image frame.
Lifecycle and failure behavior
- REST and WebSocket failures raise typed FaceMode exceptions without including
API keys or token values in log messages;
facemode.credential prefixes are redacted alongsideBearermaterial. - The ingestion-ready poll budget and the WebSocket open/handshake budget are
each 240 seconds. The
start/startedacknowledgement usesprotocol_timeout(default 15 seconds). - WebSocket receive, keepalive, audio, and video tasks are cancelled and awaited during
EndFrame,cleanup, or a failed startup. Graceful shutdown sendsend_sessionand waits up to three seconds for canonicalendedbefore closing the socket. endedis a WebSocket protocol acknowledgement, not proof that backend worker cleanup, provider cancellation, or billing finalization is complete. Callers that need backend cleanup confirmation poll the public session endpoint until it becomes terminal.- LiveKit tracks published before the connection are discovered after startup;
later
track_subscribedevents are handled automatically. avatar_participant_identitydefaults tofacemode-avatarto avoid forwarding other remote participants. Set it to the identity in the worker room token when that token uses a different identity. The local subscriber track is always ignored.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pipecat_facemode-0.1.0.tar.gz.
File metadata
- Download URL: pipecat_facemode-0.1.0.tar.gz
- Upload date:
- Size: 30.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9e5d34702ddf70eb5bc91fe1e7bfc48dc07b46be39af330e360bd75b7a47c980
|
|
| MD5 |
19145109194594c38946041c42a1530c
|
|
| BLAKE2b-256 |
9446b39faf8000e94b0e7211b352076da1ac3dec8b9d7f13914a8b86d5f70b78
|
File details
Details for the file pipecat_facemode-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pipecat_facemode-0.1.0-py3-none-any.whl
- Upload date:
- Size: 22.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37cf4a1c187d20033bc7d29ab28302ac8d3c08c592d0c3b45cce5c0a323703c4
|
|
| MD5 |
648069b45b2b39713b84d8eb3abe65b4
|
|
| BLAKE2b-256 |
b1f7a7eb31a288ba5407aa5a1062c70bb9a328635313758a47f7bc5817751566
|