typecast-python 0.3.7__py3-none-any.whl → 0.3.9__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.
- typecast/__init__.py +2 -0
- typecast/async_client.py +91 -18
- typecast/client.py +91 -17
- typecast/models/__init__.py +2 -0
- typecast/models/tts.py +6 -4
- typecast/models/voices.py +20 -5
- {typecast_python-0.3.7.dist-info → typecast_python-0.3.9.dist-info}/METADATA +14 -10
- typecast_python-0.3.9.dist-info/RECORD +18 -0
- {typecast_python-0.3.7.dist-info → typecast_python-0.3.9.dist-info}/WHEEL +1 -1
- typecast_python-0.3.7.dist-info/RECORD +0 -18
- {typecast_python-0.3.7.dist-info → typecast_python-0.3.9.dist-info}/licenses/LICENSE +0 -0
typecast/__init__.py
CHANGED
|
@@ -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",
|
typecast/async_client.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
from typing import AsyncIterator, BinaryIO, Optional, Union
|
|
@@ -29,6 +31,7 @@ from .models import (
|
|
|
29
31
|
CustomVoice,
|
|
30
32
|
LanguageCode,
|
|
31
33
|
Output,
|
|
34
|
+
RecommendedVoice,
|
|
32
35
|
SubscriptionResponse,
|
|
33
36
|
TTSModel,
|
|
34
37
|
TTSPrompt,
|
|
@@ -62,34 +65,48 @@ class AsyncTypecast:
|
|
|
62
65
|
... f.write(response.audio_data)
|
|
63
66
|
"""
|
|
64
67
|
|
|
65
|
-
def __init__(
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
host: Optional[str] = None,
|
|
71
|
+
api_key: Optional[str] = None,
|
|
72
|
+
session: Optional[aiohttp.ClientSession] = None,
|
|
73
|
+
):
|
|
66
74
|
"""Initialize the async Typecast client.
|
|
67
75
|
|
|
68
76
|
Args:
|
|
69
77
|
host: API host URL. Defaults to TYPECAST_API_HOST env var
|
|
70
78
|
or 'https://api.typecast.ai'.
|
|
71
79
|
api_key: API key for authentication. Defaults to TYPECAST_API_KEY env var.
|
|
80
|
+
session: Optional externally-managed aiohttp.ClientSession. When provided,
|
|
81
|
+
__aenter__ will not create a new session and __aexit__ will not close it
|
|
82
|
+
(the caller owns its lifecycle). Auth headers (`X-API-KEY`, `User-Agent`)
|
|
83
|
+
are attached per-request via `_request_headers()`.
|
|
72
84
|
|
|
73
85
|
Raises:
|
|
74
|
-
ValueError: If no API key is provided and TYPECAST_API_KEY is not set
|
|
86
|
+
ValueError: If no API key is provided and TYPECAST_API_KEY is not set
|
|
87
|
+
for the default host.
|
|
75
88
|
"""
|
|
76
89
|
self.host = conf.get_host(host)
|
|
77
90
|
self.api_key = conf.get_api_key(api_key)
|
|
78
91
|
if not self.api_key and conf.is_default_host(self.host):
|
|
79
92
|
raise ValueError("API key is required for the default Typecast API host")
|
|
80
|
-
self.session
|
|
93
|
+
self._owns_session = session is None
|
|
94
|
+
self.session: Optional[aiohttp.ClientSession] = session
|
|
81
95
|
|
|
82
96
|
async def __aenter__(self):
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
# When an external session is injected, do not create a new one.
|
|
98
|
+
# Per-request auth headers are attached via _request_headers() on each call.
|
|
99
|
+
if self.session is None:
|
|
100
|
+
headers = {"User-Agent": aiohttp_user_agent(self.host)}
|
|
101
|
+
if self.api_key:
|
|
102
|
+
headers["X-API-KEY"] = self.api_key
|
|
103
|
+
self.session = aiohttp.ClientSession(headers=headers)
|
|
89
104
|
return self
|
|
90
105
|
|
|
91
106
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
92
|
-
|
|
107
|
+
# Only close sessions we created. External sessions are caller-owned;
|
|
108
|
+
# leave the reference intact so the client can be re-entered.
|
|
109
|
+
if self.session and self._owns_session:
|
|
93
110
|
await self.session.close()
|
|
94
111
|
|
|
95
112
|
def _handle_error(self, status_code: int, response_text: str):
|
|
@@ -114,6 +131,20 @@ class AsyncTypecast:
|
|
|
114
131
|
status_code=status_code,
|
|
115
132
|
)
|
|
116
133
|
|
|
134
|
+
def _request_headers(self) -> Optional[dict]:
|
|
135
|
+
"""Headers to attach to each individual request.
|
|
136
|
+
|
|
137
|
+
For owned sessions, auth is set at session scope, so return None and let
|
|
138
|
+
aiohttp use the session headers. For external sessions, the session has
|
|
139
|
+
no auth headers, so we attach X-API-KEY and User-Agent per-request.
|
|
140
|
+
"""
|
|
141
|
+
if self._owns_session:
|
|
142
|
+
return None
|
|
143
|
+
headers = {"User-Agent": aiohttp_user_agent(self.host)}
|
|
144
|
+
if self.api_key:
|
|
145
|
+
headers["X-API-KEY"] = self.api_key
|
|
146
|
+
return headers
|
|
147
|
+
|
|
117
148
|
async def text_to_speech(self, request: TTSRequest) -> TTSResponse:
|
|
118
149
|
"""Convert text to speech asynchronously.
|
|
119
150
|
|
|
@@ -135,7 +166,9 @@ class AsyncTypecast:
|
|
|
135
166
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
136
167
|
endpoint = "/v1/text-to-speech"
|
|
137
168
|
async with self.session.post(
|
|
138
|
-
f"{self.host}{endpoint}",
|
|
169
|
+
f"{self.host}{endpoint}",
|
|
170
|
+
json=request.model_dump(exclude_none=True),
|
|
171
|
+
headers=self._request_headers(),
|
|
139
172
|
) as response:
|
|
140
173
|
if response.status != 200:
|
|
141
174
|
error_text = await response.text()
|
|
@@ -177,7 +210,8 @@ class AsyncTypecast:
|
|
|
177
210
|
seed=seed,
|
|
178
211
|
)
|
|
179
212
|
)
|
|
180
|
-
|
|
213
|
+
loop = asyncio.get_running_loop()
|
|
214
|
+
await loop.run_in_executor(None, output_path.write_bytes, response.audio_data)
|
|
181
215
|
return response
|
|
182
216
|
|
|
183
217
|
async def text_to_speech_stream(
|
|
@@ -218,12 +252,13 @@ class AsyncTypecast:
|
|
|
218
252
|
f"{self.host}{endpoint}",
|
|
219
253
|
json=request.model_dump(exclude_none=True),
|
|
220
254
|
timeout=stream_timeout,
|
|
255
|
+
headers=self._request_headers(),
|
|
221
256
|
) as response:
|
|
222
257
|
if response.status != 200:
|
|
223
258
|
error_text = await response.text()
|
|
224
259
|
self._handle_error(response.status, error_text)
|
|
225
260
|
|
|
226
|
-
async for chunk in response.content.iter_chunked(chunk_size):
|
|
261
|
+
async for chunk in response.content.iter_chunked(chunk_size): # pragma: no branch
|
|
227
262
|
yield chunk
|
|
228
263
|
|
|
229
264
|
async def text_to_speech_with_timestamps(
|
|
@@ -261,6 +296,7 @@ class AsyncTypecast:
|
|
|
261
296
|
f"{self.host}{endpoint}",
|
|
262
297
|
json=request.model_dump(exclude_none=True),
|
|
263
298
|
params=params,
|
|
299
|
+
headers=self._request_headers(),
|
|
264
300
|
) as response:
|
|
265
301
|
if response.status != 200:
|
|
266
302
|
text = await response.text()
|
|
@@ -310,6 +346,7 @@ class AsyncTypecast:
|
|
|
310
346
|
f"{self.host}/v1/voices/clone",
|
|
311
347
|
data=form,
|
|
312
348
|
timeout=timeout,
|
|
349
|
+
headers=self._request_headers(),
|
|
313
350
|
) as response:
|
|
314
351
|
if response.status != 200:
|
|
315
352
|
text = await response.text()
|
|
@@ -334,6 +371,7 @@ class AsyncTypecast:
|
|
|
334
371
|
async with self.session.delete(
|
|
335
372
|
f"{self.host}/v1/voices/{quote(voice_id, safe='')}",
|
|
336
373
|
timeout=timeout,
|
|
374
|
+
headers=self._request_headers(),
|
|
337
375
|
) as response:
|
|
338
376
|
if response.status not in (200, 204):
|
|
339
377
|
text = await response.text()
|
|
@@ -360,7 +398,9 @@ class AsyncTypecast:
|
|
|
360
398
|
params["model"] = model
|
|
361
399
|
|
|
362
400
|
async with self.session.get(
|
|
363
|
-
f"{self.host}{endpoint}",
|
|
401
|
+
f"{self.host}{endpoint}",
|
|
402
|
+
params=params,
|
|
403
|
+
headers=self._request_headers(),
|
|
364
404
|
) as response:
|
|
365
405
|
if response.status != 200:
|
|
366
406
|
error_text = await response.text()
|
|
@@ -388,7 +428,9 @@ class AsyncTypecast:
|
|
|
388
428
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
389
429
|
endpoint = f"/v1/voices/{voice_id}"
|
|
390
430
|
|
|
391
|
-
async with self.session.get(
|
|
431
|
+
async with self.session.get(
|
|
432
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
433
|
+
) as response:
|
|
392
434
|
if response.status != 200:
|
|
393
435
|
error_text = await response.text()
|
|
394
436
|
self._handle_error(response.status, error_text)
|
|
@@ -425,7 +467,9 @@ class AsyncTypecast:
|
|
|
425
467
|
params[key] = getattr(value, "value", value)
|
|
426
468
|
|
|
427
469
|
async with self.session.get(
|
|
428
|
-
f"{self.host}{endpoint}",
|
|
470
|
+
f"{self.host}{endpoint}",
|
|
471
|
+
params=params,
|
|
472
|
+
headers=self._request_headers(),
|
|
429
473
|
) as response:
|
|
430
474
|
if response.status != 200:
|
|
431
475
|
error_text = await response.text()
|
|
@@ -452,7 +496,9 @@ class AsyncTypecast:
|
|
|
452
496
|
if not self.session:
|
|
453
497
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
454
498
|
endpoint = "/v1/users/me/subscription"
|
|
455
|
-
async with self.session.get(
|
|
499
|
+
async with self.session.get(
|
|
500
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
501
|
+
) as response:
|
|
456
502
|
if response.status != 200:
|
|
457
503
|
error_text = await response.text()
|
|
458
504
|
self._handle_error(response.status, error_text)
|
|
@@ -475,10 +521,37 @@ class AsyncTypecast:
|
|
|
475
521
|
raise TypecastError("Client session not initialized. Use async with.")
|
|
476
522
|
endpoint = f"/v2/voices/{voice_id}"
|
|
477
523
|
|
|
478
|
-
async with self.session.get(
|
|
524
|
+
async with self.session.get(
|
|
525
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
526
|
+
) as response:
|
|
479
527
|
if response.status != 200:
|
|
480
528
|
error_text = await response.text()
|
|
481
529
|
self._handle_error(response.status, error_text)
|
|
482
530
|
|
|
483
531
|
data = await response.json()
|
|
484
532
|
return VoiceV2Response.model_validate(data)
|
|
533
|
+
|
|
534
|
+
async def recommend_voices(
|
|
535
|
+
self, query: str, count: int = 5
|
|
536
|
+
) -> list[RecommendedVoice]:
|
|
537
|
+
"""Recommend voices from a text description.
|
|
538
|
+
|
|
539
|
+
Recommendation results only include ``voice_id``, ``voice_name``, and
|
|
540
|
+
``score``. Use ``voice_v2`` or ``voices_v2`` to fetch detailed metadata
|
|
541
|
+
for returned voice IDs.
|
|
542
|
+
"""
|
|
543
|
+
if count < 1 or count > 10:
|
|
544
|
+
raise ValueError("count must be between 1 and 10")
|
|
545
|
+
if not self.session:
|
|
546
|
+
raise TypecastError("Client session not initialized. Use async with.")
|
|
547
|
+
|
|
548
|
+
async with self.session.get(
|
|
549
|
+
f"{self.host}/v1/voices/recommendations",
|
|
550
|
+
params={"query": query, "count": count},
|
|
551
|
+
) as response:
|
|
552
|
+
if response.status != 200:
|
|
553
|
+
error_text = await response.text()
|
|
554
|
+
self._handle_error(response.status, error_text)
|
|
555
|
+
|
|
556
|
+
data = await response.json()
|
|
557
|
+
return [RecommendedVoice.model_validate(item) for item in data]
|
typecast/client.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from pathlib import Path
|
|
2
4
|
from typing import BinaryIO, Iterator, Optional, Union
|
|
3
5
|
from urllib.parse import quote
|
|
@@ -26,6 +28,7 @@ from .models import (
|
|
|
26
28
|
CustomVoice,
|
|
27
29
|
LanguageCode,
|
|
28
30
|
Output,
|
|
31
|
+
RecommendedVoice,
|
|
29
32
|
SubscriptionResponse,
|
|
30
33
|
TTSModel,
|
|
31
34
|
TTSPrompt,
|
|
@@ -96,13 +99,22 @@ class Typecast:
|
|
|
96
99
|
... f.write(response.audio_data)
|
|
97
100
|
"""
|
|
98
101
|
|
|
99
|
-
def __init__(
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
host: Optional[str] = None,
|
|
105
|
+
api_key: Optional[str] = None,
|
|
106
|
+
session: Optional[requests.Session] = None,
|
|
107
|
+
):
|
|
100
108
|
"""Initialize the Typecast client.
|
|
101
109
|
|
|
102
110
|
Args:
|
|
103
111
|
host: API host URL. Defaults to TYPECAST_API_HOST env var
|
|
104
112
|
or 'https://api.typecast.ai'.
|
|
105
113
|
api_key: API key for authentication. Defaults to TYPECAST_API_KEY env var.
|
|
114
|
+
session: Optional externally-managed requests.Session. When provided,
|
|
115
|
+
the client will not create a new session nor close it; auth headers
|
|
116
|
+
(`X-API-KEY`, `User-Agent`) are attached per-request via
|
|
117
|
+
`_request_headers()`.
|
|
106
118
|
|
|
107
119
|
Raises:
|
|
108
120
|
ValueError: If no API key is provided and TYPECAST_API_KEY is not set.
|
|
@@ -111,14 +123,18 @@ class Typecast:
|
|
|
111
123
|
self.api_key = conf.get_api_key(api_key)
|
|
112
124
|
if not self.api_key and conf.is_default_host(self.host):
|
|
113
125
|
raise ValueError("API key is required for the default Typecast API host")
|
|
114
|
-
self.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
self._owns_session = session is None
|
|
127
|
+
if session is not None:
|
|
128
|
+
self.session = session
|
|
129
|
+
else:
|
|
130
|
+
self.session = requests.Session()
|
|
131
|
+
headers = {
|
|
132
|
+
"Content-Type": "application/json",
|
|
133
|
+
"User-Agent": requests_user_agent(self.host),
|
|
134
|
+
}
|
|
135
|
+
if self.api_key:
|
|
136
|
+
headers["X-API-KEY"] = self.api_key
|
|
137
|
+
self.session.headers.update(headers)
|
|
122
138
|
|
|
123
139
|
def _handle_error(self, status_code: int, response_text: str):
|
|
124
140
|
"""Handle HTTP error responses with specific exception types."""
|
|
@@ -142,6 +158,20 @@ class Typecast:
|
|
|
142
158
|
status_code=status_code,
|
|
143
159
|
)
|
|
144
160
|
|
|
161
|
+
def _request_headers(self) -> Optional[dict]:
|
|
162
|
+
"""Headers to attach to each individual request.
|
|
163
|
+
|
|
164
|
+
For owned sessions, auth is set at session scope, so return None and let
|
|
165
|
+
requests use the session headers. For external sessions, the session has
|
|
166
|
+
no auth headers, so we attach X-API-KEY and User-Agent per-request.
|
|
167
|
+
"""
|
|
168
|
+
if self._owns_session:
|
|
169
|
+
return None
|
|
170
|
+
headers = {"User-Agent": requests_user_agent(self.host)}
|
|
171
|
+
if self.api_key:
|
|
172
|
+
headers["X-API-KEY"] = self.api_key
|
|
173
|
+
return headers
|
|
174
|
+
|
|
145
175
|
def text_to_speech(self, request: TTSRequest) -> TTSResponse:
|
|
146
176
|
"""Convert text to speech.
|
|
147
177
|
|
|
@@ -160,7 +190,9 @@ class Typecast:
|
|
|
160
190
|
"""
|
|
161
191
|
endpoint = "/v1/text-to-speech"
|
|
162
192
|
response = self.session.post(
|
|
163
|
-
f"{self.host}{endpoint}",
|
|
193
|
+
f"{self.host}{endpoint}",
|
|
194
|
+
json=request.model_dump(exclude_none=True),
|
|
195
|
+
headers=self._request_headers(),
|
|
164
196
|
)
|
|
165
197
|
if response.status_code != 200:
|
|
166
198
|
self._handle_error(response.status_code, response.text)
|
|
@@ -252,6 +284,7 @@ class Typecast:
|
|
|
252
284
|
json=request.model_dump(exclude_none=True),
|
|
253
285
|
stream=True,
|
|
254
286
|
timeout=(10, 300),
|
|
287
|
+
headers=self._request_headers(),
|
|
255
288
|
)
|
|
256
289
|
if response.status_code != 200:
|
|
257
290
|
error_text = response.text
|
|
@@ -299,6 +332,7 @@ class Typecast:
|
|
|
299
332
|
json=request.model_dump(exclude_none=True),
|
|
300
333
|
params=params,
|
|
301
334
|
timeout=(10, 300),
|
|
335
|
+
headers=self._request_headers(),
|
|
302
336
|
)
|
|
303
337
|
if response.status_code != 200:
|
|
304
338
|
self._handle_error(response.status_code, response.text)
|
|
@@ -335,12 +369,19 @@ class Typecast:
|
|
|
335
369
|
}
|
|
336
370
|
data = {"name": name, "model": model_str}
|
|
337
371
|
# Remove the session-level Content-Type so requests can set the
|
|
338
|
-
# correct multipart/form-data boundary for this request.
|
|
372
|
+
# correct multipart/form-data boundary for this request. For external
|
|
373
|
+
# sessions, also attach the per-request auth headers (X-API-KEY,
|
|
374
|
+
# User-Agent); _request_headers() returns None for owned sessions, so
|
|
375
|
+
# owned-session behavior is unchanged.
|
|
376
|
+
headers = {"Content-Type": None}
|
|
377
|
+
per_request = self._request_headers()
|
|
378
|
+
if per_request:
|
|
379
|
+
headers.update(per_request)
|
|
339
380
|
response = self.session.post(
|
|
340
381
|
f"{self.host}/v1/voices/clone",
|
|
341
382
|
files=files,
|
|
342
383
|
data=data,
|
|
343
|
-
headers=
|
|
384
|
+
headers=headers,
|
|
344
385
|
timeout=(10, 300),
|
|
345
386
|
)
|
|
346
387
|
if response.status_code != 200:
|
|
@@ -361,6 +402,7 @@ class Typecast:
|
|
|
361
402
|
response = self.session.delete(
|
|
362
403
|
f"{self.host}/v1/voices/{quote(voice_id, safe='')}",
|
|
363
404
|
timeout=(10, 60),
|
|
405
|
+
headers=self._request_headers(),
|
|
364
406
|
)
|
|
365
407
|
if response.status_code not in (200, 204):
|
|
366
408
|
self._handle_error(response.status_code, response.text)
|
|
@@ -383,7 +425,9 @@ class Typecast:
|
|
|
383
425
|
if model:
|
|
384
426
|
params["model"] = model
|
|
385
427
|
|
|
386
|
-
response = self.session.get(
|
|
428
|
+
response = self.session.get(
|
|
429
|
+
f"{self.host}{endpoint}", params=params, headers=self._request_headers()
|
|
430
|
+
)
|
|
387
431
|
|
|
388
432
|
if response.status_code != 200:
|
|
389
433
|
self._handle_error(response.status_code, response.text)
|
|
@@ -406,7 +450,9 @@ class Typecast:
|
|
|
406
450
|
This method is deprecated. Use voices_v2() for enhanced metadata.
|
|
407
451
|
"""
|
|
408
452
|
endpoint = f"/v1/voices/{voice_id}"
|
|
409
|
-
response = self.session.get(
|
|
453
|
+
response = self.session.get(
|
|
454
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
455
|
+
)
|
|
410
456
|
|
|
411
457
|
if response.status_code != 200:
|
|
412
458
|
self._handle_error(response.status_code, response.text)
|
|
@@ -440,7 +486,9 @@ class Typecast:
|
|
|
440
486
|
for key, value in filter_dict.items():
|
|
441
487
|
params[key] = getattr(value, "value", value)
|
|
442
488
|
|
|
443
|
-
response = self.session.get(
|
|
489
|
+
response = self.session.get(
|
|
490
|
+
f"{self.host}{endpoint}", params=params, headers=self._request_headers()
|
|
491
|
+
)
|
|
444
492
|
|
|
445
493
|
if response.status_code != 200:
|
|
446
494
|
self._handle_error(response.status_code, response.text)
|
|
@@ -462,7 +510,9 @@ class Typecast:
|
|
|
462
510
|
InternalServerError: On server-side failures.
|
|
463
511
|
"""
|
|
464
512
|
endpoint = "/v1/users/me/subscription"
|
|
465
|
-
response = self.session.get(
|
|
513
|
+
response = self.session.get(
|
|
514
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
515
|
+
)
|
|
466
516
|
if response.status_code != 200:
|
|
467
517
|
self._handle_error(response.status_code, response.text)
|
|
468
518
|
return SubscriptionResponse.model_validate(response.json())
|
|
@@ -480,9 +530,33 @@ class Typecast:
|
|
|
480
530
|
NotFoundError: If the voice ID does not exist.
|
|
481
531
|
"""
|
|
482
532
|
endpoint = f"/v2/voices/{voice_id}"
|
|
483
|
-
response = self.session.get(
|
|
533
|
+
response = self.session.get(
|
|
534
|
+
f"{self.host}{endpoint}", headers=self._request_headers()
|
|
535
|
+
)
|
|
484
536
|
|
|
485
537
|
if response.status_code != 200:
|
|
486
538
|
self._handle_error(response.status_code, response.text)
|
|
487
539
|
|
|
488
540
|
return VoiceV2Response.model_validate(response.json())
|
|
541
|
+
|
|
542
|
+
def recommend_voices(self, query: str, count: int = 5) -> list[RecommendedVoice]:
|
|
543
|
+
"""Recommend voices from a text description.
|
|
544
|
+
|
|
545
|
+
Recommendation results only include ``voice_id``, ``voice_name``, and
|
|
546
|
+
``score``. Use ``voice_v2`` or ``voices_v2`` to fetch detailed metadata
|
|
547
|
+
for returned voice IDs.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
query: Text description of the desired voice.
|
|
551
|
+
count: Number of recommendations to return, from 1 to 10. Defaults to 5.
|
|
552
|
+
"""
|
|
553
|
+
if count < 1 or count > 10:
|
|
554
|
+
raise ValueError("count must be between 1 and 10")
|
|
555
|
+
|
|
556
|
+
response = self.session.get(
|
|
557
|
+
f"{self.host}/v1/voices/recommendations",
|
|
558
|
+
params={"query": query, "count": count},
|
|
559
|
+
)
|
|
560
|
+
if response.status_code != 200:
|
|
561
|
+
self._handle_error(response.status_code, response.text)
|
|
562
|
+
return [RecommendedVoice.model_validate(item) for item in response.json()]
|
typecast/models/__init__.py
CHANGED
|
@@ -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",
|
typecast/models/tts.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from enum import Enum
|
|
2
|
-
from typing import Literal, Optional, Union
|
|
4
|
+
from typing import List, Literal, Optional, Union
|
|
3
5
|
|
|
4
6
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
5
7
|
|
|
@@ -330,7 +332,7 @@ def _group_into_cues(
|
|
|
330
332
|
word_mode=True: parts are joined with a single space.
|
|
331
333
|
word_mode=False: parts are concatenated directly.
|
|
332
334
|
|
|
333
|
-
Returns
|
|
335
|
+
Returns List[(text, start, end)] tuples.
|
|
334
336
|
"""
|
|
335
337
|
cues = []
|
|
336
338
|
cur_text_parts = []
|
|
@@ -404,11 +406,11 @@ class TTSWithTimestampsResponse(BaseModel):
|
|
|
404
406
|
audio: str = Field(description="Base64-encoded audio bytes.")
|
|
405
407
|
audio_format: Literal["wav", "mp3"] = Field(description="Audio encoding format.")
|
|
406
408
|
audio_duration: float = Field(description="Length of audio in seconds.")
|
|
407
|
-
words: Optional[
|
|
409
|
+
words: Optional[List[AlignmentSegmentWord]] = Field(
|
|
408
410
|
default=None,
|
|
409
411
|
description="Word-level timestamps; null when granularity=char.",
|
|
410
412
|
)
|
|
411
|
-
characters: Optional[
|
|
413
|
+
characters: Optional[List[AlignmentSegmentCharacter]] = Field(
|
|
412
414
|
default=None,
|
|
413
415
|
description="Character-level timestamps; null when granularity=word.",
|
|
414
416
|
)
|
typecast/models/voices.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from enum import Enum
|
|
2
|
-
from typing import Optional
|
|
4
|
+
from typing import List, Optional
|
|
3
5
|
|
|
4
6
|
from pydantic import BaseModel, Field
|
|
5
7
|
|
|
@@ -12,7 +14,7 @@ class VoicesResponse(BaseModel):
|
|
|
12
14
|
voice_id: str
|
|
13
15
|
voice_name: str
|
|
14
16
|
model: str
|
|
15
|
-
emotions:
|
|
17
|
+
emotions: List[str]
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class GenderEnum(str, Enum):
|
|
@@ -54,7 +56,7 @@ class ModelInfo(BaseModel):
|
|
|
54
56
|
"""Model information with supported emotions"""
|
|
55
57
|
|
|
56
58
|
version: TTSModel
|
|
57
|
-
emotions:
|
|
59
|
+
emotions: List[str]
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
class VoiceV2Response(BaseModel):
|
|
@@ -62,10 +64,23 @@ class VoiceV2Response(BaseModel):
|
|
|
62
64
|
|
|
63
65
|
voice_id: str
|
|
64
66
|
voice_name: str
|
|
65
|
-
models:
|
|
67
|
+
models: List[ModelInfo]
|
|
66
68
|
gender: Optional[GenderEnum] = None
|
|
67
69
|
age: Optional[AgeEnum] = None
|
|
68
|
-
use_cases: Optional[
|
|
70
|
+
use_cases: Optional[List[str]] = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class RecommendedVoice(BaseModel):
|
|
74
|
+
"""Recommended voice result.
|
|
75
|
+
|
|
76
|
+
Recommendation results only include the matched voice ID, voice name, and
|
|
77
|
+
similarity score. Use ``voice_v2`` or ``voices_v2`` to fetch detailed voice
|
|
78
|
+
metadata for a returned ``voice_id``.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
voice_id: str
|
|
82
|
+
voice_name: str
|
|
83
|
+
score: float
|
|
69
84
|
|
|
70
85
|
|
|
71
86
|
class VoicesV2Filter(BaseModel):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: typecast-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
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
|
|
@@ -217,26 +217,30 @@ Classifier: Intended Audience :: Developers
|
|
|
217
217
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
218
218
|
Classifier: Operating System :: OS Independent
|
|
219
219
|
Classifier: Programming Language :: Python :: 3
|
|
220
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
221
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
222
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
220
223
|
Classifier: Programming Language :: Python :: 3.11
|
|
221
224
|
Classifier: Programming Language :: Python :: 3.12
|
|
222
225
|
Classifier: Programming Language :: Python :: 3.13
|
|
226
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
223
227
|
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
224
228
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
225
|
-
Requires-Python:
|
|
226
|
-
Requires-Dist: aiohttp>=3.
|
|
229
|
+
Requires-Python: <3.15,>=3.8
|
|
230
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
227
231
|
Requires-Dist: pydantic>=2.0.0
|
|
228
232
|
Requires-Dist: requests>=2.28.0
|
|
229
233
|
Requires-Dist: typing-extensions>=4.0.0
|
|
230
234
|
Provides-Extra: dev
|
|
231
235
|
Requires-Dist: aioresponses>=0.7.6; extra == 'dev'
|
|
232
|
-
Requires-Dist: black
|
|
233
|
-
Requires-Dist: flake8
|
|
236
|
+
Requires-Dist: black<25.0.0,>=23.0.0; extra == 'dev'
|
|
237
|
+
Requires-Dist: flake8<6.0.0,>=5.0.0; extra == 'dev'
|
|
234
238
|
Requires-Dist: isort>=5.0.0; extra == 'dev'
|
|
235
|
-
Requires-Dist: mypy
|
|
236
|
-
Requires-Dist: pytest-asyncio
|
|
237
|
-
Requires-Dist: pytest-cov
|
|
239
|
+
Requires-Dist: mypy<1.11.0,>=1.0.0; extra == 'dev'
|
|
240
|
+
Requires-Dist: pytest-asyncio<1.0.0,>=0.21.0; extra == 'dev'
|
|
241
|
+
Requires-Dist: pytest-cov<6.0.0,>=5.0.0; extra == 'dev'
|
|
238
242
|
Requires-Dist: pytest-mock>=3.14.0; extra == 'dev'
|
|
239
|
-
Requires-Dist: pytest
|
|
243
|
+
Requires-Dist: pytest<9.0.0,>=7.0.0; extra == 'dev'
|
|
240
244
|
Description-Content-Type: text/markdown
|
|
241
245
|
|
|
242
246
|
<div align="center">
|
|
@@ -250,7 +254,7 @@ Convert text to lifelike speech using AI-powered voices
|
|
|
250
254
|
[](https://pypi.org/project/typecast-python/)
|
|
251
255
|
[](../docs/coverage-policy.md)
|
|
252
256
|
[](LICENSE)
|
|
253
|
-
[](https://www.python.org/)
|
|
254
258
|
|
|
255
259
|
[Documentation](https://typecast.ai/docs) | [API Reference](https://typecast.ai/docs/api-reference) | [Get API Key](https://typecast.ai/developers/api/api-key)
|
|
256
260
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
typecast/__init__.py,sha256=L6m0Z1jn1UgGjPp9AFiTwhHk9DVV_B4WrLfQDEfbcZI,1093
|
|
2
|
+
typecast/_user_agent.py,sha256=R_0eSKpuP9jLp_wDJcJGFvFtUh9BZwEgsT1buVeHRAA,1786
|
|
3
|
+
typecast/_voice_clone.py,sha256=TN2tbB3b5lC5uFCBnbERhz37bNJlbv6VZ-vj70EfTs4,3464
|
|
4
|
+
typecast/async_client.py,sha256=Gidl-gj0zGMzZCPzmMRcr2GiYFi0cBmZOwrLhSVRL_Y,21700
|
|
5
|
+
typecast/client.py,sha256=CL_Uw_PYRUwGT5CqLhZRXTGwY6Sum69TJ_S_2XPe56s,20886
|
|
6
|
+
typecast/composer.py,sha256=r1kpRGlSJ1s4C1PkE7yIpGwnKtxYfIdFSLWaf8v1RAQ,9422
|
|
7
|
+
typecast/conf.py,sha256=yfOHYZDvIshWWMriN5wE0GFPR8AX3zv4Rg2655OvA9Q,724
|
|
8
|
+
typecast/exceptions.py,sha256=Y0ZzYebe8zOSOSAHbXfKR0G_RJgdmZXxi15Z7ZxPLIk,1568
|
|
9
|
+
typecast/utils.py,sha256=XuNuX7gW8_CGKqZ-cv_tKlPVMPBluAYJBw2clwmjIMI,708
|
|
10
|
+
typecast/models/__init__.py,sha256=OmaFdGpH6gK7EJyNgvbwqZHXVxjnWKJzynn7D3pfULo,1249
|
|
11
|
+
typecast/models/error.py,sha256=XomIjx7jvlCjItqzJuCAT4mXC9jwTjxR8lLDUk6P8KA,152
|
|
12
|
+
typecast/models/subscription.py,sha256=EIaAAo3cCRw8LYT_O6D9AVwxqIHrWCijzl4UTx7FZB8,894
|
|
13
|
+
typecast/models/tts.py,sha256=xiO1e936vFr7vZqUErTEhexydoaMem6BcY_x1A1zFow,16495
|
|
14
|
+
typecast/models/voices.py,sha256=3UeBLwAlaNbQQUrt7FO18zqczLoP7jmi2x68yaUtllU,2687
|
|
15
|
+
typecast_python-0.3.9.dist-info/METADATA,sha256=taFaWuIBmeksyE5K_jUbiyx5376Cxq4xElMRTHASads,25785
|
|
16
|
+
typecast_python-0.3.9.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
17
|
+
typecast_python-0.3.9.dist-info/licenses/LICENSE,sha256=HvtJ-S89uUkuYmt-OvVk4MRxmzwtbn84__qJtSrGU2Q,11348
|
|
18
|
+
typecast_python-0.3.9.dist-info/RECORD,,
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
typecast/__init__.py,sha256=3pdJqNkXCZ7svzqab4sBR_qwyoM5E2sPjfuci1g1Ub8,1047
|
|
2
|
-
typecast/_user_agent.py,sha256=R_0eSKpuP9jLp_wDJcJGFvFtUh9BZwEgsT1buVeHRAA,1786
|
|
3
|
-
typecast/_voice_clone.py,sha256=TN2tbB3b5lC5uFCBnbERhz37bNJlbv6VZ-vj70EfTs4,3464
|
|
4
|
-
typecast/async_client.py,sha256=65dx24vxiZGpNAGMzxr_7CsG51qZMNAraCuzKjW0e-E,18746
|
|
5
|
-
typecast/client.py,sha256=jFts3z0vmtGb32rGJ8_Wcswu6mo6oXVNzdW9MD3GqGE,17974
|
|
6
|
-
typecast/composer.py,sha256=r1kpRGlSJ1s4C1PkE7yIpGwnKtxYfIdFSLWaf8v1RAQ,9422
|
|
7
|
-
typecast/conf.py,sha256=yfOHYZDvIshWWMriN5wE0GFPR8AX3zv4Rg2655OvA9Q,724
|
|
8
|
-
typecast/exceptions.py,sha256=Y0ZzYebe8zOSOSAHbXfKR0G_RJgdmZXxi15Z7ZxPLIk,1568
|
|
9
|
-
typecast/utils.py,sha256=XuNuX7gW8_CGKqZ-cv_tKlPVMPBluAYJBw2clwmjIMI,708
|
|
10
|
-
typecast/models/__init__.py,sha256=UEPUjg86fpCMXUvAzcNgxSPPhuPwCY9aQbzK3w90Fj0,1203
|
|
11
|
-
typecast/models/error.py,sha256=XomIjx7jvlCjItqzJuCAT4mXC9jwTjxR8lLDUk6P8KA,152
|
|
12
|
-
typecast/models/subscription.py,sha256=EIaAAo3cCRw8LYT_O6D9AVwxqIHrWCijzl4UTx7FZB8,894
|
|
13
|
-
typecast/models/tts.py,sha256=W2Bp9M-xPJPODlk89P9CnLA2fwb__4SR4yoMkG_O8kA,16453
|
|
14
|
-
typecast/models/voices.py,sha256=-EXP35jDy7_G30k5bDnVrFJHp6svEDTA5jJ8oHAgXNQ,2310
|
|
15
|
-
typecast_python-0.3.7.dist-info/METADATA,sha256=louuBNIHV_BGlSbchFiyXEaxSW4B-NxyH4AThzJqSAc,25530
|
|
16
|
-
typecast_python-0.3.7.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
17
|
-
typecast_python-0.3.7.dist-info/licenses/LICENSE,sha256=HvtJ-S89uUkuYmt-OvVk4MRxmzwtbn84__qJtSrGU2Q,11348
|
|
18
|
-
typecast_python-0.3.7.dist-info/RECORD,,
|
|
File without changes
|