typecast-python 0.3.13__tar.gz → 0.3.14__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.
Files changed (20) hide show
  1. {typecast_python-0.3.13 → typecast_python-0.3.14}/PKG-INFO +1 -1
  2. {typecast_python-0.3.13 → typecast_python-0.3.14}/pyproject.toml +1 -1
  3. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/_voice_clone.py +6 -0
  4. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/async_client.py +87 -7
  5. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/client.py +77 -4
  6. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/models/__init__.py +4 -0
  7. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/models/subscription.py +5 -0
  8. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/models/voices.py +25 -1
  9. {typecast_python-0.3.13 → typecast_python-0.3.14}/.gitignore +0 -0
  10. {typecast_python-0.3.13 → typecast_python-0.3.14}/LICENSE +0 -0
  11. {typecast_python-0.3.13 → typecast_python-0.3.14}/README.md +0 -0
  12. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/__init__.py +0 -0
  13. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/_httpx_compat.py +0 -0
  14. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/_user_agent.py +0 -0
  15. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/composer.py +0 -0
  16. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/conf.py +0 -0
  17. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/exceptions.py +0 -0
  18. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/models/error.py +0 -0
  19. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/models/tts.py +0 -0
  20. {typecast_python-0.3.13 → typecast_python-0.3.14}/src/typecast/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: typecast-python
3
- Version: 0.3.13
3
+ Version: 0.3.14
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.13"
7
+ version = "0.3.14"
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"}
@@ -36,6 +36,12 @@ def validate_custom_voice_id(voice_id: str) -> None:
36
36
  )
37
37
 
38
38
 
39
+ def validate_voice_id(voice_id: str) -> None:
40
+ """Reject blank voice IDs before constructing a detail URL."""
41
+ if not isinstance(voice_id, str) or not voice_id.strip():
42
+ raise ValueError("voice_id must not be blank")
43
+
44
+
39
45
  def validate_clone_inputs(audio: AudioInput, name: str) -> tuple[bytes, str]:
40
46
  """Pre-validate `clone_voice` inputs and return (audio_bytes, filename).
41
47
 
@@ -12,18 +12,21 @@ else: # pragma: no cover
12
12
  aiohttp = None # type: ignore[assignment]
13
13
 
14
14
  from . import conf
15
+ from ._user_agent import aiohttp_user_agent, attribution_suffix, httpx_user_agent
15
16
  from ._voice_clone import (
16
17
  normalize_clone_model,
17
18
  validate_clone_inputs,
18
19
  validate_custom_voice_id,
20
+ validate_voice_id,
19
21
  )
20
- from ._user_agent import aiohttp_user_agent, attribution_suffix, httpx_user_agent
21
22
 
22
23
  if TYPE_CHECKING or sys.version_info < (3, 10): # pragma: no cover
23
24
  from ._httpx_compat import AiohttpCompatSession, ClientTimeout, FormData
24
- from .client import _guess_audio_mime
25
- from .client import _output_with_inferred_format
26
- from .client import _validate_output_path
25
+ from .client import (
26
+ _guess_audio_mime,
27
+ _output_with_inferred_format,
28
+ _validate_output_path,
29
+ )
27
30
  from .exceptions import (
28
31
  BadRequestError,
29
32
  InternalServerError,
@@ -50,6 +53,7 @@ from .models import (
50
53
  VoicesResponse,
51
54
  VoicesV2Filter,
52
55
  VoiceV2Response,
56
+ VoiceV3Response,
53
57
  )
54
58
 
55
59
 
@@ -388,12 +392,12 @@ class AsyncTypecast:
388
392
  else ClientTimeout(total=300, connect=10)
389
393
  )
390
394
  async with self.session.post(
391
- f"{self.host}/v1/voices/clone",
395
+ f"{self.host}/v1/custom-voices/instant-clone",
392
396
  data=form,
393
397
  timeout=timeout,
394
398
  headers=self._request_headers(),
395
399
  ) as response:
396
- if response.status != 200:
400
+ if response.status not in (200, 201):
397
401
  text = await response.text()
398
402
  self._handle_error(response.status, text)
399
403
  body = await response.json()
@@ -418,7 +422,7 @@ class AsyncTypecast:
418
422
  else ClientTimeout(total=60, connect=10)
419
423
  )
420
424
  async with self.session.delete(
421
- f"{self.host}/v1/voices/{quote(voice_id, safe='')}",
425
+ f"{self.host}/v1/custom-voices/{quote(voice_id, safe='')}",
422
426
  timeout=timeout,
423
427
  headers=self._request_headers(),
424
428
  ) as response:
@@ -426,6 +430,52 @@ class AsyncTypecast:
426
430
  text = await response.text()
427
431
  self._handle_error(response.status, text)
428
432
 
433
+ async def create_professional_voice(
434
+ self,
435
+ audio: Union[str, Path, bytes, BinaryIO],
436
+ name: str,
437
+ language: Union[str, LanguageCode],
438
+ model: Union[str, "TTSModel"],
439
+ ) -> CustomVoice:
440
+ """Start an asynchronous professional custom-voice clone."""
441
+ if self.session is None:
442
+ raise TypecastError("Client session not initialized; use 'async with'.")
443
+ audio_bytes, filename = validate_clone_inputs(audio, name)
444
+ form: Any = aiohttp.FormData() if aiohttp else FormData()
445
+ form.add_field("name", name)
446
+ form.add_field("language", str(language.value if hasattr(language, "value") else language))
447
+ form.add_field("model", normalize_clone_model(model))
448
+ form.add_field("files", audio_bytes, filename=filename, content_type=_guess_audio_mime(filename))
449
+ timeout = aiohttp.ClientTimeout(total=300, connect=10) if aiohttp else ClientTimeout(total=300, connect=10)
450
+ async with self.session.post(
451
+ f"{self.host}/v1/custom-voices/professional-clone",
452
+ data=form, timeout=timeout, headers=self._request_headers(),
453
+ ) as response:
454
+ if response.status != 202:
455
+ self._handle_error(response.status, await response.text())
456
+ return CustomVoice.model_validate(await response.json())
457
+
458
+ async def get_custom_voices(self) -> list[CustomVoice]:
459
+ """List custom voices owned by the authenticated user."""
460
+ if self.session is None:
461
+ raise TypecastError("Client session not initialized; use 'async with'.")
462
+ async with self.session.get(f"{self.host}/v1/custom-voices", headers=self._request_headers()) as response:
463
+ if response.status != 200:
464
+ self._handle_error(response.status, await response.text())
465
+ return [CustomVoice.model_validate(item) for item in await response.json()]
466
+
467
+ async def get_custom_voice(self, voice_id: str) -> CustomVoice:
468
+ """Get a custom voice, including professional-clone status."""
469
+ if self.session is None:
470
+ raise TypecastError("Client session not initialized; use 'async with'.")
471
+ validate_custom_voice_id(voice_id)
472
+ async with self.session.get(
473
+ f"{self.host}/v1/custom-voices/{quote(voice_id, safe='')}", headers=self._request_headers()
474
+ ) as response:
475
+ if response.status != 200:
476
+ self._handle_error(response.status, await response.text())
477
+ return CustomVoice.model_validate(await response.json())
478
+
429
479
  async def voices(self, model: Optional[str] = None) -> list[VoicesResponse]:
430
480
  """Get available voices (V1 API) asynchronously.
