Blog

How to add a talking avatar to a LiveKit voice agent

Give a LiveKit Agents voice agent a real-time, lip-synced face in a few lines of Python, keeping your own STT, LLM and TTS. The full agent, step by step.

The Anva team · · 4 min read

You have a LiveKit voice agent that listens, thinks and answers. This guide gives it a face: a photoreal avatar that joins the same room and speaks your agent's words with its lips in time. Your pipeline keeps its own speech-to-text, LLM and text-to-speech. The avatar only turns the audio your agent already produces into video.

It takes about ten minutes, most of it waiting for pip.

What you need

  • A working agent built on LiveKit Agents 1.8.2 or later, in Python 3.10+.
  • A LiveKit project: LiveKit Cloud or your own server. You need its URL, API key and secret.
  • An Anva account on the Developer plan or above, and an API key from Dashboard → API keys.

How it works

LiveKit already has a standard way for an avatar to stand in for an agent, and Anva follows it:

  1. When your agent's job starts, the plugin mints a room token for an avatar participant and asks Anva to join your room with it.
  2. Anva joins as a second participant and publishes a video track and an audio track on behalf of your agent, marked with the lk.publish_on_behalf attribute.
  3. Your agent's speech no longer goes straight into the room. It goes to the avatar over the room's data stream, and the avatar publishes the voice and the face together, so they cannot drift apart.

Interruptions keep working. When the user barges in, LiveKit clears the avatar's buffer (the lk.clear_buffer RPC) and the avatar stops mid-word, like the voice-only agent did.

Step 1: Install the plugin

pip install livekit-plugins-anva

It brings in nothing beyond livekit-agents and aiohttp.

Step 2: Set your keys

The plugin reads the same LiveKit variables your agent already uses, plus your Anva key:

export LIVEKIT_URL=wss://your-project.livekit.cloud
export LIVEKIT_API_KEY=...
export LIVEKIT_API_SECRET=...
export ANVA_API_KEY=anva_key_...

Step 3: Choose an avatar

Every avatar has an id that starts with av_. List the ones your account can use:

curl https://anva.ai/api/v2/avatars -H "Authorization: Bearer $ANVA_API_KEY"

Copy the id of the one you want. You can also set ANVA_AVATAR_ID and leave it out of the code.

Step 4: Give the agent its face

Here is a complete agent. The avatar is the two lines before session.start:

from livekit import agents
from livekit.agents import Agent, AgentSession, WorkerOptions, cli
from livekit.plugins import anva, deepgram, openai, silero


async def entrypoint(ctx: agents.JobContext):
    session = AgentSession(
        stt=deepgram.STT(),
        llm=openai.LLM(),
        tts=openai.TTS(),
        vad=silero.VAD.load(),
    )

    avatar = anva.AvatarSession(avatar_id="av_...")
    await avatar.start(session, room=ctx.room)

    await session.start(
        agent=Agent(instructions="You are a friendly guide. Keep answers short."),
        room=ctx.room,
    )


if __name__ == "__main__":
    cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))

Order matters: start the avatar before the session, so the session's audio output is already pointed at the avatar when the agent first speaks.

avatar.start() returns once the avatar is in the room with its tracks published. In our tests against LiveKit Cloud that took about eleven seconds from a cold start. The plugin ends the Anva session when the job shuts down.

Step 5: Run it

python agent.py dev

Join the room from any LiveKit front end. You will see two agent participants: your agent, and the avatar publishing on its behalf.

Showing the avatar in your front end

LiveKit's React components already know about avatar workers. The agent's video and audio come from the avatar participant, and useVoiceAssistant() hands you the right tracks.

If you are wiring tracks by hand, find the participant whose lk.publish_on_behalf attribute equals your agent's identity, and play its video and audio tracks. The agent itself publishes no audio once the avatar is speaking for it.

Options

Argument What it does
avatar_id The avatar to use. Or pass preset_id instead. Give exactly one.
api_key Your Anva key. Defaults to ANVA_API_KEY.
avatar_participant_identity The avatar's identity in the room. Default anva-avatar.
avatar_participant_name Its display name. Default Anva avatar.
max_duration_seconds End the avatar this long after it goes live, whatever happens.
metadata Your own key-value pairs, echoed on Anva's session and webhooks.

A preset carries a voice and a system prompt, but in a LiveKit room neither applies: your agent does the thinking and the talking.

When something goes wrong

  • AnvaException: Anva refused the session. The request itself was wrong, and the message says how: an avatar your account cannot use (avatar_not_allowed), a plan without API access (plan_required), or no tokens left (credits_exhausted). The plugin does not retry these.
  • A 503 no_capacity. Every avatar server was busy. The plugin waits for the Retry-After Anva sends and tries again.
  • "livekit_url, livekit_api_key and livekit_api_secret must be set". One of the three LIVEKIT_ variables is missing from the agent's environment.
  • The avatar joins but never speaks. Check that avatar.start() runs before session.start().

What it costs

A LiveKit avatar runs on Anva's avatar-only rate: 10 tokens per connected minute, because your own pipeline does the listening and thinking. The Developer plan is $25 a month and includes 25,000 tokens, which is 2,500 avatar minutes; top-ups are $10 for another 10,000 tokens. Either way it comes to about one cent a minute. A join that fails costs nothing.

Next steps