avartha-python-sdk 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- avartha/__init__.py +42 -0
- avartha/_config.py +140 -0
- avartha/_realtime_tts.py +138 -0
- avartha/_version.py +24 -0
- avartha/client.py +159 -0
- avartha/control.py +441 -0
- avartha/conversational_ai/__init__.py +19 -0
- avartha/conversational_ai/conversation.py +19 -0
- avartha/conversational_ai/default_audio_interface.py +5 -0
- avartha/elevenlabs.py +120 -0
- avartha/errors.py +35 -0
- avartha/openai.py +106 -0
- avartha/py.typed +0 -0
- avartha_python_sdk-0.0.1.dist-info/METADATA +417 -0
- avartha_python_sdk-0.0.1.dist-info/RECORD +18 -0
- avartha_python_sdk-0.0.1.dist-info/WHEEL +5 -0
- avartha_python_sdk-0.0.1.dist-info/licenses/LICENSE +23 -0
- avartha_python_sdk-0.0.1.dist-info/top_level.txt +1 -0
avartha/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Avartha Python SDK: upstream protocol compatibility and platform management."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _dist_version
|
|
5
|
+
|
|
6
|
+
from elevenlabs import AudioFormat, CommitStrategy, RealtimeEvents, VoiceSettings
|
|
7
|
+
from elevenlabs.play import play, save, stream
|
|
8
|
+
|
|
9
|
+
from ._realtime_tts import DEFAULT_OUTPUT_FORMAT as DEFAULT_TTS_OUTPUT_FORMAT
|
|
10
|
+
from .client import AsyncAvartha, Avartha
|
|
11
|
+
from .control import AsyncControl, Control
|
|
12
|
+
from .elevenlabs import AsyncElevenLabs, ElevenLabs
|
|
13
|
+
from .errors import ConfigurationError, EndpointFailedError, PlatformAPIError
|
|
14
|
+
from .openai import AsyncOpenAI, OpenAI
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
# Populated from the git tag at build time by setuptools_scm.
|
|
18
|
+
__version__ = _dist_version("avartha-python-sdk")
|
|
19
|
+
except PackageNotFoundError: # pragma: no cover - running from an uninstalled tree
|
|
20
|
+
__version__ = "0.0.0.dev0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AsyncAvartha",
|
|
24
|
+
"AsyncControl",
|
|
25
|
+
"AsyncElevenLabs",
|
|
26
|
+
"AsyncOpenAI",
|
|
27
|
+
"AudioFormat",
|
|
28
|
+
"Avartha",
|
|
29
|
+
"CommitStrategy",
|
|
30
|
+
"ConfigurationError",
|
|
31
|
+
"DEFAULT_TTS_OUTPUT_FORMAT",
|
|
32
|
+
"Control",
|
|
33
|
+
"ElevenLabs",
|
|
34
|
+
"EndpointFailedError",
|
|
35
|
+
"OpenAI",
|
|
36
|
+
"PlatformAPIError",
|
|
37
|
+
"RealtimeEvents",
|
|
38
|
+
"VoiceSettings",
|
|
39
|
+
"play",
|
|
40
|
+
"save",
|
|
41
|
+
"stream",
|
|
42
|
+
]
|
avartha/_config.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Avartha configuration; vendor environment variables are never credentials."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from ipaddress import IPv6Address
|
|
6
|
+
from typing import Literal
|
|
7
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
Tier = Literal["serverless", "dedicated"]
|
|
12
|
+
DEFAULT_BASE_URL = "https://platform.avartha.ai"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigurationError(ValueError):
|
|
16
|
+
"""Missing credentials or an invalid service URL."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def api_key(value: str | None) -> str:
|
|
20
|
+
key = value if value is not None else os.environ.get("AVARTHA_API_KEY")
|
|
21
|
+
if not key or not key.strip():
|
|
22
|
+
raise ConfigurationError("Set api_key or AVARTHA_API_KEY to an Avartha API key.")
|
|
23
|
+
return key
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def http_url(value: str) -> str:
|
|
27
|
+
parts = urlsplit(value)
|
|
28
|
+
if (
|
|
29
|
+
parts.scheme not in {"http", "https"}
|
|
30
|
+
or not parts.hostname
|
|
31
|
+
or parts.username is not None
|
|
32
|
+
or parts.password is not None
|
|
33
|
+
or parts.query
|
|
34
|
+
or parts.fragment
|
|
35
|
+
):
|
|
36
|
+
raise ConfigurationError(
|
|
37
|
+
"Use an absolute http(s) base URL without credentials, query, or fragment."
|
|
38
|
+
)
|
|
39
|
+
return value.rstrip("/")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def platform_url(value: str | None = None) -> str:
|
|
43
|
+
url = http_url(value if value is not None else os.getenv("AVARTHA_BASE_URL", DEFAULT_BASE_URL))
|
|
44
|
+
path = urlsplit(url).path
|
|
45
|
+
if "/inference/" in path or path.endswith("/control/v1"):
|
|
46
|
+
raise ConfigurationError(
|
|
47
|
+
"base_url must be the platform root, before /inference or /control."
|
|
48
|
+
)
|
|
49
|
+
return url
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def openai_url(root: str | None = None, tier: Tier = "serverless") -> str:
|
|
53
|
+
if tier not in {"serverless", "dedicated"}:
|
|
54
|
+
raise ConfigurationError("tier must be 'serverless' or 'dedicated'.")
|
|
55
|
+
return f"{platform_url(root)}/inference/{tier}/openai/v1"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def elevenlabs_url(
|
|
59
|
+
value: str | None = None, *, root: str | None = None, tier: Tier = "serverless"
|
|
60
|
+
) -> str:
|
|
61
|
+
override = value if value is not None else os.getenv("AVARTHA_ELEVENLABS_BASE_URL")
|
|
62
|
+
if override is not None:
|
|
63
|
+
return http_url(override)
|
|
64
|
+
if tier not in {"serverless", "dedicated"}:
|
|
65
|
+
raise ConfigurationError("tier must be 'serverless' or 'dedicated'.")
|
|
66
|
+
return f"{platform_url(root)}/inference/{tier}/elevenlabs"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def websocket_url(value: str) -> str:
|
|
70
|
+
parts = urlsplit(value)
|
|
71
|
+
return urlunsplit(parts._replace(scheme={"https": "wss", "http": "ws"}[parts.scheme]))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
VENDOR_HOSTS = ("openai.com", "openai.azure.com")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def not_vendor_host(value: str, *, field: str) -> str:
|
|
78
|
+
"""Refuse a URL that would send an Avartha credential to a vendor endpoint."""
|
|
79
|
+
host = (urlsplit(value).hostname or "").lower().rstrip(".")
|
|
80
|
+
if any(host == vendor or host.endswith(f".{vendor}") for vendor in VENDOR_HOSTS):
|
|
81
|
+
raise ConfigurationError(
|
|
82
|
+
f"{field} resolves to the vendor host {host!r}. An Avartha API key must never "
|
|
83
|
+
"be sent to a vendor endpoint; use the official vendor package for that."
|
|
84
|
+
)
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def websocket_override(value: str) -> str:
|
|
89
|
+
"""Return a canonical WebSocket service base suitable for upstream joining.
|
|
90
|
+
|
|
91
|
+
OpenAI appends `/realtime` to this URL's raw path. Query strings would be
|
|
92
|
+
corrupted by that join, so request query parameters belong in connect().
|
|
93
|
+
Validate before parsing as urllib silently removes some whitespace/controls.
|
|
94
|
+
"""
|
|
95
|
+
message = (
|
|
96
|
+
"websocket_base_url must be an absolute wss:// service URL with a "
|
|
97
|
+
"valid hostname and port (1-65535), without credentials, whitespace, "
|
|
98
|
+
"backslashes, query, or fragment."
|
|
99
|
+
)
|
|
100
|
+
try:
|
|
101
|
+
if (
|
|
102
|
+
not value
|
|
103
|
+
or any(char.isspace() or ord(char) < 32 or ord(char) == 127 for char in value)
|
|
104
|
+
or any(char in value for char in "\\?#")
|
|
105
|
+
or re.search(r"%(?![0-9a-fA-F]{2})", value)
|
|
106
|
+
):
|
|
107
|
+
raise ValueError("Invalid URL characters")
|
|
108
|
+
parts = urlsplit(value)
|
|
109
|
+
if (
|
|
110
|
+
parts.scheme != "wss"
|
|
111
|
+
or not parts.hostname
|
|
112
|
+
or parts.username is not None
|
|
113
|
+
or parts.password is not None
|
|
114
|
+
or parts.netloc.endswith(":")
|
|
115
|
+
or (parts.port is not None and not 1 <= parts.port <= 65535)
|
|
116
|
+
):
|
|
117
|
+
raise ValueError("Invalid URL authority")
|
|
118
|
+
# Use the declared HTTPX dependency for IDNA, IPv6, and path escaping.
|
|
119
|
+
url = httpx.URL(urlunsplit(parts))
|
|
120
|
+
host = url.raw_host.decode("ascii")
|
|
121
|
+
if ":" in host:
|
|
122
|
+
url = url.copy_with(host=str(IPv6Address(host)))
|
|
123
|
+
else:
|
|
124
|
+
hostname = host.removesuffix(".")
|
|
125
|
+
if len(hostname) > 253 or any(
|
|
126
|
+
re.fullmatch(r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?", label) is None
|
|
127
|
+
for label in hostname.split(".")
|
|
128
|
+
):
|
|
129
|
+
raise ValueError("Invalid hostname")
|
|
130
|
+
except (ValueError, httpx.InvalidURL):
|
|
131
|
+
# Do not expose userinfo or credential-like query values in diagnostics.
|
|
132
|
+
raise ConfigurationError(message) from None
|
|
133
|
+
|
|
134
|
+
normalized = str(url).rstrip("/")
|
|
135
|
+
if url.path.rstrip("/").endswith("/realtime"):
|
|
136
|
+
raise ConfigurationError(
|
|
137
|
+
"websocket_base_url must be the service base before /realtime; "
|
|
138
|
+
"the SDK appends /realtime when connecting."
|
|
139
|
+
)
|
|
140
|
+
return not_vendor_host(normalized, field="websocket_base_url")
|
avartha/_realtime_tts.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Realtime text-to-speech for the Avartha gateway.
|
|
2
|
+
|
|
3
|
+
The official `convert_realtime` hard-codes three values the gateway refuses and
|
|
4
|
+
`request_options` cannot reach: it omits `auto_mode`, sends
|
|
5
|
+
`try_trigger_generation: true`, and sends a populated `generation_config`. Its
|
|
6
|
+
`mp3_44100_128` default format is refused too, and every rejection closes the
|
|
7
|
+
socket 1011, so upstream's helper fails on every Avartha TTS model. Only the
|
|
8
|
+
request envelope is corrected here; chunking, the message sequence, the yielded
|
|
9
|
+
audio and the raised `ApiError` stay upstream's.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import json
|
|
14
|
+
from collections.abc import Iterator, Mapping
|
|
15
|
+
from typing import Any, cast
|
|
16
|
+
|
|
17
|
+
import websockets
|
|
18
|
+
from elevenlabs.core.api_error import ApiError
|
|
19
|
+
from elevenlabs.core.jsonable_encoder import jsonable_encoder
|
|
20
|
+
from elevenlabs.core.remove_none_from_dict import remove_none_from_dict
|
|
21
|
+
from elevenlabs.core.request_options import RequestOptions
|
|
22
|
+
from elevenlabs.realtime_tts import RealtimeTextToSpeechClient, text_chunker
|
|
23
|
+
from elevenlabs.types import OutputFormat
|
|
24
|
+
from elevenlabs.types.voice_settings import VoiceSettings
|
|
25
|
+
from elevenlabs.url_utils import build_ws_url
|
|
26
|
+
from websockets.sync.client import connect
|
|
27
|
+
|
|
28
|
+
OMIT = cast(Any, ...)
|
|
29
|
+
|
|
30
|
+
# Avartha publishes no sample rate and the TTS models refuse every other format,
|
|
31
|
+
# so their native PCM rate is the only workable default.
|
|
32
|
+
DEFAULT_OUTPUT_FORMAT: OutputFormat = "pcm_24000"
|
|
33
|
+
|
|
34
|
+
# How long a send loop looks for audio before pushing the next chunk.
|
|
35
|
+
DRAIN_TIMEOUT = 1e-2
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _given[T](value: T) -> T | None:
|
|
39
|
+
"""Upstream's OMIT sentinel means "not supplied" and must never reach the wire."""
|
|
40
|
+
return None if value is OMIT else value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _path(voice_id: str, endpoint: str) -> list[str]:
|
|
44
|
+
return ["v1", "text-to-speech", voice_id, endpoint]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _query(model_id: str | None, output_format: OutputFormat | None) -> dict[str, Any]:
|
|
48
|
+
return remove_none_from_dict(
|
|
49
|
+
{
|
|
50
|
+
"model_id": model_id,
|
|
51
|
+
"output_format": output_format or DEFAULT_OUTPUT_FORMAT,
|
|
52
|
+
"auto_mode": "true",
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _headers(base: Mapping[str, str], request_options: RequestOptions | None) -> dict[str, str]:
|
|
58
|
+
extra = request_options.get("additional_headers", {}) if request_options else {}
|
|
59
|
+
return cast(dict[str, str], jsonable_encoder(remove_none_from_dict({**base, **extra})))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _opening(voice_settings: VoiceSettings | None) -> str:
|
|
63
|
+
"""The single-space message the gateway requires before any text."""
|
|
64
|
+
frame: dict[str, Any] = {"text": " "}
|
|
65
|
+
if voice_settings is not None:
|
|
66
|
+
frame["voice_settings"] = voice_settings.dict()
|
|
67
|
+
return json.dumps(frame)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _audio(payload: Any) -> bytes | None:
|
|
71
|
+
encoded = payload.get("audio") if isinstance(payload, Mapping) else None
|
|
72
|
+
return base64.b64decode(encoded) if encoded else None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _failure(received: Any, closed: websockets.exceptions.ConnectionClosed) -> ApiError | None:
|
|
76
|
+
"""The error a closed socket deserves, or None once the stream finished cleanly.
|
|
77
|
+
|
|
78
|
+
Upstream inspects the last payload *sent* here, so the gateway's own
|
|
79
|
+
diagnosis is thrown away; report what actually arrived instead.
|
|
80
|
+
"""
|
|
81
|
+
if isinstance(received, Mapping) and "message" in received:
|
|
82
|
+
return ApiError(body=dict(received), status_code=closed.code)
|
|
83
|
+
if closed.code != 1000:
|
|
84
|
+
return ApiError(body=closed.reason, status_code=closed.code)
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AvarthaRealtimeTextToSpeechClient(RealtimeTextToSpeechClient):
|
|
89
|
+
"""Upstream realtime TTS with the gateway-compatible request envelope."""
|
|
90
|
+
|
|
91
|
+
def convert_realtime(
|
|
92
|
+
self,
|
|
93
|
+
voice_id: str,
|
|
94
|
+
*,
|
|
95
|
+
text: Iterator[str],
|
|
96
|
+
model_id: str | None = OMIT,
|
|
97
|
+
output_format: OutputFormat | None = None,
|
|
98
|
+
voice_settings: VoiceSettings | None = OMIT,
|
|
99
|
+
request_options: RequestOptions | None = None,
|
|
100
|
+
) -> Iterator[bytes]:
|
|
101
|
+
"""Stream `text` to `voice_id` and yield raw audio frames.
|
|
102
|
+
|
|
103
|
+
`output_format` defaults to `DEFAULT_OUTPUT_FORMAT`. `voice_settings` is
|
|
104
|
+
forwarded when supplied, but the gateway rejects any non-null value;
|
|
105
|
+
leave it unset unless the platform has gained support for it.
|
|
106
|
+
"""
|
|
107
|
+
with connect(
|
|
108
|
+
build_ws_url(
|
|
109
|
+
self._ws_base_url,
|
|
110
|
+
_path(voice_id, "stream-input"),
|
|
111
|
+
_query(_given(model_id), output_format),
|
|
112
|
+
),
|
|
113
|
+
additional_headers=_headers(self._client_wrapper.get_headers(), request_options),
|
|
114
|
+
) as socket:
|
|
115
|
+
received: Any = None
|
|
116
|
+
try:
|
|
117
|
+
socket.send(_opening(_given(voice_settings)))
|
|
118
|
+
except websockets.exceptions.ConnectionClosedError as closed:
|
|
119
|
+
raise ApiError(body=closed.reason, status_code=closed.code) from closed
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
for chunk in text_chunker(text):
|
|
123
|
+
socket.send(json.dumps({"text": chunk}))
|
|
124
|
+
try:
|
|
125
|
+
received = json.loads(socket.recv(DRAIN_TIMEOUT))
|
|
126
|
+
except TimeoutError:
|
|
127
|
+
continue
|
|
128
|
+
if audio := _audio(received):
|
|
129
|
+
yield audio
|
|
130
|
+
|
|
131
|
+
socket.send(json.dumps({"text": ""}))
|
|
132
|
+
while True:
|
|
133
|
+
received = json.loads(socket.recv())
|
|
134
|
+
if audio := _audio(received):
|
|
135
|
+
yield audio
|
|
136
|
+
except websockets.exceptions.ConnectionClosed as closed:
|
|
137
|
+
if failure := _failure(received, closed):
|
|
138
|
+
raise failure from closed
|
avartha/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.0.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 1)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
avartha/client.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Convenience clients combining protocol clients with Avartha management."""
|
|
2
|
+
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import Any, Self
|
|
6
|
+
|
|
7
|
+
from . import _config
|
|
8
|
+
from ._config import Tier
|
|
9
|
+
from .control import AsyncControl, Control
|
|
10
|
+
from .elevenlabs import AsyncElevenLabs, ElevenLabs
|
|
11
|
+
from .openai import AsyncOpenAI, OpenAI
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Avartha:
|
|
15
|
+
"""Use a platform root URL; protocol-specific clients accept full service URLs.
|
|
16
|
+
|
|
17
|
+
openai_options, elevenlabs_options and control_options are forwarded to
|
|
18
|
+
their respective constructors (for timeouts, transports, retry settings).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
api_key: str | None = None,
|
|
25
|
+
base_url: str | None = None,
|
|
26
|
+
tier: Tier = "serverless",
|
|
27
|
+
elevenlabs_base_url: str | None = None,
|
|
28
|
+
elevenlabs_api_key: str | None = None,
|
|
29
|
+
openai_options: dict[str, Any] | None = None,
|
|
30
|
+
elevenlabs_options: dict[str, Any] | None = None,
|
|
31
|
+
control_options: dict[str, Any] | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
key, root = _config.api_key(api_key), _config.platform_url(base_url)
|
|
34
|
+
self._elevenlabs_base_url = _config.elevenlabs_url(
|
|
35
|
+
elevenlabs_base_url, root=root, tier=tier
|
|
36
|
+
)
|
|
37
|
+
self._elevenlabs_api_key = elevenlabs_api_key if elevenlabs_api_key is not None else key
|
|
38
|
+
self._elevenlabs_options = dict(elevenlabs_options or {})
|
|
39
|
+
self._control_config = {"api_key": key, "base_url": root, **(control_options or {})}
|
|
40
|
+
self.openai = OpenAI(
|
|
41
|
+
api_key=key,
|
|
42
|
+
base_url=_config.openai_url(root, tier),
|
|
43
|
+
**(openai_options or {}),
|
|
44
|
+
)
|
|
45
|
+
# These are the actual upstream resources, preserving their typed methods.
|
|
46
|
+
self.chat = self.openai.chat
|
|
47
|
+
self.responses = self.openai.responses
|
|
48
|
+
self.completions = self.openai.completions
|
|
49
|
+
self.realtime = self.openai.realtime
|
|
50
|
+
self.audio = self.openai.audio
|
|
51
|
+
self.models = self.openai.models
|
|
52
|
+
|
|
53
|
+
@cached_property
|
|
54
|
+
def elevenlabs(self) -> ElevenLabs:
|
|
55
|
+
return ElevenLabs(
|
|
56
|
+
api_key=self._elevenlabs_api_key,
|
|
57
|
+
base_url=self._elevenlabs_base_url,
|
|
58
|
+
**self._elevenlabs_options,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@cached_property
|
|
62
|
+
def control(self) -> Control:
|
|
63
|
+
return Control(**self._control_config)
|
|
64
|
+
|
|
65
|
+
def close(self) -> None:
|
|
66
|
+
try:
|
|
67
|
+
self.openai.close()
|
|
68
|
+
finally:
|
|
69
|
+
try:
|
|
70
|
+
if "elevenlabs" in self.__dict__:
|
|
71
|
+
self.elevenlabs.close()
|
|
72
|
+
finally:
|
|
73
|
+
if "control" in self.__dict__:
|
|
74
|
+
self.control.close()
|
|
75
|
+
|
|
76
|
+
def __enter__(self) -> Self:
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def __exit__(
|
|
80
|
+
self,
|
|
81
|
+
exc_type: type[BaseException] | None,
|
|
82
|
+
exc: BaseException | None,
|
|
83
|
+
traceback: TracebackType | None,
|
|
84
|
+
) -> None:
|
|
85
|
+
self.close()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AsyncAvartha:
|
|
89
|
+
"""Use a platform root URL; protocol-specific clients accept full service URLs.
|
|
90
|
+
|
|
91
|
+
openai_options, elevenlabs_options and control_options are forwarded to
|
|
92
|
+
their respective constructors (for timeouts, transports, retry settings).
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
api_key: str | None = None,
|
|
99
|
+
base_url: str | None = None,
|
|
100
|
+
tier: Tier = "serverless",
|
|
101
|
+
elevenlabs_base_url: str | None = None,
|
|
102
|
+
elevenlabs_api_key: str | None = None,
|
|
103
|
+
openai_options: dict[str, Any] | None = None,
|
|
104
|
+
elevenlabs_options: dict[str, Any] | None = None,
|
|
105
|
+
control_options: dict[str, Any] | None = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
key, root = _config.api_key(api_key), _config.platform_url(base_url)
|
|
108
|
+
self._elevenlabs_base_url = _config.elevenlabs_url(
|
|
109
|
+
elevenlabs_base_url, root=root, tier=tier
|
|
110
|
+
)
|
|
111
|
+
self._elevenlabs_api_key = elevenlabs_api_key if elevenlabs_api_key is not None else key
|
|
112
|
+
self._elevenlabs_options = dict(elevenlabs_options or {})
|
|
113
|
+
self._control_config = {"api_key": key, "base_url": root, **(control_options or {})}
|
|
114
|
+
self.openai = AsyncOpenAI(
|
|
115
|
+
api_key=key,
|
|
116
|
+
base_url=_config.openai_url(root, tier),
|
|
117
|
+
**(openai_options or {}),
|
|
118
|
+
)
|
|
119
|
+
# These are the actual upstream resources, preserving their typed methods.
|
|
120
|
+
self.chat = self.openai.chat
|
|
121
|
+
self.responses = self.openai.responses
|
|
122
|
+
self.completions = self.openai.completions
|
|
123
|
+
self.realtime = self.openai.realtime
|
|
124
|
+
self.audio = self.openai.audio
|
|
125
|
+
self.models = self.openai.models
|
|
126
|
+
|
|
127
|
+
@cached_property
|
|
128
|
+
def elevenlabs(self) -> AsyncElevenLabs:
|
|
129
|
+
return AsyncElevenLabs(
|
|
130
|
+
api_key=self._elevenlabs_api_key,
|
|
131
|
+
base_url=self._elevenlabs_base_url,
|
|
132
|
+
**self._elevenlabs_options,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@cached_property
|
|
136
|
+
def control(self) -> AsyncControl:
|
|
137
|
+
return AsyncControl(**self._control_config)
|
|
138
|
+
|
|
139
|
+
async def close(self) -> None:
|
|
140
|
+
try:
|
|
141
|
+
await self.openai.close()
|
|
142
|
+
finally:
|
|
143
|
+
try:
|
|
144
|
+
if "elevenlabs" in self.__dict__:
|
|
145
|
+
await self.elevenlabs.close()
|
|
146
|
+
finally:
|
|
147
|
+
if "control" in self.__dict__:
|
|
148
|
+
await self.control.close()
|
|
149
|
+
|
|
150
|
+
async def __aenter__(self) -> Self:
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
async def __aexit__(
|
|
154
|
+
self,
|
|
155
|
+
exc_type: type[BaseException] | None,
|
|
156
|
+
exc: BaseException | None,
|
|
157
|
+
traceback: TracebackType | None,
|
|
158
|
+
) -> None:
|
|
159
|
+
await self.close()
|