typecast-python 0.3.3__py3-none-any.whl → 0.3.5__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/async_client.py +38 -0
- typecast/client.py +73 -0
- typecast/composer.py +275 -0
- typecast/models/tts.py +15 -3
- {typecast_python-0.3.3.dist-info → typecast_python-0.3.5.dist-info}/METADATA +1 -1
- {typecast_python-0.3.3.dist-info → typecast_python-0.3.5.dist-info}/RECORD +8 -7
- {typecast_python-0.3.3.dist-info → typecast_python-0.3.5.dist-info}/WHEEL +0 -0
- {typecast_python-0.3.3.dist-info → typecast_python-0.3.5.dist-info}/licenses/LICENSE +0 -0
typecast/async_client.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import AsyncIterator, BinaryIO, Optional, Union
|
|
3
4
|
from urllib.parse import quote
|
|
@@ -11,6 +12,8 @@ from ._voice_clone import (
|
|
|
11
12
|
validate_custom_voice_id,
|
|
12
13
|
)
|
|
13
14
|
from .client import _guess_audio_mime
|
|
15
|
+
from .client import _output_with_inferred_format
|
|
16
|
+
from .client import _validate_output_path
|
|
14
17
|
from .exceptions import (
|
|
15
18
|
BadRequestError,
|
|
16
19
|
InternalServerError,
|
|
@@ -23,8 +26,11 @@ from .exceptions import (
|
|
|
23
26
|
)
|
|
24
27
|
from .models import (
|
|
25
28
|
CustomVoice,
|
|
29
|
+
LanguageCode,
|
|
30
|
+
Output,
|
|
26
31
|
SubscriptionResponse,
|
|
27
32
|
TTSModel,
|
|
33
|
+
TTSPrompt,
|
|
28
34
|
TTSRequest,
|
|
29
35
|
TTSRequestStream,
|
|
30
36
|
TTSRequestWithTimestamps,
|
|
@@ -141,6 +147,38 @@ class AsyncTypecast:
|
|
|
141
147
|
format=response.headers.get("Content-Type", "audio/wav").split("/")[-1],
|
|
142
148
|
)
|
|
143
149
|
|
|
150
|
+
async def generate_to_file(
|
|
151
|
+
self,
|
|
152
|
+
path: Union[str, Path],
|
|
153
|
+
*,
|
|
154
|
+
text: str,
|
|
155
|
+
voice_id: str,
|
|
156
|
+
model: TTSModel = TTSModel.SSFM_V30,
|
|
157
|
+
language: Optional[Union[LanguageCode, str]] = None,
|
|
158
|
+
prompt: Optional[TTSPrompt] = None,
|
|
159
|
+
output: Optional[Output] = None,
|
|
160
|
+
seed: Optional[int] = None,
|
|
161
|
+
) -> TTSResponse:
|
|
162
|
+
"""Convert text to speech and write the audio bytes to a file.
|
|
163
|
+
|
|
164
|
+
Browse available API voices at
|
|
165
|
+
``https://typecast.ai/developers/api/voices``.
|
|
166
|
+
"""
|
|
167
|
+
output_path = _validate_output_path(path)
|
|
168
|
+
response = await self.text_to_speech(
|
|
169
|
+
TTSRequest(
|
|
170
|
+
text=text,
|
|
171
|
+
voice_id=voice_id,
|
|
172
|
+
model=model,
|
|
173
|
+
language=language,
|
|
174
|
+
prompt=prompt,
|
|
175
|
+
output=_output_with_inferred_format(output, path),
|
|
176
|
+
seed=seed,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
await asyncio.to_thread(output_path.write_bytes, response.audio_data)
|
|
180
|
+
return response
|
|
181
|
+
|
|
144
182
|
async def text_to_speech_stream(
|
|
145
183
|
self, request: TTSRequestStream, chunk_size: int = 8192
|
|
146
184
|
) -> AsyncIterator[bytes]:
|
typecast/client.py
CHANGED
|
@@ -10,6 +10,7 @@ from ._voice_clone import (
|
|
|
10
10
|
validate_clone_inputs,
|
|
11
11
|
validate_custom_voice_id,
|
|
12
12
|
)
|
|
13
|
+
from .composer import SpeechComposer
|
|
13
14
|
from .exceptions import (
|
|
14
15
|
BadRequestError,
|
|
15
16
|
InternalServerError,
|
|
@@ -22,8 +23,11 @@ from .exceptions import (
|
|
|
22
23
|
)
|
|
23
24
|
from .models import (
|
|
24
25
|
CustomVoice,
|
|
26
|
+
LanguageCode,
|
|
27
|
+
Output,
|
|
25
28
|
SubscriptionResponse,
|
|
26
29
|
TTSModel,
|
|
30
|
+
TTSPrompt,
|
|
27
31
|
TTSRequest,
|
|
28
32
|
TTSRequestStream,
|
|
29
33
|
TTSRequestWithTimestamps,
|
|
@@ -35,6 +39,33 @@ from .models import (
|
|
|
35
39
|
)
|
|
36
40
|
|
|
37
41
|
|
|
42
|
+
def _infer_audio_format_from_path(path: Union[str, Path]) -> Optional[str]:
|
|
43
|
+
suffix = Path(path).suffix.lower()
|
|
44
|
+
if suffix == ".mp3":
|
|
45
|
+
return "mp3"
|
|
46
|
+
if suffix == ".wav":
|
|
47
|
+
return "wav"
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _output_with_inferred_format(
|
|
52
|
+
output: Optional[Output],
|
|
53
|
+
path: Union[str, Path],
|
|
54
|
+
) -> Optional[Output]:
|
|
55
|
+
audio_format = _infer_audio_format_from_path(path)
|
|
56
|
+
if audio_format is None or (output is not None and output.audio_format is not None):
|
|
57
|
+
return output
|
|
58
|
+
if output is None:
|
|
59
|
+
return Output(audio_format=audio_format)
|
|
60
|
+
return output.model_copy(update={"audio_format": audio_format})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _validate_output_path(path: Union[str, Path]) -> Path:
|
|
64
|
+
if isinstance(path, str) and not path.strip():
|
|
65
|
+
raise ValueError("path cannot be empty")
|
|
66
|
+
return Path(path)
|
|
67
|
+
|
|
68
|
+
|
|
38
69
|
def _guess_audio_mime(filename: str) -> str:
|
|
39
70
|
"""Guess audio MIME type from filename extension; fall back to octet-stream."""
|
|
40
71
|
lower = filename.lower()
|
|
@@ -136,6 +167,48 @@ class Typecast:
|
|
|
136
167
|
format=response.headers.get("Content-Type", "audio/wav").split("/")[-1],
|
|
137
168
|
)
|
|
138
169
|
|
|
170
|
+
def compose_speech(self) -> SpeechComposer:
|
|
171
|
+
"""Build composed speech from multiple text and pause segments.
|
|
172
|
+
|
|
173
|
+
Text passed to ``say()`` may include pause markup such as ``<|0.3s|>``.
|
|
174
|
+
``pause(seconds)`` also uses seconds, e.g. ``0.3`` for 300 ms.
|
|
175
|
+
"""
|
|
176
|
+
return SpeechComposer(self.text_to_speech)
|
|
177
|
+
|
|
178
|
+
def generate_to_file(
|
|
179
|
+
self,
|
|
180
|
+
path: Union[str, Path],
|
|
181
|
+
*,
|
|
182
|
+
text: str,
|
|
183
|
+
voice_id: str,
|
|
184
|
+
model: TTSModel = TTSModel.SSFM_V30,
|
|
185
|
+
language: Optional[Union[LanguageCode, str]] = None,
|
|
186
|
+
prompt: Optional[TTSPrompt] = None,
|
|
187
|
+
output: Optional[Output] = None,
|
|
188
|
+
seed: Optional[int] = None,
|
|
189
|
+
) -> TTSResponse:
|
|
190
|
+
"""Convert text to speech and write the audio bytes to a file.
|
|
191
|
+
|
|
192
|
+
``model`` defaults to ``ssfm-v30``. If ``output.audio_format`` is not
|
|
193
|
+
set, the format is inferred from a ``.mp3`` or ``.wav`` extension.
|
|
194
|
+
Browse available API voices at
|
|
195
|
+
``https://typecast.ai/developers/api/voices``.
|
|
196
|
+
"""
|
|
197
|
+
output_path = _validate_output_path(path)
|
|
198
|
+
response = self.text_to_speech(
|
|
199
|
+
TTSRequest(
|
|
200
|
+
text=text,
|
|
201
|
+
voice_id=voice_id,
|
|
202
|
+
model=model,
|
|
203
|
+
language=language,
|
|
204
|
+
prompt=prompt,
|
|
205
|
+
output=_output_with_inferred_format(output, path),
|
|
206
|
+
seed=seed,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
output_path.write_bytes(response.audio_data)
|
|
210
|
+
return response
|
|
211
|
+
|
|
139
212
|
def text_to_speech_stream(
|
|
140
213
|
self, request: TTSRequestStream, chunk_size: int = 8192
|
|
141
214
|
) -> Iterator[bytes]:
|
typecast/composer.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import math
|
|
5
|
+
import re
|
|
6
|
+
import struct
|
|
7
|
+
import wave
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Callable, Literal, Optional, Union
|
|
10
|
+
|
|
11
|
+
from .models import LanguageCode, Output, TTSModel, TTSPrompt, TTSRequest, TTSResponse
|
|
12
|
+
|
|
13
|
+
_PAUSE_TOKEN = re.compile(r"<\|(\d+(?:\.\d+)?)s\|>")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class _TextPart:
|
|
18
|
+
kind: Literal["text"]
|
|
19
|
+
text: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class _PausePart:
|
|
24
|
+
kind: Literal["pause"]
|
|
25
|
+
seconds: float
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class _SpeechPart:
|
|
30
|
+
text: str
|
|
31
|
+
settings: "_ComposerSettings"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class _WavSpec:
|
|
36
|
+
sample_rate: int
|
|
37
|
+
channels: int
|
|
38
|
+
sample_width: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class _ParsedWav:
|
|
43
|
+
spec: _WavSpec
|
|
44
|
+
samples: list[int]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class _ComposerSettings:
|
|
49
|
+
voice_id: Optional[str] = None
|
|
50
|
+
model: Optional[Union[TTSModel, str]] = None
|
|
51
|
+
language: Optional[Union[LanguageCode, str]] = None
|
|
52
|
+
prompt: Optional[TTSPrompt] = None
|
|
53
|
+
output: Optional[Output] = None
|
|
54
|
+
seed: Optional[int] = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SpeechComposer:
|
|
58
|
+
def __init__(self, text_to_speech: Callable[[TTSRequest], TTSResponse]):
|
|
59
|
+
self._text_to_speech = text_to_speech
|
|
60
|
+
self._defaults = _ComposerSettings()
|
|
61
|
+
self._parts: list[Union[_SpeechPart, _PausePart]] = []
|
|
62
|
+
|
|
63
|
+
def defaults(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
voice_id: Optional[str] = None,
|
|
67
|
+
model: Optional[Union[TTSModel, str]] = None,
|
|
68
|
+
language: Optional[Union[LanguageCode, str]] = None,
|
|
69
|
+
prompt: Optional[TTSPrompt] = None,
|
|
70
|
+
output: Optional[Output] = None,
|
|
71
|
+
seed: Optional[int] = None,
|
|
72
|
+
) -> "SpeechComposer":
|
|
73
|
+
self._defaults = _merge_settings(
|
|
74
|
+
self._defaults,
|
|
75
|
+
_ComposerSettings(
|
|
76
|
+
voice_id=voice_id,
|
|
77
|
+
model=model,
|
|
78
|
+
language=language,
|
|
79
|
+
prompt=prompt,
|
|
80
|
+
output=output,
|
|
81
|
+
seed=seed,
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def say(
|
|
87
|
+
self,
|
|
88
|
+
text: str,
|
|
89
|
+
*,
|
|
90
|
+
voice_id: Optional[str] = None,
|
|
91
|
+
model: Optional[Union[TTSModel, str]] = None,
|
|
92
|
+
language: Optional[Union[LanguageCode, str]] = None,
|
|
93
|
+
prompt: Optional[TTSPrompt] = None,
|
|
94
|
+
output: Optional[Output] = None,
|
|
95
|
+
seed: Optional[int] = None,
|
|
96
|
+
) -> "SpeechComposer":
|
|
97
|
+
settings = _merge_settings(
|
|
98
|
+
self._defaults,
|
|
99
|
+
_ComposerSettings(
|
|
100
|
+
voice_id=voice_id,
|
|
101
|
+
model=model,
|
|
102
|
+
language=language,
|
|
103
|
+
prompt=prompt,
|
|
104
|
+
output=output,
|
|
105
|
+
seed=seed,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
self._parts.append(_SpeechPart(text=text, settings=settings))
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def pause(self, seconds: float) -> "SpeechComposer":
|
|
112
|
+
"""Insert silence between speech segments.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
seconds: Duration in seconds. Use 0.3 for 300 ms, 3 for 3 seconds.
|
|
116
|
+
"""
|
|
117
|
+
if not math.isfinite(seconds) or seconds <= 0:
|
|
118
|
+
raise ValueError("pause seconds must be greater than 0")
|
|
119
|
+
self._parts.append(_PausePart(kind="pause", seconds=seconds))
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
def generate(self) -> TTSResponse:
|
|
123
|
+
plan = self._build_plan()
|
|
124
|
+
if not any(isinstance(part, _SpeechPart) for part in plan):
|
|
125
|
+
raise ValueError("at least one speech segment is required")
|
|
126
|
+
|
|
127
|
+
output_format = (
|
|
128
|
+
self._defaults.output.audio_format if self._defaults.output else "wav"
|
|
129
|
+
)
|
|
130
|
+
if output_format not in ("wav", "mp3"):
|
|
131
|
+
raise ValueError(f"unsupported composed speech output format: {output_format}")
|
|
132
|
+
|
|
133
|
+
wav_spec: Optional[_WavSpec] = None
|
|
134
|
+
output_samples: list[int] = []
|
|
135
|
+
for part in plan:
|
|
136
|
+
if isinstance(part, _PausePart):
|
|
137
|
+
if wav_spec is None:
|
|
138
|
+
raise ValueError("pause cannot be the first composed part")
|
|
139
|
+
output_samples.extend(
|
|
140
|
+
[0] * _seconds_to_samples(part.seconds, wav_spec.sample_rate)
|
|
141
|
+
)
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
request = _settings_to_request(part.text, part.settings)
|
|
145
|
+
response = self._text_to_speech(request)
|
|
146
|
+
wav = _parse_wav(response.audio_data)
|
|
147
|
+
if wav_spec is not None and wav.spec != wav_spec:
|
|
148
|
+
raise ValueError("all composed WAV segments must use the same PCM format")
|
|
149
|
+
wav_spec = wav.spec
|
|
150
|
+
output_samples.extend(_trim_silence(wav.samples))
|
|
151
|
+
|
|
152
|
+
final_spec = wav_spec
|
|
153
|
+
assert final_spec is not None
|
|
154
|
+
wav_bytes = _encode_wav(output_samples, final_spec)
|
|
155
|
+
if output_format == "mp3":
|
|
156
|
+
raise ValueError("ffmpeg is required to encode composed speech as mp3")
|
|
157
|
+
return TTSResponse(
|
|
158
|
+
audio_data=wav_bytes,
|
|
159
|
+
duration=len(output_samples) / final_spec.sample_rate,
|
|
160
|
+
format="wav",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def _build_plan(self) -> list[Union[_SpeechPart, _PausePart]]:
|
|
164
|
+
plan: list[Union[_SpeechPart, _PausePart]] = []
|
|
165
|
+
for part in self._parts:
|
|
166
|
+
if isinstance(part, _PausePart):
|
|
167
|
+
plan.append(part)
|
|
168
|
+
continue
|
|
169
|
+
for parsed in parse_pause_markup(part.text):
|
|
170
|
+
if isinstance(parsed, _PausePart):
|
|
171
|
+
plan.append(parsed)
|
|
172
|
+
continue
|
|
173
|
+
if not parsed.text.strip():
|
|
174
|
+
continue
|
|
175
|
+
if not part.settings.voice_id:
|
|
176
|
+
raise ValueError("voice_id is required for composed speech segments")
|
|
177
|
+
if not part.settings.model:
|
|
178
|
+
raise ValueError("model is required for composed speech segments")
|
|
179
|
+
plan.append(_SpeechPart(text=parsed.text, settings=part.settings))
|
|
180
|
+
return plan
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def parse_pause_markup(text: str) -> list[Union[_TextPart, _PausePart]]:
|
|
184
|
+
parts: list[Union[_TextPart, _PausePart]] = []
|
|
185
|
+
last_index = 0
|
|
186
|
+
for match in _PAUSE_TOKEN.finditer(text):
|
|
187
|
+
if match.start() > last_index:
|
|
188
|
+
parts.append(_TextPart(kind="text", text=text[last_index : match.start()]))
|
|
189
|
+
parts.append(_PausePart(kind="pause", seconds=float(match.group(1))))
|
|
190
|
+
last_index = match.end()
|
|
191
|
+
if last_index < len(text):
|
|
192
|
+
parts.append(_TextPart(kind="text", text=text[last_index:]))
|
|
193
|
+
return parts
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _merge_settings(
|
|
197
|
+
base: _ComposerSettings,
|
|
198
|
+
override: _ComposerSettings,
|
|
199
|
+
) -> _ComposerSettings:
|
|
200
|
+
return _ComposerSettings(
|
|
201
|
+
voice_id=override.voice_id if override.voice_id is not None else base.voice_id,
|
|
202
|
+
model=override.model if override.model is not None else base.model,
|
|
203
|
+
language=override.language if override.language is not None else base.language,
|
|
204
|
+
prompt=override.prompt if override.prompt is not None else base.prompt,
|
|
205
|
+
output=_merge_output(base.output, override.output),
|
|
206
|
+
seed=override.seed if override.seed is not None else base.seed,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _merge_output(base: Optional[Output], override: Optional[Output]) -> Optional[Output]:
|
|
211
|
+
if base is None and override is None:
|
|
212
|
+
return None
|
|
213
|
+
data = base.model_dump(exclude_none=True, exclude_unset=True) if base is not None else {}
|
|
214
|
+
if override is not None:
|
|
215
|
+
data.update(override.model_dump(exclude_none=True, exclude_unset=True))
|
|
216
|
+
return Output(**data)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _settings_to_request(text: str, settings: _ComposerSettings) -> TTSRequest:
|
|
220
|
+
output = _merge_output(settings.output, Output(audio_format="wav"))
|
|
221
|
+
return TTSRequest(
|
|
222
|
+
text=text,
|
|
223
|
+
voice_id=settings.voice_id or "",
|
|
224
|
+
model=settings.model if isinstance(settings.model, TTSModel) else TTSModel(settings.model),
|
|
225
|
+
language=settings.language,
|
|
226
|
+
prompt=settings.prompt,
|
|
227
|
+
output=output,
|
|
228
|
+
seed=settings.seed,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _parse_wav(data: bytes) -> _ParsedWav:
|
|
233
|
+
try:
|
|
234
|
+
with wave.open(io.BytesIO(data), "rb") as reader:
|
|
235
|
+
channels = reader.getnchannels()
|
|
236
|
+
sample_width = reader.getsampwidth()
|
|
237
|
+
sample_rate = reader.getframerate()
|
|
238
|
+
if channels != 1 or sample_width != 2:
|
|
239
|
+
raise ValueError("only mono 16-bit PCM WAV is supported for composed speech")
|
|
240
|
+
frames = reader.readframes(reader.getnframes())
|
|
241
|
+
except (EOFError, wave.Error) as exc:
|
|
242
|
+
raise ValueError("unsupported WAV data") from exc
|
|
243
|
+
samples = [
|
|
244
|
+
struct.unpack("<h", frames[offset : offset + 2])[0]
|
|
245
|
+
for offset in range(0, len(frames), 2)
|
|
246
|
+
]
|
|
247
|
+
return _ParsedWav(
|
|
248
|
+
spec=_WavSpec(sample_rate=sample_rate, channels=channels, sample_width=sample_width),
|
|
249
|
+
samples=samples,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _encode_wav(samples: list[int], spec: _WavSpec) -> bytes:
|
|
254
|
+
payload = b"".join(struct.pack("<h", sample) for sample in samples)
|
|
255
|
+
out = io.BytesIO()
|
|
256
|
+
with wave.open(out, "wb") as writer:
|
|
257
|
+
writer.setnchannels(spec.channels)
|
|
258
|
+
writer.setsampwidth(spec.sample_width)
|
|
259
|
+
writer.setframerate(spec.sample_rate)
|
|
260
|
+
writer.writeframes(payload)
|
|
261
|
+
return out.getvalue()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _trim_silence(samples: list[int]) -> list[int]:
|
|
265
|
+
start = 0
|
|
266
|
+
end = len(samples)
|
|
267
|
+
while start < end and samples[start] == 0:
|
|
268
|
+
start += 1
|
|
269
|
+
while end > start and samples[end - 1] == 0:
|
|
270
|
+
end -= 1
|
|
271
|
+
return samples[start:end]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _seconds_to_samples(seconds: float, sample_rate: int) -> int:
|
|
275
|
+
return round(seconds * sample_rate)
|
typecast/models/tts.py
CHANGED
|
@@ -158,7 +158,11 @@ class TTSRequest(BaseModel):
|
|
|
158
158
|
model_config = ConfigDict(json_schema_extra={"exclude_none": True})
|
|
159
159
|
|
|
160
160
|
voice_id: str = Field(
|
|
161
|
-
description=
|
|
161
|
+
description=(
|
|
162
|
+
"Voice ID. Browse available API voices at "
|
|
163
|
+
"https://typecast.ai/developers/api/voices."
|
|
164
|
+
),
|
|
165
|
+
examples=["tc_62a8975e695ad26f7fb514d1"],
|
|
162
166
|
)
|
|
163
167
|
text: str = Field(description="Text", examples=["Hello. How are you?"])
|
|
164
168
|
model: TTSModel = Field(description="Voice model name", examples=["ssfm-v21"])
|
|
@@ -202,7 +206,11 @@ class TTSRequestStream(BaseModel):
|
|
|
202
206
|
model_config = ConfigDict(json_schema_extra={"exclude_none": True})
|
|
203
207
|
|
|
204
208
|
voice_id: str = Field(
|
|
205
|
-
description=
|
|
209
|
+
description=(
|
|
210
|
+
"Voice ID. Browse available API voices at "
|
|
211
|
+
"https://typecast.ai/developers/api/voices."
|
|
212
|
+
),
|
|
213
|
+
examples=["tc_62a8975e695ad26f7fb514d1"],
|
|
206
214
|
)
|
|
207
215
|
text: str = Field(description="Text", examples=["Hello. How are you?"])
|
|
208
216
|
model: TTSModel = Field(description="Voice model name", examples=["ssfm-v21"])
|
|
@@ -253,7 +261,11 @@ class TTSRequestWithTimestamps(BaseModel):
|
|
|
253
261
|
model_config = ConfigDict(json_schema_extra={"exclude_none": True})
|
|
254
262
|
|
|
255
263
|
voice_id: str = Field(
|
|
256
|
-
description=
|
|
264
|
+
description=(
|
|
265
|
+
"Voice ID. Browse available API voices at "
|
|
266
|
+
"https://typecast.ai/developers/api/voices."
|
|
267
|
+
),
|
|
268
|
+
examples=["tc_62a8975e695ad26f7fb514d1"],
|
|
257
269
|
)
|
|
258
270
|
text: str = Field(description="Text", examples=["Hello. How are you?"])
|
|
259
271
|
model: TTSModel = Field(description="Voice model name", examples=["ssfm-v30"])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: typecast-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
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
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
typecast/__init__.py,sha256=3pdJqNkXCZ7svzqab4sBR_qwyoM5E2sPjfuci1g1Ub8,1047
|
|
2
2
|
typecast/_voice_clone.py,sha256=TN2tbB3b5lC5uFCBnbERhz37bNJlbv6VZ-vj70EfTs4,3464
|
|
3
|
-
typecast/async_client.py,sha256=
|
|
4
|
-
typecast/client.py,sha256=
|
|
3
|
+
typecast/async_client.py,sha256=5Tz6j2waj4XHgHhoaxM__W3YcJxUS7r8Wc3RkZA-LBc,18645
|
|
4
|
+
typecast/client.py,sha256=U2X_TEesBxBBvm9ta8M9mZS7ofkx0iXIBqh1k_5mX2I,17834
|
|
5
|
+
typecast/composer.py,sha256=r1kpRGlSJ1s4C1PkE7yIpGwnKtxYfIdFSLWaf8v1RAQ,9422
|
|
5
6
|
typecast/conf.py,sha256=yfOHYZDvIshWWMriN5wE0GFPR8AX3zv4Rg2655OvA9Q,724
|
|
6
7
|
typecast/exceptions.py,sha256=Y0ZzYebe8zOSOSAHbXfKR0G_RJgdmZXxi15Z7ZxPLIk,1568
|
|
7
8
|
typecast/utils.py,sha256=XuNuX7gW8_CGKqZ-cv_tKlPVMPBluAYJBw2clwmjIMI,708
|
|
8
9
|
typecast/models/__init__.py,sha256=UEPUjg86fpCMXUvAzcNgxSPPhuPwCY9aQbzK3w90Fj0,1203
|
|
9
10
|
typecast/models/error.py,sha256=XomIjx7jvlCjItqzJuCAT4mXC9jwTjxR8lLDUk6P8KA,152
|
|
10
11
|
typecast/models/subscription.py,sha256=EIaAAo3cCRw8LYT_O6D9AVwxqIHrWCijzl4UTx7FZB8,894
|
|
11
|
-
typecast/models/tts.py,sha256=
|
|
12
|
+
typecast/models/tts.py,sha256=uZ8QEevpAnRF-W4_hoEx4EA1WP1TN0ZJIak1OYE1ThQ,16370
|
|
12
13
|
typecast/models/voices.py,sha256=-EXP35jDy7_G30k5bDnVrFJHp6svEDTA5jJ8oHAgXNQ,2310
|
|
13
|
-
typecast_python-0.3.
|
|
14
|
-
typecast_python-0.3.
|
|
15
|
-
typecast_python-0.3.
|
|
16
|
-
typecast_python-0.3.
|
|
14
|
+
typecast_python-0.3.5.dist-info/METADATA,sha256=el2lM6XISdSo4t5q84f9bttUey6qPsJ3_iHVT_twXN0,25530
|
|
15
|
+
typecast_python-0.3.5.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
16
|
+
typecast_python-0.3.5.dist-info/licenses/LICENSE,sha256=HvtJ-S89uUkuYmt-OvVk4MRxmzwtbn84__qJtSrGU2Q,11348
|
|
17
|
+
typecast_python-0.3.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|