typecast-python 0.3.2__py3-none-any.whl → 0.3.4__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 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,
@@ -68,6 +74,8 @@ class AsyncTypecast:
68
74
  """
69
75
  self.host = conf.get_host(host)
70
76
  self.api_key = conf.get_api_key(api_key)
77
+ if not self.api_key and conf.is_default_host(self.host):
78
+ raise ValueError("API key is required for the default Typecast API host")
71
79
  self.session: Optional[aiohttp.ClientSession] = None
72
80
 
73
81
  async def __aenter__(self):
@@ -139,6 +147,38 @@ class AsyncTypecast:
139
147
  format=response.headers.get("Content-Type", "audio/wav").split("/")[-1],
140
148
  )
141
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
+
142
182
  async def text_to_speech_stream(
143
183
  self, request: TTSRequestStream, chunk_size: int = 8192
144
184
  ) -> AsyncIterator[bytes]:
typecast/client.py CHANGED
@@ -22,8 +22,11 @@ from .exceptions import (
22
22
  )
23
23
  from .models import (
24
24
  CustomVoice,
25
+ LanguageCode,
26
+ Output,
25
27
  SubscriptionResponse,
26
28
  TTSModel,
29
+ TTSPrompt,
27
30
  TTSRequest,
28
31
  TTSRequestStream,
29
32
  TTSRequestWithTimestamps,
@@ -35,6 +38,33 @@ from .models import (
35
38
  )
36
39
 
37
40
 
41
+ def _infer_audio_format_from_path(path: Union[str, Path]) -> Optional[str]:
42
+ suffix = Path(path).suffix.lower()
43
+ if suffix == ".mp3":
44
+ return "mp3"
45
+ if suffix == ".wav":
46
+ return "wav"
47
+ return None
48
+
49
+
50
+ def _output_with_inferred_format(
51
+ output: Optional[Output],
52
+ path: Union[str, Path],
53
+ ) -> Optional[Output]:
54
+ audio_format = _infer_audio_format_from_path(path)
55
+ if audio_format is None or (output is not None and output.audio_format is not None):
56
+ return output
57
+ if output is None:
58
+ return Output(audio_format=audio_format)
59
+ return output.model_copy(update={"audio_format": audio_format})
60
+
61
+
62
+ def _validate_output_path(path: Union[str, Path]) -> Path:
63
+ if isinstance(path, str) and not path.strip():
64
+ raise ValueError("path cannot be empty")
65
+ return Path(path)
66
+
67
+
38
68
  def _guess_audio_mime(filename: str) -> str:
39
69
  """Guess audio MIME type from filename extension; fall back to octet-stream."""
40
70
  lower = filename.lower()
@@ -77,10 +107,13 @@ class Typecast:
77
107
  """
78
108
  self.host = conf.get_host(host)
79
109
  self.api_key = conf.get_api_key(api_key)
110
+ if not self.api_key and conf.is_default_host(self.host):
111
+ raise ValueError("API key is required for the default Typecast API host")
80
112
  self.session = requests.Session()
81
- self.session.headers.update(
82
- {"X-API-KEY": self.api_key, "Content-Type": "application/json"}
83
- )
113
+ headers = {"Content-Type": "application/json"}
114
+ if self.api_key:
115
+ headers["X-API-KEY"] = self.api_key
116
+ self.session.headers.update(headers)
84
117
 
85
118
  def _handle_error(self, status_code: int, response_text: str):
86
119
  """Handle HTTP error responses with specific exception types."""
@@ -133,6 +166,40 @@ class Typecast:
133
166
  format=response.headers.get("Content-Type", "audio/wav").split("/")[-1],
134
167
  )
135
168
 
169
+ def generate_to_file(
170
+ self,
171
+ path: Union[str, Path],
172
+ *,
173
+ text: str,
174
+ voice_id: str,
175
+ model: TTSModel = TTSModel.SSFM_V30,
176
+ language: Optional[Union[LanguageCode, str]] = None,
177
+ prompt: Optional[TTSPrompt] = None,
178
+ output: Optional[Output] = None,
179
+ seed: Optional[int] = None,
180
+ ) -> TTSResponse:
181
+ """Convert text to speech and write the audio bytes to a file.
182
+
183
+ ``model`` defaults to ``ssfm-v30``. If ``output.audio_format`` is not
184
+ set, the format is inferred from a ``.mp3`` or ``.wav`` extension.
185
+ Browse available API voices at
186
+ ``https://typecast.ai/developers/api/voices``.
187
+ """
188
+ output_path = _validate_output_path(path)
189
+ response = self.text_to_speech(
190
+ TTSRequest(
191
+ text=text,
192
+ voice_id=voice_id,
193
+ model=model,
194
+ language=language,
195
+ prompt=prompt,
196
+ output=_output_with_inferred_format(output, path),
197
+ seed=seed,
198
+ )
199
+ )
200
+ output_path.write_bytes(response.audio_data)
201
+ return response
202
+
136
203
  def text_to_speech_stream(
137
204
  self, request: TTSRequestStream, chunk_size: int = 8192
138
205
  ) -> Iterator[bytes]:
