typecast-python 0.3.7__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.7 → typecast_python-0.3.8}/PKG-INFO +1 -1
- {typecast_python-0.3.7 → typecast_python-0.3.8}/pyproject.toml +4 -1
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/__init__.py +2 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/async_client.py +86 -16
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/client.py +89 -17
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/models/__init__.py +2 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/models/voices.py +13 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/.gitignore +0 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/LICENSE +0 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/README.md +0 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/_user_agent.py +0 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/_voice_clone.py +0 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/composer.py +0 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/conf.py +0 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/exceptions.py +0 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/models/error.py +0 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/models/subscription.py +0 -0
- {typecast_python-0.3.7 → typecast_python-0.3.8}/src/typecast/models/tts.py +0 -0
- {typecast_python-0.3.7 → 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",
|
|
@@ -29,6 +29,7 @@ from .models import (
|
|
|
29
29
|
CustomVoice,
|
|
30
30
|
LanguageCode,
|
|
31
31
|
Output,
|
|
32
|
+
RecommendedVoice,
|
|
32
33
|
SubscriptionResponse,
|
|
33
34
|
TTSModel,
|
|
34
35
|
TTSPrompt,
|
|
@@ -62,34 +63,48 @@ class AsyncTypecast:
|
|
|
62
63
|
... f.write(response.audio_data)
|
|
63
64
|
"""
|
|
64
65
|
|
|
65
|
-
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
|
+
):
|
|
66
72
|
"""Initialize the async Typecast client.
|
|
67
73
|
|
|
68
74
|
Args:
|
|
69
75
|
host: API host URL. Defaults to TYPECAST_API_HOST env var
|
|
70
76
|
or 'https://api.typecast.ai'.
|
|
71
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()`.
|
|
72
82
|
|
|
73
83
|
Raises:
|
|
74
|
-
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.
|
|
75
86
|
"""
|
|
76
87
|
self.host = conf.get_host(host)
|
|
77
88
|
self.api_key = conf.get_api_key(api_key)
|
|
78
89
|
if not self.api_key and conf.is_default_host(self.host):
|
|
79
90
|
raise ValueError("API key is required for the default Typecast API host")
|
|
80
|
-
self.session
|
|
91
|
+
self._owns_session = session is None
|
|
92
|
+
self.session: Optional[aiohttp.ClientSession] = session
|
|
81
93
|
|
|
82
94
|
async def __aenter__(self):
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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)
|
|
89
102
|
return self
|
|
90
103
|
|
|
91
104
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
92
|
-
|
|
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:
|
|
93
108
|
await self.session.close()
|
|
94
109
|
|
|
95
110
|
def _handle_error(self, status_code: int, response_text: str):
|
|
@@ -114,6 +129,20 @@ class AsyncTypecast:
|
|
|
114
129
|
status_code=status_code,
|
|
115
130
|
)
|
|
116
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
|
+
|
|
117
146
|
async def text_to_speech(self, request: TTSRequest) -> TTSResponse:
|
|
118
147
|
"""Convert text to speech asynchronously.
|
|
119
148
|
|
|
@@ -135,7 +164,9 @@ class AsyncTypecast:
|
|
|
135
164
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
136
165
|
endpoint = "/v1/text-to-speech"
|
|
137
166
|
async with self.session.post(
|
|
138
|
-
f"{self.host}{endpoint}",
|
|
167
|
+
f"{self.host}{endpoint}",
|
|
168
|
+
json=request.model_dump(exclude_none=True),
|
|
169
|
+
headers=self._request_headers(),
|
|
139
170
|
) as response:
|
|
140
171
|
if response.status != 200:
|
|
141
172
|
error_text = await response.text()
|
|
@@ -218,6 +249,7 @@ class AsyncTypecast:
|
|
|
218
249
|
f"{self.host}{endpoint}",
|
|
219
250
|
json=request.model_dump(exclude_none=True),
|
|
220
251
|
timeout=stream_timeout,
|
|
252
|
+
headers=self._request_headers(),
|
|
221
253
|
) as response:
|
|
222
254
|
if response.status != 200:
|
|
223
255
|
error_text = await response.text()
|
|
@@ -261,6 +293,7 @@ class AsyncTypecast:
|
|
|
261
293
|
f"{self.host}{endpoint}",
|
|
262
294
|
json=request.model_dump(exclude_none=True),
|
|
263
295
|
params=params,
|
|
296
|
+
headers=self._request_headers(),
|
|
264
297
|
) as response:
|
|
265
298
|
if response.status != 200:
|
|
266
299
|
text = await response.text()
|
|
@@ -310,6 +343,7 @@ class AsyncTypecast:
|
|
|
310
343
|
f"{self.host}/v1/voices/clone",
|
|
311
344
|
data=form,
|
|
312
345
|
timeout=timeout,
|
|
346
|
+
headers=self._request_headers(),
|
|
313
347
|
) as response:
|
|
314
348
|
if response.status != 200:
|
|
315
349
|
text = await response.text()
|
|
@@ -334,6 +368,7 @@ class AsyncTypecast:
|
|
|
334
368
|
async with self.session.delete(
|
|
335
369
|
f"{self.host}/v1/voices/{quote(voice_id, safe='')}",
|
|
336
370
|
timeout=timeout,
|
|
371
|
+
headers=self._request_headers(),
|
|
337
372
|
) as response:
|
|
338
373
|
if response.status not in (200, 204):
|
|
339
374
|
text = await response.text()
|
|
@@ -360,7 +395,9 @@ class AsyncTypecast:
|
|
|
360
395
|
params["model"] = model
|
|
361
396
|
|
|
362
397
|
async with self.session.get(
|
|
363
|
-
f"{self.host}{endpoint}",
|
|
398
|
+
f"{self.host}{endpoint}",
|
|
399
|
+
params=params,
|
|
400
|
+
headers=self._request_headers(),
|
|
364
401
|
) as response:
|
|
365
402
|
if response.status != 200:
|
|
366
403
|
error_text = await response.text()
|
|
@@ -388,7 +425,9 @@ class AsyncTypecast:
|
|
|
388
425
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
389
426
|
endpoint = f"/v1/voices/{voice_id}"
|
|
390
427
|
|
|
391
|
-
async with self.session.get(
|
|
428
|
+
async with self.session.get(
|
|
429
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
430
|
+
) as response:
|
|
392
431
|
if response.status != 200:
|
|
393
432
|
error_text = await response.text()
|
|
394
433
|
self._handle_error(response.status, error_text)
|
|
@@ -425,7 +464,9 @@ class AsyncTypecast:
|
|
|
425
464
|
params[key] = getattr(value, "value", value)
|
|
426
465
|
|
|
427
466
|
async with self.session.get(
|
|
428
|
-
f"{self.host}{endpoint}",
|
|
467
|
+
f"{self.host}{endpoint}",
|
|
468
|
+
params=params,
|
|
469
|
+
headers=self._request_headers(),
|
|
429
470
|
) as response:
|
|
430
471
|
if response.status != 200:
|
|
431
472
|
error_text = await response.text()
|
|
@@ -452,7 +493,9 @@ class AsyncTypecast:
|
|
|
452
493
|
if not self.session:
|
|
453
494
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
454
495
|
endpoint = "/v1/users/me/subscription"
|
|
455
|
-
async with self.session.get(
|
|
496
|
+
async with self.session.get(
|
|
497
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
498
|
+
) as response:
|
|
456
499
|
if response.status != 200:
|
|
457
500
|
error_text = await response.text()
|
|
458
501
|
self._handle_error(response.status, error_text)
|
|
@@ -475,10 +518,37 @@ class AsyncTypecast:
|
|
|
475
518
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
476
519
|
endpoint = f"/v2/voices/{voice_id}"
|
|
477
520
|
|
|
478
|
-
async with self.session.get(
|
|
521
|
+
async with self.session.get(
|
|
522
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
523
|
+
) as response:
|
|
479
524
|
if response.status != 200:
|
|
480
525
|
error_text = await response.text()
|
|
481
526
|
self._handle_error(response.status, error_text)
|
|
482
527
|
|
|
483
528
|
data = await response.json()
|
|
484
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]
|
|
@@ -26,6 +26,7 @@ from .models import (
|
|
|
26
26
|
CustomVoice,
|
|
27
27
|
LanguageCode,
|
|
28
28
|
Output,
|
|
29
|
+
RecommendedVoice,
|
|
29
30
|
SubscriptionResponse,
|
|
30
31
|
TTSModel,
|
|
31
32
|
TTSPrompt,
|
|
@@ -96,13 +97,22 @@ class Typecast:
|
|
|
96
97
|
... f.write(response.audio_data)
|
|
97
98
|
"""
|
|
98
99
|
|
|
99
|
-
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
|
+
):
|
|
100
106
|
"""Initialize the Typecast client.
|
|
101
107
|
|
|
102
108
|
Args:
|
|
103
109
|
host: API host URL. Defaults to TYPECAST_API_HOST env var
|
|
104
110
|
or 'https://api.typecast.ai'.
|
|
105
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()`.
|
|
106
116
|
|
|
107
117
|
Raises:
|
|
108
118
|
ValueError: If no API key is provided and TYPECAST_API_KEY is not set.
|
|
@@ -111,14 +121,18 @@ class Typecast:
|
|
|
111
121
|
self.api_key = conf.get_api_key(api_key)
|
|
112
122
|
if not self.api_key and conf.is_default_host(self.host):
|
|
113
123
|
raise ValueError("API key is required for the default Typecast API host")
|
|
114
|
-
self.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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)
|
|
122
136
|
|
|
123
137
|
def _handle_error(self, status_code: int, response_text: str):
|
|
124
138
|
"""Handle HTTP error responses with specific exception types."""
|
|
@@ -142,6 +156,20 @@ class Typecast:
|
|
|
142
156
|
status_code=status_code,
|
|
143
157
|
)
|
|
144
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
|
+
|
|
145
173
|
def text_to_speech(self, request: TTSRequest) -> TTSResponse:
|
|
146
174
|
"""Convert text to speech.
|
|
147
175
|
|
|
@@ -160,7 +188,9 @@ class Typecast:
|
|
|
160
188
|
"""
|
|
161
189
|
endpoint = "/v1/text-to-speech"
|
|
162
190
|
response = self.session.post(
|
|
163
|
-
f"{self.host}{endpoint}",
|
|
191
|
+
f"{self.host}{endpoint}",
|
|
192
|
+
json=request.model_dump(exclude_none=True),
|
|
193
|
+
headers=self._request_headers(),
|
|
164
194
|
)
|
|
165
195
|
if response.status_code != 200:
|
|
166
196
|
self._handle_error(response.status_code, response.text)
|
|
@@ -252,6 +282,7 @@ class Typecast:
|
|
|
252
282
|
json=request.model_dump(exclude_none=True),
|
|
253
283
|
stream=True,
|
|
254
284
|
timeout=(10, 300),
|
|
285
|
+
headers=self._request_headers(),
|
|
255
286
|
)
|
|
256
287
|
if response.status_code != 200:
|
|
257
288
|
error_text = response.text
|
|
@@ -299,6 +330,7 @@ class Typecast:
|
|
|
299
330
|
json=request.model_dump(exclude_none=True),
|
|
300
331
|
params=params,
|
|
301
332
|
timeout=(10, 300),
|
|
333
|
+
headers=self._request_headers(),
|
|
302
334
|
)
|
|
303
335
|
if response.status_code != 200:
|
|
304
336
|
self._handle_error(response.status_code, response.text)
|
|
@@ -335,12 +367,19 @@ class Typecast:
|
|
|
335
367
|
}
|
|
336
368
|
data = {"name": name, "model": model_str}
|
|
337
369
|
# Remove the session-level Content-Type so requests can set the
|
|
338
|
-
# 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)
|
|
339
378
|
response = self.session.post(
|
|
340
379
|
f"{self.host}/v1/voices/clone",
|
|
341
380
|
files=files,
|
|
342
381
|
data=data,
|
|
343
|
-
headers=
|
|
382
|
+
headers=headers,
|
|
344
383
|
timeout=(10, 300),
|
|
345
384
|
)
|
|
346
385
|
if response.status_code != 200:
|
|
@@ -361,6 +400,7 @@ class Typecast:
|
|
|
361
400
|
response = self.session.delete(
|
|
362
401
|
f"{self.host}/v1/voices/{quote(voice_id, safe='')}",
|
|
363
402
|
timeout=(10, 60),
|
|
403
|
+
headers=self._request_headers(),
|
|
364
404
|
)
|
|
365
405
|
if response.status_code not in (200, 204):
|
|
366
406
|
self._handle_error(response.status_code, response.text)
|
|
@@ -383,7 +423,9 @@ class Typecast:
|
|
|
383
423
|
if model:
|
|
384
424
|
params["model"] = model
|
|
385
425
|
|
|
386
|
-
response = self.session.get(
|
|
426
|
+
response = self.session.get(
|
|
427
|
+
f"{self.host}{endpoint}", params=params, headers=self._request_headers()
|
|
428
|
+
)
|
|
387
429
|
|
|
388
430
|
if response.status_code != 200:
|
|
389
431
|
self._handle_error(response.status_code, response.text)
|
|
@@ -406,7 +448,9 @@ class Typecast:
|
|
|
406
448
|
This method is deprecated. Use voices_v2() for enhanced metadata.
|
|
407
449
|
"""
|
|
408
450
|
endpoint = f"/v1/voices/{voice_id}"
|
|
409
|
-
response = self.session.get(
|
|
451
|
+
response = self.session.get(
|
|
452
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
453
|
+
)
|
|
410
454
|
|
|
411
455
|
if response.status_code != 200:
|
|
412
456
|
self._handle_error(response.status_code, response.text)
|
|
@@ -440,7 +484,9 @@ class Typecast:
|
|
|
440
484
|
for key, value in filter_dict.items():
|
|
441
485
|
params[key] = getattr(value, "value", value)
|
|
442
486
|
|
|
443
|
-
response = self.session.get(
|
|
487
|
+
response = self.session.get(
|
|
488
|
+
f"{self.host}{endpoint}", params=params, headers=self._request_headers()
|
|
489
|
+
)
|
|
444
490
|
|
|
445
491
|
if response.status_code != 200:
|
|
446
492
|
self._handle_error(response.status_code, response.text)
|
|
@@ -462,7 +508,9 @@ class Typecast:
|
|
|
462
508
|
InternalServerError: On server-side failures.
|
|
463
509
|
"""
|
|
464
510
|
endpoint = "/v1/users/me/subscription"
|
|
465
|
-
response = self.session.get(
|
|
511
|
+
response = self.session.get(
|
|
512
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
513
|
+
)
|
|
466
514
|
if response.status_code != 200:
|
|
467
515
|
self._handle_error(response.status_code, response.text)
|
|
468
516
|
return SubscriptionResponse.model_validate(response.json())
|
|
@@ -480,9 +528,33 @@ class Typecast:
|
|
|
480
528
|
NotFoundError: If the voice ID does not exist.
|
|
481
529
|
"""
|
|
482
530
|
endpoint = f"/v2/voices/{voice_id}"
|
|
483
|
-
response = self.session.get(
|
|
531
|
+
response = self.session.get(
|
|
532
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
533
|
+
)
|
|
484
534
|
|
|
485
535
|
if response.status_code != 200:
|
|
486
536
|
self._handle_error(response.status_code, response.text)
|
|
487
537
|
|
|
488
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
|
|
File without changes
|