typecast-python 0.3.10__tar.gz → 0.3.12__tar.gz
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.
- {typecast_python-0.3.10 → typecast_python-0.3.12}/PKG-INFO +12 -10
- {typecast_python-0.3.10 → typecast_python-0.3.12}/pyproject.toml +19 -13
- typecast_python-0.3.12/src/typecast/_httpx_compat.py +191 -0
- typecast_python-0.3.12/src/typecast/_user_agent.py +124 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/async_client.py +62 -13
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/client.py +30 -7
- typecast_python-0.3.10/src/typecast/_user_agent.py +0 -70
- {typecast_python-0.3.10 → typecast_python-0.3.12}/.gitignore +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/LICENSE +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/README.md +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/__init__.py +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/_voice_clone.py +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/composer.py +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/conf.py +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/exceptions.py +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/models/__init__.py +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/models/error.py +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/models/subscription.py +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/models/tts.py +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/models/voices.py +0 -0
- {typecast_python-0.3.10 → typecast_python-0.3.12}/src/typecast/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: typecast-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.12
|
|
4
4
|
Summary: Official Typecast Python SDK - Convert text to lifelike speech using AI-powered voices
|
|
5
5
|
Project-URL: Homepage, https://typecast.ai
|
|
6
6
|
Project-URL: Documentation, https://typecast.ai/docs/overview
|
|
@@ -227,20 +227,22 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
227
227
|
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
228
228
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
229
229
|
Requires-Python: <3.15,>=3.8
|
|
230
|
-
Requires-Dist: aiohttp>=3.
|
|
230
|
+
Requires-Dist: aiohttp>=3.14.1; python_version >= '3.10'
|
|
231
|
+
Requires-Dist: httpx<1,>=0.28.1; python_version < '3.10'
|
|
231
232
|
Requires-Dist: pydantic>=2.0.0
|
|
232
|
-
Requires-Dist: requests>=2.
|
|
233
|
+
Requires-Dist: requests>=2.33.0; python_version >= '3.10'
|
|
233
234
|
Requires-Dist: typing-extensions>=4.0.0
|
|
234
235
|
Provides-Extra: dev
|
|
235
236
|
Requires-Dist: aioresponses>=0.7.6; extra == 'dev'
|
|
236
|
-
Requires-Dist: black<
|
|
237
|
-
Requires-Dist: flake8<
|
|
237
|
+
Requires-Dist: black<27.0.0,>=26.3.1; extra == 'dev'
|
|
238
|
+
Requires-Dist: flake8<8.0.0,>=5.0.0; extra == 'dev'
|
|
239
|
+
Requires-Dist: httpx<1.0.0,>=0.28.1; extra == 'dev'
|
|
238
240
|
Requires-Dist: isort>=5.0.0; extra == 'dev'
|
|
239
|
-
Requires-Dist: mypy<
|
|
240
|
-
Requires-Dist: pytest-asyncio<
|
|
241
|
-
Requires-Dist: pytest-cov<
|
|
241
|
+
Requires-Dist: mypy<2.4.0,>=1.0.0; extra == 'dev'
|
|
242
|
+
Requires-Dist: pytest-asyncio<2.0.0,>=1.4.0; extra == 'dev'
|
|
243
|
+
Requires-Dist: pytest-cov<8.0.0,>=5.0.0; extra == 'dev'
|
|
242
244
|
Requires-Dist: pytest-mock>=3.14.0; extra == 'dev'
|
|
243
|
-
Requires-Dist: pytest<
|
|
245
|
+
Requires-Dist: pytest<10.0.0,>=9.0.3; extra == 'dev'
|
|
244
246
|
Description-Content-Type: text/markdown
|
|
245
247
|
|
|
246
248
|
<div align="center">
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "typecast-python"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.12"
|
|
8
8
|
description = "Official Typecast Python SDK - Convert text to lifelike speech using AI-powered voices"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Neosapience", email = "help@typecast.ai"}
|
|
@@ -33,22 +33,24 @@ classifiers = [
|
|
|
33
33
|
"Operating System :: OS Independent",
|
|
34
34
|
]
|
|
35
35
|
dependencies = [
|
|
36
|
-
"aiohttp>=3.
|
|
37
|
-
"requests>=2.
|
|
36
|
+
"aiohttp>=3.14.1; python_version >= '3.10'",
|
|
37
|
+
"requests>=2.33.0; python_version >= '3.10'",
|
|
38
|
+
"httpx>=0.28.1,<1; python_version < '3.10'",
|
|
38
39
|
"pydantic>=2.0.0",
|
|
39
40
|
"typing-extensions>=4.0.0",
|
|
40
41
|
]
|
|
41
42
|
|
|
42
43
|
[project.optional-dependencies]
|
|
43
44
|
dev = [
|
|
44
|
-
"pytest>=
|
|
45
|
-
"pytest-cov>=5.0.0,<
|
|
45
|
+
"pytest>=9.0.3,<10.0.0",
|
|
46
|
+
"pytest-cov>=5.0.0,<8.0.0",
|
|
46
47
|
"pytest-mock>=3.14.0",
|
|
47
|
-
"pytest-asyncio>=
|
|
48
|
+
"pytest-asyncio>=1.4.0,<2.0.0",
|
|
48
49
|
"aioresponses>=0.7.6",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
50
|
+
"httpx>=0.28.1,<1.0.0",
|
|
51
|
+
"black>=26.3.1,<27.0.0",
|
|
52
|
+
"flake8>=5.0.0,<8.0.0",
|
|
53
|
+
"mypy>=1.0.0,<2.4.0",
|
|
52
54
|
"isort>=5.0.0",
|
|
53
55
|
]
|
|
54
56
|
|
|
@@ -90,15 +92,19 @@ multi_line_output = 3
|
|
|
90
92
|
|
|
91
93
|
[dependency-groups]
|
|
92
94
|
dev = [
|
|
93
|
-
"pytest>=
|
|
94
|
-
"pytest-cov>=5.0.0,<
|
|
95
|
+
"pytest>=9.0.3,<10.0.0",
|
|
96
|
+
"pytest-cov>=5.0.0,<8.0.0",
|
|
95
97
|
"pytest-mock>=3.14.0",
|
|
96
|
-
"pytest-asyncio>=
|
|
98
|
+
"pytest-asyncio>=1.4.0,<2.0.0",
|
|
97
99
|
"aioresponses>=0.7.6",
|
|
98
|
-
"
|
|
100
|
+
"httpx>=0.28.1,<1.0.0",
|
|
101
|
+
"python-dotenv>=1.2.2,<2.0.0",
|
|
99
102
|
"ruff>=0.14.0",
|
|
100
103
|
]
|
|
101
104
|
|
|
105
|
+
[tool.uv]
|
|
106
|
+
environments = ["python_version >= '3.10'"]
|
|
107
|
+
|
|
102
108
|
[tool.pytest.ini_options]
|
|
103
109
|
asyncio_mode = "auto"
|
|
104
110
|
testpaths = ["tests"]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _timeout(value: Any) -> Any:
|
|
9
|
+
if isinstance(value, ClientTimeout):
|
|
10
|
+
total = value.total or value.sock_read or 5.0
|
|
11
|
+
return httpx.Timeout(
|
|
12
|
+
total, connect=value.connect or value.sock_connect or total
|
|
13
|
+
)
|
|
14
|
+
if isinstance(value, tuple):
|
|
15
|
+
return httpx.Timeout(value[1], connect=value[0])
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _headers(defaults: Dict[str, str], supplied: Optional[dict]) -> Dict[str, str]:
|
|
20
|
+
headers = dict(defaults)
|
|
21
|
+
for key, value in (supplied or {}).items():
|
|
22
|
+
if value is None:
|
|
23
|
+
headers.pop(key, None)
|
|
24
|
+
else:
|
|
25
|
+
headers[key] = value
|
|
26
|
+
return headers
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RequestsCompatResponse:
|
|
30
|
+
def __init__(self, response: httpx.Response):
|
|
31
|
+
self._response = response
|
|
32
|
+
|
|
33
|
+
def __getattr__(self, name: str) -> Any:
|
|
34
|
+
return getattr(self._response, name)
|
|
35
|
+
|
|
36
|
+
def iter_content(self, chunk_size: int) -> Iterator[bytes]:
|
|
37
|
+
return self._response.iter_bytes(chunk_size)
|
|
38
|
+
|
|
39
|
+
def close(self) -> None:
|
|
40
|
+
self._response.close()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RequestsCompatSession:
|
|
44
|
+
def __init__(self, client: Optional[httpx.Client] = None):
|
|
45
|
+
self._client = client or httpx.Client(timeout=None)
|
|
46
|
+
self.headers: Dict[str, str] = {}
|
|
47
|
+
|
|
48
|
+
def _request(self, method: str, url: str, **kwargs: Any) -> RequestsCompatResponse:
|
|
49
|
+
stream = kwargs.pop("stream", False)
|
|
50
|
+
kwargs["headers"] = _headers(self.headers, kwargs.get("headers"))
|
|
51
|
+
if "timeout" in kwargs:
|
|
52
|
+
kwargs["timeout"] = _timeout(kwargs["timeout"])
|
|
53
|
+
if stream:
|
|
54
|
+
request = self._client.build_request(method, url, **kwargs)
|
|
55
|
+
response = self._client.send(request, stream=True)
|
|
56
|
+
else:
|
|
57
|
+
response = self._client.request(method, url, **kwargs)
|
|
58
|
+
return RequestsCompatResponse(response)
|
|
59
|
+
|
|
60
|
+
def get(self, url: str, **kwargs: Any) -> RequestsCompatResponse:
|
|
61
|
+
return self._request("GET", url, **kwargs)
|
|
62
|
+
|
|
63
|
+
def post(self, url: str, **kwargs: Any) -> RequestsCompatResponse:
|
|
64
|
+
return self._request("POST", url, **kwargs)
|
|
65
|
+
|
|
66
|
+
def delete(self, url: str, **kwargs: Any) -> RequestsCompatResponse:
|
|
67
|
+
return self._request("DELETE", url, **kwargs)
|
|
68
|
+
|
|
69
|
+
def close(self) -> None:
|
|
70
|
+
self._client.close()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ClientTimeout:
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
total: Optional[float] = None,
|
|
78
|
+
connect: Optional[float] = None,
|
|
79
|
+
sock_connect: Optional[float] = None,
|
|
80
|
+
sock_read: Optional[float] = None,
|
|
81
|
+
):
|
|
82
|
+
self.total = total
|
|
83
|
+
self.connect = connect
|
|
84
|
+
self.sock_connect = sock_connect
|
|
85
|
+
self.sock_read = sock_read
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FormData:
|
|
89
|
+
def __init__(self):
|
|
90
|
+
self.fields: List[Tuple[str, Any, Optional[str], Optional[str]]] = []
|
|
91
|
+
|
|
92
|
+
def add_field(
|
|
93
|
+
self,
|
|
94
|
+
name: str,
|
|
95
|
+
value: Any,
|
|
96
|
+
*,
|
|
97
|
+
filename: Optional[str] = None,
|
|
98
|
+
content_type: Optional[str] = None,
|
|
99
|
+
) -> None:
|
|
100
|
+
self.fields.append((name, value, filename, content_type))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class _AsyncContent:
|
|
104
|
+
def __init__(self, response: httpx.Response):
|
|
105
|
+
self._response = response
|
|
106
|
+
|
|
107
|
+
async def iter_chunked(self, chunk_size: int) -> AsyncIterator[bytes]:
|
|
108
|
+
async for chunk in self._response.aiter_bytes(chunk_size):
|
|
109
|
+
yield chunk
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AiohttpCompatResponse:
|
|
113
|
+
def __init__(self, response: httpx.Response):
|
|
114
|
+
self._response = response
|
|
115
|
+
self.status = response.status_code
|
|
116
|
+
self.headers = response.headers
|
|
117
|
+
self.content = _AsyncContent(response)
|
|
118
|
+
|
|
119
|
+
async def read(self) -> bytes:
|
|
120
|
+
return await self._response.aread()
|
|
121
|
+
|
|
122
|
+
async def text(self) -> str:
|
|
123
|
+
await self._response.aread()
|
|
124
|
+
return self._response.text
|
|
125
|
+
|
|
126
|
+
async def json(self) -> Any:
|
|
127
|
+
await self._response.aread()
|
|
128
|
+
return self._response.json()
|
|
129
|
+
|
|
130
|
+
async def close(self) -> None:
|
|
131
|
+
await self._response.aclose()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class _AsyncRequestContext:
|
|
135
|
+
def __init__(self, request: Any):
|
|
136
|
+
self._request = request
|
|
137
|
+
self._response: Optional[AiohttpCompatResponse] = None
|
|
138
|
+
|
|
139
|
+
async def __aenter__(self) -> AiohttpCompatResponse:
|
|
140
|
+
response = await self._request
|
|
141
|
+
self._response = response
|
|
142
|
+
return response
|
|
143
|
+
|
|
144
|
+
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
145
|
+
if self._response:
|
|
146
|
+
await self._response.close()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class AiohttpCompatSession:
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
headers: Optional[dict] = None,
|
|
153
|
+
client: Optional[httpx.AsyncClient] = None,
|
|
154
|
+
):
|
|
155
|
+
self._client = client or httpx.AsyncClient(timeout=300)
|
|
156
|
+
self.headers = dict(headers or {})
|
|
157
|
+
|
|
158
|
+
async def _request(
|
|
159
|
+
self, method: str, url: str, **kwargs: Any
|
|
160
|
+
) -> AiohttpCompatResponse:
|
|
161
|
+
kwargs["headers"] = _headers(self.headers, kwargs.get("headers"))
|
|
162
|
+
if "timeout" in kwargs:
|
|
163
|
+
kwargs["timeout"] = _timeout(kwargs["timeout"])
|
|
164
|
+
form = kwargs.get("data")
|
|
165
|
+
if isinstance(form, FormData):
|
|
166
|
+
kwargs.pop("data")
|
|
167
|
+
kwargs["data"] = {
|
|
168
|
+
name: value
|
|
169
|
+
for name, value, filename, _ in form.fields
|
|
170
|
+
if filename is None
|
|
171
|
+
}
|
|
172
|
+
kwargs["files"] = [
|
|
173
|
+
(name, (filename, value, content_type))
|
|
174
|
+
for name, value, filename, content_type in form.fields
|
|
175
|
+
if filename is not None
|
|
176
|
+
]
|
|
177
|
+
request = self._client.build_request(method, url, **kwargs)
|
|
178
|
+
response = await self._client.send(request, stream=True)
|
|
179
|
+
return AiohttpCompatResponse(response)
|
|
180
|
+
|
|
181
|
+
def get(self, url: str, **kwargs: Any) -> _AsyncRequestContext:
|
|
182
|
+
return _AsyncRequestContext(self._request("GET", url, **kwargs))
|
|
183
|
+
|
|
184
|
+
def post(self, url: str, **kwargs: Any) -> _AsyncRequestContext:
|
|
185
|
+
return _AsyncRequestContext(self._request("POST", url, **kwargs))
|
|
186
|
+
|
|
187
|
+
def delete(self, url: str, **kwargs: Any) -> _AsyncRequestContext:
|
|
188
|
+
return _AsyncRequestContext(self._request("DELETE", url, **kwargs))
|
|
189
|
+
|
|
190
|
+
async def close(self) -> None:
|
|
191
|
+
await self._client.aclose()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
from importlib import metadata
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from . import conf
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_GENERATED_BY_PATTERN = re.compile(r"[a-z0-9][a-z0-9._-]{0,31}\Z")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def attribution_suffix(
|
|
14
|
+
source: Optional[str] = None,
|
|
15
|
+
generated_by: Optional[str] = None,
|
|
16
|
+
) -> str:
|
|
17
|
+
if source is None and generated_by is None:
|
|
18
|
+
return ""
|
|
19
|
+
if (
|
|
20
|
+
not isinstance(source, str)
|
|
21
|
+
or not isinstance(generated_by, str)
|
|
22
|
+
or source not in {"llms", "skill"}
|
|
23
|
+
or not generated_by
|
|
24
|
+
):
|
|
25
|
+
raise ValueError("source (llms or skill) and generated_by must be provided together")
|
|
26
|
+
if not _GENERATED_BY_PATTERN.fullmatch(generated_by):
|
|
27
|
+
raise ValueError("generated_by must be a lowercase token of at most 32 characters")
|
|
28
|
+
return f" typecast-integration/1 (source={source}; generated_by={generated_by})"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _package_version(package_name: str) -> str:
|
|
32
|
+
try:
|
|
33
|
+
return metadata.version(package_name)
|
|
34
|
+
except metadata.PackageNotFoundError:
|
|
35
|
+
return "dev"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _os_name() -> str:
|
|
39
|
+
system = platform.system().lower()
|
|
40
|
+
if system == "darwin":
|
|
41
|
+
return "macos"
|
|
42
|
+
if system.startswith("windows"):
|
|
43
|
+
return "windows"
|
|
44
|
+
return system or "unknown"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _arch_name() -> str:
|
|
48
|
+
machine = platform.machine().lower()
|
|
49
|
+
if machine in {"x86_64", "amd64"}:
|
|
50
|
+
return "x64"
|
|
51
|
+
if machine in {"aarch64", "arm64"}:
|
|
52
|
+
return "arm64"
|
|
53
|
+
return machine or "unknown"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_user_agent(
|
|
57
|
+
*,
|
|
58
|
+
mode: str,
|
|
59
|
+
http_library: str,
|
|
60
|
+
host: str,
|
|
61
|
+
transport: str = "rest",
|
|
62
|
+
source: Optional[str] = None,
|
|
63
|
+
generated_by: Optional[str] = None,
|
|
64
|
+
) -> str:
|
|
65
|
+
sdk_version = _package_version("typecast-python")
|
|
66
|
+
base = "default" if conf.is_default_host(host) else "custom"
|
|
67
|
+
return (
|
|
68
|
+
f"typecast-python/{sdk_version} "
|
|
69
|
+
f"Python/{sys.version_info.major}.{sys.version_info.minor} "
|
|
70
|
+
f"{platform.python_implementation()}/{platform.python_version()} "
|
|
71
|
+
f"{http_library} "
|
|
72
|
+
f"(mode={mode}; base={base}; transport={transport}; "
|
|
73
|
+
f"os={_os_name()}; arch={_arch_name()}; sdk_env=python; platform=server)"
|
|
74
|
+
f"{attribution_suffix(source, generated_by)}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def requests_user_agent(
|
|
79
|
+
host: str,
|
|
80
|
+
transport: str = "rest",
|
|
81
|
+
source: Optional[str] = None,
|
|
82
|
+
generated_by: Optional[str] = None,
|
|
83
|
+
) -> str:
|
|
84
|
+
return build_user_agent(
|
|
85
|
+
mode="sync",
|
|
86
|
+
http_library=f"requests/{_package_version('requests')}",
|
|
87
|
+
host=host,
|
|
88
|
+
transport=transport,
|
|
89
|
+
source=source,
|
|
90
|
+
generated_by=generated_by,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def aiohttp_user_agent(
|
|
95
|
+
host: str,
|
|
96
|
+
transport: str = "rest",
|
|
97
|
+
source: Optional[str] = None,
|
|
98
|
+
generated_by: Optional[str] = None,
|
|
99
|
+
) -> str:
|
|
100
|
+
return build_user_agent(
|
|
101
|
+
mode="async",
|
|
102
|
+
http_library=f"aiohttp/{_package_version('aiohttp')}",
|
|
103
|
+
host=host,
|
|
104
|
+
transport=transport,
|
|
105
|
+
source=source,
|
|
106
|
+
generated_by=generated_by,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def httpx_user_agent(
|
|
111
|
+
host: str,
|
|
112
|
+
mode: str,
|
|
113
|
+
transport: str = "rest",
|
|
114
|
+
source: Optional[str] = None,
|
|
115
|
+
generated_by: Optional[str] = None,
|
|
116
|
+
) -> str:
|
|
117
|
+
return build_user_agent(
|
|
118
|
+
mode=mode,
|
|
119
|
+
http_library=f"httpx/{_package_version('httpx')}",
|
|
120
|
+
host=host,
|
|
121
|
+
transport=transport,
|
|
122
|
+
source=source,
|
|
123
|
+
generated_by=generated_by,
|
|
124
|
+
)
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import AsyncIterator, BinaryIO, Optional, Union
|
|
6
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator, BinaryIO, Optional, Union
|
|
6
7
|
from urllib.parse import quote
|
|
7
8
|
|
|
8
|
-
import
|
|
9
|
+
if sys.version_info >= (3, 10): # pragma: no cover - version-specific import
|
|
10
|
+
import aiohttp
|
|
11
|
+
else: # pragma: no cover
|
|
12
|
+
aiohttp = None # type: ignore[assignment]
|
|
9
13
|
|
|
10
14
|
from . import conf
|
|
11
15
|
from ._voice_clone import (
|
|
@@ -13,7 +17,10 @@ from ._voice_clone import (
|
|
|
13
17
|
validate_clone_inputs,
|
|
14
18
|
validate_custom_voice_id,
|
|
15
19
|
)
|
|
16
|
-
from ._user_agent import aiohttp_user_agent
|
|
20
|
+
from ._user_agent import aiohttp_user_agent, attribution_suffix, httpx_user_agent
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING or sys.version_info < (3, 10): # pragma: no cover
|
|
23
|
+
from ._httpx_compat import AiohttpCompatSession, ClientTimeout, FormData
|
|
17
24
|
from .client import _guess_audio_mime
|
|
18
25
|
from .client import _output_with_inferred_format
|
|
19
26
|
from .client import _validate_output_path
|
|
@@ -69,7 +76,9 @@ class AsyncTypecast:
|
|
|
69
76
|
self,
|
|
70
77
|
host: Optional[str] = None,
|
|
71
78
|
api_key: Optional[str] = None,
|
|
72
|
-
session: Optional[
|
|
79
|
+
session: Optional[Any] = None,
|
|
80
|
+
source: Optional[str] = None,
|
|
81
|
+
generated_by: Optional[str] = None,
|
|
73
82
|
):
|
|
74
83
|
"""Initialize the async Typecast client.
|
|
75
84
|
|
|
@@ -81,6 +90,8 @@ class AsyncTypecast:
|
|
|
81
90
|
__aenter__ will not create a new session and __aexit__ will not close it
|
|
82
91
|
(the caller owns its lifecycle). Auth headers (`X-API-KEY`, `User-Agent`)
|
|
83
92
|
are attached per-request via `_request_headers()`.
|
|
93
|
+
source: Integration source, either 'llms' or 'skill'.
|
|
94
|
+
generated_by: Lowercase token identifying the coding agent.
|
|
84
95
|
|
|
85
96
|
Raises:
|
|
86
97
|
ValueError: If no API key is provided and TYPECAST_API_KEY is not set
|
|
@@ -88,19 +99,39 @@ class AsyncTypecast:
|
|
|
88
99
|
"""
|
|
89
100
|
self.host = conf.get_host(host)
|
|
90
101
|
self.api_key = conf.get_api_key(api_key)
|
|
102
|
+
attribution_suffix(source, generated_by)
|
|
103
|
+
self.source = source
|
|
104
|
+
self.generated_by = generated_by
|
|
91
105
|
if not self.api_key and conf.is_default_host(self.host):
|
|
92
106
|
raise ValueError("API key is required for the default Typecast API host")
|
|
93
107
|
self._owns_session = session is None
|
|
94
|
-
self.session: Optional[
|
|
108
|
+
self.session: Optional[Any] = session
|
|
95
109
|
|
|
96
110
|
async def __aenter__(self):
|
|
97
111
|
# When an external session is injected, do not create a new one.
|
|
98
112
|
# Per-request auth headers are attached via _request_headers() on each call.
|
|
99
113
|
if self.session is None:
|
|
100
|
-
headers = {
|
|
114
|
+
headers = {
|
|
115
|
+
"User-Agent": (
|
|
116
|
+
aiohttp_user_agent(
|
|
117
|
+
self.host, source=self.source, generated_by=self.generated_by
|
|
118
|
+
)
|
|
119
|
+
if aiohttp
|
|
120
|
+
else httpx_user_agent(
|
|
121
|
+
self.host,
|
|
122
|
+
"async",
|
|
123
|
+
source=self.source,
|
|
124
|
+
generated_by=self.generated_by,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
}
|
|
101
128
|
if self.api_key:
|
|
102
129
|
headers["X-API-KEY"] = self.api_key
|
|
103
|
-
self.session =
|
|
130
|
+
self.session = (
|
|
131
|
+
aiohttp.ClientSession(headers=headers)
|
|
132
|
+
if aiohttp
|
|
133
|
+
else AiohttpCompatSession(headers=headers)
|
|
134
|
+
)
|
|
104
135
|
return self
|
|
105
136
|
|
|
106
137
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
@@ -140,7 +171,11 @@ class AsyncTypecast:
|
|
|
140
171
|
"""
|
|
141
172
|
if self._owns_session:
|
|
142
173
|
return None
|
|
143
|
-
headers = {
|
|
174
|
+
headers = {
|
|
175
|
+
"User-Agent": aiohttp_user_agent(
|
|
176
|
+
self.host, source=self.source, generated_by=self.generated_by
|
|
177
|
+
)
|
|
178
|
+
}
|
|
144
179
|
if self.api_key:
|
|
145
180
|
headers["X-API-KEY"] = self.api_key
|
|
146
181
|
return headers
|
|
@@ -247,7 +282,11 @@ class AsyncTypecast:
|
|
|
247
282
|
if not self.session:
|
|
248
283
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
249
284
|
endpoint = "/v1/text-to-speech/stream"
|
|
250
|
-
stream_timeout =
|
|
285
|
+
stream_timeout = (
|
|
286
|
+
aiohttp.ClientTimeout(sock_connect=10, sock_read=300)
|
|
287
|
+
if aiohttp
|
|
288
|
+
else ClientTimeout(sock_connect=10, sock_read=300)
|
|
289
|
+
)
|
|
251
290
|
async with self.session.post(
|
|
252
291
|
f"{self.host}{endpoint}",
|
|
253
292
|
json=request.model_dump(exclude_none=True),
|
|
@@ -258,7 +297,9 @@ class AsyncTypecast:
|
|
|
258
297
|
error_text = await response.text()
|
|
259
298
|
self._handle_error(response.status, error_text)
|
|
260
299
|
|
|
261
|
-
async for chunk in response.content.iter_chunked(
|
|
300
|
+
async for chunk in response.content.iter_chunked(
|
|
301
|
+
chunk_size
|
|
302
|
+
): # pragma: no branch
|
|
262
303
|
yield chunk
|
|
263
304
|
|
|
264
305
|
async def text_to_speech_with_timestamps(
|
|
@@ -332,7 +373,7 @@ class AsyncTypecast:
|
|
|
332
373
|
audio_bytes, filename = validate_clone_inputs(audio, name)
|
|
333
374
|
model_str = normalize_clone_model(model)
|
|
334
375
|
|
|
335
|
-
form = aiohttp.FormData()
|
|
376
|
+
form: Any = aiohttp.FormData() if aiohttp else FormData()
|
|
336
377
|
form.add_field("name", name)
|
|
337
378
|
form.add_field("model", model_str)
|
|
338
379
|
form.add_field(
|
|
@@ -341,7 +382,11 @@ class AsyncTypecast:
|
|
|
341
382
|
filename=filename,
|
|
342
383
|
content_type=_guess_audio_mime(filename),
|
|
343
384
|
)
|
|
344
|
-
timeout =
|
|
385
|
+
timeout = (
|
|
386
|
+
aiohttp.ClientTimeout(total=300, connect=10)
|
|
387
|
+
if aiohttp
|
|
388
|
+
else ClientTimeout(total=300, connect=10)
|
|
389
|
+
)
|
|
345
390
|
async with self.session.post(
|
|
346
391
|
f"{self.host}/v1/voices/clone",
|
|
347
392
|
data=form,
|
|
@@ -367,7 +412,11 @@ class AsyncTypecast:
|
|
|
367
412
|
raise TypecastError("Client session not initialized; use 'async with'.")
|
|
368
413
|
|
|
369
414
|
validate_custom_voice_id(voice_id)
|
|
370
|
-
timeout =
|
|
415
|
+
timeout = (
|
|
416
|
+
aiohttp.ClientTimeout(total=60, connect=10)
|
|
417
|
+
if aiohttp
|
|
418
|
+
else ClientTimeout(total=60, connect=10)
|
|
419
|
+
)
|
|
371
420
|
async with self.session.delete(
|
|
372
421
|
f"{self.host}/v1/voices/{quote(voice_id, safe='')}",
|
|
373
422
|
timeout=timeout,
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from typing import BinaryIO, Iterator, Optional, Union
|
|
5
|
+
from typing import TYPE_CHECKING, Any, BinaryIO, Iterator, Optional, Union
|
|
5
6
|
from urllib.parse import quote
|
|
6
7
|
|
|
7
|
-
import
|
|
8
|
+
if sys.version_info >= (3, 10): # pragma: no cover - version-specific import
|
|
9
|
+
import requests
|
|
10
|
+
else: # pragma: no cover
|
|
11
|
+
requests = None # type: ignore[assignment]
|
|
8
12
|
|
|
9
13
|
from . import conf
|
|
10
14
|
from ._voice_clone import (
|
|
@@ -12,7 +16,10 @@ from ._voice_clone import (
|
|
|
12
16
|
validate_clone_inputs,
|
|
13
17
|
validate_custom_voice_id,
|
|
14
18
|
)
|
|
15
|
-
from ._user_agent import requests_user_agent
|
|
19
|
+
from ._user_agent import attribution_suffix, httpx_user_agent, requests_user_agent
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING or sys.version_info < (3, 10): # pragma: no cover
|
|
22
|
+
from ._httpx_compat import RequestsCompatSession
|
|
16
23
|
from .composer import SpeechComposer
|
|
17
24
|
from .exceptions import (
|
|
18
25
|
BadRequestError,
|
|
@@ -103,7 +110,9 @@ class Typecast:
|
|
|
103
110
|
self,
|
|
104
111
|
host: Optional[str] = None,
|
|
105
112
|
api_key: Optional[str] = None,
|
|
106
|
-
session: Optional[
|
|
113
|
+
session: Optional[Any] = None,
|
|
114
|
+
source: Optional[str] = None,
|
|
115
|
+
generated_by: Optional[str] = None,
|
|
107
116
|
):
|
|
108
117
|
"""Initialize the Typecast client.
|
|
109
118
|
|
|
@@ -115,22 +124,32 @@ class Typecast:
|
|
|
115
124
|
the client will not create a new session nor close it; auth headers
|
|
116
125
|
(`X-API-KEY`, `User-Agent`) are attached per-request via
|
|
117
126
|
`_request_headers()`.
|
|
127
|
+
source: Integration source, either 'llms' or 'skill'.
|
|
128
|
+
generated_by: Lowercase token identifying the coding agent.
|
|
118
129
|
|
|
119
130
|
Raises:
|
|
120
131
|
ValueError: If no API key is provided and TYPECAST_API_KEY is not set.
|
|
121
132
|
"""
|
|
122
133
|
self.host = conf.get_host(host)
|
|
123
134
|
self.api_key = conf.get_api_key(api_key)
|
|
135
|
+
attribution_suffix(source, generated_by)
|
|
136
|
+
self.source = source
|
|
137
|
+
self.generated_by = generated_by
|
|
124
138
|
if not self.api_key and conf.is_default_host(self.host):
|
|
125
139
|
raise ValueError("API key is required for the default Typecast API host")
|
|
126
140
|
self._owns_session = session is None
|
|
141
|
+
self.session: Any
|
|
127
142
|
if session is not None:
|
|
128
143
|
self.session = session
|
|
129
144
|
else:
|
|
130
|
-
self.session = requests.Session()
|
|
145
|
+
self.session = requests.Session() if requests else RequestsCompatSession()
|
|
131
146
|
headers = {
|
|
132
147
|
"Content-Type": "application/json",
|
|
133
|
-
"User-Agent":
|
|
148
|
+
"User-Agent": (
|
|
149
|
+
requests_user_agent(self.host, source=source, generated_by=generated_by)
|
|
150
|
+
if requests
|
|
151
|
+
else httpx_user_agent(self.host, "sync", source=source, generated_by=generated_by)
|
|
152
|
+
),
|
|
134
153
|
}
|
|
135
154
|
if self.api_key:
|
|
136
155
|
headers["X-API-KEY"] = self.api_key
|
|
@@ -167,7 +186,11 @@ class Typecast:
|
|
|
167
186
|
"""
|
|
168
187
|
if self._owns_session:
|
|
169
188
|
return None
|
|
170
|
-
headers = {
|
|
189
|
+
headers = {
|
|
190
|
+
"User-Agent": requests_user_agent(
|
|
191
|
+
self.host, source=self.source, generated_by=self.generated_by
|
|
192
|
+
)
|
|
193
|
+
}
|
|
171
194
|
if self.api_key:
|
|
172
195
|
headers["X-API-KEY"] = self.api_key
|
|
173
196
|
return headers
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import platform
|
|
2
|
-
import sys
|
|
3
|
-
from importlib import metadata
|
|
4
|
-
|
|
5
|
-
import aiohttp
|
|
6
|
-
import requests
|
|
7
|
-
|
|
8
|
-
from . import conf
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _package_version(package_name: str) -> str:
|
|
12
|
-
try:
|
|
13
|
-
return metadata.version(package_name)
|
|
14
|
-
except metadata.PackageNotFoundError:
|
|
15
|
-
return "dev"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _os_name() -> str:
|
|
19
|
-
system = platform.system().lower()
|
|
20
|
-
if system == "darwin":
|
|
21
|
-
return "macos"
|
|
22
|
-
if system.startswith("windows"):
|
|
23
|
-
return "windows"
|
|
24
|
-
return system or "unknown"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _arch_name() -> str:
|
|
28
|
-
machine = platform.machine().lower()
|
|
29
|
-
if machine in {"x86_64", "amd64"}:
|
|
30
|
-
return "x64"
|
|
31
|
-
if machine in {"aarch64", "arm64"}:
|
|
32
|
-
return "arm64"
|
|
33
|
-
return machine or "unknown"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def build_user_agent(
|
|
37
|
-
*,
|
|
38
|
-
mode: str,
|
|
39
|
-
http_library: str,
|
|
40
|
-
host: str,
|
|
41
|
-
transport: str = "rest",
|
|
42
|
-
) -> str:
|
|
43
|
-
sdk_version = _package_version("typecast-python")
|
|
44
|
-
base = "default" if conf.is_default_host(host) else "custom"
|
|
45
|
-
return (
|
|
46
|
-
f"typecast-python/{sdk_version} "
|
|
47
|
-
f"Python/{sys.version_info.major}.{sys.version_info.minor} "
|
|
48
|
-
f"{platform.python_implementation()}/{platform.python_version()} "
|
|
49
|
-
f"{http_library} "
|
|
50
|
-
f"(mode={mode}; base={base}; transport={transport}; "
|
|
51
|
-
f"os={_os_name()}; arch={_arch_name()}; sdk_env=python; platform=server)"
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def requests_user_agent(host: str, transport: str = "rest") -> str:
|
|
56
|
-
return build_user_agent(
|
|
57
|
-
mode="sync",
|
|
58
|
-
http_library=f"requests/{requests.__version__}",
|
|
59
|
-
host=host,
|
|
60
|
-
transport=transport,
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def aiohttp_user_agent(host: str, transport: str = "rest") -> str:
|
|
65
|
-
return build_user_agent(
|
|
66
|
-
mode="async",
|
|
67
|
-
http_library=f"aiohttp/{aiohttp.__version__}",
|
|
68
|
-
host=host,
|
|
69
|
-
transport=transport,
|
|
70
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|