Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/avatar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ all change without dropping the call.

Try it in the [LiveKit Playground](https://agents.livekit.io/?example=avatar).

> **Inference variant:** [`inference_agent.py`](./inference_agent.py) is a
> minimal version that provisions the avatar through **LiveKit Inference**
> instead of the BYOK plugin — the agent needs only `LIVEKIT_API_KEY` /
> `LIVEKIT_API_SECRET` (no `LEMONSLICE_API_KEY`); the gateway creates the
> provider session with LiveKit's wholesale key. Requires the
> `avatar_lemonslice` feature flag on your project. Run with
> `python inference_agent.py dev` and set `LEMONSLICE_IMAGE_URL`.

## What's in here

- **9 personas** to choose from — each has its own face, voice, system
Expand Down
69 changes: 69 additions & 0 deletions examples/avatar/inference_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Avatar agent provisioned through LiveKit Inference (no provider key).

Unlike agent.py (which uses the BYOK lemonslice plugin with a LEMONSLICE_API_KEY),
this starts the avatar with inference.AvatarSession: the agent authenticates only
with LIVEKIT_API_KEY / LIVEKIT_API_SECRET, and the Inference gateway creates the
LemonSlice session using LiveKit's wholesale key. Media and lip-sync still flow
in-room over DataStream, exactly as the BYOK path does.

Requires the `avatar_lemonslice` feature flag to be enabled for your project on
the Inference gateway.

Run:
python inference_agent.py dev
"""

import logging
import os

from dotenv import load_dotenv

from livekit.agents import (
Agent,
AgentServer,
AgentSession,
JobContext,
cli,
inference,
)

logger = logging.getLogger("inference-avatar-example")
logger.setLevel(logging.INFO)

load_dotenv()


server = AgentServer()


@server.rtc_session()
async def entrypoint(ctx: JobContext) -> None:
session = AgentSession(
stt=inference.STT("deepgram/nova-3"),
llm=inference.LLM("google/gemini-2.5-flash"),
tts=inference.TTS("cartesia/sonic-3"),
)

# Avatar provisioning goes through LiveKit Inference: only LIVEKIT_API_KEY /
# LIVEKIT_API_SECRET are needed (no provider key). The gateway creates the
# LemonSlice session with LiveKit's wholesale key; media stays in-room.
#
# Pass a catalog agent id instead of an image with
# inference.AvatarSession("lemonslice/<agent_id>", ...).
avatar_image_url = os.getenv("LEMONSLICE_IMAGE_URL")
if not avatar_image_url:
raise ValueError("LEMONSLICE_IMAGE_URL must be set")
avatar = inference.AvatarSession(
"lemonslice",
image_url=avatar_image_url,
prompt="Be expressive in your movements and use your hands while talking.",
)
await avatar.start(session, room=ctx.room)
await avatar.wait_for_join()

await session.start(agent=Agent(instructions="Talk to me!"), room=ctx.room)
session.generate_reply(instructions="say hello to the user")


if __name__ == "__main__":
cli.run_app(server)
21 changes: 21 additions & 0 deletions livekit-agents/livekit/agents/inference/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import TYPE_CHECKING, Any

from .eot import TurnDetector, TurnDetectorModels, TurnDetectorVersions
from .interruption import (
AdaptiveInterruptionDetector,
Expand All @@ -10,11 +12,30 @@
from .tts import TTS, TTSModels
from .vad import VAD, VADModels

if TYPE_CHECKING:
from .avatar import AvatarSession, LemonSliceOptions


# AvatarSession subclasses voice.avatar.AvatarSession. Because this package is
# imported *during* voice package initialization (voice.agent imports
# inference), importing .avatar eagerly here would form a circular import. Load
# it lazily on first attribute access, by which point voice is fully
# initialized. See PEP 562.
def __getattr__(name: str) -> Any:
if name in ("AvatarSession", "LemonSliceOptions"):
from . import avatar

return getattr(avatar, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


__all__ = [
"STT",
"TTS",
"LLM",
"VAD",
"AvatarSession",
"LemonSliceOptions",
"LLMStream",
"STTModels",
"TTSModels",
Expand Down
Loading