From c9856efcffebbb2c300419d0326deae76745aff2 Mon Sep 17 00:00:00 2001 From: sebvanleuven Date: Tue, 14 Apr 2026 17:24:24 +0100 Subject: [PATCH 1/6] default Python SDK video bitrate to high --- README.md | 32 ++++++++++++++++++++++++++++++++ src/anam/client.py | 2 +- src/anam/types.py | 4 ++++ tests/test_client.py | 31 ++++++++++++++++++++++++++++++- 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cb8e727..c3b4148 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,38 @@ asyncio.run(main()) - 📝 **Fully typed** - Complete type hints for IDE support - 🔒 **Server-side ready** - Designed for server-side Python applications (e.g. for backend pipelines) +## Video Quality Notes (Server-to-Server) + +The Python SDK is primarily used from server environments with server-to-server communication to Anam. In this setup, adaptive bitrate (ABR) is often not required. + +Therefore, the video bitrate is fixed to "high" by default. + +```python +from anam import SessionOptions +` +session_options = SessionOptions(video_quality="high") +``` +This sends `sessionOptions.videoQuality="high"` to the API and pins the video bitrate for the session to the highest available bitrate. + +If you want to use ABR, use None or "abr" instead: + +```python +from anam import SessionOptions +` +session_options = SessionOptions(video_quality="abr") +``` +This sends `sessionOptions.videoQuality="abr"` to the API and enables ABR for the session. + +If you want to use the default video quality, use None: + +```python +from anam import SessionOptions +` +session_options = SessionOptions(video_quality=None) +``` + +Currently, only `None`, `"high"`, or `"abr"` are supported `video_quality` value. + ## API Reference ### AnamClient diff --git a/src/anam/client.py b/src/anam/client.py index 4f4237a..d8f4b8e 100644 --- a/src/anam/client.py +++ b/src/anam/client.py @@ -222,7 +222,7 @@ async def connect_async(self, session_options: SessionOptions = SessionOptions() """Connect to Anam and start streaming (without context manager). Args: - session_options: Session options (default: SessionOptions(enable_session_replay=True)). + session_options: Session options (default: SessionOptions(enable_session_replay=True, video_quality="high")). Returns: A Session object for interacting with the avatar. diff --git a/src/anam/types.py b/src/anam/types.py index 3100248..394ee1f 100644 --- a/src/anam/types.py +++ b/src/anam/types.py @@ -123,9 +123,11 @@ class SessionOptions: Args: enable_session_replay: If True (default), session is recorded. Set False to disable. + video_quality: Optional video quality profile to pin the video quality and disable ABR. Currently only None or "high" (default) are supported. """ enable_session_replay: bool = True + video_quality: str | None = "high" def __post_init__(self) -> None: self._session_replay = SessionReplayOptions( @@ -135,6 +137,8 @@ def __post_init__(self) -> None: def to_dict(self) -> dict[str, Any]: result: dict[str, Any] = {} result["sessionReplay"] = self._session_replay.to_dict() + if self.video_quality is not None: + result["videoQuality"] = self.video_quality return result diff --git a/tests/test_client.py b/tests/test_client.py index b2373ef..b3f0ed4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,7 +4,15 @@ import pytest -from anam import AnamClient, AnamEvent, ClientOptions, MessageRole, MessageStreamEvent, PersonaConfig +from anam import ( + AnamClient, + AnamEvent, + ClientOptions, + MessageRole, + MessageStreamEvent, + PersonaConfig, + SessionOptions, +) from anam.errors import ConfigurationError @@ -210,3 +218,24 @@ def test_to_dict_full(self) -> None: assert result["languageCode"] == "en" assert result["llmId"] == "gpt-4" assert result["maxSessionLengthSeconds"] == 300 + + +class TestSessionOptions: + """Tests for SessionOptions serialization.""" + + def test_to_dict_defaults(self) -> None: + options = SessionOptions() + result = options.to_dict() + + assert result == { + "sessionReplay": {"enableSessionReplay": True}, + } + + def test_to_dict_with_video_quality(self) -> None: + options = SessionOptions(video_quality="high") + result = options.to_dict() + + assert result == { + "sessionReplay": {"enableSessionReplay": True}, + "videoQuality": "high", + } From 2e4118e9da9e45b483881e56c0242ce67c157385 Mon Sep 17 00:00:00 2001 From: sebvanleuven Date: Wed, 15 Apr 2026 14:54:22 +0100 Subject: [PATCH 2/6] update readme with abr pinning, add SessionOptions to the client, update example, add comment --- README.md | 11 ++++------- examples/persona_interactive_video.py | 3 ++- src/anam/client.py | 9 +++++---- uv.lock | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c3b4148..7232178 100644 --- a/README.md +++ b/README.md @@ -88,24 +88,21 @@ session_options = SessionOptions(video_quality="high") ``` This sends `sessionOptions.videoQuality="high"` to the API and pins the video bitrate for the session to the highest available bitrate. -If you want to use ABR, use None or "abr" instead: +If you want to use ABR, use `None` or "auto" instead: ```python from anam import SessionOptions ` -session_options = SessionOptions(video_quality="abr") +session_options = SessionOptions(video_quality="auto") ``` -This sends `sessionOptions.videoQuality="abr"` to the API and enables ABR for the session. - -If you want to use the default video quality, use None: - +or ```python from anam import SessionOptions ` session_options = SessionOptions(video_quality=None) ``` -Currently, only `None`, `"high"`, or `"abr"` are supported `video_quality` value. +Currently, only `None`, `"high"`, or `"auto"` are supported `video_quality` value. ## API Reference diff --git a/examples/persona_interactive_video.py b/examples/persona_interactive_video.py index 082429f..0ed40be 100644 --- a/examples/persona_interactive_video.py +++ b/examples/persona_interactive_video.py @@ -30,7 +30,7 @@ from dotenv import load_dotenv -from anam import AnamClient, AnamEvent, ClientOptions +from anam import AnamClient, AnamEvent, ClientOptions, SessionOptions from anam.types import MessageRole, PersonaConfig # Add parent directory to path to allow importing from examples @@ -237,6 +237,7 @@ async def consume_audio_frames(session) -> None: except Exception as e: logger.error(f"Error consuming audio frames: {e}") + # connect defaults high quality video rendition and disables ABR async with client.connect() as session: print(f"Session: {session.session_id}") print("Type 'q' in CLI to quit") diff --git a/src/anam/client.py b/src/anam/client.py index d8f4b8e..57a6367 100644 --- a/src/anam/client.py +++ b/src/anam/client.py @@ -190,7 +190,7 @@ async def _emit(self, event: AnamEvent, *args: Any, **kwargs: Any) -> None: except Exception as e: logger.error("Error in event callback for %s: %s", event.value, e) - def connect(self) -> "_SessionContextManager": + def connect(self, session_options: SessionOptions = SessionOptions()) -> "_SessionContextManager": """Connect to Anam and start streaming. Returns: @@ -216,7 +216,7 @@ def connect(self) -> "_SessionContextManager": await session.close() ``` """ - return _SessionContextManager(self) + return _SessionContextManager(self, session_options) async def connect_async(self, session_options: SessionOptions = SessionOptions()) -> "Session": """Connect to Anam and start streaming (without context manager). @@ -448,13 +448,14 @@ def get_persona_config(self) -> PersonaConfig | None: class _SessionContextManager: """Async context manager for AnamClient.connect().""" - def __init__(self, client: AnamClient): + def __init__(self, client: AnamClient, session_options: SessionOptions): self._client = client self._session: Session | None = None + self._session_options = session_options async def __aenter__(self) -> "Session": """Enter the context and connect.""" - self._session = await self._client.connect_async() + self._session = await self._client.connect_async(self._session_options) return self._session async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: diff --git a/uv.lock b/uv.lock index b98206d..d16d761 100644 --- a/uv.lock +++ b/uv.lock @@ -188,7 +188,7 @@ wheels = [ [[package]] name = "anam" -version = "0.4.0a1" +version = "0.4.0a2" source = { editable = "." } dependencies = [ { name = "aiohttp" }, From ca5c7526350fb200f3633180240ac9fcb26d153d Mon Sep 17 00:00:00 2001 From: sebvanleuven Date: Wed, 15 Apr 2026 17:14:58 +0100 Subject: [PATCH 3/6] improve readme and comments --- README.md | 17 +++++++---------- examples/persona_interactive_video.py | 2 +- src/anam/types.py | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7232178..f796ccc 100644 --- a/README.md +++ b/README.md @@ -77,32 +77,29 @@ asyncio.run(main()) ## Video Quality Notes (Server-to-Server) -The Python SDK is primarily used from server environments with server-to-server communication to Anam. In this setup, adaptive bitrate (ABR) is often not required. +The Python SDK is primarily intended for server-side use with a high-capacity network connection. In this setup, adaptive bitrate (ABR) is not required. To avoid any potential issues with ABR, the video bitrate is fixed to "high" by default, which will provide the highest quality video rendition available. -Therefore, the video bitrate is fixed to "high" by default. +Omitting `session_options` or setting `video_quality="high"` achieves this: ```python from anam import SessionOptions ` session_options = SessionOptions(video_quality="high") +async with client.connect(session_options=session_options) as session: ``` + This sends `sessionOptions.videoQuality="high"` to the API and pins the video bitrate for the session to the highest available bitrate. -If you want to use ABR, use `None` or "auto" instead: +If you want to use ABR, set `video_quality="auto"` instead: ```python from anam import SessionOptions ` session_options = SessionOptions(video_quality="auto") -``` -or -```python -from anam import SessionOptions -` -session_options = SessionOptions(video_quality=None) +async with client.connect(session_options=session_options) as session: ``` -Currently, only `None`, `"high"`, or `"auto"` are supported `video_quality` value. +Currently, only `"high"` or `"auto"` are supported `video_quality` values. ## API Reference diff --git a/examples/persona_interactive_video.py b/examples/persona_interactive_video.py index 0ed40be..e32e56a 100644 --- a/examples/persona_interactive_video.py +++ b/examples/persona_interactive_video.py @@ -30,7 +30,7 @@ from dotenv import load_dotenv -from anam import AnamClient, AnamEvent, ClientOptions, SessionOptions +from anam import AnamClient, AnamEvent, ClientOptions from anam.types import MessageRole, PersonaConfig # Add parent directory to path to allow importing from examples diff --git a/src/anam/types.py b/src/anam/types.py index 394ee1f..a324082 100644 --- a/src/anam/types.py +++ b/src/anam/types.py @@ -123,7 +123,7 @@ class SessionOptions: Args: enable_session_replay: If True (default), session is recorded. Set False to disable. - video_quality: Optional video quality profile to pin the video quality and disable ABR. Currently only None or "high" (default) are supported. + video_quality: Optional video quality profile to pin the video quality and disable ABR. Currently only "high" (default) or "auto" are supported values. """ enable_session_replay: bool = True From 2bfaf7e436cc40a84b312dc140637a3d3c1dca41 Mon Sep 17 00:00:00 2001 From: sebvanleuven Date: Wed, 15 Apr 2026 17:36:17 +0100 Subject: [PATCH 4/6] simplify video_quality options --- README.md | 4 +--- src/anam/_api.py | 4 ++-- src/anam/types.py | 11 ++++++----- tests/test_client.py | 16 +++++++++++++++- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f796ccc..1820acc 100644 --- a/README.md +++ b/README.md @@ -77,13 +77,12 @@ asyncio.run(main()) ## Video Quality Notes (Server-to-Server) -The Python SDK is primarily intended for server-side use with a high-capacity network connection. In this setup, adaptive bitrate (ABR) is not required. To avoid any potential issues with ABR, the video bitrate is fixed to "high" by default, which will provide the highest quality video rendition available. +The Python SDK is primarily intended for server-side use with a high-capacity network connection. In this setup, adaptive bitrate (ABR) is often not required. By default, `SessionOptions` uses `video_quality="high"` which disables ABR and pin the video quality to the highest available rendition. Omitting `session_options` or setting `video_quality="high"` achieves this: ```python from anam import SessionOptions -` session_options = SessionOptions(video_quality="high") async with client.connect(session_options=session_options) as session: ``` @@ -94,7 +93,6 @@ If you want to use ABR, set `video_quality="auto"` instead: ```python from anam import SessionOptions -` session_options = SessionOptions(video_quality="auto") async with client.connect(session_options=session_options) as session: ``` diff --git a/src/anam/_api.py b/src/anam/_api.py index c3c05ba..e224237 100644 --- a/src/anam/_api.py +++ b/src/anam/_api.py @@ -46,7 +46,7 @@ async def get_session_token( Args: persona_config: The persona configuration to use. - session_options: Session options (optional). + session_options: Session options. Returns: The session token string. @@ -107,7 +107,7 @@ async def start_session( Args: persona_config: The persona configuration. - session_options: Additional session options (optional). + session_options: Additional session options. Returns: SessionInfo with connection details. diff --git a/src/anam/types.py b/src/anam/types.py index a324082..a8db8e4 100644 --- a/src/anam/types.py +++ b/src/anam/types.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Any +from typing import Any, Literal class AnamEvent(str, Enum): @@ -123,22 +123,23 @@ class SessionOptions: Args: enable_session_replay: If True (default), session is recorded. Set False to disable. - video_quality: Optional video quality profile to pin the video quality and disable ABR. Currently only "high" (default) or "auto" are supported values. + video_quality: Video quality profile to pin the video quality. Supported values are "high" (default) and "auto". """ enable_session_replay: bool = True - video_quality: str | None = "high" + video_quality: Literal["high", "auto"] = "high" def __post_init__(self) -> None: self._session_replay = SessionReplayOptions( enable_session_replay=self.enable_session_replay ) + if self.video_quality not in {"high", "auto"}: + raise ValueError('video_quality must be either "high" or "auto"') def to_dict(self) -> dict[str, Any]: result: dict[str, Any] = {} result["sessionReplay"] = self._session_replay.to_dict() - if self.video_quality is not None: - result["videoQuality"] = self.video_quality + result["videoQuality"] = self.video_quality return result diff --git a/tests/test_client.py b/tests/test_client.py index b3f0ed4..4c98340 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -229,9 +229,10 @@ def test_to_dict_defaults(self) -> None: assert result == { "sessionReplay": {"enableSessionReplay": True}, + "videoQuality": "high", } - def test_to_dict_with_video_quality(self) -> None: + def test_to_dict_with_video_quality_high(self) -> None: options = SessionOptions(video_quality="high") result = options.to_dict() @@ -239,3 +240,16 @@ def test_to_dict_with_video_quality(self) -> None: "sessionReplay": {"enableSessionReplay": True}, "videoQuality": "high", } + + def test_to_dict_with_video_quality_auto(self) -> None: + options = SessionOptions(video_quality="auto") + result = options.to_dict() + + assert result == { + "sessionReplay": {"enableSessionReplay": True}, + "videoQuality": "auto", + } + + def test_invalid_video_quality_raises_value_error(self) -> None: + with pytest.raises(ValueError, match='video_quality must be either "high" or "auto"'): + SessionOptions(video_quality="medium") # type: ignore[arg-type] From b9048c8e7b5d14deab948ad4b38ccc282afacf86 Mon Sep 17 00:00:00 2001 From: sebvanleuven Date: Wed, 15 Apr 2026 17:50:32 +0100 Subject: [PATCH 5/6] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1820acc..43a279d 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ asyncio.run(main()) ## Video Quality Notes (Server-to-Server) -The Python SDK is primarily intended for server-side use with a high-capacity network connection. In this setup, adaptive bitrate (ABR) is often not required. By default, `SessionOptions` uses `video_quality="high"` which disables ABR and pin the video quality to the highest available rendition. +The Python SDK is primarily intended for server-side use with a high-capacity network connection. In this setup, adaptive bitrate (ABR) is often not required. By default, `SessionOptions` uses `video_quality="high"` which disables ABR and pins the video quality to the highest available rendition. Omitting `session_options` or setting `video_quality="high"` achieves this: From 8ed4417d884d14a032b9019e60c0d0a7298d9185 Mon Sep 17 00:00:00 2001 From: sebvanleuven Date: Thu, 16 Apr 2026 10:40:55 +0100 Subject: [PATCH 6/6] format linter --- examples/user_audio_from_wav.py | 18 ++++++++++++------ src/anam/client.py | 4 +++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/examples/user_audio_from_wav.py b/examples/user_audio_from_wav.py index 4cd00dc..e726448 100644 --- a/examples/user_audio_from_wav.py +++ b/examples/user_audio_from_wav.py @@ -61,6 +61,7 @@ if missing: raise EnvironmentError(f"Missing required environment variables: {', '.join(missing)}") + def _build_persona_config() -> PersonaConfig: """Build a persona configuration from environment variables.""" avatar_id = os.environ.get("ANAM_AVATAR_ID", "").strip().strip('"') @@ -76,11 +77,14 @@ def _build_persona_config() -> PersonaConfig: enable_audio_passthrough=False, ) + def _format_ids(correlation_ids: list[str | None]) -> str: """Format correlation IDs for log output.""" if not correlation_ids: return "none" - return ", ".join("None" if correlation_id is None else correlation_id for correlation_id in correlation_ids) + return ", ".join( + "None" if correlation_id is None else correlation_id for correlation_id in correlation_ids + ) def _compact_text(text: str) -> str: @@ -141,9 +145,7 @@ async def _stream_wav_file_realtime( total_frames = wav_file.getnframes() if sample_width != 2: - raise ValueError( - f"Expected 16-bit PCM WAV input (sample width 2), got {sample_width}" - ) + raise ValueError(f"Expected 16-bit PCM WAV input (sample width 2), got {sample_width}") if num_channels not in (1, 2): raise ValueError(f"Expected mono or stereo WAV input, got {num_channels} channels") @@ -258,7 +260,9 @@ async def on_message_stream_event(event: MessageStreamEvent) -> None: role = "user" if event.role == MessageRole.USER else "assistant" role_emoji = "👤" if event.role == MessageRole.USER else "🤖" status_emoji = "✗" if event.interrupted else "✓" - await log(f"{role_emoji} {role} [{event.correlation_id}] ({status_emoji} {status}): {message}") + await log( + f"{role_emoji} {role} [{event.correlation_id}] ({status_emoji} {status}): {message}" + ) if event.role == MessageRole.USER: _append_unique(transcript_ids, event.correlation_id) @@ -285,7 +289,9 @@ async def on_message_stream_event(event: MessageStreamEvent) -> None: await asyncio.sleep(2.0) wav_duration, sample_rate, num_channels = await _stream_wav_file_realtime(session, wav_path) - await log(f"📤 Finished sending {wav_duration:.2f}s of WAV input. Sending trailing silence...") + await log( + f"📤 Finished sending {wav_duration:.2f}s of WAV input. Sending trailing silence..." + ) silence_task = asyncio.create_task( _send_silence_until_cancelled( diff --git a/src/anam/client.py b/src/anam/client.py index 57a6367..377ebf4 100644 --- a/src/anam/client.py +++ b/src/anam/client.py @@ -190,7 +190,9 @@ async def _emit(self, event: AnamEvent, *args: Any, **kwargs: Any) -> None: except Exception as e: logger.error("Error in event callback for %s: %s", event.value, e) - def connect(self, session_options: SessionOptions = SessionOptions()) -> "_SessionContextManager": + def connect( + self, session_options: SessionOptions = SessionOptions() + ) -> "_SessionContextManager": """Connect to Anam and start streaming. Returns: