diff --git a/.fern/metadata.json b/.fern/metadata.json index c81f513..baf0338 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -1,5 +1,5 @@ { - "cliVersion": "5.50.3", + "cliVersion": "5.64.0", "generatorName": "fernapi/fern-python-sdk", "generatorVersion": "5.14.20", "generatorConfig": { @@ -9,8 +9,9 @@ }, "pyproject_python_version": ">=3.9" }, - "originGitCommit": "13f26db544c7ebda83d5d229fb387a34c8410ee0", + "originGitCommit": "9f50f669dbb3d0db633a6d904db8a5077b94764f", "originGitCommitIsDirty": true, - "invokedBy": "manual", - "sdkVersion": "1.2.4" + "invokedBy": "ci", + "ciProvider": "unknown", + "sdkVersion": "2.0.1" } \ No newline at end of file diff --git a/README.md b/README.md index 81877f0..f15bb1a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# Speechifyinc Python Library +# Speechify Python Library [![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=https%3A%2F%2Fgithub.com%2Fspeechifyinc%2Fspeechify-api-sdk-python) [![pypi](https://img.shields.io/pypi/v/speechify-api)](https://pypi.python.org/pypi/speechify-api) -The Speechifyinc Python library provides convenient access to the Speechifyinc APIs from Python. +The Speechify Python library provides convenient access to the Speechify APIs from Python. ## Table of Contents @@ -14,6 +14,7 @@ The Speechifyinc Python library provides convenient access to the Speechifyinc A - [Environments](#environments) - [Async Client](#async-client) - [Exception Handling](#exception-handling) +- [Pagination](#pagination) - [Advanced](#advanced) - [Access Raw Response Data](#access-raw-response-data) - [Retries](#retries) @@ -43,7 +44,7 @@ Instantiate and use the client with the following: from speechify import Speechify client = Speechify( - api_key="", + token="", ) client.audio.speech( @@ -77,7 +78,7 @@ import asyncio from speechify import AsyncSpeechify client = AsyncSpeechify( - api_key="", + token="", ) @@ -108,6 +109,29 @@ except ApiError as e: print(e.body) ``` +## Pagination + +Paginated requests will return a `SyncPager` or `AsyncPager`, which can be used as generators for the underlying object. + +```python +from speechify import Speechify + +client = Speechify( + token="", +) + +client.voices.list() +``` + +```python +# You can also iterate through pages and access the typed response per page +pager = client.voices.list(...) +for page in pager.iter_pages(): + print(page.response) # access the typed response for each page + for item in page: + print(item) +``` + ## Advanced ### Access Raw Response Data diff --git a/poetry.lock b/poetry.lock index bc07681..a7caaa7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,15 +2,15 @@ [[package]] name = "aiohappyeyeballs" -version = "2.6.2" +version = "2.7.1" description = "Happy Eyeballs for asyncio" optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"aiohttp\"" files = [ - {file = "aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4"}, - {file = "aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64"}, + {file = "aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472"}, + {file = "aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d"}, ] [[package]] @@ -188,14 +188,14 @@ files = [ [[package]] name = "anyio" -version = "4.14.0" +version = "4.14.1" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9"}, - {file = "anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89"}, + {file = "anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72"}, + {file = "anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e"}, ] [package.dependencies] @@ -1297,14 +1297,14 @@ files = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 02eafa2..29a9e64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] [tool.poetry] name = "speechify-api" -version = "2.0.0" +version = "2.0.1" description = "Official Speechify API SDK" readme = "README.md" authors = [ diff --git a/reference.md b/reference.md index c65161b..95b946a 100644 --- a/reference.md +++ b/reference.md @@ -15,6 +15,8 @@ Synthesize speech audio from text or SSML. Returns the complete audio file plus billing and speech-mark metadata in a single JSON response. For low-latency playback or long-form text, use POST /v1/audio/stream. +Set `output_format` for explicit sample-rate/bitrate control (e.g. +`pcm_16000` or `ulaw_8000` for telephony). @@ -33,7 +35,7 @@ from speechify import Speechify from speechify.environment import SpeechifyEnvironment client = Speechify( - api_key="", + token="", environment=SpeechifyEnvironment.DEFAULT, ) @@ -97,7 +99,7 @@ Please refer to the list of the supported languages and recommendations regardin
-**model:** `typing.Optional[GetSpeechRequestModel]` — Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. +**model:** `typing.Optional[GetSpeechRequestModel]` — Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships.
@@ -113,6 +115,14 @@ Please refer to the list of the supported languages and recommendations regardin
+**output_format:** `typing.Optional[AudioOutputFormat]` — The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over `audio_format` when set. + +
+
+ +
+
+ **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -138,9 +148,11 @@ Please refer to the list of the supported languages and recommendations regardin
Synthesize speech and stream the audio back as it is generated, for -low-latency playback. The Accept header selects the audio container; -the response is raw audio bytes (HTTP chunked). For Base64-encoded -audio with speech-mark metadata in a single JSON response, use +low-latency playback. Set `output_format` in the body for explicit +codec/sample-rate/bitrate control (e.g. `pcm_16000` or `ulaw_8000` for +telephony), or fall back to the Accept header for the container; the +response is raw audio bytes (HTTP chunked). For Base64-encoded audio +with speech-mark metadata in a single JSON response, use POST /v1/audio/speech.
@@ -160,12 +172,11 @@ from speechify import Speechify from speechify.environment import SpeechifyEnvironment client = Speechify( - api_key="", + token="", environment=SpeechifyEnvironment.DEFAULT, ) client.audio.stream( - accept="audio/mpeg", input="input", voice_id="voice_id", ) @@ -184,12 +195,11 @@ client.audio.stream(
-**accept:** `StreamAudioRequestAccept` +**input:** `str` -Selects the audio container/codec for the streamed response. The -response Content-Type echoes this value, except `audio/pcm` returns -`audio/L16` with rate and channels parameters (raw 16-bit linear -PCM, 24 kHz mono, little-endian). +Plain text or SSML to be synthesized to speech. +Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. +Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody
@@ -197,11 +207,7 @@ PCM, 24 kHz mono, little-endian).
-**input:** `str` - -Plain text or SSML to be synthesized to speech. -Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. -Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody +**voice_id:** `str` — Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices
@@ -209,7 +215,15 @@ Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to
-**voice_id:** `str` — Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices +**accept:** `typing.Optional[StreamAudioRequestAccept]` + +Selects the audio container/codec for the streamed response when +`output_format` is not set in the request body. The response +Content-Type echoes this value, except `audio/pcm` returns +`audio/L16` with rate and channels parameters (raw 16-bit linear +PCM, 24 kHz mono, little-endian). For explicit sample-rate/bitrate +control (e.g. `pcm_16000`, `ulaw_8000`), set `output_format` in the +body instead; it takes precedence over this header.
@@ -228,7 +242,7 @@ Please refer to the list of the supported languages and recommendations regardin
-**model:** `typing.Optional[GetStreamRequestModel]` — Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. +**model:** `typing.Optional[GetStreamRequestModel]` — Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships.
@@ -244,6 +258,14 @@ Please refer to the list of the supported languages and recommendations regardin
+**output_format:** `typing.Optional[AudioStreamOutputFormat]` — The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over the `Accept` header when set, so you can request formats the `Accept` enum does not cover (e.g. `pcm_16000`, `ulaw_8000`). `wav_*` formats are not supported on streaming - use `POST /v1/audio/speech` for wav. + +
+
+ +
+
+ **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -257,7 +279,7 @@ Please refer to the list of the supported languages and recommendations regardin ## voices -
client.voices.list() -> typing.List[GetVoice] +
client.voices.list(...) -> ListVoicesResponse
@@ -269,7 +291,12 @@ Please refer to the list of the supported languages and recommendations regardin
-Gets the list of voices available for the user +Lists the voices available to the caller - the shared voice +catalog plus the workspace's personal cloned voices. By default +the full catalogue is returned in one response. Pagination is +opt-in: pass `limit` (and then `cursor` from the previous +response) to page through the list while `has_more` is true. Max +page size is 200.
@@ -288,7 +315,7 @@ from speechify import Speechify from speechify.environment import SpeechifyEnvironment client = Speechify( - api_key="", + token="", environment=SpeechifyEnvironment.DEFAULT, ) @@ -308,6 +335,22 @@ client.voices.list()
+**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response. + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Max items per page (default 50, max 200). + +
+
+ +
+
+ **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -320,7 +363,7 @@ client.voices.list()
-
client.voices.create(...) -> CreatedVoice +
client.voices.create(...) -> GetVoice
@@ -351,11 +394,12 @@ from speechify import Speechify from speechify.environment import SpeechifyEnvironment client = Speechify( - api_key="", + token="", environment=SpeechifyEnvironment.DEFAULT, ) client.voices.create( + idempotency_key="a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", sample="example_sample", avatar="example_avatar", name="name", @@ -390,7 +434,7 @@ client.voices.create( Gender marker for the personal voice male GenderMale female GenderFemale -notSpecified GenderNotSpecified +not_specified GenderNotSpecified
@@ -418,6 +462,21 @@ For example, `{"fullName": "John Doe", "email": "john@example.com"}`
+**idempotency_key:** `typing.Optional[str]` + +A client-generated key (an opaque string, max 255 chars) that makes a +side-effect POST safe to retry: the server runs the operation exactly +once and replays the first response (its status and body) for 24 hours. +Reusing a key with a different request body, or while the first request +is still in flight, returns `409 idempotency_conflict`. A replayed +response carries the `Idempotent-Replayed: true` header. + +
+
+ +
+
+ **locale:** `typing.Optional[str]` — Native language (locale) of the personal voice (e.g. en-US, es-ES, etc.)
@@ -442,6 +501,82 @@ For example, `{"fullName": "John Doe", "email": "john@example.com"}`
+ +
+ + +
client.voices.get(...) -> GetVoice +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Fetch a single voice by id - a shared catalogue voice or one of +the caller's own personal (cloned) voices. A personal voice that +belongs to another workspace returns 404, identical to an +unknown id, so voice inventory is never enumerable across tenants. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from speechify import Speechify +from speechify.environment import SpeechifyEnvironment + +client = Speechify( + token="", + environment=SpeechifyEnvironment.DEFAULT, +) + +client.voices.get( + voice_id="voice_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**voice_id:** `str` — The ID of the voice to fetch + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
@@ -477,12 +612,12 @@ from speechify import Speechify from speechify.environment import SpeechifyEnvironment client = Speechify( - api_key="", + token="", environment=SpeechifyEnvironment.DEFAULT, ) client.voices.delete( - id="id", + voice_id="voice_id", ) ``` @@ -499,7 +634,7 @@ client.voices.delete(
-**id:** `str` — The ID of the voice to delete +**voice_id:** `str` — The ID of the voice to delete
@@ -550,12 +685,12 @@ from speechify import Speechify from speechify.environment import SpeechifyEnvironment client = Speechify( - api_key="", + token="", environment=SpeechifyEnvironment.DEFAULT, ) client.voices.download_sample( - id="id", + voice_id="voice_id", ) ``` @@ -572,7 +707,7 @@ client.voices.download_sample(
-**id:** `str` — The ID of the voice to download sample for +**voice_id:** `str` — The ID of the voice to download sample for
diff --git a/src/speechify/__init__.py b/src/speechify/__init__.py index 48b4289..fd29896 100644 --- a/src/speechify/__init__.py +++ b/src/speechify/__init__.py @@ -7,12 +7,8 @@ if typing.TYPE_CHECKING: from .types import ( - CreateVoiceLanguage, - CreateVoiceModel, - CreateVoiceModelName, - CreatedVoice, - CreatedVoiceGender, - CreatedVoiceType, + AudioOutputFormat, + AudioStreamOutputFormat, Error, ErrorCode, ErrorDetail, @@ -26,12 +22,15 @@ GetVoiceType, GetVoicesModel, GetVoicesModelName, + ListVoicesResponse, NestedChunk, + PaginationMeta, SpeechMarks, ) from .errors import ( BadGatewayError, BadRequestError, + ConflictError, ForbiddenError, InternalServerError, NotFoundError, @@ -55,15 +54,12 @@ from .voices import CreateVoicesRequestGender _dynamic_imports: typing.Dict[str, str] = { "AsyncSpeechify": ".client", + "AudioOutputFormat": ".types", + "AudioStreamOutputFormat": ".types", "BadGatewayError": ".errors", "BadRequestError": ".errors", - "CreateVoiceLanguage": ".types", - "CreateVoiceModel": ".types", - "CreateVoiceModelName": ".types", + "ConflictError": ".errors", "CreateVoicesRequestGender": ".voices", - "CreatedVoice": ".types", - "CreatedVoiceGender": ".types", - "CreatedVoiceType": ".types", "DefaultAioHttpClient": "._default_clients", "DefaultAsyncHttpxClient": "._default_clients", "Error": ".types", @@ -84,8 +80,10 @@ "GetVoicesModel": ".types", "GetVoicesModelName": ".types", "InternalServerError": ".errors", + "ListVoicesResponse": ".types", "NestedChunk": ".types", "NotFoundError": ".errors", + "PaginationMeta": ".types", "PaymentRequiredError": ".errors", "ServiceUnavailableError": ".errors", "SpeechMarks": ".types", @@ -124,15 +122,12 @@ def __dir__(): __all__ = [ "AsyncSpeechify", + "AudioOutputFormat", + "AudioStreamOutputFormat", "BadGatewayError", "BadRequestError", - "CreateVoiceLanguage", - "CreateVoiceModel", - "CreateVoiceModelName", + "ConflictError", "CreateVoicesRequestGender", - "CreatedVoice", - "CreatedVoiceGender", - "CreatedVoiceType", "DefaultAioHttpClient", "DefaultAsyncHttpxClient", "Error", @@ -153,8 +148,10 @@ def __dir__(): "GetVoicesModel", "GetVoicesModelName", "InternalServerError", + "ListVoicesResponse", "NestedChunk", "NotFoundError", + "PaginationMeta", "PaymentRequiredError", "ServiceUnavailableError", "SpeechMarks", diff --git a/src/speechify/audio/client.py b/src/speechify/audio/client.py index 033c1c6..aecb9e1 100644 --- a/src/speechify/audio/client.py +++ b/src/speechify/audio/client.py @@ -4,6 +4,8 @@ from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.request_options import RequestOptions +from ..types.audio_output_format import AudioOutputFormat +from ..types.audio_stream_output_format import AudioStreamOutputFormat from ..types.get_speech_options_request import GetSpeechOptionsRequest from ..types.get_speech_response import GetSpeechResponse from ..types.get_stream_options_request import GetStreamOptionsRequest @@ -41,12 +43,15 @@ def speech( language: typing.Optional[str] = OMIT, model: typing.Optional[GetSpeechRequestModel] = OMIT, options: typing.Optional[GetSpeechOptionsRequest] = OMIT, + output_format: typing.Optional[AudioOutputFormat] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> GetSpeechResponse: """ Synthesize speech audio from text or SSML. Returns the complete audio file plus billing and speech-mark metadata in a single JSON response. For low-latency playback or long-form text, use POST /v1/audio/stream. + Set `output_format` for explicit sample-rate/bitrate control (e.g. + `pcm_16000` or `ulaw_8000` for telephony). Parameters ---------- @@ -66,10 +71,13 @@ def speech( Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetSpeechRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. options : typing.Optional[GetSpeechOptionsRequest] + output_format : typing.Optional[AudioOutputFormat] + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over `audio_format` when set. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -83,7 +91,8 @@ def speech( from speechify import Speechify client = Speechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) client.audio.speech( audio_format="mp3", @@ -99,6 +108,7 @@ def speech( language=language, model=model, options=options, + output_format=output_format, request_options=request_options, ) return _response.data @@ -106,29 +116,26 @@ def speech( def stream( self, *, - accept: StreamAudioRequestAccept, input: str, voice_id: str, + accept: typing.Optional[StreamAudioRequestAccept] = None, language: typing.Optional[str] = OMIT, model: typing.Optional[GetStreamRequestModel] = OMIT, options: typing.Optional[GetStreamOptionsRequest] = OMIT, + output_format: typing.Optional[AudioStreamOutputFormat] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> typing.Iterator[bytes]: """ Synthesize speech and stream the audio back as it is generated, for - low-latency playback. The Accept header selects the audio container; - the response is raw audio bytes (HTTP chunked). For Base64-encoded - audio with speech-mark metadata in a single JSON response, use + low-latency playback. Set `output_format` in the body for explicit + codec/sample-rate/bitrate control (e.g. `pcm_16000` or `ulaw_8000` for + telephony), or fall back to the Accept header for the container; the + response is raw audio bytes (HTTP chunked). For Base64-encoded audio + with speech-mark metadata in a single JSON response, use POST /v1/audio/speech. Parameters ---------- - accept : StreamAudioRequestAccept - Selects the audio container/codec for the streamed response. The - response Content-Type echoes this value, except `audio/pcm` returns - `audio/L16` with rate and channels parameters (raw 16-bit linear - PCM, 24 kHz mono, little-endian). - input : str Plain text or SSML to be synthesized to speech. Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. @@ -137,43 +144,60 @@ def stream( voice_id : str Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + accept : typing.Optional[StreamAudioRequestAccept] + Selects the audio container/codec for the streamed response when + `output_format` is not set in the request body. The response + Content-Type echoes this value, except `audio/pcm` returns + `audio/L16` with rate and channels parameters (raw 16-bit linear + PCM, 24 kHz mono, little-endian). For explicit sample-rate/bitrate + control (e.g. `pcm_16000`, `ulaw_8000`), set `output_format` in the + body instead; it takes precedence over this header. + language : typing.Optional[str] Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetStreamRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. options : typing.Optional[GetStreamOptionsRequest] + output_format : typing.Optional[AudioStreamOutputFormat] + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over the `Accept` header when set, so you can request formats the `Accept` enum does not cover (e.g. `pcm_16000`, `ulaw_8000`). `wav_*` formats are not supported on streaming - use `POST /v1/audio/speech` for wav. + request_options : typing.Optional[RequestOptions] Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. Returns ------- typing.Iterator[bytes] - Chunked audio stream for the requested input. + Streamed audio. When `output_format` is set it selects the + codec/sample rate; otherwise the Accept header does. The + Content-Type reflects the selected format: it matches the Accept + header, except raw PCM returns `audio/L16` (with rate and channels + parameters) and u-law returns `audio/basic`. Examples -------- from speechify import Speechify client = Speechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) client.audio.stream( - accept="audio/mpeg", input="input", voice_id="voice_id", ) """ with self._raw_client.stream( - accept=accept, input=input, voice_id=voice_id, + accept=accept, language=language, model=model, options=options, + output_format=output_format, request_options=request_options, ) as r: yield from r.data @@ -203,12 +227,15 @@ async def speech( language: typing.Optional[str] = OMIT, model: typing.Optional[GetSpeechRequestModel] = OMIT, options: typing.Optional[GetSpeechOptionsRequest] = OMIT, + output_format: typing.Optional[AudioOutputFormat] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> GetSpeechResponse: """ Synthesize speech audio from text or SSML. Returns the complete audio file plus billing and speech-mark metadata in a single JSON response. For low-latency playback or long-form text, use POST /v1/audio/stream. + Set `output_format` for explicit sample-rate/bitrate control (e.g. + `pcm_16000` or `ulaw_8000` for telephony). Parameters ---------- @@ -228,10 +255,13 @@ async def speech( Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetSpeechRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. options : typing.Optional[GetSpeechOptionsRequest] + output_format : typing.Optional[AudioOutputFormat] + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over `audio_format` when set. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -247,7 +277,8 @@ async def speech( from speechify import AsyncSpeechify client = AsyncSpeechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) @@ -269,6 +300,7 @@ async def main() -> None: language=language, model=model, options=options, + output_format=output_format, request_options=request_options, ) return _response.data @@ -276,29 +308,26 @@ async def main() -> None: async def stream( self, *, - accept: StreamAudioRequestAccept, input: str, voice_id: str, + accept: typing.Optional[StreamAudioRequestAccept] = None, language: typing.Optional[str] = OMIT, model: typing.Optional[GetStreamRequestModel] = OMIT, options: typing.Optional[GetStreamOptionsRequest] = OMIT, + output_format: typing.Optional[AudioStreamOutputFormat] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> typing.AsyncIterator[bytes]: """ Synthesize speech and stream the audio back as it is generated, for - low-latency playback. The Accept header selects the audio container; - the response is raw audio bytes (HTTP chunked). For Base64-encoded - audio with speech-mark metadata in a single JSON response, use + low-latency playback. Set `output_format` in the body for explicit + codec/sample-rate/bitrate control (e.g. `pcm_16000` or `ulaw_8000` for + telephony), or fall back to the Accept header for the container; the + response is raw audio bytes (HTTP chunked). For Base64-encoded audio + with speech-mark metadata in a single JSON response, use POST /v1/audio/speech. Parameters ---------- - accept : StreamAudioRequestAccept - Selects the audio container/codec for the streamed response. The - response Content-Type echoes this value, except `audio/pcm` returns - `audio/L16` with rate and channels parameters (raw 16-bit linear - PCM, 24 kHz mono, little-endian). - input : str Plain text or SSML to be synthesized to speech. Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. @@ -307,22 +336,38 @@ async def stream( voice_id : str Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + accept : typing.Optional[StreamAudioRequestAccept] + Selects the audio container/codec for the streamed response when + `output_format` is not set in the request body. The response + Content-Type echoes this value, except `audio/pcm` returns + `audio/L16` with rate and channels parameters (raw 16-bit linear + PCM, 24 kHz mono, little-endian). For explicit sample-rate/bitrate + control (e.g. `pcm_16000`, `ulaw_8000`), set `output_format` in the + body instead; it takes precedence over this header. + language : typing.Optional[str] Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetStreamRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. options : typing.Optional[GetStreamOptionsRequest] + output_format : typing.Optional[AudioStreamOutputFormat] + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over the `Accept` header when set, so you can request formats the `Accept` enum does not cover (e.g. `pcm_16000`, `ulaw_8000`). `wav_*` formats are not supported on streaming - use `POST /v1/audio/speech` for wav. + request_options : typing.Optional[RequestOptions] Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. Returns ------- typing.AsyncIterator[bytes] - Chunked audio stream for the requested input. + Streamed audio. When `output_format` is set it selects the + codec/sample rate; otherwise the Accept header does. The + Content-Type reflects the selected format: it matches the Accept + header, except raw PCM returns `audio/L16` (with rate and channels + parameters) and u-law returns `audio/basic`. Examples -------- @@ -331,13 +376,13 @@ async def stream( from speechify import AsyncSpeechify client = AsyncSpeechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) async def main() -> None: await client.audio.stream( - accept="audio/mpeg", input="input", voice_id="voice_id", ) @@ -346,12 +391,13 @@ async def main() -> None: asyncio.run(main()) """ async with self._raw_client.stream( - accept=accept, input=input, voice_id=voice_id, + accept=accept, language=language, model=model, options=options, + output_format=output_format, request_options=request_options, ) as r: async for _chunk in r.data: diff --git a/src/speechify/audio/raw_client.py b/src/speechify/audio/raw_client.py index e0f5483..d02ac39 100644 --- a/src/speechify/audio/raw_client.py +++ b/src/speechify/audio/raw_client.py @@ -20,6 +20,8 @@ from ..errors.service_unavailable_error import ServiceUnavailableError from ..errors.too_many_requests_error import TooManyRequestsError from ..errors.unauthorized_error import UnauthorizedError +from ..types.audio_output_format import AudioOutputFormat +from ..types.audio_stream_output_format import AudioStreamOutputFormat from ..types.error import Error from ..types.get_speech_options_request import GetSpeechOptionsRequest from ..types.get_speech_response import GetSpeechResponse @@ -47,12 +49,15 @@ def speech( language: typing.Optional[str] = OMIT, model: typing.Optional[GetSpeechRequestModel] = OMIT, options: typing.Optional[GetSpeechOptionsRequest] = OMIT, + output_format: typing.Optional[AudioOutputFormat] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[GetSpeechResponse]: """ Synthesize speech audio from text or SSML. Returns the complete audio file plus billing and speech-mark metadata in a single JSON response. For low-latency playback or long-form text, use POST /v1/audio/stream. + Set `output_format` for explicit sample-rate/bitrate control (e.g. + `pcm_16000` or `ulaw_8000` for telephony). Parameters ---------- @@ -72,10 +77,13 @@ def speech( Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetSpeechRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. options : typing.Optional[GetSpeechOptionsRequest] + output_format : typing.Optional[AudioOutputFormat] + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over `audio_format` when set. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -95,6 +103,7 @@ def speech( "options": convert_and_respect_annotation_metadata( object_=options, annotation=GetSpeechOptionsRequest, direction="write" ), + "output_format": output_format, "voice_id": voice_id, }, headers={ @@ -225,29 +234,26 @@ def speech( def stream( self, *, - accept: StreamAudioRequestAccept, input: str, voice_id: str, + accept: typing.Optional[StreamAudioRequestAccept] = None, language: typing.Optional[str] = OMIT, model: typing.Optional[GetStreamRequestModel] = OMIT, options: typing.Optional[GetStreamOptionsRequest] = OMIT, + output_format: typing.Optional[AudioStreamOutputFormat] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> typing.Iterator[HttpResponse[typing.Iterator[bytes]]]: """ Synthesize speech and stream the audio back as it is generated, for - low-latency playback. The Accept header selects the audio container; - the response is raw audio bytes (HTTP chunked). For Base64-encoded - audio with speech-mark metadata in a single JSON response, use + low-latency playback. Set `output_format` in the body for explicit + codec/sample-rate/bitrate control (e.g. `pcm_16000` or `ulaw_8000` for + telephony), or fall back to the Accept header for the container; the + response is raw audio bytes (HTTP chunked). For Base64-encoded audio + with speech-mark metadata in a single JSON response, use POST /v1/audio/speech. Parameters ---------- - accept : StreamAudioRequestAccept - Selects the audio container/codec for the streamed response. The - response Content-Type echoes this value, except `audio/pcm` returns - `audio/L16` with rate and channels parameters (raw 16-bit linear - PCM, 24 kHz mono, little-endian). - input : str Plain text or SSML to be synthesized to speech. Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. @@ -256,22 +262,38 @@ def stream( voice_id : str Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + accept : typing.Optional[StreamAudioRequestAccept] + Selects the audio container/codec for the streamed response when + `output_format` is not set in the request body. The response + Content-Type echoes this value, except `audio/pcm` returns + `audio/L16` with rate and channels parameters (raw 16-bit linear + PCM, 24 kHz mono, little-endian). For explicit sample-rate/bitrate + control (e.g. `pcm_16000`, `ulaw_8000`), set `output_format` in the + body instead; it takes precedence over this header. + language : typing.Optional[str] Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetStreamRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. options : typing.Optional[GetStreamOptionsRequest] + output_format : typing.Optional[AudioStreamOutputFormat] + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over the `Accept` header when set, so you can request formats the `Accept` enum does not cover (e.g. `pcm_16000`, `ulaw_8000`). `wav_*` formats are not supported on streaming - use `POST /v1/audio/speech` for wav. + request_options : typing.Optional[RequestOptions] Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. Returns ------- typing.Iterator[HttpResponse[typing.Iterator[bytes]]] - Chunked audio stream for the requested input. + Streamed audio. When `output_format` is set it selects the + codec/sample rate; otherwise the Accept header does. The + Content-Type reflects the selected format: it matches the Accept + header, except raw PCM returns `audio/L16` (with rate and channels + parameters) and u-law returns `audio/basic`. """ with self._client_wrapper.httpx_client.stream( "v1/audio/stream", @@ -283,6 +305,7 @@ def stream( "options": convert_and_respect_annotation_metadata( object_=options, annotation=GetStreamOptionsRequest, direction="write" ), + "output_format": output_format, "voice_id": voice_id, }, headers={ @@ -430,12 +453,15 @@ async def speech( language: typing.Optional[str] = OMIT, model: typing.Optional[GetSpeechRequestModel] = OMIT, options: typing.Optional[GetSpeechOptionsRequest] = OMIT, + output_format: typing.Optional[AudioOutputFormat] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[GetSpeechResponse]: """ Synthesize speech audio from text or SSML. Returns the complete audio file plus billing and speech-mark metadata in a single JSON response. For low-latency playback or long-form text, use POST /v1/audio/stream. + Set `output_format` for explicit sample-rate/bitrate control (e.g. + `pcm_16000` or `ulaw_8000` for telephony). Parameters ---------- @@ -455,10 +481,13 @@ async def speech( Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetSpeechRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. options : typing.Optional[GetSpeechOptionsRequest] + output_format : typing.Optional[AudioOutputFormat] + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over `audio_format` when set. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -478,6 +507,7 @@ async def speech( "options": convert_and_respect_annotation_metadata( object_=options, annotation=GetSpeechOptionsRequest, direction="write" ), + "output_format": output_format, "voice_id": voice_id, }, headers={ @@ -608,29 +638,26 @@ async def speech( async def stream( self, *, - accept: StreamAudioRequestAccept, input: str, voice_id: str, + accept: typing.Optional[StreamAudioRequestAccept] = None, language: typing.Optional[str] = OMIT, model: typing.Optional[GetStreamRequestModel] = OMIT, options: typing.Optional[GetStreamOptionsRequest] = OMIT, + output_format: typing.Optional[AudioStreamOutputFormat] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]]: """ Synthesize speech and stream the audio back as it is generated, for - low-latency playback. The Accept header selects the audio container; - the response is raw audio bytes (HTTP chunked). For Base64-encoded - audio with speech-mark metadata in a single JSON response, use + low-latency playback. Set `output_format` in the body for explicit + codec/sample-rate/bitrate control (e.g. `pcm_16000` or `ulaw_8000` for + telephony), or fall back to the Accept header for the container; the + response is raw audio bytes (HTTP chunked). For Base64-encoded audio + with speech-mark metadata in a single JSON response, use POST /v1/audio/speech. Parameters ---------- - accept : StreamAudioRequestAccept - Selects the audio container/codec for the streamed response. The - response Content-Type echoes this value, except `audio/pcm` returns - `audio/L16` with rate and channels parameters (raw 16-bit linear - PCM, 24 kHz mono, little-endian). - input : str Plain text or SSML to be synthesized to speech. Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. @@ -639,22 +666,38 @@ async def stream( voice_id : str Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + accept : typing.Optional[StreamAudioRequestAccept] + Selects the audio container/codec for the streamed response when + `output_format` is not set in the request body. The response + Content-Type echoes this value, except `audio/pcm` returns + `audio/L16` with rate and channels parameters (raw 16-bit linear + PCM, 24 kHz mono, little-endian). For explicit sample-rate/bitrate + control (e.g. `pcm_16000`, `ulaw_8000`), set `output_format` in the + body instead; it takes precedence over this header. + language : typing.Optional[str] Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetStreamRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.0` is the streaming-native model with lower TTFB and richer expressivity. Currently English only; multilingual coming soon. Non-English voices return 400 until multilingual support ships. + Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. options : typing.Optional[GetStreamOptionsRequest] + output_format : typing.Optional[AudioStreamOutputFormat] + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over the `Accept` header when set, so you can request formats the `Accept` enum does not cover (e.g. `pcm_16000`, `ulaw_8000`). `wav_*` formats are not supported on streaming - use `POST /v1/audio/speech` for wav. + request_options : typing.Optional[RequestOptions] Request-specific configuration. You can pass in configuration such as `chunk_size`, and more to customize the request and response. Returns ------- typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]] - Chunked audio stream for the requested input. + Streamed audio. When `output_format` is set it selects the + codec/sample rate; otherwise the Accept header does. The + Content-Type reflects the selected format: it matches the Accept + header, except raw PCM returns `audio/L16` (with rate and channels + parameters) and u-law returns `audio/basic`. """ async with self._client_wrapper.httpx_client.stream( "v1/audio/stream", @@ -666,6 +709,7 @@ async def stream( "options": convert_and_respect_annotation_metadata( object_=options, annotation=GetStreamOptionsRequest, direction="write" ), + "output_format": output_format, "voice_id": voice_id, }, headers={ diff --git a/src/speechify/audio/types/get_speech_request_model.py b/src/speechify/audio/types/get_speech_request_model.py index da407de..cd9a92d 100644 --- a/src/speechify/audio/types/get_speech_request_model.py +++ b/src/speechify/audio/types/get_speech_request_model.py @@ -2,4 +2,6 @@ import typing -GetSpeechRequestModel = typing.Union[typing.Literal["simba-english", "simba-multilingual", "simba-3.0"], typing.Any] +GetSpeechRequestModel = typing.Union[ + typing.Literal["simba-english", "simba-multilingual", "simba-3.0", "simba-3.2"], typing.Any +] diff --git a/src/speechify/audio/types/get_stream_request_model.py b/src/speechify/audio/types/get_stream_request_model.py index 76d1582..d7fa1c4 100644 --- a/src/speechify/audio/types/get_stream_request_model.py +++ b/src/speechify/audio/types/get_stream_request_model.py @@ -2,4 +2,6 @@ import typing -GetStreamRequestModel = typing.Union[typing.Literal["simba-english", "simba-multilingual", "simba-3.0"], typing.Any] +GetStreamRequestModel = typing.Union[ + typing.Literal["simba-english", "simba-multilingual", "simba-3.0", "simba-3.2"], typing.Any +] diff --git a/src/speechify/client.py b/src/speechify/client.py index 4f9beee..2cf7eef 100644 --- a/src/speechify/client.py +++ b/src/speechify/client.py @@ -34,7 +34,8 @@ class Speechify: - api_key : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + version : typing.Optional[str] + token : typing.Optional[typing.Union[str, typing.Callable[[], str]]] headers : typing.Optional[typing.Dict[str, str]] Additional headers to send with every request. @@ -58,7 +59,8 @@ class Speechify: from speechify import Speechify client = Speechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) """ @@ -67,7 +69,8 @@ def __init__( *, base_url: typing.Optional[str] = None, environment: SpeechifyEnvironment = SpeechifyEnvironment.DEFAULT, - api_key: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SPEECHIFY_API_KEY"), + version: typing.Optional[str] = "2026-07-07", + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SPEECHIFY_API_KEY"), headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, max_retries: typing.Optional[int] = None, @@ -79,13 +82,14 @@ def __init__( timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read ) _defaulted_max_retries = max_retries if max_retries is not None else 2 - if api_key is None: + if token is None: raise ApiError( - body="The client must be instantiated be either passing in api_key or setting SPEECHIFY_API_KEY" + body="The client must be instantiated be either passing in token or setting SPEECHIFY_API_KEY" ) self._client_wrapper = SyncClientWrapper( base_url=_get_base_url(base_url=base_url, environment=environment), - api_key=api_key, + version=version, + token=token, headers=headers, httpx_client=httpx_client if httpx_client is not None @@ -152,7 +156,8 @@ class AsyncSpeechify: - api_key : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + version : typing.Optional[str] + token : typing.Optional[typing.Union[str, typing.Callable[[], str]]] headers : typing.Optional[typing.Dict[str, str]] Additional headers to send with every request. @@ -179,7 +184,8 @@ class AsyncSpeechify: from speechify import AsyncSpeechify client = AsyncSpeechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) """ @@ -188,7 +194,8 @@ def __init__( *, base_url: typing.Optional[str] = None, environment: SpeechifyEnvironment = SpeechifyEnvironment.DEFAULT, - api_key: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SPEECHIFY_API_KEY"), + version: typing.Optional[str] = "2026-07-07", + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SPEECHIFY_API_KEY"), headers: typing.Optional[typing.Dict[str, str]] = None, async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, timeout: typing.Optional[float] = None, @@ -201,13 +208,14 @@ def __init__( timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read ) _defaulted_max_retries = max_retries if max_retries is not None else 2 - if api_key is None: + if token is None: raise ApiError( - body="The client must be instantiated be either passing in api_key or setting SPEECHIFY_API_KEY" + body="The client must be instantiated be either passing in token or setting SPEECHIFY_API_KEY" ) self._client_wrapper = AsyncClientWrapper( base_url=_get_base_url(base_url=base_url, environment=environment), - api_key=api_key, + version=version, + token=token, headers=headers, async_token=async_token, httpx_client=httpx_client diff --git a/src/speechify/core/__init__.py b/src/speechify/core/__init__.py index 5bc159a..e2be580 100644 --- a/src/speechify/core/__init__.py +++ b/src/speechify/core/__init__.py @@ -14,6 +14,7 @@ from .http_response import AsyncHttpResponse, HttpResponse from .jsonable_encoder import encode_path_param, jsonable_encoder from .logging import ConsoleLogger, ILogger, LogConfig, LogLevel, Logger, create_logger + from .pagination import AsyncPager, SyncPager from .parse_error import ParsingError from .pydantic_utilities import ( IS_PYDANTIC_V2, @@ -33,6 +34,7 @@ "AsyncClientWrapper": ".client_wrapper", "AsyncHttpClient": ".http_client", "AsyncHttpResponse": ".http_response", + "AsyncPager": ".pagination", "BaseClientWrapper": ".client_wrapper", "ConsoleLogger": ".logging", "FieldMetadata": ".serialization", @@ -48,6 +50,7 @@ "RequestOptions": ".request_options", "Rfc2822DateTime": ".datetime_utils", "SyncClientWrapper": ".client_wrapper", + "SyncPager": ".pagination", "UniversalBaseModel": ".pydantic_utilities", "UniversalRootModel": ".pydantic_utilities", "convert_and_respect_annotation_metadata": ".serialization", @@ -93,6 +96,7 @@ def __dir__(): "AsyncClientWrapper", "AsyncHttpClient", "AsyncHttpResponse", + "AsyncPager", "BaseClientWrapper", "ConsoleLogger", "FieldMetadata", @@ -108,6 +112,7 @@ def __dir__(): "RequestOptions", "Rfc2822DateTime", "SyncClientWrapper", + "SyncPager", "UniversalBaseModel", "UniversalRootModel", "convert_and_respect_annotation_metadata", diff --git a/src/speechify/core/client_wrapper.py b/src/speechify/core/client_wrapper.py index b676d97..64bbde8 100644 --- a/src/speechify/core/client_wrapper.py +++ b/src/speechify/core/client_wrapper.py @@ -11,14 +11,16 @@ class BaseClientWrapper: def __init__( self, *, - api_key: typing.Union[str, typing.Callable[[], str]], + version: typing.Optional[str] = None, + token: typing.Union[str, typing.Callable[[], str]], headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, max_retries: int = 2, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, ): - self._api_key = api_key + self._version = version + self._token = token self._headers = headers self._base_url = base_url self._timeout = timeout @@ -29,22 +31,24 @@ def get_headers(self) -> typing.Dict[str, str]: import platform headers: typing.Dict[str, str] = { - "User-Agent": "speechify-api/1.2.4", + "User-Agent": "speechify-api/2.0.1", "X-Fern-Language": "Python", "X-Fern-Runtime": f"python/{platform.python_version()}", "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", "X-Fern-SDK-Name": "speechify-api", - "X-Fern-SDK-Version": "2.0.0", + "X-Fern-SDK-Version": "2.0.1", **(self.get_custom_headers() or {}), } - headers["Authorization"] = f"Bearer {self._get_api_key()}" + if self._version is not None: + headers["Speechify-Version"] = self._version + headers["Authorization"] = f"Bearer {self._get_token()}" return headers - def _get_api_key(self) -> str: - if isinstance(self._api_key, str): - return self._api_key + def _get_token(self) -> str: + if isinstance(self._token, str): + return self._token else: - return self._api_key() + return self._token() def get_custom_headers(self) -> typing.Optional[typing.Dict[str, str]]: return self._headers @@ -63,7 +67,8 @@ class SyncClientWrapper(BaseClientWrapper): def __init__( self, *, - api_key: typing.Union[str, typing.Callable[[], str]], + version: typing.Optional[str] = None, + token: typing.Union[str, typing.Callable[[], str]], headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, @@ -72,7 +77,8 @@ def __init__( httpx_client: httpx.Client, ): super().__init__( - api_key=api_key, + version=version, + token=token, headers=headers, base_url=base_url, timeout=timeout, @@ -93,7 +99,8 @@ class AsyncClientWrapper(BaseClientWrapper): def __init__( self, *, - api_key: typing.Union[str, typing.Callable[[], str]], + version: typing.Optional[str] = None, + token: typing.Union[str, typing.Callable[[], str]], headers: typing.Optional[typing.Dict[str, str]] = None, base_url: str, timeout: typing.Optional[float] = None, @@ -103,7 +110,8 @@ def __init__( httpx_client: httpx.AsyncClient, ): super().__init__( - api_key=api_key, + version=version, + token=token, headers=headers, base_url=base_url, timeout=timeout, diff --git a/src/speechify/core/pagination.py b/src/speechify/core/pagination.py new file mode 100644 index 0000000..760b089 --- /dev/null +++ b/src/speechify/core/pagination.py @@ -0,0 +1,82 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import AsyncIterator, Awaitable, Callable, Generic, Iterator, List, Optional, TypeVar + +# Generic to represent the underlying type of the results within a page +T = TypeVar("T") +# Generic to represent the type of the API response +R = TypeVar("R") + + +# SDKs implement a Page ABC per-pagination request, the endpoint then returns a pager that wraps this type +# for example, an endpoint will return SyncPager[UserPage] where UserPage implements the Page ABC. ex: +# +# SyncPager( +# has_next=response.list_metadata.after is not None, +# items=response.data, +# # This should be the outer function that returns the SyncPager again +# get_next=lambda: list(..., cursor: response.cursor) (or list(..., offset: offset + 1)) +# ) + + +@dataclass(frozen=True) +class SyncPager(Generic[T, R]): + get_next: Optional[Callable[[], Optional[SyncPager[T, R]]]] + has_next: bool + items: Optional[List[T]] + response: R + + # Here we type ignore the iterator to avoid a mypy error + # caused by the type conflict with Pydanitc's __iter__ method + # brought in by extending the base model + def __iter__(self) -> Iterator[T]: # type: ignore[override] + for page in self.iter_pages(): + if page.items is not None: + yield from page.items + + def iter_pages(self) -> Iterator[SyncPager[T, R]]: + page: Optional[SyncPager[T, R]] = self + while page is not None: + yield page + + if not page.has_next or page.get_next is None: + return + + page = page.get_next() + if page is None or page.items is None or len(page.items) == 0: + return + + def next_page(self) -> Optional[SyncPager[T, R]]: + return self.get_next() if self.get_next is not None else None + + +@dataclass(frozen=True) +class AsyncPager(Generic[T, R]): + get_next: Optional[Callable[[], Awaitable[Optional[AsyncPager[T, R]]]]] + has_next: bool + items: Optional[List[T]] + response: R + + async def __aiter__(self) -> AsyncIterator[T]: + async for page in self.iter_pages(): + if page.items is not None: + for item in page.items: + yield item + + async def iter_pages(self) -> AsyncIterator[AsyncPager[T, R]]: + page: Optional[AsyncPager[T, R]] = self + while page is not None: + yield page + + if not page.has_next or page.get_next is None: + return + + page = await page.get_next() + if page is None or page.items is None or len(page.items) == 0: + return + + async def next_page(self) -> Optional[AsyncPager[T, R]]: + return await self.get_next() if self.get_next is not None else None diff --git a/src/speechify/errors/__init__.py b/src/speechify/errors/__init__.py index a5a2b3f..d620695 100644 --- a/src/speechify/errors/__init__.py +++ b/src/speechify/errors/__init__.py @@ -8,6 +8,7 @@ if typing.TYPE_CHECKING: from .bad_gateway_error import BadGatewayError from .bad_request_error import BadRequestError + from .conflict_error import ConflictError from .forbidden_error import ForbiddenError from .internal_server_error import InternalServerError from .not_found_error import NotFoundError @@ -19,6 +20,7 @@ _dynamic_imports: typing.Dict[str, str] = { "BadGatewayError": ".bad_gateway_error", "BadRequestError": ".bad_request_error", + "ConflictError": ".conflict_error", "ForbiddenError": ".forbidden_error", "InternalServerError": ".internal_server_error", "NotFoundError": ".not_found_error", @@ -54,6 +56,7 @@ def __dir__(): __all__ = [ "BadGatewayError", "BadRequestError", + "ConflictError", "ForbiddenError", "InternalServerError", "NotFoundError", diff --git a/src/speechify/errors/conflict_error.py b/src/speechify/errors/conflict_error.py new file mode 100644 index 0000000..be04e01 --- /dev/null +++ b/src/speechify/errors/conflict_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class ConflictError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=409, headers=headers, body=body) diff --git a/src/speechify/types/__init__.py b/src/speechify/types/__init__.py index bbc6a1d..9d924d4 100644 --- a/src/speechify/types/__init__.py +++ b/src/speechify/types/__init__.py @@ -6,12 +6,8 @@ from importlib import import_module if typing.TYPE_CHECKING: - from .create_voice_language import CreateVoiceLanguage - from .create_voice_model import CreateVoiceModel - from .create_voice_model_name import CreateVoiceModelName - from .created_voice import CreatedVoice - from .created_voice_gender import CreatedVoiceGender - from .created_voice_type import CreatedVoiceType + from .audio_output_format import AudioOutputFormat + from .audio_stream_output_format import AudioStreamOutputFormat from .error import Error from .error_code import ErrorCode from .error_detail import ErrorDetail @@ -25,15 +21,13 @@ from .get_voice_type import GetVoiceType from .get_voices_model import GetVoicesModel from .get_voices_model_name import GetVoicesModelName + from .list_voices_response import ListVoicesResponse from .nested_chunk import NestedChunk + from .pagination_meta import PaginationMeta from .speech_marks import SpeechMarks _dynamic_imports: typing.Dict[str, str] = { - "CreateVoiceLanguage": ".create_voice_language", - "CreateVoiceModel": ".create_voice_model", - "CreateVoiceModelName": ".create_voice_model_name", - "CreatedVoice": ".created_voice", - "CreatedVoiceGender": ".created_voice_gender", - "CreatedVoiceType": ".created_voice_type", + "AudioOutputFormat": ".audio_output_format", + "AudioStreamOutputFormat": ".audio_stream_output_format", "Error": ".error", "ErrorCode": ".error_code", "ErrorDetail": ".error_detail", @@ -47,7 +41,9 @@ "GetVoiceType": ".get_voice_type", "GetVoicesModel": ".get_voices_model", "GetVoicesModelName": ".get_voices_model_name", + "ListVoicesResponse": ".list_voices_response", "NestedChunk": ".nested_chunk", + "PaginationMeta": ".pagination_meta", "SpeechMarks": ".speech_marks", } @@ -74,12 +70,8 @@ def __dir__(): __all__ = [ - "CreateVoiceLanguage", - "CreateVoiceModel", - "CreateVoiceModelName", - "CreatedVoice", - "CreatedVoiceGender", - "CreatedVoiceType", + "AudioOutputFormat", + "AudioStreamOutputFormat", "Error", "ErrorCode", "ErrorDetail", @@ -93,6 +85,8 @@ def __dir__(): "GetVoiceType", "GetVoicesModel", "GetVoicesModelName", + "ListVoicesResponse", "NestedChunk", + "PaginationMeta", "SpeechMarks", ] diff --git a/src/speechify/types/audio_output_format.py b/src/speechify/types/audio_output_format.py new file mode 100644 index 0000000..dfc3132 --- /dev/null +++ b/src/speechify/types/audio_output_format.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +AudioOutputFormat = typing.Union[ + typing.Literal[ + "pcm_8000", + "pcm_16000", + "pcm_22050", + "pcm_24000", + "pcm_44100", + "pcm_48000", + "mp3_22050_32", + "mp3_22050_64", + "mp3_22050_96", + "mp3_22050_128", + "mp3_22050_192", + "mp3_24000_32", + "mp3_24000_64", + "mp3_24000_96", + "mp3_24000_128", + "mp3_24000_192", + "wav_24000", + "wav_48000", + "ulaw_8000", + "ogg_24000", + "aac_24000", + ], + typing.Any, +] diff --git a/src/speechify/types/audio_stream_output_format.py b/src/speechify/types/audio_stream_output_format.py new file mode 100644 index 0000000..ca15256 --- /dev/null +++ b/src/speechify/types/audio_stream_output_format.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +AudioStreamOutputFormat = typing.Union[ + typing.Literal[ + "pcm_8000", + "pcm_16000", + "pcm_22050", + "pcm_24000", + "pcm_44100", + "pcm_48000", + "mp3_22050_32", + "mp3_22050_64", + "mp3_22050_96", + "mp3_22050_128", + "mp3_22050_192", + "mp3_24000_32", + "mp3_24000_64", + "mp3_24000_96", + "mp3_24000_128", + "mp3_24000_192", + "ulaw_8000", + "ogg_24000", + "aac_24000", + ], + typing.Any, +] diff --git a/src/speechify/types/create_voice_language.py b/src/speechify/types/create_voice_language.py deleted file mode 100644 index bf99e97..0000000 --- a/src/speechify/types/create_voice_language.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class CreateVoiceLanguage(UniversalBaseModel): - locale: typing.Optional[str] = None - preview_audio: typing.Optional[str] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/types/create_voice_model.py b/src/speechify/types/create_voice_model.py deleted file mode 100644 index 00be24f..0000000 --- a/src/speechify/types/create_voice_model.py +++ /dev/null @@ -1,22 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .create_voice_language import CreateVoiceLanguage -from .create_voice_model_name import CreateVoiceModelName - - -class CreateVoiceModel(UniversalBaseModel): - languages: typing.Optional[typing.List[CreateVoiceLanguage]] = None - name: typing.Optional[CreateVoiceModelName] = None - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/types/create_voice_model_name.py b/src/speechify/types/create_voice_model_name.py deleted file mode 100644 index e82711f..0000000 --- a/src/speechify/types/create_voice_model_name.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -CreateVoiceModelName = typing.Union[typing.Literal["simba-english", "simba-multilingual", "simba-3.0"], typing.Any] diff --git a/src/speechify/types/created_voice.py b/src/speechify/types/created_voice.py deleted file mode 100644 index 6c05018..0000000 --- a/src/speechify/types/created_voice.py +++ /dev/null @@ -1,28 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .create_voice_model import CreateVoiceModel -from .created_voice_gender import CreatedVoiceGender -from .created_voice_type import CreatedVoiceType - - -class CreatedVoice(UniversalBaseModel): - avatar_image: typing.Optional[str] = None - display_name: str - gender: CreatedVoiceGender - locale: str - id: str - models: typing.List[CreateVoiceModel] - type: CreatedVoiceType - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/speechify/types/created_voice_gender.py b/src/speechify/types/created_voice_gender.py deleted file mode 100644 index 7c9aac4..0000000 --- a/src/speechify/types/created_voice_gender.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -CreatedVoiceGender = typing.Union[typing.Literal["male", "female", "notSpecified"], typing.Any] diff --git a/src/speechify/types/created_voice_type.py b/src/speechify/types/created_voice_type.py deleted file mode 100644 index 8b29ebf..0000000 --- a/src/speechify/types/created_voice_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -CreatedVoiceType = typing.Union[typing.Literal["shared", "personal"], typing.Any] diff --git a/src/speechify/types/error_code.py b/src/speechify/types/error_code.py index a4ce0b1..ce5e090 100644 --- a/src/speechify/types/error_code.py +++ b/src/speechify/types/error_code.py @@ -12,14 +12,18 @@ "not_found", "method_not_allowed", "conflict", + "idempotency_conflict", "payload_too_large", "unsupported_media_type", "rate_limited", + "concurrency_limit_reached", + "invalid_api_version", "internal_error", "upstream_failure", "service_unavailable", "caller_not_found", "credential_not_found", + "credential_in_use", "agent_not_found", "kb_not_found", "kb_document_not_found", @@ -44,6 +48,13 @@ "workspace_last_workspace", "invite_email_mismatch", "invite_already_pending", + "service_account_limit_reached", + "service_accounts_not_in_plan", + "tool_config_shared", + "spend_cap_exceeded", + "spend_budget_exceeded", + "destination_not_allowed", + "international_dialing_not_enabled", ], typing.Any, ] diff --git a/src/speechify/types/error_detail.py b/src/speechify/types/error_detail.py index e2c1fa8..c697538 100644 --- a/src/speechify/types/error_detail.py +++ b/src/speechify/types/error_detail.py @@ -27,6 +27,16 @@ class ErrorDetail(UniversalBaseModel): offending form field. """ + details: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field(default=None) + """ + Structured, endpoint-specific context beyond the flat + `fields` map. Present only on the few errors that carry + it (e.g. the `used_by` referrer list on a credential + delete-conflict); its shape depends on the error `code`. + Clients that don't recognise a `details` shape can ignore + it - the `code` + `message` contract is unchanged. + """ + if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 else: diff --git a/src/speechify/types/get_speech_response.py b/src/speechify/types/get_speech_response.py index bf5a964..54a2d93 100644 --- a/src/speechify/types/get_speech_response.py +++ b/src/speechify/types/get_speech_response.py @@ -4,6 +4,7 @@ import pydantic from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .audio_output_format import AudioOutputFormat from .get_speech_response_audio_format import GetSpeechResponseAudioFormat from .speech_marks import SpeechMarks @@ -16,7 +17,7 @@ class GetSpeechResponse(UniversalBaseModel): audio_format: GetSpeechResponseAudioFormat = pydantic.Field() """ - The format of the audio data + The codec of the audio data """ billable_characters_count: int = pydantic.Field() @@ -24,6 +25,11 @@ class GetSpeechResponse(UniversalBaseModel): The number of billable characters processed in the request. """ + output_format: typing.Optional[AudioOutputFormat] = pydantic.Field(default=None) + """ + The full `codec_sampleRate_bitrate` format, echoed back when the request set `output_format`. + """ + speech_marks: SpeechMarks if IS_PYDANTIC_V2: diff --git a/src/speechify/types/get_speech_response_audio_format.py b/src/speechify/types/get_speech_response_audio_format.py index b62111c..d062e53 100644 --- a/src/speechify/types/get_speech_response_audio_format.py +++ b/src/speechify/types/get_speech_response_audio_format.py @@ -2,4 +2,4 @@ import typing -GetSpeechResponseAudioFormat = typing.Union[typing.Literal["wav", "mp3", "ogg", "aac", "pcm"], typing.Any] +GetSpeechResponseAudioFormat = typing.Union[typing.Literal["wav", "mp3", "ogg", "aac", "pcm", "ulaw"], typing.Any] diff --git a/src/speechify/types/get_voice_gender.py b/src/speechify/types/get_voice_gender.py index 3cec6e7..c23ebd8 100644 --- a/src/speechify/types/get_voice_gender.py +++ b/src/speechify/types/get_voice_gender.py @@ -2,4 +2,4 @@ import typing -GetVoiceGender = typing.Union[typing.Literal["male", "female", "notSpecified"], typing.Any] +GetVoiceGender = typing.Union[typing.Literal["male", "female", "not_specified"], typing.Any] diff --git a/src/speechify/types/get_voices_model_name.py b/src/speechify/types/get_voices_model_name.py index 7ec7d98..19cc815 100644 --- a/src/speechify/types/get_voices_model_name.py +++ b/src/speechify/types/get_voices_model_name.py @@ -2,4 +2,6 @@ import typing -GetVoicesModelName = typing.Union[typing.Literal["simba-english", "simba-multilingual", "simba-3.0"], typing.Any] +GetVoicesModelName = typing.Union[ + typing.Literal["simba-english", "simba-multilingual", "simba-3.0", "simba-3.2"], typing.Any +] diff --git a/src/speechify/types/list_voices_response.py b/src/speechify/types/list_voices_response.py new file mode 100644 index 0000000..9804146 --- /dev/null +++ b/src/speechify/types/list_voices_response.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from .get_voice import GetVoice +from .pagination_meta import PaginationMeta + + +class ListVoicesResponse(PaginationMeta): + """ + Payload for `GET /v1/voices` - the shared voice catalog plus the + workspace's personal cloned voices. Carries the shared pagination + metadata so the list can be paged when `limit` is supplied; + `next_cursor` is null and `has_more` is false when the full + catalogue is returned in one response. + """ + + voices: typing.List[GetVoice] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/speechify/types/pagination_meta.py b/src/speechify/types/pagination_meta.py new file mode 100644 index 0000000..d696bd9 --- /dev/null +++ b/src/speechify/types/pagination_meta.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class PaginationMeta(UniversalBaseModel): + """ + Shared pagination metadata composed into every cursor-paginated + list response via `allOf`. Ships `has_more` alongside `next_cursor` + as two equivalent end-of-pages signals (defense-in-depth). + """ + + next_cursor: typing.Optional[str] = pydantic.Field(default=None) + """ + Opaque keyset cursor for the next page. Pass back as the + `cursor` request parameter. `null` when the caller has + reached the end of the list (`has_more` is also `false` + in that case). + """ + + has_more: bool = pydantic.Field() + """ + True when more rows exist beyond this page. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/speechify/voices/client.py b/src/speechify/voices/client.py index 1ddb3fa..036c918 100644 --- a/src/speechify/voices/client.py +++ b/src/speechify/voices/client.py @@ -4,9 +4,10 @@ from .. import core from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.pagination import AsyncPager, SyncPager from ..core.request_options import RequestOptions -from ..types.created_voice import CreatedVoice from ..types.get_voice import GetVoice +from ..types.list_voices_response import ListVoicesResponse from .raw_client import AsyncRawVoicesClient, RawVoicesClient from .types.create_voices_request_gender import CreateVoicesRequestGender @@ -29,31 +30,53 @@ def with_raw_response(self) -> RawVoicesClient: """ return self._raw_client - def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.List[GetVoice]: + def list( + self, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> SyncPager[GetVoice, ListVoicesResponse]: """ - Gets the list of voices available for the user + Lists the voices available to the caller - the shared voice + catalog plus the workspace's personal cloned voices. By default + the full catalogue is returned in one response. Pagination is + opt-in: pass `limit` (and then `cursor` from the previous + response) to page through the list while `has_more` is true. Max + page size is 200. Parameters ---------- + cursor : typing.Optional[str] + Opaque pagination cursor from a previous response. + + limit : typing.Optional[int] + Max items per page (default 50, max 200). + request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - typing.List[GetVoice] - A list of voices + SyncPager[GetVoice, ListVoicesResponse] + The voice catalogue (or a page of it when `limit` is set). Examples -------- from speechify import Speechify client = Speechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) - client.voices.list() + response = client.voices.list() + for item in response: + yield item + # alternatively, you can paginate page-by-page + for page in response.iter_pages(): + yield page """ - _response = self._raw_client.list(request_options=request_options) - return _response.data + return self._raw_client.list(cursor=cursor, limit=limit, request_options=request_options) def create( self, @@ -62,10 +85,11 @@ def create( gender: CreateVoicesRequestGender, sample: core.File, consent: str, + idempotency_key: typing.Optional[str] = None, locale: typing.Optional[str] = OMIT, avatar: typing.Optional[core.File] = OMIT, request_options: typing.Optional[RequestOptions] = None, - ) -> CreatedVoice: + ) -> GetVoice: """ Create a personal (cloned) voice for the user @@ -78,7 +102,7 @@ def create( Gender marker for the personal voice male GenderMale female GenderFemale - notSpecified GenderNotSpecified + not_specified GenderNotSpecified sample : core.File See core.File for more documentation @@ -88,6 +112,14 @@ def create( This should include the fullName and email of the consenting individual. For example, `{"fullName": "John Doe", "email": "john@example.com"}` + idempotency_key : typing.Optional[str] + A client-generated key (an opaque string, max 255 chars) that makes a + side-effect POST safe to retry: the server runs the operation exactly + once and replays the first response (its status and body) for 24 hours. + Reusing a key with a different request body, or while the first request + is still in flight, returns `409 idempotency_conflict`. A replayed + response carries the `Idempotent-Replayed: true` header. + locale : typing.Optional[str] Native language (locale) of the personal voice (e.g. en-US, es-ES, etc.) @@ -99,7 +131,7 @@ def create( Returns ------- - CreatedVoice + GetVoice A created voice Examples @@ -107,9 +139,11 @@ def create( from speechify import Speechify client = Speechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) client.voices.create( + idempotency_key="a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", name="name", gender="male", consent="consent", @@ -120,19 +154,55 @@ def create( gender=gender, sample=sample, consent=consent, + idempotency_key=idempotency_key, locale=locale, avatar=avatar, request_options=request_options, ) return _response.data - def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: + def get(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> GetVoice: + """ + Fetch a single voice by id - a shared catalogue voice or one of + the caller's own personal (cloned) voices. A personal voice that + belongs to another workspace returns 404, identical to an + unknown id, so voice inventory is never enumerable across tenants. + + Parameters + ---------- + voice_id : str + The ID of the voice to fetch + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetVoice + The voice. + + Examples + -------- + from speechify import Speechify + + client = Speechify( + "2026-07-07", + token="YOUR_TOKEN", + ) + client.voices.get( + voice_id="voice_id", + ) + """ + _response = self._raw_client.get(voice_id, request_options=request_options) + return _response.data + + def delete(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: """ Delete a personal (cloned) voice Parameters ---------- - id : str + voice_id : str The ID of the voice to delete request_options : typing.Optional[RequestOptions] @@ -147,24 +217,25 @@ def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = from speechify import Speechify client = Speechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) client.voices.delete( - id="id", + voice_id="voice_id", ) """ - _response = self._raw_client.delete(id, request_options=request_options) + _response = self._raw_client.delete(voice_id, request_options=request_options) return _response.data def download_sample( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None + self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> typing.Iterator[bytes]: """ Download a personal (cloned) voice sample Parameters ---------- - id : str + voice_id : str The ID of the voice to download sample for request_options : typing.Optional[RequestOptions] @@ -180,13 +251,14 @@ def download_sample( from speechify import Speechify client = Speechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) client.voices.download_sample( - id="id", + voice_id="voice_id", ) """ - with self._raw_client.download_sample(id, request_options=request_options) as r: + with self._raw_client.download_sample(voice_id, request_options=request_options) as r: yield from r.data @@ -205,19 +277,36 @@ def with_raw_response(self) -> AsyncRawVoicesClient: """ return self._raw_client - async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.List[GetVoice]: + async def list( + self, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncPager[GetVoice, ListVoicesResponse]: """ - Gets the list of voices available for the user + Lists the voices available to the caller - the shared voice + catalog plus the workspace's personal cloned voices. By default + the full catalogue is returned in one response. Pagination is + opt-in: pass `limit` (and then `cursor` from the previous + response) to page through the list while `has_more` is true. Max + page size is 200. Parameters ---------- + cursor : typing.Optional[str] + Opaque pagination cursor from a previous response. + + limit : typing.Optional[int] + Max items per page (default 50, max 200). + request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - typing.List[GetVoice] - A list of voices + AsyncPager[GetVoice, ListVoicesResponse] + The voice catalogue (or a page of it when `limit` is set). Examples -------- @@ -226,18 +315,24 @@ async def list(self, *, request_options: typing.Optional[RequestOptions] = None) from speechify import AsyncSpeechify client = AsyncSpeechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) async def main() -> None: - await client.voices.list() + response = await client.voices.list() + async for item in response: + yield item + + # alternatively, you can paginate page-by-page + async for page in response.iter_pages(): + yield page asyncio.run(main()) """ - _response = await self._raw_client.list(request_options=request_options) - return _response.data + return await self._raw_client.list(cursor=cursor, limit=limit, request_options=request_options) async def create( self, @@ -246,10 +341,11 @@ async def create( gender: CreateVoicesRequestGender, sample: core.File, consent: str, + idempotency_key: typing.Optional[str] = None, locale: typing.Optional[str] = OMIT, avatar: typing.Optional[core.File] = OMIT, request_options: typing.Optional[RequestOptions] = None, - ) -> CreatedVoice: + ) -> GetVoice: """ Create a personal (cloned) voice for the user @@ -262,7 +358,7 @@ async def create( Gender marker for the personal voice male GenderMale female GenderFemale - notSpecified GenderNotSpecified + not_specified GenderNotSpecified sample : core.File See core.File for more documentation @@ -272,6 +368,14 @@ async def create( This should include the fullName and email of the consenting individual. For example, `{"fullName": "John Doe", "email": "john@example.com"}` + idempotency_key : typing.Optional[str] + A client-generated key (an opaque string, max 255 chars) that makes a + side-effect POST safe to retry: the server runs the operation exactly + once and replays the first response (its status and body) for 24 hours. + Reusing a key with a different request body, or while the first request + is still in flight, returns `409 idempotency_conflict`. A replayed + response carries the `Idempotent-Replayed: true` header. + locale : typing.Optional[str] Native language (locale) of the personal voice (e.g. en-US, es-ES, etc.) @@ -283,7 +387,7 @@ async def create( Returns ------- - CreatedVoice + GetVoice A created voice Examples @@ -293,12 +397,14 @@ async def create( from speechify import AsyncSpeechify client = AsyncSpeechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) async def main() -> None: await client.voices.create( + idempotency_key="a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", name="name", gender="male", consent="consent", @@ -312,19 +418,63 @@ async def main() -> None: gender=gender, sample=sample, consent=consent, + idempotency_key=idempotency_key, locale=locale, avatar=avatar, request_options=request_options, ) return _response.data - async def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: + async def get(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> GetVoice: + """ + Fetch a single voice by id - a shared catalogue voice or one of + the caller's own personal (cloned) voices. A personal voice that + belongs to another workspace returns 404, identical to an + unknown id, so voice inventory is never enumerable across tenants. + + Parameters + ---------- + voice_id : str + The ID of the voice to fetch + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + GetVoice + The voice. + + Examples + -------- + import asyncio + + from speechify import AsyncSpeechify + + client = AsyncSpeechify( + "2026-07-07", + token="YOUR_TOKEN", + ) + + + async def main() -> None: + await client.voices.get( + voice_id="voice_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(voice_id, request_options=request_options) + return _response.data + + async def delete(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: """ Delete a personal (cloned) voice Parameters ---------- - id : str + voice_id : str The ID of the voice to delete request_options : typing.Optional[RequestOptions] @@ -341,30 +491,31 @@ async def delete(self, id: str, *, request_options: typing.Optional[RequestOptio from speechify import AsyncSpeechify client = AsyncSpeechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) async def main() -> None: await client.voices.delete( - id="id", + voice_id="voice_id", ) asyncio.run(main()) """ - _response = await self._raw_client.delete(id, request_options=request_options) + _response = await self._raw_client.delete(voice_id, request_options=request_options) return _response.data async def download_sample( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None + self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> typing.AsyncIterator[bytes]: """ Download a personal (cloned) voice sample Parameters ---------- - id : str + voice_id : str The ID of the voice to download sample for request_options : typing.Optional[RequestOptions] @@ -382,18 +533,19 @@ async def download_sample( from speechify import AsyncSpeechify client = AsyncSpeechify( - api_key="YOUR_API_KEY", + "2026-07-07", + token="YOUR_TOKEN", ) async def main() -> None: await client.voices.download_sample( - id="id", + voice_id="voice_id", ) asyncio.run(main()) """ - async with self._raw_client.download_sample(id, request_options=request_options) as r: + async with self._raw_client.download_sample(voice_id, request_options=request_options) as r: async for _chunk in r.data: yield _chunk diff --git a/src/speechify/voices/raw_client.py b/src/speechify/voices/raw_client.py index 84ae632..8645cf9 100644 --- a/src/speechify/voices/raw_client.py +++ b/src/speechify/voices/raw_client.py @@ -9,11 +9,13 @@ from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.http_response import AsyncHttpResponse, HttpResponse from ..core.jsonable_encoder import encode_path_param +from ..core.pagination import AsyncPager, SyncPager from ..core.parse_error import ParsingError from ..core.pydantic_utilities import parse_obj_as from ..core.request_options import RequestOptions from ..errors.bad_gateway_error import BadGatewayError from ..errors.bad_request_error import BadRequestError +from ..errors.conflict_error import ConflictError from ..errors.forbidden_error import ForbiddenError from ..errors.internal_server_error import InternalServerError from ..errors.not_found_error import NotFoundError @@ -22,9 +24,9 @@ from ..errors.too_many_requests_error import TooManyRequestsError from ..errors.unauthorized_error import UnauthorizedError from ..errors.unprocessable_entity_error import UnprocessableEntityError -from ..types.created_voice import CreatedVoice from ..types.error import Error from ..types.get_voice import GetVoice +from ..types.list_voices_response import ListVoicesResponse from .types.create_voices_request_gender import CreateVoicesRequestGender from pydantic import ValidationError @@ -36,35 +38,75 @@ class RawVoicesClient: def __init__(self, *, client_wrapper: SyncClientWrapper): self._client_wrapper = client_wrapper - def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[typing.List[GetVoice]]: + def list( + self, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> SyncPager[GetVoice, ListVoicesResponse]: """ - Gets the list of voices available for the user + Lists the voices available to the caller - the shared voice + catalog plus the workspace's personal cloned voices. By default + the full catalogue is returned in one response. Pagination is + opt-in: pass `limit` (and then `cursor` from the previous + response) to page through the list while `has_more` is true. Max + page size is 200. Parameters ---------- + cursor : typing.Optional[str] + Opaque pagination cursor from a previous response. + + limit : typing.Optional[int] + Max items per page (default 50, max 200). + request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - HttpResponse[typing.List[GetVoice]] - A list of voices + SyncPager[GetVoice, ListVoicesResponse] + The voice catalogue (or a page of it when `limit` is set). """ _response = self._client_wrapper.httpx_client.request( "v1/voices", method="GET", + params={ + "cursor": cursor, + "limit": limit, + }, request_options=request_options, ) try: if 200 <= _response.status_code < 300: - _data = typing.cast( - typing.List[GetVoice], + _parsed_response = typing.cast( + ListVoicesResponse, parse_obj_as( - type_=typing.List[GetVoice], # type: ignore + type_=ListVoicesResponse, # type: ignore object_=_response.json(), ), ) - return HttpResponse(response=_response, data=_data) + _items = _parsed_response.voices + _parsed_next = _parsed_response.next_cursor + _has_next = _parsed_next is not None and _parsed_next != "" + _get_next = lambda: self.list( + cursor=_parsed_next, + limit=limit, + request_options=request_options, + ) + return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) if _response.status_code == 401: raise UnauthorizedError( headers=dict(_response.headers), @@ -125,10 +167,11 @@ def create( gender: CreateVoicesRequestGender, sample: core.File, consent: str, + idempotency_key: typing.Optional[str] = None, locale: typing.Optional[str] = OMIT, avatar: typing.Optional[core.File] = OMIT, request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[CreatedVoice]: + ) -> HttpResponse[GetVoice]: """ Create a personal (cloned) voice for the user @@ -141,7 +184,7 @@ def create( Gender marker for the personal voice male GenderMale female GenderFemale - notSpecified GenderNotSpecified + not_specified GenderNotSpecified sample : core.File See core.File for more documentation @@ -151,6 +194,14 @@ def create( This should include the fullName and email of the consenting individual. For example, `{"fullName": "John Doe", "email": "john@example.com"}` + idempotency_key : typing.Optional[str] + A client-generated key (an opaque string, max 255 chars) that makes a + side-effect POST safe to retry: the server runs the operation exactly + once and replays the first response (its status and body) for 24 hours. + Reusing a key with a different request body, or while the first request + is still in flight, returns `409 idempotency_conflict`. A replayed + response carries the `Idempotent-Replayed: true` header. + locale : typing.Optional[str] Native language (locale) of the personal voice (e.g. en-US, es-ES, etc.) @@ -162,7 +213,7 @@ def create( Returns ------- - HttpResponse[CreatedVoice] + HttpResponse[GetVoice] A created voice """ _response = self._client_wrapper.httpx_client.request( @@ -178,6 +229,9 @@ def create( "sample": sample, **({"avatar": avatar} if avatar is not None else {}), }, + headers={ + "Idempotency-Key": str(idempotency_key) if idempotency_key is not None else None, + }, request_options=request_options, omit=OMIT, force_multipart=True, @@ -185,9 +239,9 @@ def create( try: if 200 <= _response.status_code < 300: _data = typing.cast( - CreatedVoice, + GetVoice, parse_obj_as( - type_=CreatedVoice, # type: ignore + type_=GetVoice, # type: ignore object_=_response.json(), ), ) @@ -236,6 +290,17 @@ def create( ), ), ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) if _response.status_code == 422: raise UnprocessableEntityError( headers=dict(_response.headers), @@ -300,13 +365,134 @@ def create( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[None]: + def get(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[GetVoice]: + """ + Fetch a single voice by id - a shared catalogue voice or one of + the caller's own personal (cloned) voices. A personal voice that + belongs to another workspace returns 404, identical to an + unknown id, so voice inventory is never enumerable across tenants. + + Parameters + ---------- + voice_id : str + The ID of the voice to fetch + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[GetVoice] + The voice. + """ + _response = self._client_wrapper.httpx_client.request( + f"v1/voices/{encode_path_param(voice_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetVoice, + parse_obj_as( + type_=GetVoice, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[None]: """ Delete a personal (cloned) voice Parameters ---------- - id : str + voice_id : str The ID of the voice to delete request_options : typing.Optional[RequestOptions] @@ -317,7 +503,7 @@ def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = HttpResponse[None] """ _response = self._client_wrapper.httpx_client.request( - f"v1/voices/{encode_path_param(id)}", + f"v1/voices/{encode_path_param(voice_id)}", method="DELETE", request_options=request_options, ) @@ -423,14 +609,14 @@ def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = @contextlib.contextmanager def download_sample( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None + self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> typing.Iterator[HttpResponse[typing.Iterator[bytes]]]: """ Download a personal (cloned) voice sample Parameters ---------- - id : str + voice_id : str The ID of the voice to download sample for request_options : typing.Optional[RequestOptions] @@ -442,7 +628,7 @@ def download_sample( Voice sample audio file """ with self._client_wrapper.httpx_client.stream( - f"v1/voices/{encode_path_param(id)}/sample", + f"v1/voices/{encode_path_param(voice_id)}/sample", method="GET", request_options=request_options, ) as _response: @@ -565,36 +751,77 @@ def __init__(self, *, client_wrapper: AsyncClientWrapper): self._client_wrapper = client_wrapper async def list( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[typing.List[GetVoice]]: + self, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncPager[GetVoice, ListVoicesResponse]: """ - Gets the list of voices available for the user + Lists the voices available to the caller - the shared voice + catalog plus the workspace's personal cloned voices. By default + the full catalogue is returned in one response. Pagination is + opt-in: pass `limit` (and then `cursor` from the previous + response) to page through the list while `has_more` is true. Max + page size is 200. Parameters ---------- + cursor : typing.Optional[str] + Opaque pagination cursor from a previous response. + + limit : typing.Optional[int] + Max items per page (default 50, max 200). + request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - AsyncHttpResponse[typing.List[GetVoice]] - A list of voices + AsyncPager[GetVoice, ListVoicesResponse] + The voice catalogue (or a page of it when `limit` is set). """ _response = await self._client_wrapper.httpx_client.request( "v1/voices", method="GET", + params={ + "cursor": cursor, + "limit": limit, + }, request_options=request_options, ) try: if 200 <= _response.status_code < 300: - _data = typing.cast( - typing.List[GetVoice], + _parsed_response = typing.cast( + ListVoicesResponse, parse_obj_as( - type_=typing.List[GetVoice], # type: ignore + type_=ListVoicesResponse, # type: ignore object_=_response.json(), ), ) - return AsyncHttpResponse(response=_response, data=_data) + _items = _parsed_response.voices + _parsed_next = _parsed_response.next_cursor + _has_next = _parsed_next is not None and _parsed_next != "" + + async def _get_next(): + return await self.list( + cursor=_parsed_next, + limit=limit, + request_options=request_options, + ) + + return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) if _response.status_code == 401: raise UnauthorizedError( headers=dict(_response.headers), @@ -655,10 +882,11 @@ async def create( gender: CreateVoicesRequestGender, sample: core.File, consent: str, + idempotency_key: typing.Optional[str] = None, locale: typing.Optional[str] = OMIT, avatar: typing.Optional[core.File] = OMIT, request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[CreatedVoice]: + ) -> AsyncHttpResponse[GetVoice]: """ Create a personal (cloned) voice for the user @@ -671,7 +899,7 @@ async def create( Gender marker for the personal voice male GenderMale female GenderFemale - notSpecified GenderNotSpecified + not_specified GenderNotSpecified sample : core.File See core.File for more documentation @@ -681,6 +909,14 @@ async def create( This should include the fullName and email of the consenting individual. For example, `{"fullName": "John Doe", "email": "john@example.com"}` + idempotency_key : typing.Optional[str] + A client-generated key (an opaque string, max 255 chars) that makes a + side-effect POST safe to retry: the server runs the operation exactly + once and replays the first response (its status and body) for 24 hours. + Reusing a key with a different request body, or while the first request + is still in flight, returns `409 idempotency_conflict`. A replayed + response carries the `Idempotent-Replayed: true` header. + locale : typing.Optional[str] Native language (locale) of the personal voice (e.g. en-US, es-ES, etc.) @@ -692,7 +928,7 @@ async def create( Returns ------- - AsyncHttpResponse[CreatedVoice] + AsyncHttpResponse[GetVoice] A created voice """ _response = await self._client_wrapper.httpx_client.request( @@ -708,6 +944,9 @@ async def create( "sample": sample, **({"avatar": avatar} if avatar is not None else {}), }, + headers={ + "Idempotency-Key": str(idempotency_key) if idempotency_key is not None else None, + }, request_options=request_options, omit=OMIT, force_multipart=True, @@ -715,9 +954,9 @@ async def create( try: if 200 <= _response.status_code < 300: _data = typing.cast( - CreatedVoice, + GetVoice, parse_obj_as( - type_=CreatedVoice, # type: ignore + type_=GetVoice, # type: ignore object_=_response.json(), ), ) @@ -766,6 +1005,17 @@ async def create( ), ), ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) if _response.status_code == 422: raise UnprocessableEntityError( headers=dict(_response.headers), @@ -830,15 +1080,138 @@ async def create( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + async def get( + self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[GetVoice]: + """ + Fetch a single voice by id - a shared catalogue voice or one of + the caller's own personal (cloned) voices. A personal voice that + belongs to another workspace returns 404, identical to an + unknown id, so voice inventory is never enumerable across tenants. + + Parameters + ---------- + voice_id : str + The ID of the voice to fetch + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[GetVoice] + The voice. + """ + _response = await self._client_wrapper.httpx_client.request( + f"v1/voices/{encode_path_param(voice_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + GetVoice, + parse_obj_as( + type_=GetVoice, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + async def delete( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None + self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[None]: """ Delete a personal (cloned) voice Parameters ---------- - id : str + voice_id : str The ID of the voice to delete request_options : typing.Optional[RequestOptions] @@ -849,7 +1222,7 @@ async def delete( AsyncHttpResponse[None] """ _response = await self._client_wrapper.httpx_client.request( - f"v1/voices/{encode_path_param(id)}", + f"v1/voices/{encode_path_param(voice_id)}", method="DELETE", request_options=request_options, ) @@ -955,14 +1328,14 @@ async def delete( @contextlib.asynccontextmanager async def download_sample( - self, id: str, *, request_options: typing.Optional[RequestOptions] = None + self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]]: """ Download a personal (cloned) voice sample Parameters ---------- - id : str + voice_id : str The ID of the voice to download sample for request_options : typing.Optional[RequestOptions] @@ -974,7 +1347,7 @@ async def download_sample( Voice sample audio file """ async with self._client_wrapper.httpx_client.stream( - f"v1/voices/{encode_path_param(id)}/sample", + f"v1/voices/{encode_path_param(voice_id)}/sample", method="GET", request_options=request_options, ) as _response: diff --git a/src/speechify/voices/types/create_voices_request_gender.py b/src/speechify/voices/types/create_voices_request_gender.py index 7ed89de..320e938 100644 --- a/src/speechify/voices/types/create_voices_request_gender.py +++ b/src/speechify/voices/types/create_voices_request_gender.py @@ -2,4 +2,4 @@ import typing -CreateVoicesRequestGender = typing.Union[typing.Literal["male", "female", "notSpecified"], typing.Any] +CreateVoicesRequestGender = typing.Union[typing.Literal["male", "female", "not_specified"], typing.Any]