431
481
 
@@ -580,6 +630,36 @@ class AsyncTypecast:
580
630
  data = await response.json()
581
631
  return VoiceV2Response.model_validate(data)
582
632
 
633
+ async def voices_v3(
634
+ self, filter: Optional[VoicesV2Filter] = None
635
+ ) -> list[VoiceV3Response]:
636
+ """Get voices using the current V3 Voice API asynchronously."""
637
+ if not self.session:
638
+ raise TypecastError("Client session not initialized. Use async with.")
639
+ params = {}
640
+ if filter:
641
+ for key, value in filter.model_dump(exclude_none=True).items():
642
+ params[key] = getattr(value, "value", value)
643
+ async with self.session.get(
644
+ f"{self.host}/v3/voices", params=params, headers=self._request_headers()
645
+ ) as response:
646
+ if response.status != 200:
647
+ self._handle_error(response.status, await response.text())
648
+ return [VoiceV3Response.model_validate(item) for item in await response.json()]
649
+
650
+ async def voice_v3(self, voice_id: str) -> VoiceV3Response:
651
+ """Get a voice by ID using the current V3 Voice API asynchronously."""
652
+ validate_voice_id(voice_id)
653
+ if not self.session:
654
+ raise TypecastError("Client session not initialized. Use async with.")
655
+ async with self.session.get(
656
+ f"{self.host}/v3/voices/{quote(voice_id, safe='')}",
657
+ headers=self._request_headers(),
658
+ ) as response:
659
+ if response.status != 200:
660
+ self._handle_error(response.status, await response.text())
661
+ return VoiceV3Response.model_validate(await response.json())
662
+
583
663
  async def recommend_voices(
584
664
  self, query: str, count: int = 5
585
665
  ) -> list[RecommendedVoice]:
@@ -11,12 +11,13 @@ else: # pragma: no cover
11
11
  requests = None # type: ignore[assignment]
12
12
 
13
13
  from . import conf
