typecast-python 0.1.9__tar.gz → 0.2.1__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.
@@ -83,6 +83,10 @@ CMakeFiles/
83
83
  CMakeCache.txt
84
84
  cmake_install.cmake
85
85
  Makefile
86
+ !typecast-go/Makefile
87
+ !typecast-java/Makefile
88
+ !typecast-rust/Makefile
89
+ !typecast-kotlin/Makefile
86
90
  *.cmake
87
91
 
88
92
  # Compiled objects
@@ -254,6 +258,7 @@ DerivedData/
254
258
  .swiftpm/
255
259
  Packages/
256
260
  Package.resolved
261
+ typecast-swift/coverage-summary.txt
257
262
 
258
263
  # CocoaPods
259
264
  Pods/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typecast-python
3
- Version: 0.1.9
3
+ Version: 0.2.1
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
@@ -226,9 +226,7 @@ Requires-Python: >=3.11
226
226
  Requires-Dist: aiohttp>=3.8.0
227
227
  Requires-Dist: pydantic>=2.0.0
228
228
  Requires-Dist: requests>=2.28.0
229
- Requires-Dist: sseclient-py>=1.7.2
230
229
  Requires-Dist: typing-extensions>=4.0.0
231
- Requires-Dist: websockets>=10.0
232
230
  Provides-Extra: dev
233
231
  Requires-Dist: aioresponses>=0.7.6; extra == 'dev'
234
232
  Requires-Dist: black>=23.0.0; extra == 'dev'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "typecast-python"
7
- version = "0.1.9"
7
+ version = "0.2.1"
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"}
@@ -32,8 +32,6 @@ dependencies = [
32
32
  "aiohttp>=3.8.0",
33
33
  "requests>=2.28.0",
34
34
  "pydantic>=2.0.0",
35
- "sseclient-py>=1.7.2",
36
- "websockets>=10.0",
37
35
  "typing-extensions>=4.0.0",
38
36
  ]
39
37
 
@@ -11,14 +11,19 @@ from .exceptions import (
11
11
  UnprocessableEntityError,
12
12
  )
13
13
  from .models import (
14
+ Credits,
14
15
  Error,
15
16
  LanguageCode,
17
+ Limits,
16
18
  Output,
19
+ OutputStream,
20
+ PlanTier,
17
21
  Prompt,
22
+ SubscriptionResponse,
18
23
  TTSRequest,
24
+ TTSRequestStream,
19
25
  TTSResponse,
20
26
  VoicesResponse,
21
- WebSocketMessage,
22
27
  )
23
28
 
24
29
  __all__ = [
@@ -35,12 +40,17 @@ __all__ = [
35
40
  "UnauthorizedError",
36
41
  "UnprocessableEntityError",
37
42
  # Models
43
+ "Credits",
38
44
  "Error",
39
45
  "LanguageCode",
46
+ "Limits",
40
47
  "Output",
48
+ "OutputStream",
49
+ "PlanTier",
41
50
  "Prompt",
51
+ "SubscriptionResponse",
42
52
  "TTSRequest",
53
+ "TTSRequestStream",
43
54
  "TTSResponse",
44
55
  "VoicesResponse",
45
- "WebSocketMessage",
46
56
  ]
@@ -1,4 +1,4 @@
1
- from typing import Optional
1
+ from typing import AsyncIterator, Optional
2
2
 
3
3
  import aiohttp
4
4
 
@@ -13,7 +13,15 @@ from .exceptions import (
13
13
  UnauthorizedError,
14
14
  UnprocessableEntityError,
15
15
  )
16
- from .models import TTSRequest, TTSResponse, VoicesResponse, VoiceV2Response, VoicesV2Filter
16
+ from .models import (
17
+ SubscriptionResponse,
18
+ TTSRequest,
19
+ TTSRequestStream,
20
+ TTSResponse,
21
+ VoicesResponse,
22
+ VoicesV2Filter,
23
+ VoiceV2Response,
24
+ )
17
25
 
18
26
 
19
27
  class AsyncTypecast:
@@ -117,6 +125,49 @@ class AsyncTypecast:
117
125
  format=response.headers.get("Content-Type", "audio/wav").split("/")[-1],
118
126
  )
119
127
 
