typecast-python 0.3.6__tar.gz → 0.3.8__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.6 → typecast_python-0.3.8}/PKG-INFO +1 -1
- {typecast_python-0.3.6 → typecast_python-0.3.8}/pyproject.toml +4 -1
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/__init__.py +2 -0
- typecast_python-0.3.8/src/typecast/_user_agent.py +70 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/async_client.py +92 -17
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/client.py +95 -15
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/models/__init__.py +2 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/models/voices.py +13 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/.gitignore +0 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/LICENSE +0 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/README.md +0 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/_voice_clone.py +0 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/composer.py +0 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/conf.py +0 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/exceptions.py +0 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/models/error.py +0 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/models/subscription.py +0 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/models/tts.py +0 -0
- {typecast_python-0.3.6 → typecast_python-0.3.8}/src/typecast/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: typecast-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
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
|
|
@@ -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.8"
|
|
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"}
|
|
@@ -98,6 +98,9 @@ dev = [
|
|
|
98
98
|
[tool.pytest.ini_options]
|
|
99
99
|
asyncio_mode = "auto"
|
|
100
100
|
testpaths = ["tests"]
|
|
101
|
+
markers = [
|
|
102
|
+
"e2e: end-to-end tests against the real Typecast API (require credentials)",
|
|
103
|
+
]
|
|
101
104
|
filterwarnings = [
|
|
102
105
|
"ignore::DeprecationWarning",
|
|
103
106
|
]
|
|
@@ -19,6 +19,7 @@ from .models import (
|
|
|
19
19
|
OutputStream,
|
|
20
20
|
PlanTier,
|
|
21
21
|
Prompt,
|
|
22
|
+
RecommendedVoice,
|
|
22
23
|
SubscriptionResponse,
|
|
23
24
|
TTSRequest,
|
|
24
25
|
TTSRequestStream,
|
|
@@ -48,6 +49,7 @@ __all__ = [
|
|
|
48
49
|
"OutputStream",
|
|
49
50
|
"PlanTier",
|
|
50
51
|
"Prompt",
|
|
52
|
+
"RecommendedVoice",
|
|
51
53
|
"SubscriptionResponse",
|
|
52
54
|
"TTSRequest",
|
|
53
55
|
"TTSRequestStream",
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
)
|
|
@@ -11,6 +11,7 @@ from ._voice_clone import (
|
|
|
11
11
|
validate_clone_inputs,
|
|
12
12
|
validate_custom_voice_id,
|
|
13
13
|
)
|
|
14
|
+
from ._user_agent import aiohttp_user_agent
|
|
14
15
|
from .client import _guess_audio_mime
|
|
15
16
|
from .client import _output_with_inferred_format
|
|
16
17
|
from .client import _validate_output_path
|
|
@@ -28,6 +29,7 @@ from .models import (
|
|
|
28
29
|
CustomVoice,
|
|
29
30
|
LanguageCode,
|
|
30
31
|
Output,
|
|
32
|
+
RecommendedVoice,
|
|
31
33
|
SubscriptionResponse,
|
|
32
34
|
TTSModel,
|
|
33
35
|
TTSPrompt,
|
|
@@ -61,34 +63,48 @@ class AsyncTypecast:
|
|
|
61
63
|
... f.write(response.audio_data)
|
|
62
64
|
"""
|
|
63
65
|
|
|
64
|
-
def __init__(
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
host: Optional[str] = None,
|
|
69
|
+
api_key: Optional[str] = None,
|
|
70
|
+
session: Optional[aiohttp.ClientSession] = None,
|
|
71
|
+
):
|
|
65
72
|
"""Initialize the async Typecast client.
|
|
66
73
|
|
|
67
74
|
Args:
|
|
68
75
|
host: API host URL. Defaults to TYPECAST_API_HOST env var
|
|
69
76
|
or 'https://api.typecast.ai'.
|
|
70
77
|
api_key: API key for authentication. Defaults to TYPECAST_API_KEY env var.
|
|
78
|
+
session: Optional externally-managed aiohttp.ClientSession. When provided,
|
|
79
|
+
__aenter__ will not create a new session and __aexit__ will not close it
|
|
80
|
+
(the caller owns its lifecycle). Auth headers (`X-API-KEY`, `User-Agent`)
|
|
81
|
+
are attached per-request via `_request_headers()`.
|
|
71
82
|
|
|
72
83
|
Raises:
|
|
73
|
-
ValueError: If no API key is provided and TYPECAST_API_KEY is not set
|
|
84
|
+
ValueError: If no API key is provided and TYPECAST_API_KEY is not set
|
|
85
|
+
for the default host.
|
|
74
86
|
"""
|
|
75
87
|
self.host = conf.get_host(host)
|
|
76
88
|
self.api_key = conf.get_api_key(api_key)
|
|
77
89
|
if not self.api_key and conf.is_default_host(self.host):
|
|
78
90
|
raise ValueError("API key is required for the default Typecast API host")
|
|
79
|
-
self.session
|
|
91
|
+
self._owns_session = session is None
|
|
92
|
+
self.session: Optional[aiohttp.ClientSession] = session
|
|
80
93
|
|
|
81
94
|
async def __aenter__(self):
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
# When an external session is injected, do not create a new one.
|
|
96
|
+
# Per-request auth headers are attached via _request_headers() on each call.
|
|
97
|
+
if self.session is None:
|
|
98
|
+
headers = {"User-Agent": aiohttp_user_agent(self.host)}
|
|
99
|
+
if self.api_key:
|
|
100
|
+
headers["X-API-KEY"] = self.api_key
|
|
101
|
+
self.session = aiohttp.ClientSession(headers=headers)
|
|
88
102
|
return self
|
|
89
103
|
|
|
90
104
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
91
|
-
|
|
105
|
+
# Only close sessions we created. External sessions are caller-owned;
|
|
106
|
+
# leave the reference intact so the client can be re-entered.
|
|
107
|
+
if self.session and self._owns_session:
|
|
92
108
|
await self.session.close()
|
|
93
109
|
|
|
94
110
|
def _handle_error(self, status_code: int, response_text: str):
|
|
@@ -113,6 +129,20 @@ class AsyncTypecast:
|
|
|
113
129
|
status_code=status_code,
|
|
114
130
|
)
|
|
115
131
|
|
|
132
|
+
def _request_headers(self) -> Optional[dict]:
|
|
133
|
+
"""Headers to attach to each individual request.
|
|
134
|
+
|
|
135
|
+
For owned sessions, auth is set at session scope, so return None and let
|
|
136
|
+
aiohttp use the session headers. For external sessions, the session has
|
|
137
|
+
no auth headers, so we attach X-API-KEY and User-Agent per-request.
|
|
138
|
+
"""
|
|
139
|
+
if self._owns_session:
|
|
140
|
+
return None
|
|
141
|
+
headers = {"User-Agent": aiohttp_user_agent(self.host)}
|
|
142
|
+
if self.api_key:
|
|
143
|
+
headers["X-API-KEY"] = self.api_key
|
|
144
|
+
return headers
|
|
145
|
+
|
|
116
146
|
async def text_to_speech(self, request: TTSRequest) -> TTSResponse:
|
|
117
147
|
"""Convert text to speech asynchronously.
|
|
118
148
|
|
|
@@ -134,7 +164,9 @@ class AsyncTypecast:
|
|
|
134
164
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
135
165
|
endpoint = "/v1/text-to-speech"
|
|
136
166
|
async with self.session.post(
|
|
137
|
-
f"{self.host}{endpoint}",
|
|
167
|
+
f"{self.host}{endpoint}",
|
|
168
|
+
json=request.model_dump(exclude_none=True),
|
|
169
|
+
headers=self._request_headers(),
|
|
138
170
|
) as response:
|
|
139
171
|
if response.status != 200:
|
|
140
172
|
error_text = await response.text()
|
|
@@ -203,7 +235,11 @@ class AsyncTypecast:
|
|
|
203
235
|
NotFoundError, UnprocessableEntityError, RateLimitError,
|
|
204
236
|
InternalServerError, TypecastError: depending on response status.
|
|
205
237
|
"""
|
|
206
|
-
if
|
|
238
|
+
if (
|
|
239
|
+
not isinstance(chunk_size, int)
|
|
240
|
+
or isinstance(chunk_size, bool)
|
|
241
|
+
or chunk_size < 1
|
|
242
|
+
):
|
|
207
243
|
raise ValueError("chunk_size must be a positive integer")
|
|
208
244
|
if not self.session:
|
|
209
245
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
@@ -213,6 +249,7 @@ class AsyncTypecast:
|
|
|
213
249
|
f"{self.host}{endpoint}",
|
|
214
250
|
json=request.model_dump(exclude_none=True),
|
|
215
251
|
timeout=stream_timeout,
|
|
252
|
+
headers=self._request_headers(),
|
|
216
253
|
) as response:
|
|
217
254
|
if response.status != 200:
|
|
218
255
|
error_text = await response.text()
|
|
@@ -256,6 +293,7 @@ class AsyncTypecast:
|
|
|
256
293
|
f"{self.host}{endpoint}",
|
|
257
294
|
json=request.model_dump(exclude_none=True),
|
|
258
295
|
params=params,
|
|
296
|
+
headers=self._request_headers(),
|
|
259
297
|
) as response:
|
|
260
298
|
if response.status != 200:
|
|
261
299
|
text = await response.text()
|
|
@@ -305,6 +343,7 @@ class AsyncTypecast:
|
|
|
305
343
|
f"{self.host}/v1/voices/clone",
|
|
306
344
|
data=form,
|
|
307
345
|
timeout=timeout,
|
|
346
|
+
headers=self._request_headers(),
|
|
308
347
|
) as response:
|
|
309
348
|
if response.status != 200:
|
|
310
349
|
text = await response.text()
|
|
@@ -329,6 +368,7 @@ class AsyncTypecast:
|
|
|
329
368
|
async with self.session.delete(
|
|
330
369
|
f"{self.host}/v1/voices/{quote(voice_id, safe='')}",
|
|
331
370
|
timeout=timeout,
|
|
371
|
+
headers=self._request_headers(),
|
|
332
372
|
) as response:
|
|
333
373
|
if response.status not in (200, 204):
|
|
334
374
|
text = await response.text()
|
|
@@ -355,7 +395,9 @@ class AsyncTypecast:
|
|
|
355
395
|
params["model"] = model
|
|
356
396
|
|
|
357
397
|
async with self.session.get(
|
|
358
|
-
f"{self.host}{endpoint}",
|
|
398
|
+
f"{self.host}{endpoint}",
|
|
399
|
+
params=params,
|
|
400
|
+
headers=self._request_headers(),
|
|
359
401
|
) as response:
|
|
360
402
|
if response.status != 200:
|
|
361
403
|
error_text = await response.text()
|
|
@@ -383,7 +425,9 @@ class AsyncTypecast:
|
|
|
383
425
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
384
426
|
endpoint = f"/v1/voices/{voice_id}"
|
|
385
427
|
|
|
386
|
-
async with self.session.get(
|
|
428
|
+
async with self.session.get(
|
|
429
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
430
|
+
) as response:
|
|
387
431
|
if response.status != 200:
|
|
388
432
|
error_text = await response.text()
|
|
389
433
|
self._handle_error(response.status, error_text)
|
|
@@ -420,7 +464,9 @@ class AsyncTypecast:
|
|
|
420
464
|
params[key] = getattr(value, "value", value)
|
|
421
465
|
|
|
422
466
|
async with self.session.get(
|
|
423
|
-
f"{self.host}{endpoint}",
|
|
467
|
+
f"{self.host}{endpoint}",
|
|
468
|
+
params=params,
|
|
469
|
+
headers=self._request_headers(),
|
|
424
470
|
) as response:
|
|
425
471
|
if response.status != 200:
|
|
426
472
|
error_text = await response.text()
|
|
@@ -447,7 +493,9 @@ class AsyncTypecast:
|
|
|
447
493
|
if not self.session:
|
|
448
494
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
449
495
|
endpoint = "/v1/users/me/subscription"
|
|
450
|
-
async with self.session.get(
|
|
496
|
+
async with self.session.get(
|
|
497
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
498
|
+
) as response:
|
|
451
499
|
if response.status != 200:
|
|
452
500
|
error_text = await response.text()
|
|
453
501
|
self._handle_error(response.status, error_text)
|
|
@@ -470,10 +518,37 @@ class AsyncTypecast:
|
|
|
470
518
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
471
519
|
endpoint = f"/v2/voices/{voice_id}"
|
|
472
520
|
|
|
473
|
-
async with self.session.get(
|
|
521
|
+
async with self.session.get(
|
|
522
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
523
|
+
) as response:
|
|
474
524
|
if response.status != 200:
|
|
475
525
|
error_text = await response.text()
|
|
476
526
|
self._handle_error(response.status, error_text)
|
|
477
527
|
|
|
478
528
|
data = await response.json()
|
|
479
529
|
return VoiceV2Response.model_validate(data)
|
|
530
|
+
|
|
531
|
+
async def recommend_voices(
|
|
532
|
+
self, query: str, count: int = 5
|
|
533
|
+
) -> list[RecommendedVoice]:
|
|
534
|
+
"""Recommend voices from a text description.
|
|
535
|
+
|
|
536
|
+
Recommendation results only include ``voice_id``, ``voice_name``, and
|
|
537
|
+
``score``. Use ``voice_v2`` or ``voices_v2`` to fetch detailed metadata
|
|
538
|
+
for returned voice IDs.
|
|
539
|
+
"""
|
|
540
|
+
if count < 1 or count > 10:
|
|
541
|
+
raise ValueError("count must be between 1 and 10")
|
|
542
|
+
if not self.session:
|
|
543
|
+
raise TypecastError("Client session not initialized. Use async with.")
|
|
544
|
+
|
|
545
|
+
async with self.session.get(
|
|
546
|
+
f"{self.host}/v1/voices/recommendations",
|
|
547
|
+
params={"query": query, "count": count},
|
|
548
|
+
) as response:
|
|
549
|
+
if response.status != 200:
|
|
550
|
+
error_text = await response.text()
|
|
551
|
+
self._handle_error(response.status, error_text)
|
|
552
|
+
|
|
553
|
+
data = await response.json()
|
|
554
|
+
return [RecommendedVoice.model_validate(item) for item in data]
|
|
@@ -10,6 +10,7 @@ from ._voice_clone import (
|
|
|
10
10
|
validate_clone_inputs,
|
|
11
11
|
validate_custom_voice_id,
|
|
12
12
|
)
|
|
13
|
+
from ._user_agent import requests_user_agent
|
|
13
14
|
from .composer import SpeechComposer
|
|
14
15
|
from .exceptions import (
|
|
15
16
|
BadRequestError,
|
|
@@ -25,6 +26,7 @@ from .models import (
|
|
|
25
26
|
CustomVoice,
|
|
26
27
|
LanguageCode,
|
|
27
28
|
Output,
|
|
29
|
+
RecommendedVoice,
|
|
28
30
|
SubscriptionResponse,
|
|
29
31
|
TTSModel,
|
|
30
32
|
TTSPrompt,
|
|
@@ -95,13 +97,22 @@ class Typecast:
|
|
|
95
97
|
... f.write(response.audio_data)
|
|
96
98
|
"""
|
|
97
99
|
|
|
98
|
-
def __init__(
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
host: Optional[str] = None,
|
|
103
|
+
api_key: Optional[str] = None,
|
|
104
|
+
session: Optional[requests.Session] = None,
|
|
105
|
+
):
|
|
99
106
|
"""Initialize the Typecast client.
|
|
100
107
|
|
|
101
108
|
Args:
|
|
102
109
|
host: API host URL. Defaults to TYPECAST_API_HOST env var
|
|
103
110
|
or 'https://api.typecast.ai'.
|
|
104
111
|
api_key: API key for authentication. Defaults to TYPECAST_API_KEY env var.
|
|
112
|
+
session: Optional externally-managed requests.Session. When provided,
|
|
113
|
+
the client will not create a new session nor close it; auth headers
|
|
114
|
+
(`X-API-KEY`, `User-Agent`) are attached per-request via
|
|
115
|
+
`_request_headers()`.
|
|
105
116
|
|
|
106
117
|
Raises:
|
|
107
118
|
ValueError: If no API key is provided and TYPECAST_API_KEY is not set.
|
|
@@ -110,11 +121,18 @@ class Typecast:
|
|
|
110
121
|
self.api_key = conf.get_api_key(api_key)
|
|
111
122
|
if not self.api_key and conf.is_default_host(self.host):
|
|
112
123
|
raise ValueError("API key is required for the default Typecast API host")
|
|
113
|
-
self.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
self._owns_session = session is None
|
|
125
|
+
if session is not None:
|
|
126
|
+
self.session = session
|
|
127
|
+
else:
|
|
128
|
+
self.session = requests.Session()
|
|
129
|
+
headers = {
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
"User-Agent": requests_user_agent(self.host),
|
|
132
|
+
}
|
|
133
|
+
if self.api_key:
|
|
134
|
+
headers["X-API-KEY"] = self.api_key
|
|
135
|
+
self.session.headers.update(headers)
|
|
118
136
|
|
|
119
137
|
def _handle_error(self, status_code: int, response_text: str):
|
|
120
138
|
"""Handle HTTP error responses with specific exception types."""
|
|
@@ -138,6 +156,20 @@ class Typecast:
|
|
|
138
156
|
status_code=status_code,
|
|
139
157
|
)
|
|
140
158
|
|
|
159
|
+
def _request_headers(self) -> Optional[dict]:
|
|
160
|
+
"""Headers to attach to each individual request.
|
|
161
|
+
|
|
162
|
+
For owned sessions, auth is set at session scope, so return None and let
|
|
163
|
+
requests use the session headers. For external sessions, the session has
|
|
164
|
+
no auth headers, so we attach X-API-KEY and User-Agent per-request.
|
|
165
|
+
"""
|
|
166
|
+
if self._owns_session:
|
|
167
|
+
return None
|
|
168
|
+
headers = {"User-Agent": requests_user_agent(self.host)}
|
|
169
|
+
if self.api_key:
|
|
170
|
+
headers["X-API-KEY"] = self.api_key
|
|
171
|
+
return headers
|
|
172
|
+
|
|
141
173
|
def text_to_speech(self, request: TTSRequest) -> TTSResponse:
|
|
142
174
|
"""Convert text to speech.
|
|
143
175
|
|
|
@@ -156,7 +188,9 @@ class Typecast:
|
|
|
156
188
|
"""
|
|
157
189
|
endpoint = "/v1/text-to-speech"
|
|
158
190
|
response = self.session.post(
|
|
159
|
-
f"{self.host}{endpoint}",
|
|
191
|
+
f"{self.host}{endpoint}",
|
|
192
|
+
json=request.model_dump(exclude_none=True),
|
|
193
|
+
headers=self._request_headers(),
|
|
160
194
|
)
|
|
161
195
|
if response.status_code != 200:
|
|
162
196
|
self._handle_error(response.status_code, response.text)
|
|
@@ -236,7 +270,11 @@ class Typecast:
|
|
|
236
270
|
NotFoundError, UnprocessableEntityError, RateLimitError,
|
|
237
271
|
InternalServerError, TypecastError: depending on response status.
|
|
238
272
|
"""
|
|
239
|
-
if
|
|
273
|
+
if (
|
|
274
|
+
not isinstance(chunk_size, int)
|
|
275
|
+
or isinstance(chunk_size, bool)
|
|
276
|
+
or chunk_size < 1
|
|
277
|
+
):
|
|
240
278
|
raise ValueError("chunk_size must be a positive integer")
|
|
241
279
|
endpoint = "/v1/text-to-speech/stream"
|
|
242
280
|
response = self.session.post(
|
|
@@ -244,6 +282,7 @@ class Typecast:
|
|
|
244
282
|
json=request.model_dump(exclude_none=True),
|
|
245
283
|
stream=True,
|
|
246
284
|
timeout=(10, 300),
|
|
285
|
+
headers=self._request_headers(),
|
|
247
286
|
)
|
|
248
287
|
if response.status_code != 200:
|
|
249
288
|
error_text = response.text
|
|
@@ -291,6 +330,7 @@ class Typecast:
|
|
|
291
330
|
json=request.model_dump(exclude_none=True),
|
|
292
331
|
params=params,
|
|
293
332
|
timeout=(10, 300),
|
|
333
|
+
headers=self._request_headers(),
|
|
294
334
|
)
|
|
295
335
|
if response.status_code != 200:
|
|
296
336
|
self._handle_error(response.status_code, response.text)
|
|
@@ -327,12 +367,19 @@ class Typecast:
|
|
|
327
367
|
}
|
|
328
368
|
data = {"name": name, "model": model_str}
|
|
329
369
|
# Remove the session-level Content-Type so requests can set the
|
|
330
|
-
# correct multipart/form-data boundary for this request.
|
|
370
|
+
# correct multipart/form-data boundary for this request. For external
|
|
371
|
+
# sessions, also attach the per-request auth headers (X-API-KEY,
|
|
372
|
+
# User-Agent); _request_headers() returns None for owned sessions, so
|
|
373
|
+
# owned-session behavior is unchanged.
|
|
374
|
+
headers = {"Content-Type": None}
|
|
375
|
+
per_request = self._request_headers()
|
|
376
|
+
if per_request:
|
|
377
|
+
headers.update(per_request)
|
|
331
378
|
response = self.session.post(
|
|
332
379
|
f"{self.host}/v1/voices/clone",
|
|
333
380
|
files=files,
|
|
334
381
|
data=data,
|
|
335
|
-
headers=
|
|
382
|
+
headers=headers,
|
|
336
383
|
timeout=(10, 300),
|
|
337
384
|
)
|
|
338
385
|
if response.status_code != 200:
|
|
@@ -353,6 +400,7 @@ class Typecast:
|
|
|
353
400
|
response = self.session.delete(
|
|
354
401
|
f"{self.host}/v1/voices/{quote(voice_id, safe='')}",
|
|
355
402
|
timeout=(10, 60),
|
|
403
|
+
headers=self._request_headers(),
|
|
356
404
|
)
|
|
357
405
|
if response.status_code not in (200, 204):
|
|
358
406
|
self._handle_error(response.status_code, response.text)
|
|
@@ -375,7 +423,9 @@ class Typecast:
|
|
|
375
423
|
if model:
|
|
376
424
|
params["model"] = model
|
|
377
425
|
|
|
378
|
-
response = self.session.get(
|
|
426
|
+
response = self.session.get(
|
|
427
|
+
f"{self.host}{endpoint}", params=params, headers=self._request_headers()
|
|
428
|
+
)
|
|
379
429
|
|
|
380
430
|
if response.status_code != 200:
|
|
381
431
|
self._handle_error(response.status_code, response.text)
|
|
@@ -398,7 +448,9 @@ class Typecast:
|
|
|
398
448
|
This method is deprecated. Use voices_v2() for enhanced metadata.
|
|
399
449
|
"""
|
|
400
450
|
endpoint = f"/v1/voices/{voice_id}"
|
|
401
|
-
response = self.session.get(
|
|
451
|
+
response = self.session.get(
|
|
452
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
453
|
+
)
|
|
402
454
|
|
|
403
455
|
if response.status_code != 200:
|
|
404
456
|
self._handle_error(response.status_code, response.text)
|
|
@@ -432,7 +484,9 @@ class Typecast:
|
|
|
432
484
|
for key, value in filter_dict.items():
|
|
433
485
|
params[key] = getattr(value, "value", value)
|
|
434
486
|
|
|
435
|
-
response = self.session.get(
|
|
487
|
+
response = self.session.get(
|
|
488
|
+
f"{self.host}{endpoint}", params=params, headers=self._request_headers()
|
|
489
|
+
)
|
|
436
490
|
|
|
437
491
|
if response.status_code != 200:
|
|
438
492
|
self._handle_error(response.status_code, response.text)
|
|
@@ -454,7 +508,9 @@ class Typecast:
|
|
|
454
508
|
InternalServerError: On server-side failures.
|
|
455
509
|
"""
|
|
456
510
|
endpoint = "/v1/users/me/subscription"
|
|
457
|
-
response = self.session.get(
|
|
511
|
+
response = self.session.get(
|
|
512
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
513
|
+
)
|
|
458
514
|
if response.status_code != 200:
|
|
459
515
|
self._handle_error(response.status_code, response.text)
|
|
460
516
|
return SubscriptionResponse.model_validate(response.json())
|
|
@@ -472,9 +528,33 @@ class Typecast:
|
|
|
472
528
|
NotFoundError: If the voice ID does not exist.
|
|
473
529
|
"""
|
|
474
530
|
endpoint = f"/v2/voices/{voice_id}"
|
|
475
|
-
response = self.session.get(
|
|
531
|
+
response = self.session.get(
|
|
532
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
533
|
+
)
|
|
476
534
|
|
|
477
535
|
if response.status_code != 200:
|
|
478
536
|
self._handle_error(response.status_code, response.text)
|
|
479
537
|
|
|
480
538
|
return VoiceV2Response.model_validate(response.json())
|
|
539
|
+
|
|
540
|
+
def recommend_voices(self, query: str, count: int = 5) -> list[RecommendedVoice]:
|
|
541
|
+
"""Recommend voices from a text description.
|
|
542
|
+
|
|
543
|
+
Recommendation results only include ``voice_id``, ``voice_name``, and
|
|
544
|
+
``score``. Use ``voice_v2`` or ``voices_v2`` to fetch detailed metadata
|
|
545
|
+
for returned voice IDs.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
query: Text description of the desired voice.
|
|
549
|
+
count: Number of recommendations to return, from 1 to 10. Defaults to 5.
|
|
550
|
+
"""
|
|
551
|
+
if count < 1 or count > 10:
|
|
552
|
+
raise ValueError("count must be between 1 and 10")
|
|
553
|
+
|
|
554
|
+
response = self.session.get(
|
|
555
|
+
f"{self.host}/v1/voices/recommendations",
|
|
556
|
+
params={"query": query, "count": count},
|
|
557
|
+
)
|
|
558
|
+
if response.status_code != 200:
|
|
559
|
+
self._handle_error(response.status_code, response.text)
|
|
560
|
+
return [RecommendedVoice.model_validate(item) for item in response.json()]
|
|
@@ -23,6 +23,7 @@ from .voices import (
|
|
|
23
23
|
CustomVoice,
|
|
24
24
|
GenderEnum,
|
|
25
25
|
ModelInfo,
|
|
26
|
+
RecommendedVoice,
|
|
26
27
|
UseCaseEnum,
|
|
27
28
|
VoicesResponse,
|
|
28
29
|
VoicesV2Filter,
|
|
@@ -46,6 +47,7 @@ __all__ = [
|
|
|
46
47
|
"PlanTier",
|
|
47
48
|
"Prompt",
|
|
48
49
|
"PresetPrompt",
|
|
50
|
+
"RecommendedVoice",
|
|
49
51
|
"SmartPrompt",
|
|
50
52
|
"SubscriptionResponse",
|
|
51
53
|
"TTSModel",
|
|
@@ -68,6 +68,19 @@ class VoiceV2Response(BaseModel):
|
|
|
68
68
|
use_cases: Optional[list[str]] = None
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
class RecommendedVoice(BaseModel):
|
|
72
|
+
"""Recommended voice result.
|
|
73
|
+
|
|
74
|
+
Recommendation results only include the matched voice ID, voice name, and
|
|
75
|
+
similarity score. Use ``voice_v2`` or ``voices_v2`` to fetch detailed voice
|
|
76
|
+
metadata for a returned ``voice_id``.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
voice_id: str
|
|
80
|
+
voice_name: str
|
|
81
|
+
score: float
|
|
82
|
+
|
|
83
|
+
|
|
71
84
|
class VoicesV2Filter(BaseModel):
|
|
72
85
|
"""Filter options for V2 voices endpoint"""
|
|
73
86
|
|
|
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
|