Skip to content
Open
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
4 changes: 4 additions & 0 deletions examples/avatar_agents/anam/agent_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ async def entrypoint(ctx: JobContext):
avatarId=anam_avatar_id,
),
api_key=anam_api_key,
# Optionally request explicit output dimensions (pixels). Omit to use the
# avatar model's default. Supported pairs are model-dependent, e.g. Cara 4
# landscape 1152x768.
# session_options=anam.SessionOptions(video_width=1152, video_height=768),
)
await anam_avatar.start(session, room=ctx.room)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@

from .avatar import AvatarSession
from .errors import AnamException
from .types import PersonaConfig
from .types import PersonaConfig, SessionOptions
from .version import __version__

__all__ = [
"AnamException",
"AvatarSession",
"PersonaConfig",
"SessionOptions",
"__version__",
]

Expand Down
33 changes: 30 additions & 3 deletions livekit-plugins/livekit-plugins-anam/livekit/plugins/anam/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from .errors import AnamException
from .log import logger
from .types import PersonaConfig
from .types import PersonaConfig, SessionOptions

DEFAULT_API_URL = "https://api.anam.ai"

Expand Down Expand Up @@ -62,11 +62,20 @@ async def __aexit__(
await self._session.close()

async def create_session_token(
self, persona_config: PersonaConfig, livekit_url: str, livekit_token: str
self,
persona_config: PersonaConfig,
livekit_url: str,
livekit_token: str,
session_options: SessionOptions | None = None,
) -> str:
"""
Creates a session token to authorize starting an engine session.

Args:
session_options: Optional per-session output options (e.g. explicit
video dimensions) forwarded to Anam as ``sessionOptions``. When
``None``, Anam uses the avatar model's default output.

Returns:
The created session token (a JWT string).
"""
Expand All @@ -80,13 +89,31 @@ async def create_session_token(
if persona_config.avatarModel:
persona_config_payload["avatarModel"] = persona_config.avatarModel

payload = {
payload: dict[str, Any] = {
"personaConfig": persona_config_payload,
}
payload["environment"] = {
"livekitUrl": livekit_url,
"livekitToken": livekit_token,
}

if session_options is not None and (
session_options.video_width is not None or session_options.video_height is not None
):
# Anam's public API speaks camelCase pixel dimensions and wants them
# as a matched pair: it rejects a lone width/height (and any
# unsupported pair) with an HTTP 400, surfaced below as
# APIStatusError, rather than downgrading. Fail fast on a half pair
# rather than round-tripping a 400.
if session_options.video_width is None or session_options.video_height is None:
raise ValueError(
"video_width and video_height must be set together (both or neither)"
)
payload["sessionOptions"] = {
"videoWidth": session_options.video_width,
"videoHeight": session_options.video_height,
}

headers = {
"Authorization": f"Bearer {self._api_key}", # Use API Key here
"Content-Type": "application/json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from .api import DEFAULT_API_URL, AnamAPI
from .errors import AnamException
from .log import logger
from .types import PersonaConfig
from .types import PersonaConfig, SessionOptions

SAMPLE_RATE = 24000
_AVATAR_AGENT_IDENTITY = "anam-avatar-agent"
Expand All @@ -34,6 +34,7 @@ def __init__(
self,
*,
persona_config: PersonaConfig,
session_options: NotGivenOr[SessionOptions] = NOT_GIVEN,
api_url: NotGivenOr[str] = NOT_GIVEN,
api_key: NotGivenOr[str] = NOT_GIVEN,
avatar_participant_identity: NotGivenOr[str] = NOT_GIVEN,
Expand All @@ -47,6 +48,7 @@ def __init__(
self._avatar_participant_identity = avatar_participant_identity or _AVATAR_AGENT_IDENTITY
self._avatar_participant_name = avatar_participant_name or _AVATAR_AGENT_NAME
self._persona_config: PersonaConfig = persona_config
self._session_options = session_options if utils.is_given(session_options) else None

api_url_val = (
api_url if utils.is_given(api_url) else os.getenv("ANAM_API_URL", DEFAULT_API_URL)
Expand Down Expand Up @@ -115,6 +117,7 @@ async def start(
persona_config=self._persona_config,
livekit_url=livekit_url,
livekit_token=livekit_token,
session_options=self._session_options,
)
logger.debug("Anam session token created successfully.")

Expand Down
19 changes: 19 additions & 0 deletions livekit-plugins/livekit-plugins-anam/livekit/plugins/anam/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,22 @@ class PersonaConfig:
name: str
avatarId: str
avatarModel: str | None = None


@dataclass
class SessionOptions:
"""Per-session output options forwarded to Anam's session-token API.

Mirrors the ``sessionOptions`` field of the Anam session-token request.

Attributes:
video_width: Output video frame width in pixels. Provide together with
``video_height`` (both or neither). Omit to use the avatar model's
default output size. Supported pairs are model-dependent and are
validated by Anam; an unsupported pair is rejected with an HTTP 400
rather than silently downgraded.
video_height: Output video frame height in pixels. See ``video_width``.
"""

video_width: int | None = None
video_height: int | None = None
Loading