128
+ async def text_to_speech_stream(
129
+ self, request: TTSRequestStream, chunk_size: int = 8192
130
+ ) -> AsyncIterator[bytes]:
131
+ """Stream synthesized audio from `POST /v1/text-to-speech/stream`.
132
+
133
+ Async generator that yields audio chunks as the server emits them.
134
+ For WAV the first chunk contains the WAV header (declared with size
135
+ 0xFFFFFFFF for streaming) followed by PCM data; subsequent chunks are
136
+ PCM only. For MP3 each chunk contains independently-decodable frames.
137
+
138
+ Args:
139
+ request: Streaming TTS request. Uses `OutputStream`, which omits
140
+ `volume` and `target_lufs` (not supported by the streaming
141
+ endpoint).
142
+ chunk_size: Maximum bytes returned per yielded chunk.
143
+
144
+ Yields:
145
+ Audio chunk bytes in the order produced by the server.
146
+
147
+ Raises:
148
+ TypecastError: If the client session is not initialized.
149
+ BadRequestError, UnauthorizedError, PaymentRequiredError,
150
+ NotFoundError, UnprocessableEntityError, RateLimitError,
151
+ InternalServerError, TypecastError: depending on response status.
152
+ """
153
+ if not isinstance(chunk_size, int) or isinstance(chunk_size, bool) or chunk_size < 1:
154
+ raise ValueError("chunk_size must be a positive integer")
155
+ if not self.session:
156
+ raise TypecastError("Client session not initialized. Use async with.")
157
+ endpoint = "/v1/text-to-speech/stream"
158
+ stream_timeout = aiohttp.ClientTimeout(sock_connect=10, sock_read=300)
159
+ async with self.session.post(
160
+ f"{self.host}{endpoint}",
161
+ json=request.model_dump(exclude_none=True),
162
+ timeout=stream_timeout,
163
+ ) as response:
164
+ if response.status != 200:
165
+ error_text = await response.text()
166
+ self._handle_error(response.status, error_text)
167
+
168
+ async for chunk in response.content.iter_chunked(chunk_size):
169
+ yield chunk
170
+
120
171
  async def voices(self, model: Optional[str] = None) -> list[VoicesResponse]:
121
172
  """Get available voices (V1 API) asynchronously.
122
173
 
@@ -212,6 +263,31 @@ class AsyncTypecast:
212
263
  data = await response.json()
213
264
  return [VoiceV2Response.model_validate(item) for item in data]
214
265
 
266
+ async def get_my_subscription(self) -> SubscriptionResponse:
267
+ """Get the authenticated user's current subscription asynchronously.
268
+
269
+ Returns plan tier, credit usage, and concurrency limits. Use this to
270
+ check remaining credits or verify your plan before making TTS calls.
271
+
272
+ Returns:
273
+ SubscriptionResponse with plan, credits, and limits.
274
+
275
+ Raises:
276
+ TypecastError: If the client session is not initialized.
277
+ UnauthorizedError: If the API key is invalid.
278
+ RateLimitError: If the rate limit was exceeded.
279
+ InternalServerError: On server-side failures.
280
+ """
281
+ if not self.session:
282
+ raise TypecastError("Client session not initialized. Use async with.")
283
+ endpoint = "/v1/users/me/subscription"
284
+ async with self.session.get(f"{self.host}{endpoint}") as response:
285
+ if response.status != 200:
286
+ error_text = await response.text()
287
+ self._handle_error(response.status, error_text)
288
+ data = await response.json()
289
+ return SubscriptionResponse.model_validate(data)
290
+
215
291
  async def voice_v2(self, voice_id: str) -> VoiceV2Response:
216
292
  """Get a specific voice by ID with enhanced metadata (V2 API)
217
293
 
@@ -1,4 +1,4 @@
1
- from typing import Optional
1
+ from typing import Iterator, Optional
2
2
 
3
3
  import requests
4
4
 
@@ -13,7 +13,15 @@ from .exceptions import (
13
13
  UnauthorizedError,
14
14
  UnprocessableEntityError,
15
15
  )
16
- from .models import TTSRequest, TTSResponse, VoicesResponse, VoiceV2Response, VoicesV2Filter
16
+ from .models import (
17
+ SubscriptionResponse,
18
+ TTSRequest,
19
+ TTSRequestStream,
20
+ TTSResponse,
21
+ VoicesResponse,
22
+ VoicesV2Filter,
23
+ VoiceV2Response,
24
+ )
17
25
 
