LiveKit Agents plugin

Bring your own LiveKit voice agent; Atmee renders a talking-head avatar from a single portrait into your room and bills per minute.

What it is

You already run a voice agent on LiveKit Agents with your own speech recognition, model and voice. The Atmee plugin adds a face: a talking-head avatar, rendered from one portrait, joins your LiveKit room as its own participant and speaks your agent's audio with lips in sync. Atmee never sees your LiveKit secret and never hosts your conversation; it only renders, and bills by the minute while it does. If you would rather have Atmee run the whole conversation and only join a room from your page, use the Session API instead; Choose your integration compares the two.

snippet
from livekit.plugins import atmee

avatar_id = await atmee.AtmeeAPI().create_avatar(name="Val", image="portrait.jpg")

avatar = atmee.AvatarSession(avatar_id=avatar_id)   # ATMEE_API_KEY in the environment
await avatar.start(session, room=ctx.room)
await session.start(agent=..., room=ctx.room)       # the agent's TTS drives the avatar's video

Installation

snippet
pip install livekit-plugins-atmee

Python 3.10 or newer, livekit-agents 1.6.8 or newer.

Prerequisites

  • An Atmee API key (sk_atmee_…) with the sessions scope, in ATMEE_API_KEY. Keep it on your agent's side; it never goes to a browser. See API keys.
  • Your own LiveKit project (LiveKit Cloud or self-hosted): LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET, the same variables your agent already uses. The plugin mints the avatar's room token locally with your secret and hands only that token to Atmee.

Usage

1. Create an avatar from a portrait

Once, from a script or your backend:

snippet
async with atmee.AtmeeAPI() as api:
    avatar_id = await api.create_avatar(name="Val", image="portrait.jpg")   # path, bytes, or https URL

A portrait-only avatar is render-only (kind: render_only): it has no voice or knowledge of its own and is ready immediately, because there is nothing to build. It can only be used through avatar sessions, not on atmee.ai or through POST /v1/session. Any avatar of your account that has a portrait works too, including ones created in the Studio or with a voice through POST /v1/avatars.

2. Render it in your agent

snippet
async def entrypoint(ctx: agents.JobContext):
    await ctx.connect()
    session = AgentSession(stt=..., llm=..., tts=..., vad=...)

    avatar = atmee.AvatarSession(avatar_id=os.environ["ATMEE_AVATAR_ID"])
    await avatar.start(session, room=ctx.room)        # before session.start

    await session.start(agent=MyAgent(), room=ctx.room)

start() returns as soon as Atmee's rendering worker acknowledged the start, about a second. The avatar's video and audio tracks appear in the room a few seconds later, published by the participant atmee-avatar-agent. Your agent publishes no audio of its own; the avatar speaks for it.

Options on AvatarSession:

ArgumentDefaultMeaning
avatar_idrequiredThe Atmee avatar to render.
api_key, api_urlATMEE_API_KEY, ATMEE_API_URLCredentials and API base.
avatar_participant_identityatmee-avatar-agentIdentity the avatar joins with; set it when one room hosts several avatars.
max_duration_seconds3600Hard ceiling of the session; also the most it can bill if your agent dies without a trace.
wait_for"initializing""avatar_joined" makes start() block until the avatar is in the room.
metadataNoneFree-form JSON stored with the session on the Atmee side.

avatar.session_id and avatar.session_info hold the Atmee session after start(); avatar.on("avatar_disconnected", ...) fires if the avatar participant leaves while your agent is still running.

Lifecycle and billing

Billing starts when the avatar sees your agent in the room and runs by the minute at the render rate, from your account's credits. The render rate is lower than a hosted session's, since Atmee only renders. It stops when

  • your agent leaves the room or the room closes (the rendering worker notices and reports the end itself, so a crashed agent stops being billed within seconds),
  • aclose() runs, registered automatically as a job shutdown callback: it ends the session and removes the avatar participant from your room,
  • the rendering worker itself dies or loses contact: it proves it is alive once a minute, and a session silent for a few minutes is closed and billed only up to the last proof,
  • or the session reaches max_duration_seconds.

Each session is a normal API-key session for billing purposes: your key's spend cap, concurrency limit and billingMode apply (see Reference).

Errors