typecast/conf.py CHANGED
@@ -5,12 +5,21 @@ TYPECAST_API_HOST = "https://api.typecast.ai"
5
5
 
6
6
  def get_host(host=None):
7
7
  if host: # Parameter takes priority
8
- return host
8
+ return normalize_host(host)
9
9
  env_host = os.getenv("TYPECAST_API_HOST") # Check environment variable
10
- return env_host if env_host else TYPECAST_API_HOST # Use default if not set
10
+ return normalize_host(env_host) if env_host else TYPECAST_API_HOST # Use default if not set
11
11
 
12
12
 
13
13
  def get_api_key(api_key=None):
14
14
  if api_key: # Parameter takes priority
15
- return api_key
16
- return os.getenv("TYPECAST_API_KEY") # Return from environment variable
15
+ return api_key.strip()
16
+ env_key = os.getenv("TYPECAST_API_KEY") # Return from environment variable
17
+ return env_key.strip() if env_key else None
18
+
19
+
20
+ def normalize_host(host):
21
+ return host.strip().rstrip("/")
22
+
23
+
24
+ def is_default_host(host):
25
+ return normalize_host(host).lower() == TYPECAST_API_HOST
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="Voice ID", examples=["tc_62a8975e695ad26f7fb514d1"]
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="Voice ID", examples=["tc_62a8975e695ad26f7fb514d1"]
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="Voice ID", examples=["tc_62a8975e695ad26f7fb514d1"]
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.2
3
+ Version: 0.3.4
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,16 @@
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=8mab1ai_P1TdQilR1l29n5Cg40F7lN960shODemXXWs,17287
4
- typecast/client.py,sha256=PRFF7hj8Ih88dRl0ZiLcRRD8N4ofPPbYMZAKpk_b89w,15212
5
- typecast/conf.py,sha256=Fn_T4XW7BaHRnj0tP11BT5at3Y-db7oGcbBA_E1fmF0,479
3
+ typecast/async_client.py,sha256=5Tz6j2waj4XHgHhoaxM__W3YcJxUS7r8Wc3RkZA-LBc,18645
4
+ typecast/client.py,sha256=oMjYK6NEYivY5ICSst-ZKH13ORnbzfGGlC_ZLhxk_PM,17461
5
+ typecast/conf.py,sha256=yfOHYZDvIshWWMriN5wE0GFPR8AX3zv4Rg2655OvA9Q,724
6
6
  typecast/exceptions.py,sha256=Y0ZzYebe8zOSOSAHbXfKR0G_RJgdmZXxi15Z7ZxPLIk,1568
7
7
  typecast/utils.py,sha256=XuNuX7gW8_CGKqZ-cv_tKlPVMPBluAYJBw2clwmjIMI,708
8
8
  typecast/models/__init__.py,sha256=UEPUjg86fpCMXUvAzcNgxSPPhuPwCY9aQbzK3w90Fj0,1203
9
9
  typecast/models/error.py,sha256=XomIjx7jvlCjItqzJuCAT4mXC9jwTjxR8lLDUk6P8KA,152
10
10
  typecast/models/subscription.py,sha256=EIaAAo3cCRw8LYT_O6D9AVwxqIHrWCijzl4UTx7FZB8,894
11
- typecast/models/tts.py,sha256=IDz4IN2d4MCkATB_rVlaEoMS7EgWlQsW0tIP8k67LHo,16001
11
+ typecast/models/tts.py,sha256=uZ8QEevpAnRF-W4_hoEx4EA1WP1TN0ZJIak1OYE1ThQ,16370
12
12
  typecast/models/voices.py,sha256=-EXP35jDy7_G30k5bDnVrFJHp6svEDTA5jJ8oHAgXNQ,2310
13
- typecast_python-0.3.2.dist-info/METADATA,sha256=juL719j6AgB5jgCHsCTNQ-4eKi3tKQnCuQZb0d42qXo,25530
14
- typecast_python-0.3.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
15
- typecast_python-0.3.2.dist-info/licenses/LICENSE,sha256=HvtJ-S89uUkuYmt-OvVk4MRxmzwtbn84__qJtSrGU2Q,11348
16
- typecast_python-0.3.2.dist-info/RECORD,,
13
+ typecast_python-0.3.4.dist-info/METADATA,sha256=TS2HvMhG4jAK_Y7r_dQK2Kiun3gz696PXUsuGDWZgyA,25530
14
+ typecast_python-0.3.4.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
15
+ typecast_python-0.3.4.dist-info/licenses/LICENSE,sha256=HvtJ-S89uUkuYmt-OvVk4MRxmzwtbn84__qJtSrGU2Q,11348
16
+ typecast_python-0.3.4.dist-info/RECORD,,