18
26
 
19
27
  class Typecast:
@@ -104,6 +112,55 @@ class Typecast:
104
112
  format=response.headers.get("Content-Type", "audio/wav").split("/")[-1],
105
113
  )
106
114
 
115
+ def text_to_speech_stream(
116
+ self, request: TTSRequestStream, chunk_size: int = 8192
117
+ ) -> Iterator[bytes]:
118
+ """Stream synthesized audio from `POST /v1/text-to-speech/stream`.
119
+
120
+ Yields raw audio chunks as the server produces them. For WAV the
121
+ first chunk contains the WAV header (declared with size 0xFFFFFFFF
122
+ for streaming) followed by PCM data; subsequent chunks are PCM only.
123
+ For MP3 each chunk contains independently-decodable MP3 frames.
124
+
125
+ The HTTP response is held open until the iterator is exhausted or
126
+ garbage-collected, so callers should consume the iterator promptly
127
+ (e.g. inside a `for` loop or by writing chunks to disk).
128
+
129
+ Args:
130
+ request: Streaming TTS request. Uses `OutputStream`, which omits
131
+ `volume` and `target_lufs` (not supported by the streaming
132
+ endpoint).
133
+ chunk_size: Maximum bytes returned per yielded chunk.
134
+
135
+ Yields:
136
+ Audio chunk bytes in the order produced by the server.
137
+
138
+ Raises:
139
+ BadRequestError, UnauthorizedError, PaymentRequiredError,
140
+ NotFoundError, UnprocessableEntityError, RateLimitError,
141
+ InternalServerError, TypecastError: depending on response status.
142
+ """
143
+ if not isinstance(chunk_size, int) or isinstance(chunk_size, bool) or chunk_size < 1:
144
+ raise ValueError("chunk_size must be a positive integer")
145
+ endpoint = "/v1/text-to-speech/stream"
146
+ response = self.session.post(
147
+ f"{self.host}{endpoint}",
148
+ json=request.model_dump(exclude_none=True),
149
+ stream=True,
150
+ timeout=(10, 300),
151
+ )
152
+ if response.status_code != 200:
153
+ error_text = response.text
154
+ response.close()
155
+ self._handle_error(response.status_code, error_text)
156
+
157
+ try:
158
+ for chunk in response.iter_content(chunk_size=chunk_size):
159
+ if chunk:
160
+ yield chunk
161
+ finally:
162
+ response.close()
163
+
107
164
  def voices(self, model: Optional[str] = None) -> list[VoicesResponse]:
108
165
  """Get available voices (V1 API).
109
166
 
@@ -186,6 +243,26 @@ class Typecast:
186
243
 
187
244
  return [VoiceV2Response.model_validate(item) for item in response.json()]
188
245
 
246
+ def get_my_subscription(self) -> SubscriptionResponse:
247
+ """Get the authenticated user's current subscription.
248
+
249
+ Returns plan tier, credit usage, and concurrency limits. Use this to
250
+ check remaining credits or verify your plan before making TTS calls.
251
+
252
+ Returns:
253
+ SubscriptionResponse with plan, credits, and limits.
254
+
255
+ Raises:
256
+ UnauthorizedError: If the API key is invalid.
257
+ RateLimitError: If the rate limit was exceeded.
258
+ InternalServerError: On server-side failures.
259
+ """
260
+ endpoint = "/v1/users/me/subscription"
261
+ response = self.session.get(f"{self.host}{endpoint}")
262
+ if response.status_code != 200:
263
+ self._handle_error(response.status_code, response.text)
264
+ return SubscriptionResponse.model_validate(response.json())
265
+
189
266
  def voice_v2(self, voice_id: str) -> VoiceV2Response:
190
267
  """Get a specific voice by ID with enhanced metadata (V2 API)
191
268
 
@@ -1,17 +1,19 @@
1
1
  from .error import Error