All API failures raise atmee.AtmeeException with status_code, code and message. Two subclasses are worth handling: AtmeeNoCapacityError (every rendering worker is busy; check retry_after and try again later) and AtmeeAvatarNotReadyError (the avatar has no portrait yet). The codes:

StatuscodeMeaning
400invalid_livekit_urllivekitUrl is not a ws(s):// server URL.
400invalid_livekit_tokenNot a LiveKit access token, no room grant, or expired.
400missing_publish_on_behalfThe token names no agent to render for (lk.publish_on_behalf).
400agent_identity_mismatchagentIdentity differs from the token's lk.publish_on_behalf.
402insufficient_creditsOut of credits, spend cap reached, or too many sessions for this key.
403avatar_not_allowedThe avatar is not on this key's allow-list.
409avatar_not_renderableThe avatar has no reference image to animate.
502avatar_start_failedThe worker refused the render before joining (for example the portrait has no usable face). Nothing is billed.
503no_capacityEvery rendering worker is busy; retry after Retry-After seconds.
503avatar_sessions_disabledThis deployment has no rendering backend.

A token your LiveKit server rejects never joins; the session then fails as AVATAR_DID_NOT_JOIN after the join timeout and is not billed.

Endpoints

The plugin is a thin client over three endpoints. Use them directly from any language; all are authenticated with X-Api-Key like the rest of the API.

POST /v1/avatars/{avatarId}/avatar_sessions

Starts an avatar session. Responds 202 once the worker acknowledged the start; add ?waitFor=avatar_joined to block until the avatar participant is in your room.

FieldTypeNotes
livekitUrlstring, requiredYour LiveKit server URL (wss://<project>.livekit.cloud or a self-hosted ws(s):// address).
livekitTokenstring, requiredAccess token for the avatar participant, minted with your LiveKit key and secret: identity = the avatar's participant identity, kind: agent, a roomJoin grant for your room, attribute lk.publish_on_behalf = your agent's identity. Read, never verified.
agentIdentitystringOptional cross-check; must equal the token's lk.publish_on_behalf.
maxDurationSecondsinteger 60–14400Hard ceiling (default 3600). Capped by your key's limits.
metadataobject, ≤50 keysStored with the session.

Response 202: sessionId, status (initializing or avatar_joined), avatarParticipantIdentity, agentIdentity, roomName, maxDurationSeconds, billingMode.

GET /v1/avatar_sessions/{id}

The session's durable state: status (pending, active, completed, failed), avatarJoinedAt, agentJoinedAt (billing starts here), endedAt. 403 forbidden for another account's session, 404 not_found.

POST /v1/avatar_sessions/{id}/end

Stops billing now. Idempotent: ending a session that already ended answers 200 with alreadyEnded: true, so you can call it from every teardown path. The worker's own report when your agent leaves is the primary end signal; this call is the fallback, and the way to stop paying for an avatar you are done with while your room stays open. It does not remove the avatar from your room; remove the participant yourself (the plugin does so on close).

How it works

  1. AvatarSession.start() mints a LiveKit access token for the avatar participant with your LiveKit credentials: identity atmee-avatar-agent, kind: agent, a roomJoin grant for your room, and the attribute lk.publish_on_behalf set to your agent's identity. Atmee receives only that token, never your secret.
  2. It calls POST /v1/avatars/{avatarId}/avatar_sessions with your LiveKit URL and the token. Atmee reserves a session, starts a rendering worker, and answers as soon as the worker acknowledged the start.
  3. The worker joins your room with the token, waits for your agent, receives its audio over the LiveKit data stream (lk.audio_stream, 16 kHz PCM, the standard DataStreamAudioOutput), and publishes lip-synced video and audio on behalf of your agent.
  4. When your agent leaves or the room closes, the worker notices from inside the room and reports the end to Atmee, which stops billing. aclose() removes the avatar participant and ends the session explicitly as well.

Examples

The plugin repository ships two examples: examples/agent.py, a Deepgram + OpenAI + Silero agent with an Atmee avatar (run with python examples/agent.py dev and open the LiveKit Agents Playground), and examples/create_avatar.py, which creates an avatar from a portrait and prints its id.

LiveKit Agents plugin — Atmee Docs