14
+ from ._user_agent import attribution_suffix, httpx_user_agent, requests_user_agent
14
15
  from ._voice_clone import (
15
16
  normalize_clone_model,
16
17
  validate_clone_inputs,
17
18
  validate_custom_voice_id,
19
+ validate_voice_id,
18
20
  )
19
- from ._user_agent import attribution_suffix, httpx_user_agent, requests_user_agent
20
21
 
21
22
  if TYPE_CHECKING or sys.version_info < (3, 10): # pragma: no cover
22
23
  from ._httpx_compat import RequestsCompatSession
@@ -47,6 +48,7 @@ from .models import (
47
48
  VoicesResponse,
48
49
  VoicesV2Filter,
49
50
  VoiceV2Response,
51
+ VoiceV3Response,
50
52
  )
51
53
 
52
54
 
@@ -418,13 +420,13 @@ class Typecast:
418
420
  if per_request:
419
421
  headers.update(per_request)
420
422
  response = self.session.post(
421
- f"{self.host}/v1/voices/clone",
423
+ f"{self.host}/v1/custom-voices/instant-clone",
422
424
  files=files,
423
425
  data=data,
424
426
  headers=headers,
425
427
  timeout=(10, 300),
426
428
  )
427
- if response.status_code != 200:
429
+ if response.status_code not in (200, 201):
428
430
  self._handle_error(response.status_code, response.text)
429
431
  return CustomVoice.model_validate(response.json())
430
432
 
@@ -440,13 +442,58 @@ class Typecast:
440
442
  """
441
443
  validate_custom_voice_id(voice_id)
442
444
  response = self.session.delete(
443
- f"{self.host}/v1/voices/{quote(voice_id, safe='')}",
445
+ f"{self.host}/v1/custom-voices/{quote(voice_id, safe='')}",
444
446
  timeout=(10, 60),
445
447
  headers=self._request_headers(),
446
448
  )
447
449
  if response.status_code not in (200, 204):
448
450
  self._handle_error(response.status_code, response.text)
449
451
 
452
+ def create_professional_voice(
453
+ self,
454
+ audio: Union[str, Path, bytes, BinaryIO],
455
+ name: str,
456
+ language: Union[str, LanguageCode],
457
+ model: Union[str, "TTSModel"],
458
+ ) -> CustomVoice:
459
+ """Start an asynchronous professional custom-voice clone.
460
+
461
+ Poll :meth:`get_custom_voice` until the returned status is
462
+ ``"completed"`` or ``"failed"``.
463
+ """
464
+ audio_bytes, filename = validate_clone_inputs(audio, name)
465
+ model_str = normalize_clone_model(model)
466
+ response = self.session.post(
467
+ f"{self.host}/v1/custom-voices/professional-clone",
468
+ files={"files": (filename, audio_bytes, _guess_audio_mime(filename))},
469
+ data={"name": name, "language": str(language.value if hasattr(language, "value") else language), "model": model_str},
470
+ headers={"Content-Type": None, **(self._request_headers() or {})},
471
+ timeout=(10, 300),
472
+ )
473
+ if response.status_code != 202:
474
+ self._handle_error(response.status_code, response.text)
475
+ return CustomVoice.model_validate(response.json())
476
+
477
+ def get_custom_voices(self) -> list[CustomVoice]:
478
+ """List custom voices owned by the authenticated user."""
479
+ response = self.session.get(
480
+ f"{self.host}/v1/custom-voices", headers=self._request_headers()
481
+ )
482
+ if response.status_code != 200:
483
+ self._handle_error(response.status_code, response.text)
484
+ return [CustomVoice.model_validate(item) for item in response.json()]
485
+
486
+ def get_custom_voice(self, voice_id: str) -> CustomVoice:
487
+ """Get a custom voice, including professional-clone status."""
488
+ validate_custom_voice_id(voice_id)
489
+ response = self.session.get(
490
+ f"{self.host}/v1/custom-voices/{quote(voice_id, safe='')}",
491
+ headers=self._request_headers(),
492
+ )
493
+ if response.status_code != 200:
494
+ self._handle_error(response.status_code, response.text)
495
+ return CustomVoice.model_validate(response.json())
496
+
450
497
  def voices(self, model: Optional[str] = None) -> list[VoicesResponse]:
451
498
  """Get available voices (V1 API).
452
499
 
@@ -579,6 +626,32 @@ class Typecast:
579
626
 
580
627
  return VoiceV2Response.model_validate(response.json())
581
628
 
629
+ def voices_v3(
630
+ self, filter: Optional[VoicesV2Filter] = None
631
+ ) -> list[VoiceV3Response]:
632
+ """Get voices using the current V3 Voice API."""
633
+ params = {}
634
+ if filter:
635
+ for key, value in filter.model_dump(exclude_none=True).items():
636
+ params[key] = getattr(value, "value", value)
637
+ response = self.session.get(
638
+ f"{self.host}/v3/voices", params=params, headers=self._request_headers()
639
+ )
640
+ if response.status_code != 200:
641
+ self._handle_error(response.status_code, response.text)
642
+ return [VoiceV3Response.model_validate(item) for item in response.json()]
643
+
644
+ def voice_v3(self, voice_id: str) -> VoiceV3Response:
645
+ """Get a voice by ID using the current V3 Voice API."""
646
+ validate_voice_id(voice_id)
647
+ response = self.session.get(
648
+ f"{self.host}/v3/voices/{quote(voice_id, safe='')}",
649
+ headers=self._request_headers(),
650
+ )
651
+ if response.status_code != 200:
652
+ self._handle_error(response.status_code, response.text)
653
+ return VoiceV3Response.model_validate(response.json())
654
+
582
655
  def recommend_voices(self, query: str, count: int = 5) -> list[RecommendedVoice]:
583
656
  """Recommend voices from a text description.
584
657
 
@@ -22,12 +22,14 @@ from .voices import (
22
22
  AgeEnum,
23
23
  CustomVoice,
24
24
  GenderEnum,
25
+ LocalizedVoiceName,
25
26
  ModelInfo,
26
27
  RecommendedVoice,
27
28
  UseCaseEnum,
28
29
  VoicesResponse,
29
30
  VoicesV2Filter,
30
31
  VoiceV2Response,
32
+ VoiceV3Response,
31
33
  )
32
34
 
33
35
  __all__ = [
@@ -40,6 +42,7 @@ __all__ = [
40
42
  "Error",
41
43
  "GenderEnum",
42
44
  "LanguageCode",
45
+ "LocalizedVoiceName",
43
46
  "Limits",
44
47
  "ModelInfo",
45
48
  "Output",
@@ -59,6 +62,7 @@ __all__ = [
59
62
  "TTSWithTimestampsResponse",
60
63
  "UseCaseEnum",
61
64
  "VoiceV2Response",
65
+ "VoiceV3Response",
62
66
  "VoicesResponse",
63
67
  "VoicesV2Filter",
64
68
  ]
@@ -25,6 +25,11 @@ class Limits(BaseModel):
25
25
  concurrency_limit: int = Field(
26
26
  description="Maximum number of concurrent requests allowed"
27
27
  )
28
+ custom_voice_slot: int = Field(
29
+ default=0,
30
+ ge=0,
31
+ description="Maximum active custom voices allowed by the current plan",
32
+ )
28
33
 
29
34
 
30
35
  class SubscriptionResponse(BaseModel):
@@ -70,6 +70,26 @@ class VoiceV2Response(BaseModel):
70
70
  use_cases: Optional[List[str]] = None
71
71
 
72
72
 
73
+ class LocalizedVoiceName(BaseModel):
74
+ """English and Korean display names returned by the V3 Voice API."""
75
+
76
+ eng: str
77
+ kor: str
78
+
79
+
80
+ class VoiceV3Response(BaseModel):
81
+ """V3 Voice response with localized name and preview metadata."""
82
+
83
+ voice_id: str
84
+ voice_name: LocalizedVoiceName
85
+ models: List[ModelInfo]
86
+ voice_type: str
87
+ gender: Optional[GenderEnum] = None
88
+ age: Optional[AgeEnum] = None
89
+ use_cases: Optional[List[str]] = None
90
+ preview_url: Optional[str] = None
91
+
92
+
73
93
  class RecommendedVoice(BaseModel):
74
94
  """Recommended voice result.
75
95
 
@@ -93,7 +113,7 @@ class VoicesV2Filter(BaseModel):
93
113
 
94
114
 
95
115
  class CustomVoice(BaseModel):
96
- """Quick-cloned custom voice returned by `POST /v1/voices/clone`.
116
+ """Custom voice returned by the Custom Voice API.
97
117
 
98
118
  Attributes:
99
119
  voice_id: Custom voice identifier with `uc_` prefix.
@@ -105,3 +125,7 @@ class CustomVoice(BaseModel):
105
125
  voice_id: str = Field(..., description="Custom voice identifier (uc_ prefix)")
106
126
  name: str = Field(..., description="Human-readable voice name")
107
127
  model: str = Field(..., description="Engine model: ssfm-v21 or ssfm-v30")
128
+ source: Optional[str] = Field(default=None, description="instant or professional")
129
+ status: Optional[str] = Field(default=None, description="Cloning status")
130
+ error: Optional[str] = Field(default=None, description="Safe failure reason when status is failed")
131
+ created_at: Optional[str] = Field(default=None, description="UTC creation timestamp")