2
+ from .subscription import Credits, Limits, PlanTier, SubscriptionResponse
2
3
  from .tts import (
3
4
  EmotionPreset,
4
5
  LanguageCode,
5
6
  Output,
7
+ OutputStream,
6
8
  PresetPrompt,
7
9
  Prompt,
8
10
  SmartPrompt,
9
11
  TTSModel,
10
12
  TTSPrompt,
11
13
  TTSRequest,
14
+ TTSRequestStream,
12
15
  TTSResponse,
13
16
  )
14
- from .tts_wss import WebSocketMessage
15
17
  from .voices import (
16
18
  AgeEnum,
17
19
  GenderEnum,
@@ -24,6 +26,7 @@ from .voices import (
24
26
 
25
27
  __all__ = [
26
28
  "TTSRequest",
29
+ "TTSRequestStream",
27
30
  "TTSModel",
28
31
  "TTSPrompt",
29
32
  "Prompt",
@@ -31,6 +34,7 @@ __all__ = [
31
34
  "SmartPrompt",
32
35
  "EmotionPreset",
33
36
  "Output",
37
+ "OutputStream",
34
38
  "TTSResponse",
35
39
  "VoicesResponse",
36
40
  "VoiceV2Response",
@@ -40,6 +44,9 @@ __all__ = [
40
44
  "AgeEnum",
41
45
  "UseCaseEnum",
42
46
  "Error",
43
- "WebSocketMessage",
44
47
  "LanguageCode",
48
+ "PlanTier",
49
+ "Credits",
50
+ "Limits",
51
+ "SubscriptionResponse",
45
52
  ]
@@ -0,0 +1,35 @@
1
+ from enum import Enum
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class PlanTier(str, Enum):
7
+ """Subscription plan tier."""
8
+
9
+ FREE = "free"
10
+ LITE = "lite"
11
+ PLUS = "plus"
12
+ CUSTOM = "custom"
13
+
14
+
15
+ class Credits(BaseModel):
16
+ """Credit usage information."""
17
+
18
+ plan_credits: int = Field(description="Total credits provided by the plan")
19
+ used_credits: int = Field(description="Number of credits used")
20
+
21
+
22
+ class Limits(BaseModel):
23
+ """Usage limit information."""
24
+
25
+ concurrency_limit: int = Field(
26
+ description="Maximum number of concurrent requests allowed"
27
+ )
28
+
29
+
30
+ class SubscriptionResponse(BaseModel):
31
+ """Response from `GET /v1/users/me/subscription`."""
32
+
33
+ plan: PlanTier = Field(description="Current subscription plan tier")
34
+ credits: Credits = Field(description="Credit usage information")
35
+ limits: Limits = Field(description="Usage limit information")
@@ -174,3 +174,41 @@ class TTSResponse(BaseModel):
174
174
  audio_data: bytes
175
175
  duration: float
176
176
  format: str = "wav"
177
+
178
+
179
+ class OutputStream(BaseModel):
180
+ """Audio output settings for streaming mode.
181
+
182
+ Streaming mode does not support `volume` or `target_lufs` because the
183
+ server has to commit each chunk before the full waveform is known.
184
+ Passing either field raises a validation error so misuse fails fast.
185
+ """
186
+
187
+ model_config = ConfigDict(extra="forbid")
188
+
189
+ audio_pitch: Optional[int] = Field(default=0, ge=-12, le=12)
190
+ audio_tempo: Optional[float] = Field(default=1.0, ge=0.5, le=2.0)
191
+ audio_format: Optional[str] = Field(
192
+ default="wav", description="Audio format", examples=["wav", "mp3"]
193
+ )
194
+
195
+
196
+ class TTSRequestStream(BaseModel):
197
+ """Request body for `POST /v1/text-to-speech/stream`.
198
+
199
+ Mirrors `TTSRequest` but uses `OutputStream` (no volume / target_lufs).
200
+ """
201
+
202
+ model_config = ConfigDict(json_schema_extra={"exclude_none": True})
203
+
204
+ voice_id: str = Field(
205
+ description="Voice ID", examples=["tc_62a8975e695ad26f7fb514d1"]
206
+ )
207
+ text: str = Field(description="Text", examples=["Hello. How are you?"])
208
+ model: TTSModel = Field(description="Voice model name", examples=["ssfm-v21"])
209
+ language: Optional[Union[LanguageCode, str]] = Field(
210
+ None, description="Language code (ISO 639-3)", examples=["eng"]
211
+ )
212
+ prompt: Optional[TTSPrompt] = None
213
+ output: Optional[OutputStream] = None
214
+ seed: Optional[int] = None
@@ -1,7 +0,0 @@
1
-
2
- from pydantic import BaseModel
3
-
4
-
5
- class WebSocketMessage(BaseModel):
6
- type: str
7
- payload: dict
@@ -1,58 +0,0 @@
1
- from typing import AsyncIterator, Optional
2
-
3
- import aiohttp
4
-
5
- from . import conf
6
- from .exceptions import TypecastError
7
-
8
-
9
- class TypecastSSE:
10
- """Server-Sent Events client for Typecast streaming endpoints."""
11
-
12
- def __init__(
13
- self,
14
- api_key: Optional[str] = None,
15
- host: Optional[str] = None,
16
- sse_url: Optional[str] = None,
17
- ):
18
- """Initialize the SSE client.
19
-
20
- Args:
21
- api_key: API key. Defaults to TYPECAST_API_KEY env var.
22
- host: API host. Defaults to TYPECAST_API_HOST env var or
23
- 'https://api.typecast.ai'. Used to derive sse_url when
24
- sse_url is not provided.
25
- sse_url: Full SSE base URL override (escape hatch for tests).
26
- When provided, takes precedence over host.
27
- """
28
- self.api_key = conf.get_api_key(api_key)
29
- self.host = conf.get_host(host)
30
- self._sse_url_override = sse_url
31
- self.session: Optional[aiohttp.ClientSession] = None
32
-
33
- @property
34
- def sse_url(self) -> str:
35
- if self._sse_url_override is not None:
36
- return self._sse_url_override
37
- return f"{self.host}/v1/text-to-speech/sse"
38
-
39
- async def connect(self, endpoint: str) -> AsyncIterator[str]:
40
- if self.session:
41
- await self.session.close()
42
-
43
- self.session = aiohttp.ClientSession(
44
- headers={"X-API-KEY": self.api_key, "Accept": "text/event-stream"}
45
- )
46
-
47
- async with self.session.get(f"{self.sse_url}/{endpoint}") as response:
48
- if response.status != 200:
49
- raise TypecastError(f"SSE connection failed: {response.status}")
50
-
51
- async for line in response.content:
52
- decoded_line = line.decode("utf-8").strip()
53
- if decoded_line.startswith("data: "):
54
- yield decoded_line[6:]
55
-
56
- async def close(self):
57
- if self.session:
58
- await self.session.close()
@@ -1,48 +0,0 @@
1
- import asyncio
2
- import json
3
- from typing import Callable, Optional
4
-
5
- import websockets
6
-
7
- from .exceptions import TypecastError
8
- from .models import WebSocketMessage
9
-
10
-
11
- class TypecastWebSocket:
12
- """WebSocket client for Typecast streaming TTS."""
13
-
14
- DEFAULT_WS_URL = "wss://api.typecast.ai/v1/ws"
15
-
16
- def __init__(self, api_key: str, ws_url: Optional[str] = None):
17
- self.api_key = api_key
18
- self.ws_url = ws_url or self.DEFAULT_WS_URL
19
- self.ws: Optional[websockets.WebSocketClientProtocol] = None
20
- self.callbacks: dict[str, Callable] = {}
21
-
22
- async def connect(self):
23
- self.ws = await websockets.connect(f"{self.ws_url}?token={self.api_key}")
24
- asyncio.create_task(self._message_handler())
25
-
26
- async def _message_handler(self):
27
- if not self.ws:
28
- return
29
-
30
- async for message in self.ws:
31
- data = json.loads(message)
32
- msg = WebSocketMessage(**data)
33
-
34
- if msg.type in self.callbacks:
35
- await self.callbacks[msg.type](msg.payload)
36
-
37
- def on(self, event_type: str, callback: Callable):
38
- """Register event callback."""
39
- self.callbacks[event_type] = callback
40
-
41
- async def send(self, message: WebSocketMessage):
42
- if not self.ws:
43
- raise TypecastError("WebSocket not connected")
44
- await self.ws.send(message.model_dump_json())
45
-
46
- async def close(self):
47
- if self.ws:
48
- await self.ws.close()
File without changes