voqalize-avatar
The pipecat half of voqalize/avatar — a 2-D talking head for AI voice calls that renders in the browser, not in a video track.
The widget is a state machine wearing a face: it renders a state enum, an emotion, a gaze target, interjection and hand-gesture ids, and a stream of timed viseme letters, and it decides none of them. This package is the half that decides. It reads your pipeline's frames, infers what the avatar should be doing, and pushes the result to the client as RTVI server-messages over the data channel you already have.
There is no video track, no per-minute avatar vendor, and no second media path. The face is dependency-free JavaScript on the other end — about 75 KB gzipped for the widget plus the one rig you mount.
pip install voqalize-avatar
The browser half is @voqalize/avatar.
Drop it in
AvatarProcessor goes between your TTS service and the transport's output —
the seat where it can see the audio that is about to be spoken.
from voqalize_avatar import AvatarProcessor
pipeline = Pipeline([
transport.input(),
stt,
context_aggregator.user(),
llm,
tts,
AvatarProcessor(), # <-- here
transport.output(),
context_aggregator.assistant(),
])
No arguments, no binaries to install, no environment variables — that is the
whole integration, and it needs no other application code. From
stock pipecat frames the state machine delivers IDLE, LISTENING,
THINKING, SPEAKING, TAKING_FLOOR, WAITING_FOR_USER, YIELDED,
DEGRADED and OFFLINE, plus the turn-clock anchor the client splices cues
onto and the user-speaking truth the listening engine times backchannels off.
Mouth shapes
Lipsync is the headline feature, and there is nothing to wire up: the processor
starts its viseme engine when StartFrame arrives, at the sample rate that
frame declares, and drives it from the same karaoke frames pipecat already
pushes for word-level captions.
The wheel carries its own aligner — avatarsync,
our fork of Rhubarb Lip Sync,
which emits the A–H+X mouth-shape alphabet the wire format is built on — along
with the 56 MB acoustic model it needs. That is why the wheel is ~44 MB and why
it is platform-specific. No path, no environment variable, no separate
artifact to ship into your image.
| platform | wheel |
|---|---|
| Linux x86-64 / aarch64 | manylinux_2_25 — RHEL 8+, Debian 10+, Ubuntu 18.04+ |
| macOS arm64 | macosx_11_0_arm64 — macOS 11+ |
Intel macOS is not on that list, and the reason is upstream: pipecat-ai
requires onnxruntime, which publishes no macOS x86-64 wheel, so nothing that
depends on pipecat installs there at all.
Anything else installs the sdist, which carries no binary. So does an explicit
--no-binary. Both are fine, and the two APIs differ here on purpose. The
internal one, build_viseme_engine(), is a library call and fails fast: it
raises RhubarbUnavailableError naming the paths it looked in. AvatarProcessor
is the layer that decides a missing aligner is survivable — it catches, logs
once, and runs the session state-channel only. The face still listens, thinks,
claims the floor and yields it; its mouth does not move while it speaks. Worse,
not broken.
A source checkout of this repo is found by walking up to native/avatarsync, so
the tests and the demo run against a locally built binary with no configuration
either.
The engine runs three legs and the client splices between them: a fast leg that predicts the timeline from text before the audio exists (~0.4 ms), an accurate leg that recognises phones from the rendered PCM (~15 ms), and an early-prefix leg for the first sentence, where latency is most visible.
Saying what the pipeline cannot infer
Some states need to know what your application is doing — TYPING,
SEARCHING_SCREEN, CANT_HEAR, a deliberate interjection, a hand gesture. No
amount of frame-watching infers those correctly, and a
library that guessed would nod at the wrong moment. Two seams, in order of
reach for:
Push an AvatarControlFrame from anywhere in your pipeline:
from voqalize_avatar import AvatarControlFrame, AvatarMessage, Interjection
await self.push_frame(AvatarControlFrame(message=AvatarMessage.interject(Interjection.MM_HMM)))
Hand gestures ride the same seam and are never inferred — a hand in frame is an application's decision:
from voqalize_avatar import HandGesture
await self.push_frame(AvatarControlFrame(message=AvatarMessage.gesture(HandGesture.HI)))
Or subclass AvatarStateMachine (from voqalize_avatar.state_machine) when
your application's frames are simply its own spelling of something the library already models — an LLM that runs out
of process, say, whose tool calls never appear as pipecat function-call frames:
from voqalize_avatar import AvatarProcessor
from voqalize_avatar.state_machine import AvatarStateMachine
class MyStateMachine(AvatarStateMachine):
def on_frame(self, frame):
if isinstance(frame, MyToolStartedFrame):
return self.tool_started(frame.call_id)
if isinstance(frame, MyToolResultFrame):
return self.tool_finished(frame.call_id)
return super().on_frame(frame)
class MyAvatarProcessor(AvatarProcessor):
STATE_MACHINE = MyStateMachine
STATE_MACHINE is a class attribute rather than a constructor argument
deliberately: the front door takes no arguments and must keep taking none, and a
second door that costs a class statement is not one you walk through by
accident.
tool_started / tool_finished are public for exactly this: you inherit the
call-id dedup and the parallel-call hold — a turn with three tools settles on one
THINKING instead of flickering — rather than re-implementing them
approximately.
A tool call shows THINKING, and only that. A tool_states={"search_web": ...}
map existed and was removed in 0.2 — an application that knows its tool is
searching says so in one AvatarControlFrame. See
docs/removed.md.
If your TTS pads its sentences
The one number the frame stream cannot supply. Some services append a fixed tail of silence to every sentence; those bytes are indistinguishable from a speaker pausing, so the engine cannot find them and a byte count reads as a longer sentence than was spoken. The error is cumulative — every sentence after the first starts further past where the mouth actually is.
from voqalize_avatar import AvatarProcessor
from voqalize_avatar.visemes import INTER_SENTENCE_PAD_MS # 250, for the fitted service
class MyAvatarProcessor(AvatarProcessor):
PAD_MS = INTER_SENTENCE_PAD_MS
Zero — the default — is right for most services. Measure your own once: synthesize a short sentence and look at the trailing silence.
What this package will not do
It never decides what the agent says or when. The server is the source of truth and the client only looks right while rendering it; a heuristic here that guessed at call content would be a bug, not a feature. See docs/contract-protocol.md, which is binding for both halves.
Compatibility
pipecat-ai>=1.4,<2, Python 3.12+. The floor is where
FunctionCallsStartedFrame and UserTurnInferenceCompletedFrame exist; the
test suite runs at the floor as well as at the resolved version, so "we support
1.4" is a claim something actually checks. Base pipecat only — no transport,
STT or TTS extras, because this package sits in somebody else's pipeline and
must not have an opinion about which services they chose.
License
AGPL-3.0-only. LICENSE here is a copy of the repository's, kept beside the
package because a wheel carries its own license file. Commercial licensing:
open an issue.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
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 voqalize_avatar-0.2.1.tar.gz.
File metadata
- Download URL: voqalize_avatar-0.2.1.tar.gz
- Upload date:
- Size: 371.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de4d4e3c66f3f41822c977d46d185ab5de497fec2ddb4bfc918301881b37fda5
|
|
| MD5 |
92b4ad5ff807972664a192e9fa080197
|
|
| BLAKE2b-256 |
96b762320704f71f4c4dd252968181006fc2b83b20ee5734a6028d91fccfab51
|
Provenance
The following attestation bundles were made for voqalize_avatar-0.2.1.tar.gz:
Publisher:
release.yml on voqalize/avatar
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
voqalize_avatar-0.2.1.tar.gz -
Subject digest:
de4d4e3c66f3f41822c977d46d185ab5de497fec2ddb4bfc918301881b37fda5 - Sigstore transparency entry: 2380221905
- Sigstore integration time:
-
Permalink:
voqalize/avatar@07377492007e38625ee750514644506f4b9a7414 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/voqalize
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@07377492007e38625ee750514644506f4b9a7414 -
Trigger Event:
push
-
Statement type:
File details
Details for the file voqalize_avatar-0.2.1-py3-none-manylinux_2_25_x86_64.whl.
File metadata
- Download URL: voqalize_avatar-0.2.1-py3-none-manylinux_2_25_x86_64.whl
- Upload date:
- Size: 47.2 MB
- Tags: Python 3, manylinux: glibc 2.25+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f5a9d7dab47d01171eab3b4d3f477c414fcfb95da55f19734d46998570e9035c
|
|
| MD5 |
6ec99437fbb4927c9104c02c8e6f1050
|
|
| BLAKE2b-256 |
e937741e58231956cabef7364f264eb7ab4529e9607ce83f0e88d8fb6236005c
|
Provenance
The following attestation bundles were made for voqalize_avatar-0.2.1-py3-none-manylinux_2_25_x86_64.whl:
Publisher:
release.yml on voqalize/avatar
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
voqalize_avatar-0.2.1-py3-none-manylinux_2_25_x86_64.whl -
Subject digest:
f5a9d7dab47d01171eab3b4d3f477c414fcfb95da55f19734d46998570e9035c - Sigstore transparency entry: 2380222053
- Sigstore integration time:
-
Permalink:
voqalize/avatar@07377492007e38625ee750514644506f4b9a7414 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/voqalize
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@07377492007e38625ee750514644506f4b9a7414 -
Trigger Event:
push
-
Statement type:
File details
Details for the file voqalize_avatar-0.2.1-py3-none-manylinux_2_25_aarch64.whl.
File metadata
- Download URL: voqalize_avatar-0.2.1-py3-none-manylinux_2_25_aarch64.whl
- Upload date:
- Size: 47.2 MB
- Tags: Python 3, manylinux: glibc 2.25+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46f533238b950ceef3fd12ef6add92a731b243d4f31c7b203ed62ce1d38e05c9
|
|
| MD5 |
8301fa6b10bfba42c14c94b142a64a2c
|
|
| BLAKE2b-256 |
710ada6be1a876929ef28c86d51448b6fbf17f8e6b91deaba72199182d6af36d
|
Provenance
The following attestation bundles were made for voqalize_avatar-0.2.1-py3-none-manylinux_2_25_aarch64.whl:
Publisher:
release.yml on voqalize/avatar
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
voqalize_avatar-0.2.1-py3-none-manylinux_2_25_aarch64.whl -
Subject digest:
46f533238b950ceef3fd12ef6add92a731b243d4f31c7b203ed62ce1d38e05c9 - Sigstore transparency entry: 2380222334
- Sigstore integration time:
-
Permalink:
voqalize/avatar@07377492007e38625ee750514644506f4b9a7414 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/voqalize
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@07377492007e38625ee750514644506f4b9a7414 -
Trigger Event:
push
-
Statement type:
File details
Details for the file voqalize_avatar-0.2.1-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: voqalize_avatar-0.2.1-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 46.6 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af27928e36ec69a8b9e7d7309075775b0e79af5e64c20ee8e0bb962b96f0fa77
|
|
| MD5 |
411b2e6ffd072fe138d900cdd8406f92
|
|
| BLAKE2b-256 |
60a9feeadea461278967df5b37a20daba757b38ad49b2ece8f21396ead465b14
|
Provenance
The following attestation bundles were made for voqalize_avatar-0.2.1-py3-none-macosx_11_0_arm64.whl:
Publisher:
release.yml on voqalize/avatar
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
voqalize_avatar-0.2.1-py3-none-macosx_11_0_arm64.whl -
Subject digest:
af27928e36ec69a8b9e7d7309075775b0e79af5e64c20ee8e0bb962b96f0fa77 - Sigstore transparency entry: 2380222188
- Sigstore integration time:
-
Permalink:
voqalize/avatar@07377492007e38625ee750514644506f4b9a7414 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/voqalize
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@07377492007e38625ee750514644506f4b9a7414 -
Trigger Event:
push
-
Statement type: