From 065230bbcceec73f07a75c28cf240130a11b583a Mon Sep 17 00:00:00 2001
From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com>
Date: Tue, 28 Jul 2026 08:57:16 +0000
Subject: [PATCH 1/3] [fern-generated] Update SDK
Generated by Fern
CLI Version: unknown
Generators:
- fernapi/fern-python-sdk: 5.12.12
---
.fern/metadata.json | 6 +-
README.md | 4 +-
reference.md | 888 ++++++++++++++---
src/smallestai/atoms/__init__.py | 6 +
.../atoms/agent_versioning_branches/client.py | 4 +-
.../agent_versioning_branches/raw_client.py | 26 +-
src/smallestai/atoms/compliance/raw_client.py | 41 +-
.../atoms/errors/bad_gateway_error.py | 3 +-
.../atoms/errors/too_many_requests_error.py | 3 +-
.../atoms/prompt_scoring/raw_client.py | 9 +-
src/smallestai/atoms/types/__init__.py | 3 +
.../atoms/types/conflict_error_body.py | 9 +
src/smallestai/atoms/types/webhook.py | 2 +-
src/smallestai/atoms/webhooks/__init__.py | 3 +
src/smallestai/atoms/webhooks/client.py | 141 +++
src/smallestai/atoms/webhooks/raw_client.py | 257 +++++
.../atoms/webhooks/types/__init__.py | 3 +
.../types/update_webhooks_response.py} | 8 +-
src/smallestai/waves/client.py | 607 +++++-------
src/smallestai/waves/electron/__init__.py | 64 ++
src/smallestai/waves/electron/client.py | 504 ++++++++++
src/smallestai/waves/electron/raw_client.py | 654 +++++++++++++
.../waves/electron/types/__init__.py | 62 ++
...hat_completion_request_response_format.py} | 9 +-
...completion_request_response_format_type.py | 5 +
.../types/chat_completion_request_stop.py | 5 +
.../chat_completion_request_stream_options.py | 24 +
.../chat_completion_request_tool_choice.py | 10 +
...completion_request_tool_choice_function.py | 23 +
...n_request_tool_choice_function_function.py | 20 +
...etion_request_tool_choice_function_type.py | 5 +
...hat_completion_request_tool_choice_zero.py | 5 +
src/smallestai/waves/errors/__init__.py | 21 +-
.../waves/errors/bad_gateway_error.py | 10 +
.../waves/errors/content_too_large_error.py | 11 +
.../waves/errors/forbidden_error.py | 10 +
.../waves/errors/service_unavailable_error.py | 10 +
.../waves/errors/too_many_requests_error.py | 10 +
.../waves/pulse_stt_streaming/__init__.py | 70 --
.../waves/pulse_stt_streaming/client.py | 307 ------
.../waves/pulse_stt_streaming/raw_client.py | 284 ------
.../pulse_stt_streaming/types/__init__.py | 68 --
.../pulse_close_stream_signal_message_type.py | 5 -
.../pulse_finalize_signal_message_type.py | 5 -
.../types/pulse_speech_ended_event_message.py | 34 -
.../pulse_speech_ended_event_message_type.py | 5 -
.../pulse_speech_started_event_message.py | 34 -
...pulse_speech_started_event_message_type.py | 5 -
.../pulse_transcription_response_message.py | 72 --
src/smallestai/waves/raw_client.py | 602 +++++-------
.../waves/speech_to_text/__init__.py | 91 ++
src/smallestai/waves/speech_to_text/client.py | 742 ++++++++++++++
.../waves/speech_to_text/raw_client.py | 915 ++++++++++++++++++
.../socket_client.py | 53 +-
.../waves/speech_to_text/types/__init__.py | 89 ++
.../speech_to_text/types/close_stream.py | 24 +
.../speech_to_text/types/close_stream_type.py | 5 +
.../speech_to_text/types/finalize_signal.py | 24 +
.../types/finalize_signal_type.py | 5 +
.../transcribe_request_emotion_detection.py} | 2 +-
.../transcribe_request_gender_detection.py | 5 +
.../types/transcribe_request_language.py} | 2 +-
.../types/transcribe_request_model.py | 5 +
.../types/transcribe_request_redact_pci.py | 5 +
.../types/transcribe_request_redact_pii.py | 5 +
.../transcribe_request_webhook_method.py | 5 +
.../types/transcribe_response.py | 8 +
.../types/transcription_error_event.py | 23 +
.../types/transcription_error_event_type.py} | 2 +-
.../types/transcription_event.py | 66 ++
.../types/transcription_event_type.py | 5 +
.../transcription_event_utterances_item.py} | 12 +-
.../types/transcription_event_words_item.py} | 28 +-
src/smallestai/waves/types/__init__.py | 141 +--
...response_emotions.py => async_accepted.py} | 11 +-
.../waves/types/bad_request_error_body.py | 7 +-
src/smallestai/waves/types/chat_completion.py | 28 +
.../types/chat_completion_choices_item.py | 24 +
...t_completion_choices_item_finish_reason.py | 7 +
.../waves/types/chat_completion_object.py | 5 +
.../create_voice_clone_waves_request_model.py | 5 +
.../create_voice_clone_waves_response.py | 22 +
.../create_voice_clone_waves_response_data.py | 52 +
..._clone_waves_response_data_samples_item.py | 31 +
..._voice_clone_waves_response_data_status.py | 7 +
.../waves/types/electron_message.py | 36 +
.../waves/types/electron_tool_call.py | 24 +
...onse.py => electron_tool_call_function.py} | 13 +-
.../waves/types/electron_tool_call_type.py | 5 +
src/smallestai/waves/types/error.py | 21 +
src/smallestai/waves/types/error_error.py | 37 +
.../waves/types/error_error_details_item.py | 22 +
src/smallestai/waves/types/error_response.py | 9 +-
.../waves/types/error_response_error.py | 11 +-
.../waves/types/internal_server_error_body.py | 11 +-
.../internal_server_error_body_error_code.py | 5 +
.../types/list_voice_clones_waves_response.py | 21 +
...t_voice_clones_waves_response_data_item.py | 54 ++
...s_waves_response_data_item_cloning_type.py | 5 +
..._clones_waves_response_data_item_status.py | 7 +
.../waves/types/pulse_stt_error_response.py | 67 --
...sponse_legacy.py => stt_error_response.py} | 17 +-
...anscribe_pulse_waves_request_capitalize.py | 5 -
...e_pulse_waves_request_emotion_detection.py | 5 -
...transcribe_pulse_waves_request_encoding.py | 7 -
...be_pulse_waves_request_gender_detection.py | 5 -
...ranscribe_pulse_waves_request_punctuate.py | 5 -
.../types/transcribe_pulse_waves_response.py | 63 --
.../transcribe_pulse_waves_response_gender.py | 5 -
.../waves/types/transcription_response.py | 42 +
....py => transcription_response_metadata.py} | 25 +-
.../waves/types/transcription_result.py | 38 -
.../waves/types/unauthorized_error_body.py | 18 +-
src/smallestai/waves/types/usage.py | 28 +
.../types/usage_prompt_tokens_details.py | 24 +
...sponse_utterances_item.py => utterance.py} | 7 +-
...e_waves_response_words_item.py => word.py} | 12 +-
tests/wire/test_atoms_webhooks.py | 10 +
tests/wire/test_waves.py | 19 +
tests/wire/test_waves_electron.py | 19 +
wiremock/wiremock-mappings.json | 132 ++-
121 files changed, 6128 insertions(+), 2143 deletions(-)
create mode 100644 src/smallestai/atoms/types/conflict_error_body.py
rename src/smallestai/{waves/pulse_stt_streaming/types/pulse_finalize_signal_message.py => atoms/webhooks/types/update_webhooks_response.py} (62%)
create mode 100644 src/smallestai/waves/electron/__init__.py
create mode 100644 src/smallestai/waves/electron/client.py
create mode 100644 src/smallestai/waves/electron/raw_client.py
create mode 100644 src/smallestai/waves/electron/types/__init__.py
rename src/smallestai/waves/{pulse_stt_streaming/types/pulse_close_stream_signal_message.py => electron/types/chat_completion_request_response_format.py} (60%)
create mode 100644 src/smallestai/waves/electron/types/chat_completion_request_response_format_type.py
create mode 100644 src/smallestai/waves/electron/types/chat_completion_request_stop.py
create mode 100644 src/smallestai/waves/electron/types/chat_completion_request_stream_options.py
create mode 100644 src/smallestai/waves/electron/types/chat_completion_request_tool_choice.py
create mode 100644 src/smallestai/waves/electron/types/chat_completion_request_tool_choice_function.py
create mode 100644 src/smallestai/waves/electron/types/chat_completion_request_tool_choice_function_function.py
create mode 100644 src/smallestai/waves/electron/types/chat_completion_request_tool_choice_function_type.py
create mode 100644 src/smallestai/waves/electron/types/chat_completion_request_tool_choice_zero.py
create mode 100644 src/smallestai/waves/errors/bad_gateway_error.py
create mode 100644 src/smallestai/waves/errors/content_too_large_error.py
create mode 100644 src/smallestai/waves/errors/forbidden_error.py
create mode 100644 src/smallestai/waves/errors/service_unavailable_error.py
create mode 100644 src/smallestai/waves/errors/too_many_requests_error.py
delete mode 100644 src/smallestai/waves/pulse_stt_streaming/__init__.py
delete mode 100644 src/smallestai/waves/pulse_stt_streaming/client.py
delete mode 100644 src/smallestai/waves/pulse_stt_streaming/raw_client.py
delete mode 100644 src/smallestai/waves/pulse_stt_streaming/types/__init__.py
delete mode 100644 src/smallestai/waves/pulse_stt_streaming/types/pulse_close_stream_signal_message_type.py
delete mode 100644 src/smallestai/waves/pulse_stt_streaming/types/pulse_finalize_signal_message_type.py
delete mode 100644 src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_ended_event_message.py
delete mode 100644 src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_ended_event_message_type.py
delete mode 100644 src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_started_event_message.py
delete mode 100644 src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_started_event_message_type.py
delete mode 100644 src/smallestai/waves/pulse_stt_streaming/types/pulse_transcription_response_message.py
create mode 100644 src/smallestai/waves/speech_to_text/__init__.py
create mode 100644 src/smallestai/waves/speech_to_text/client.py
create mode 100644 src/smallestai/waves/speech_to_text/raw_client.py
rename src/smallestai/waves/{pulse_stt_streaming => speech_to_text}/socket_client.py (72%)
create mode 100644 src/smallestai/waves/speech_to_text/types/__init__.py
create mode 100644 src/smallestai/waves/speech_to_text/types/close_stream.py
create mode 100644 src/smallestai/waves/speech_to_text/types/close_stream_type.py
create mode 100644 src/smallestai/waves/speech_to_text/types/finalize_signal.py
create mode 100644 src/smallestai/waves/speech_to_text/types/finalize_signal_type.py
rename src/smallestai/waves/{types/transcribe_pulse_waves_request_format.py => speech_to_text/types/transcribe_request_emotion_detection.py} (63%)
create mode 100644 src/smallestai/waves/speech_to_text/types/transcribe_request_gender_detection.py
rename src/smallestai/waves/{types/transcribe_pulse_waves_request_language.py => speech_to_text/types/transcribe_request_language.py} (91%)
create mode 100644 src/smallestai/waves/speech_to_text/types/transcribe_request_model.py
create mode 100644 src/smallestai/waves/speech_to_text/types/transcribe_request_redact_pci.py
create mode 100644 src/smallestai/waves/speech_to_text/types/transcribe_request_redact_pii.py
create mode 100644 src/smallestai/waves/speech_to_text/types/transcribe_request_webhook_method.py
create mode 100644 src/smallestai/waves/speech_to_text/types/transcribe_response.py
create mode 100644 src/smallestai/waves/speech_to_text/types/transcription_error_event.py
rename src/smallestai/waves/{types/pulse_stt_error_response_status.py => speech_to_text/types/transcription_error_event_type.py} (60%)
create mode 100644 src/smallestai/waves/speech_to_text/types/transcription_event.py
create mode 100644 src/smallestai/waves/speech_to_text/types/transcription_event_type.py
rename src/smallestai/waves/{pulse_stt_streaming/types/pulse_transcription_response_message_utterances_item.py => speech_to_text/types/transcription_event_utterances_item.py} (74%)
rename src/smallestai/waves/{pulse_stt_streaming/types/pulse_transcription_response_message_words_item.py => speech_to_text/types/transcription_event_words_item.py} (53%)
rename src/smallestai/waves/types/{transcribe_pulse_waves_response_emotions.py => async_accepted.py} (61%)
create mode 100644 src/smallestai/waves/types/chat_completion.py
create mode 100644 src/smallestai/waves/types/chat_completion_choices_item.py
create mode 100644 src/smallestai/waves/types/chat_completion_choices_item_finish_reason.py
create mode 100644 src/smallestai/waves/types/chat_completion_object.py
create mode 100644 src/smallestai/waves/types/create_voice_clone_waves_request_model.py
create mode 100644 src/smallestai/waves/types/create_voice_clone_waves_response.py
create mode 100644 src/smallestai/waves/types/create_voice_clone_waves_response_data.py
create mode 100644 src/smallestai/waves/types/create_voice_clone_waves_response_data_samples_item.py
create mode 100644 src/smallestai/waves/types/create_voice_clone_waves_response_data_status.py
create mode 100644 src/smallestai/waves/types/electron_message.py
create mode 100644 src/smallestai/waves/types/electron_tool_call.py
rename src/smallestai/waves/types/{pulse_stt_auth_missing_response.py => electron_tool_call_function.py} (60%)
create mode 100644 src/smallestai/waves/types/electron_tool_call_type.py
create mode 100644 src/smallestai/waves/types/error.py
create mode 100644 src/smallestai/waves/types/error_error.py
create mode 100644 src/smallestai/waves/types/error_error_details_item.py
create mode 100644 src/smallestai/waves/types/internal_server_error_body_error_code.py
create mode 100644 src/smallestai/waves/types/list_voice_clones_waves_response.py
create mode 100644 src/smallestai/waves/types/list_voice_clones_waves_response_data_item.py
create mode 100644 src/smallestai/waves/types/list_voice_clones_waves_response_data_item_cloning_type.py
create mode 100644 src/smallestai/waves/types/list_voice_clones_waves_response_data_item_status.py
delete mode 100644 src/smallestai/waves/types/pulse_stt_error_response.py
rename src/smallestai/waves/types/{pulse_stt_error_response_legacy.py => stt_error_response.py} (59%)
delete mode 100644 src/smallestai/waves/types/transcribe_pulse_waves_request_capitalize.py
delete mode 100644 src/smallestai/waves/types/transcribe_pulse_waves_request_emotion_detection.py
delete mode 100644 src/smallestai/waves/types/transcribe_pulse_waves_request_encoding.py
delete mode 100644 src/smallestai/waves/types/transcribe_pulse_waves_request_gender_detection.py
delete mode 100644 src/smallestai/waves/types/transcribe_pulse_waves_request_punctuate.py
delete mode 100644 src/smallestai/waves/types/transcribe_pulse_waves_response.py
delete mode 100644 src/smallestai/waves/types/transcribe_pulse_waves_response_gender.py
create mode 100644 src/smallestai/waves/types/transcription_response.py
rename src/smallestai/waves/types/{transcribe_pulse_waves_response_metadata.py => transcription_response_metadata.py} (58%)
delete mode 100644 src/smallestai/waves/types/transcription_result.py
create mode 100644 src/smallestai/waves/types/usage.py
create mode 100644 src/smallestai/waves/types/usage_prompt_tokens_details.py
rename src/smallestai/waves/types/{transcribe_pulse_waves_response_utterances_item.py => utterance.py} (76%)
rename src/smallestai/waves/types/{transcribe_pulse_waves_response_words_item.py => word.py} (78%)
create mode 100644 tests/wire/test_waves_electron.py
diff --git a/.fern/metadata.json b/.fern/metadata.json
index a4acd2a8..105761d3 100644
--- a/.fern/metadata.json
+++ b/.fern/metadata.json
@@ -45,10 +45,10 @@
}
]
},
- "originGitCommit": "ae4a1383eb215784f2c7a4caf3b1c42b6428c516",
+ "originGitCommit": "c3e8467d7de3ac4b4d54e3c9bc4828bebfc4c792",
"originGitCommitIsDirty": false,
"invokedBy": "ci",
- "requestedVersion": "5.2.0",
+ "requestedVersion": "5.3.0",
"ciProvider": "github",
- "sdkVersion": "5.2.0"
+ "sdkVersion": "5.3.0"
}
\ No newline at end of file
diff --git a/README.md b/README.md
index aec5aa8e..26c8b8f1 100644
--- a/README.md
+++ b/README.md
@@ -128,7 +128,7 @@ from smallestai import SmallestAI
client = SmallestAI(...)
# Connect to the websocket (Sync)
-with client.tts.connect() as socket:
+with client.speech_to_text.stream() as socket:
# Iterate over the messages as they arrive
for message in socket:
print(message)
@@ -142,7 +142,7 @@ from smallestai import AsyncSmallestAI
client = AsyncSmallestAI(...)
# Connect to the websocket (Async)
-async with client.tts.connect() as socket:
+async with client.speech_to_text.stream() as socket:
async for message in socket:
print(message)
```
diff --git a/reference.md b/reference.md
index ae522a60..2e8e9097 100644
--- a/reference.md
+++ b/reference.md
@@ -5752,6 +5752,121 @@ client.atoms.webhooks.delete(
+
+
+
+
+client.atoms.webhooks.update (...) -> UpdateWebhooksResponse
+
+
+
+#### 📝 Description
+
+
+
+
+
+
+
+Update a webhook's endpoint URL, description, or custom headers. At
+least one of the three fields must be present in the request body.
+
+**Event subscriptions cannot be changed here.** To add or remove an
+agent's subscription to this webhook, use `POST /agent/{agentId}/webhook-subscriptions`
+and `DELETE /agent/{agentId}/webhook-subscriptions`.
+
+**Custom `headers` behavior**
+- Send a non-empty object to replace all custom headers on the webhook.
+- Send an empty object (`{}`) to clear all custom headers.
+- Omit the field to leave existing custom headers untouched.
+
+Custom header limits: at most 10 headers per webhook, values up to
+1024 characters, header names must match RFC 7230 token syntax. The
+following names are reserved and rejected: `x-signature`, `host`,
+`content-length`, `content-type`, `connection`, `transfer-encoding`.
+
+
+
+
+
+#### 🔌 Usage
+
+
+
+
+
+
+
+```python
+from smallestai import SmallestAI
+from smallestai.environment import SmallestAIEnvironment
+
+client = SmallestAI(
+ api_key="",
+ environment=SmallestAIEnvironment.PRODUCTION,
+)
+
+client.atoms.webhooks.update(
+ id="id",
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+
+
+
+
+
+**id:** `str` — The ID of the webhook to update.
+
+
+
+
+
+
+
+**endpoint:** `typing.Optional[str]` — New endpoint URL. Must be a valid URL.
+
+
+
+
+
+
+
+**description:** `typing.Optional[str]` — New human-readable label.
+
+
+
+
+
+
+
+**headers:** `typing.Optional[typing.Dict[str, str]]`
+
+Map of custom header names to values. Non-empty object replaces
+all existing headers; empty object clears them.
+
+
+
+
+
+
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
@@ -8970,7 +9085,7 @@ client.atoms.agent_versioning_branches.get_draft(
-Upsert the open draft on this branch. If no draft is open, one is created automatically. The request body is a free-form agent config partial and must contain at least one recognized field (`prompt`, `voice`, `model`, `tools`, `post_call_analytics`, etc.); the server merges it into the existing draft and returns the resulting draft as a revision-shaped snapshot.
+Upsert the open draft on this branch. If no draft is open, one is created automatically. The request body is an agent config partial in the same camelCase shape as `GET /agent/{id}` (`globalPrompt`, `firstMessage`, `synthesizer`, `language`, `voiceDetectionConfig`, `smartTurnConfig`, ...) and must contain at least one recognized field; the server merges it into the existing draft and returns the resulting draft as a revision-shaped snapshot.
@@ -13987,7 +14102,7 @@ client.waves.synthesize_sse_tts(
-client.waves.transcribe_pulse (...) -> TranscribePulseWavesResponse
+client.waves.list_voice_clones () -> ListVoiceClonesWavesResponse
@@ -13999,81 +14114,70 @@ client.waves.synthesize_sse_tts(
-Transcribe an audio file to text using the Pulse model. The fastest way to get a transcript when you already have a recording — pass either the raw bytes or a URL.
-
-## When to use this
+Retrieve all voice clones in your organization.
+
+
+
+
-Use this endpoint when you have a complete audio file (call recording, voicemail, podcast episode) and want the transcript back in one response. For live transcription as audio arrives, use the realtime WebSocket endpoint (`WSS /waves/v1/pulse/get_text`) instead.
+#### 🔌 Usage
-## Input methods
+
+
-Send the audio in one of two ways:
+
+
-1. **Raw bytes** — `Content-Type: application/octet-stream` with the audio in the body. All knobs (`language`, `word_timestamps`, etc.) are query parameters.
-2. **URL** — `Content-Type: application/json` with `{"url": "..."}` in the body. Useful when the audio already lives in object storage. Same query parameters apply.
+```python
+from smallestai import SmallestAI
+from smallestai.environment import SmallestAIEnvironment
-Pulse autodetects the language across 30+ supported locales. Pass `language` explicitly when you already know it — detection is fast but skipping it is faster.
+client = SmallestAI(
+ api_key="",
+ environment=SmallestAIEnvironment.PRODUCTION,
+)
-## Examples
+client.waves.list_voice_clones()
-**cURL** (raw bytes)
-```bash
-curl -X POST "https://api.smallest.ai/waves/v1/pulse/get_text?language=en&word_timestamps=true" \
- -H "Authorization: Bearer $SMALLEST_API_KEY" \
- -H "Content-Type: application/octet-stream" \
- --data-binary "@./call.wav"
```
+
+
+
+
-**cURL** (URL)
-```bash
-curl -X POST "https://api.smallest.ai/waves/v1/pulse/get_text?language=en" \
- -H "Authorization: Bearer $SMALLEST_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{"url": "https://your-bucket.s3.amazonaws.com/call.wav"}'
-```
+#### ⚙️ Parameters
-**Python** (`pip install smallestai>=4.4.0`)
-```python
-from smallestai import SmallestAI
+
+
-client = SmallestAI(api_key="YOUR_API_KEY")
-with open("./call.wav", "rb") as f:
- result = client.waves.transcribe_pulse(
- request=f.read(),
- language="en",
- word_timestamps=True,
- diarize=True,
- )
-print(result.status) # "success"
-print(result.transcription) # the transcript string
-```
+
+
-**JavaScript / TypeScript** (using `fetch`)
-```typescript
-import { readFileSync } from "node:fs";
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
-const audio = readFileSync("./call.wav");
-const params = new URLSearchParams({ language: "en", word_timestamps: "true", diarize: "true" });
-const res = await fetch(`https://api.smallest.ai/waves/v1/pulse/get_text?${params}`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${process.env.SMALLEST_API_KEY}`,
- "Content-Type": "application/octet-stream",
- },
- body: audio,
-});
-const result = await res.json();
-console.log(result.transcription);
-```
+
+
+
-## Common gotchas
+client.waves.create_voice_clone (...) -> CreateVoiceCloneWavesResponse
+
+
+
+#### 📝 Description
+
+
+
-- **Max file size is 250 MB.** Larger files return HTTP `400` with `{errors: "Audio data too large", status: "error", message: "Error handling audio data"}`. Compress to mono 16 kHz PCM if you're close to the limit; quality is unaffected.
-- **Formatting flags (`format`, `punctuate`, `capitalize`)** are accepted at the wire level and exposed in the Python SDK as of `smallestai>=4.4.0`. Today they currently return the same transcript regardless of value — pass them in your integration so it works as the behavior changes.
-- **Webhook-driven flow**: pass `webhook_url` to receive the transcript asynchronously. The endpoint returns immediately; the transcript hits your webhook when ready. Useful for long files where you don't want to hold an HTTP connection open.
-- **Speaker diarization** (`diarize=true`) adds latency. Skip it if you only need the words.
-- **JavaScript / TypeScript**: the official `smallestai` npm package predates the Pulse model, so call this endpoint with `fetch` or `axios` as shown above.
+
+
+
+Create an instant voice clone in a single call. Defaults to `lightning-v3.1`.
@@ -14088,7 +14192,19 @@ console.log(result.transcription);
```python
-client.waves.transcribe_pulse(...)
+from smallestai import SmallestAI
+from smallestai.environment import SmallestAIEnvironment
+
+client = SmallestAI(
+ api_key="",
+ environment=SmallestAIEnvironment.PRODUCTION,
+)
+
+client.waves.create_voice_clone(
+ file="example_file",
+ display_name="displayName",
+)
+
```
@@ -14103,7 +14219,7 @@ client.waves.transcribe_pulse(...)
-**request:** `typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]`
+**display_name:** `str` — Human-readable name for the voice clone.
@@ -14111,18 +14227,12 @@ client.waves.transcribe_pulse(...)
-**language:** `typing.Optional[TranscribePulseWavesRequestLanguage]`
-
-Language of the audio file. Set explicitly to the known language for best accuracy.
+**file:** `core.File`
-**26 single-language codes** on this endpoint: `en`, `hi`, `de`, `es`, `ru`, `it`, `fr`, `nl`, `pt`, `uk`, `pl`, `cs`, `sk`, `lv`, `et`, `ro`, `fi`, `sv`, `bg`, `hu`, `da`, `lt`, `mt`, `zh`, `ja`, `ko`.
-
-**Regional auto-detect aggregators** for unknown audio:
-- `multi-eu` (default) — auto-detects across all 21 European codes above plus `en`.
-- `multi-asian` — auto-detects across `zh`, `ko`, `ja`, `en`.
-- `multi-indic`: auto-detects across `en`, `hi`, `gu`, `mr`, `bn`, `or`. India region only.
-
-Omitting `language` routes to `multi-eu`. See the [Pulse model card](/models/model-cards/speech-to-text/pulse) for the full table.
+Audio file to clone from. Supported MIME types:
+`audio/mpeg`, `audio/mpeg-3`, `audio/wav`, `audio/wave`,
+`audio/webm`, `video/webm`, `audio/mp4`, `video/mp4`.
+Maximum size: 5 MB.
@@ -14130,18 +14240,7 @@ Omitting `language` routes to `multi-eu`. See the [Pulse model card](/models/mod
-**encoding:** `typing.Optional[TranscribePulseWavesRequestEncoding]`
-
-Audio encoding of the bytes you upload. Mirrors the `encoding`
-parameter on the realtime WS endpoint.
-
-- `linear16`, `linear32` — raw PCM (16-bit and 32-bit)
-- `alaw`, `mulaw` — 8 kHz telephony codecs
-- `opus`, `ogg_opus` — Opus compressed audio (raw and Ogg container)
-
-When omitted, the server detects the format from the file's
-container header (works for `.wav`, `.mp3`, `.flac`, `.ogg`,
-`.m4a`, `.webm`).
+**description:** `typing.Optional[str]` — Optional longer description for the voice clone.
@@ -14149,7 +14248,7 @@ container header (works for `.wav`, `.mp3`, `.flac`, `.ogg`,
-**webhook_url:** `typing.Optional[str]`
+**accent:** `typing.Optional[str]` — Optional accent tag (e.g. "general", "indian").
@@ -14157,7 +14256,10 @@ container header (works for `.wav`, `.mp3`, `.flac`, `.ogg`,
-**webhook_extra:** `typing.Optional[str]`
+**tags:** `typing.Optional[str]`
+
+Optional comma-separated list of tags. Server splits on
+commas and trims whitespace (`"en, tone-test"` → `["en", "tone-test"]`).
@@ -14165,7 +14267,17 @@ container header (works for `.wav`, `.mp3`, `.flac`, `.ogg`,
-**word_timestamps:** `typing.Optional[bool]` — Whether to include word and utterance level timestamps in the response
+**language:** `typing.Optional[str]`
+
+Primary language the clone will be used for. Optional, but
+**strongly recommended** — set it to the language of your
+reference audio. The TTS request's `language` should also
+match this code; setting it now avoids silent language
+mismatches at inference time.
+
+Must be one of the languages supported by `lightning-v3.1`
+(e.g. `en`, `hi`). The server validates and rejects
+unsupported codes with a 400.
@@ -14173,7 +14285,12 @@ container header (works for `.wav`, `.mp3`, `.flac`, `.ogg`,
-**diarize:** `typing.Optional[bool]` — Whether to perform speaker diarization
+**model:** `typing.Optional[CreateVoiceCloneWavesRequestModel]`
+
+Voice cloning model. Defaults to `lightning-v3.1`.
+`lightning-v2` is accepted by the schema for historical
+reasons but is deprecated — the server returns 400 with
+`"Voice cloning for lightning-v2 is deprecated. Please use lightning-v3.1"`.
@@ -14181,57 +14298,594 @@ container header (works for `.wav`, `.mp3`, `.flac`, `.ogg`,
-**gender_detection:** `typing.Optional[TranscribePulseWavesRequestGenderDetection]` — Whether to predict the gender of the speaker
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
-
-
-**emotion_detection:** `typing.Optional[TranscribePulseWavesRequestEmotionDetection]` — Whether to predict speaker emotions
-
+
+## Waves SpeechToText
+client.waves.speech_to_text.transcribe (...) -> TranscribeResponse
-**format:** `typing.Optional[TranscribePulseWavesRequestFormat]`
-
-Master formatting switch for the transcript. When `false`, forces
-`punctuate=false`, `capitalize=false`, and also disables Inverse
-Text Normalization (ITN) so it cannot silently reintroduce
-punctuation or casing.
+#### 📝 Description
-When `true`, the `punctuate` and `capitalize` params take effect
-independently. Leave `format=true` and use those two to fine-tune.
-
-
-
+
+
-**punctuate:** `typing.Optional[TranscribePulseWavesRequestPunctuate]`
+Transcribe an audio file. The model is chosen via `?model=`:
-When `false`, strips end-of-sentence punctuation (`.`, `,`, `?`, `!`)
-from the transcript, `words[].word`, and
-`utterances[].transcript`. Does not affect casing — use
-`capitalize` for that. Overridden to `false` when `format=false`.
-
+- `?model=pulse-pro`: English-only, leaderboard-ranked accuracy. Raw bytes only; pass `webhook_url` to receive transcription asynchronously on long files.
+- `?model=pulse`: multilingual transcription (21 streaming + 26 pre-recorded languages), supports both raw bytes and audio-by-URL.
+
+## When to use this
+
+Use this endpoint when you have a complete audio file (call recording, voicemail, podcast episode) and want the transcript back in one response. For live transcription as audio arrives, use the realtime WebSocket endpoint (`WS /waves/v1/stt/live`) instead.
+
+Pulse Pro has no streaming worker today; calls to `WS /waves/v1/stt/live?model=pulse-pro` return `400` before the WebSocket upgrades.
+
+## Input methods
+
+- **Raw bytes**: `Content-Type: application/octet-stream` with the audio in the body. All knobs are query parameters.
+- **URL (`?model=pulse` only)**: `Content-Type: application/json` with `{"url": "..."}` in the body.
+
+## Examples
+
+**cURL**: Pulse Pro, sync
+```bash
+curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse-pro&language=en&word_timestamps=true" \
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \
+ -H "Content-Type: application/octet-stream" \
+ --data-binary "@./call.wav"
+```
+
+**cURL**: Pulse Pro, async via webhook
+```bash
+curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse-pro&language=en&webhook_url=https://your.app/cb" \
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \
+ -H "Content-Type: application/octet-stream" \
+ --data-binary "@./call.wav"
+```
+Returns `200 { "status": "processing", "request_id": "..." }` immediately. The webhook receives the full transcription when ready.
+
+**cURL**: Pulse, audio-by-URL
+```bash
+curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse&language=en" \
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"url": "https://your-bucket.s3.amazonaws.com/call.wav"}'
+```
+
+**Python**
+```python
+import requests
+
+with open("./call.wav", "rb") as f:
+ audio = f.read()
+
+r = requests.post(
+ "https://api.smallest.ai/waves/v1/stt/",
+ params={"model": "pulse-pro", "language": "en", "word_timestamps": "true"},
+ headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/octet-stream"},
+ data=audio,
+)
+r.raise_for_status()
+print(r.json()["transcription"])
+```
+
+**JavaScript / TypeScript**
+```typescript
+import { readFileSync } from "node:fs";
+
+const audio = readFileSync("./call.wav");
+const params = new URLSearchParams({ model: "pulse-pro", language: "en", word_timestamps: "true" });
+
+const res = await fetch(`https://api.smallest.ai/waves/v1/stt/?${params}`, {
+ method: "POST",
+ headers: { Authorization: `Bearer ${process.env.SMALLEST_API_KEY}`, "Content-Type": "application/octet-stream" },
+ body: audio,
+});
+console.log((await res.json()).transcription);
+```
+
+## Common gotchas
+
+- **`model` is required.** Missing or invalid values return `400` with an enum-validation error.
+- **Pulse Pro is English only.** Pass `language=en`. Other language codes are accepted at the wire level but produce unpredictable output.
+- **Pulse Pro does not support audio-by-URL.** Send raw bytes or use `?model=pulse` for the URL flow.
+- **Async (webhook) mode is Pulse Pro only.** Pulse runs sync only on this endpoint.
+- **Max payload 250 MB.** Larger requests return `413`. Compress to mono 16 kHz PCM if you are close to the limit; quality is unaffected.
+
+
+#### 🔌 Usage
+
-**capitalize:** `typing.Optional[TranscribePulseWavesRequestCapitalize]`
+
+
+
+```python
+client.waves.speech_to_text.transcribe(...)
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+
+
+
+
+
+**model:** `TranscribeRequestModel`
+
+Selects which ASR model handles the request. Required; missing or invalid values return `400`.
+
+- `pulse-pro`: English only, leaderboard-ranked accuracy, raw bytes only; supports async via `webhook_url`.
+- `pulse`: multilingual (39 languages), raw bytes OR URL.
+
+
+
+
+
+
+
+**language:** `TranscribeRequestLanguage`
+
+Language of the audio file. This endpoint is **Pre-Recorded (HTTP)** — for streaming, switch to `WSS /waves/v1/stt/live` (different supported language set).
+
+**26 single-language codes:** `en`, `hi`, `de`, `es`, `ru`, `it`, `fr`, `nl`, `pt`, `uk`, `pl`, `cs`, `sk`, `lv`, `et`, `ro`, `fi`, `sv`, `bg`, `hu`, `da`, `lt`, `mt`, `zh`, `ja`, `ko`.
+
+**Regional auto-detect aggregators** for unknown audio:
+- `multi-eu` — auto-detects across all 21 European codes plus `en`.
+- `multi-asian` — auto-detects across `zh`, `ko`, `ja`, `en`.
+- `multi-indic`: auto-detects across `en`, `hi`, `gu`, `mr`, `bn`, `or`. India region only.
+
+- **Pulse Pro**: pass `en`.
+- **Pulse**: pass any of the single-language codes above, or use the `multi-eu` / `multi-asian` / `multi-indic` aggregator for unknown audio. See the [Pulse model card](/models/model-cards/speech-to-text/pulse) for the full table with language names.
+
+
+
+
+
+
+
+**request:** `typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]`
+
+
+
+
+
+
+
+**word_timestamps:** `typing.Optional[bool]` — Include the per-word `words[]` array in the response — each entry carries the recognized `word`, its `start`/`end` timestamps, and a per-word `confidence` score (0.0–1.0). With `diarize=true`, entries also include `speaker`. On Pulse Pro this costs roughly one-third of throughput.
+
+
+
+
+
+
+
+**diarize:** `typing.Optional[bool]` — Multi-speaker identification; adds per-word and per-utterance speaker labels.
+
+
+
+
+
+
+
+**webhook_url:** `typing.Optional[str]` — Pulse Pro only. If set, the response is `200` with `{"status": "processing", "request_id": "..."}` immediately, and the full transcription is delivered to this URL when ready. Use for long files where you do not want to hold an HTTP connection open.
+
+
+
+
+
+
+
+**webhook_method:** `typing.Optional[TranscribeRequestWebhookMethod]` — HTTP method to use when calling the webhook. Pulse Pro only.
+
+
+
+
+
+
+
+**webhook_extra:** `typing.Optional[str]` — Arbitrary metadata returned to the webhook in addition to the transcription payload. Pulse Pro only.
+
+
+
+
+
+
+
+**redact_pii:** `typing.Optional[TranscribeRequestRedactPii]`
+
+Redact personally identifiable information from the transcript.
+Names → `[FIRSTNAME_*]` / `[LASTNAME_*]`, phone numbers →
+`[PHONENUMBER_*]`, addresses → `[ADDRESS_*]`, etc. The redaction
+tokens use sequential indices so multiple occurrences of the same
+entity get distinct labels (`[FIRSTNAME_1]`, `[FIRSTNAME_2]`).
+
+**Language support:** currently effective only on `en` and `hi`.
+Setting `redact_pii=true` on other language codes is accepted
+but does not redact.
+
+
+
+
+
+
+
+**redact_pci:** `typing.Optional[TranscribeRequestRedactPci]`
+
+Redact payment card information (credit-card numbers, CVV, account
+numbers, etc.). Replaces matches with `[ACCOUNTNUMBER_*]` tokens.
+Use alongside `redact_pii=true` for full PCI-compliant transcript
+handling.
+
+**Language support:** currently effective only on `en` and `hi`.
+Setting `redact_pci=true` on other language codes is accepted
+but does not redact.
+
+
+
+
+
+
+
+**emotion_detection:** `typing.Optional[TranscribeRequestEmotionDetection]`
+
+When `true`, the response adds an `emotions` object mapping detected
+emotion labels to confidence scores. Useful for voice-of-customer
+analytics on call recordings.
+
+
+
+
+
+
+
+**gender_detection:** `typing.Optional[TranscribeRequestGenderDetection]`
+
+When `true`, the response adds a `gender` field with the detected
+speaker gender label. Pulse pre-recorded only.
+
+
+
+
+
+
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+## Waves Electron
+client.waves.electron.complete (...) -> ChatCompletion
+
+
+
+#### 📝 Description
+
+
+
+
+
+
+
+Generate a chat completion with Electron. OpenAI-compatible
+request/response shape — point any OpenAI SDK at
+`https://api.smallest.ai/waves/v1` and it just works.
+
+Set `stream: true` to receive tokens via Server-Sent Events. With
+`stream_options: { include_usage: true }`, the final SSE chunk
+carries the `usage` block so token accounting is exact even on
+client disconnects.
+
+Tool calling follows OpenAI's `tools` array convention. When you
+provide a voice-agent-style system prompt, Electron emits a short
+filler phrase in the assistant message `content` field alongside
+`tool_calls` — see the [Tool Calling guide](/models/documentation/llm-electron/tool-function-calling)
+for the voice-agent pattern.
+
+## Examples
+
+**cURL**
+```bash
+curl -X POST "https://api.smallest.ai/waves/v1/chat/completions" \
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "model": "electron",
+ "messages": [
+ {"role": "user", "content": "Write one sentence about why the sky is blue."}
+ ]
+ }'
+```
+
+**Python** (`pip install openai`)
+```python
+import os
+from openai import OpenAI
+
+client = OpenAI(
+ base_url="https://api.smallest.ai/waves/v1",
+ api_key=os.environ["SMALLEST_API_KEY"],
+)
+
+response = client.chat.completions.create(
+ model="electron",
+ messages=[
+ {"role": "user", "content": "Write one sentence about why the sky is blue."}
+ ],
+)
+
+print(response.choices[0].message.content)
+```
+
+**JavaScript / TypeScript** (`npm install openai`)
+```typescript
+import OpenAI from "openai";
+
+const client = new OpenAI({
+ baseURL: "https://api.smallest.ai/waves/v1",
+ apiKey: process.env.SMALLEST_API_KEY,
+});
+
+const response = await client.chat.completions.create({
+ model: "electron",
+ messages: [
+ { role: "user", content: "Write one sentence about why the sky is blue." },
+ ],
+});
+
+console.log(response.choices[0].message.content);
+```
+
+**Streaming with usage** (Python)
+```python
+stream = client.chat.completions.create(
+ model="electron",
+ messages=[{"role": "user", "content": "Tell me a one-sentence fun fact."}],
+ stream=True,
+ stream_options={"include_usage": True},
+)
+for chunk in stream:
+ if chunk.choices and chunk.choices[0].delta.content:
+ print(chunk.choices[0].delta.content, end="", flush=True)
+ if chunk.usage:
+ print(f"\n\nTokens: {chunk.usage.total_tokens}")
+```
+
+## Common gotchas
+
+- **Base URL is `/waves/v1`**, not `/v1`. The OpenAI SDK appends `/chat/completions` for you.
+- **`stream_options.include_usage: true`** is required for exact token accounting on streaming calls — the final SSE chunk carries the `usage` block.
+- **`n > 1` and `prompt_logprobs` are rejected.** Use multiple requests if you need parallel completions.
+- **Auth header is `Authorization: Bearer $SMALLEST_API_KEY`** — get the key from the [Smallest AI Console](https://app.smallest.ai/dashboard/api-keys).
+
+
+
+
+
+#### 🔌 Usage
+
+
+
+
+
+
+
+```python
+from smallestai import SmallestAI
+from smallestai.environment import SmallestAIEnvironment
+from smallestai.waves import ElectronMessage
+
+client = SmallestAI(
+ api_key="",
+ environment=SmallestAIEnvironment.PRODUCTION,
+)
+
+client.waves.electron.complete(
+ model="electron",
+ messages=[
+ ElectronMessage(
+ role="user",
+ content="Hello!",
+ )
+ ],
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+
+
+
+
+
+**model:** `str` — Model ID. Currently only `"electron"`.
+
+
+
+
+
+
+
+**messages:** `typing.List[ElectronMessage]` — Chat history. Standard OpenAI message array.
+
+
+
+
+
+
+
+**temperature:** `typing.Optional[float]` — Sampling temperature.
+
+
+
+
+
+
+
+**top_p:** `typing.Optional[float]` — Nucleus sampling.
+
+
+
+
+
+
+
+**max_tokens:** `typing.Optional[int]`
+
+Maximum output tokens. Combined input + output context ceiling
+is 32,768.
+
+
+
+
+
+
+
+**stream:** `typing.Optional[bool]`
+
+When true, response is `text/event-stream`. See the
+[Streaming guide](/models/documentation/llm-electron/streaming).
+
+
+
+
+
+
+
+**stream_options:** `typing.Optional[ChatCompletionRequestStreamOptions]`
+
+
+
+
+
+
+
+**tools:** `typing.Optional[typing.List[typing.Dict[str, typing.Any]]]`
+
+Tool / function calling definitions. Forwarded verbatim to the
+OpenAI-compatible upstream, so the standard OpenAI shape
+(`{type: "function", function: {name, description, parameters}}`)
+is the recommended form and is what the examples below use.
+The wire schema is permissive (`array`) — any tools payload
+the upstream accepts will work. See [Tool Calling](/models/documentation/llm-electron/tool-function-calling)
+for details.
+
+
+
+
+
+
+
+**tool_choice:** `typing.Optional[ChatCompletionRequestToolChoice]`
+
+
+
+
+
+
+
+**response_format:** `typing.Optional[ChatCompletionRequestResponseFormat]` — Output shape. `{type: "text"}` (default) or `{type: "json_object"}`.
+
+
+
+
+
+
+
+**stop:** `typing.Optional[ChatCompletionRequestStop]`
+
+
+
+
+
+
+
+**seed:** `typing.Optional[int]` — Best-effort determinism.
+
+
+
+
+
+
+
+**logit_bias:** `typing.Optional[typing.Dict[str, float]]`
+
+
+
+
+
+
+
+**logprobs:** `typing.Optional[bool]`
+
+
+
+
+
+
+
+**top_logprobs:** `typing.Optional[int]`
+
+
+
+
+
+
+
+**presence_penalty:** `typing.Optional[float]`
+
+
+
+
+
+
+
+**frequency_penalty:** `typing.Optional[float]`
+
+
+
+
+
+
-When `false`, lowercases the entire transcript output (transcript,
-`words[].word`, and `utterances[].transcript`). Does not affect
-punctuation — use `punctuate` for that. Overridden to `false`
-when `format=false`.
+**user:** `typing.Optional[str]` — Opaque end-user identifier. Not interpreted by Electron.
diff --git a/src/smallestai/atoms/__init__.py b/src/smallestai/atoms/__init__.py
index 762819af..c812c10b 100644
--- a/src/smallestai/atoms/__init__.py
+++ b/src/smallestai/atoms/__init__.py
@@ -69,6 +69,7 @@
ComplianceApplicationStatus,
ComplianceApplicationUserType,
ComplianceRequirement,
+ ConflictErrorBody,
ConflictErrorResponse,
DiffChange,
DiffResult,
@@ -646,6 +647,7 @@
GetWebhookResponseData,
PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem,
PostAgentAgentIdWebhookSubscriptionsResponse,
+ UpdateWebhooksResponse,
)
_dynamic_imports: typing.Dict[str, str] = {
"ActivateVersionAgentVersioningVersionsResponse": ".agent_versioning_versions",
@@ -725,6 +727,7 @@
"ComplianceApplicationUserType": ".types",
"ComplianceRequirement": ".types",
"ConflictError": ".errors",
+ "ConflictErrorBody": ".types",
"ConflictErrorResponse": ".types",
"CreateAgentAgentsResponse": ".agents",
"CreateAgentRequestBackgroundSound": ".agents",
@@ -1173,6 +1176,7 @@
"UpdateDraftAgentVersioningBranchesResponse": ".agent_versioning_branches",
"UpdateDraftConfigAgentVersioningDraftsResponse": ".agent_versioning_drafts",
"UpdateVersionMetadataAgentVersioningVersionsResponse": ".agent_versioning_versions",
+ "UpdateWebhooksResponse": ".webhooks",
"VersionBlocks": ".types",
"VersioningV2MigrationRequiredResponse": ".types",
"VersioningV2MigrationRequiredResponseErrorType": ".types",
@@ -1341,6 +1345,7 @@ def __dir__():
"ComplianceApplicationUserType",
"ComplianceRequirement",
"ConflictError",
+ "ConflictErrorBody",
"ConflictErrorResponse",
"CreateAgentAgentsResponse",
"CreateAgentRequestBackgroundSound",
@@ -1789,6 +1794,7 @@ def __dir__():
"UpdateDraftAgentVersioningBranchesResponse",
"UpdateDraftConfigAgentVersioningDraftsResponse",
"UpdateVersionMetadataAgentVersioningVersionsResponse",
+ "UpdateWebhooksResponse",
"VersionBlocks",
"VersioningV2MigrationRequiredResponse",
"VersioningV2MigrationRequiredResponseErrorType",
diff --git a/src/smallestai/atoms/agent_versioning_branches/client.py b/src/smallestai/atoms/agent_versioning_branches/client.py
index 700fa853..f82a0fca 100644
--- a/src/smallestai/atoms/agent_versioning_branches/client.py
+++ b/src/smallestai/atoms/agent_versioning_branches/client.py
@@ -338,7 +338,7 @@ def update_draft(
request_options: typing.Optional[RequestOptions] = None,
) -> UpdateDraftAgentVersioningBranchesResponse:
"""
- Upsert the open draft on this branch. If no draft is open, one is created automatically. The request body is a free-form agent config partial and must contain at least one recognized field (`prompt`, `voice`, `model`, `tools`, `post_call_analytics`, etc.); the server merges it into the existing draft and returns the resulting draft as a revision-shaped snapshot.
+ Upsert the open draft on this branch. If no draft is open, one is created automatically. The request body is an agent config partial in the same camelCase shape as `GET /agent/{id}` (`globalPrompt`, `firstMessage`, `synthesizer`, `language`, `voiceDetectionConfig`, `smartTurnConfig`, ...) and must contain at least one recognized field; the server merges it into the existing draft and returns the resulting draft as a revision-shaped snapshot.
Parameters
----------
@@ -1023,7 +1023,7 @@ async def update_draft(
request_options: typing.Optional[RequestOptions] = None,
) -> UpdateDraftAgentVersioningBranchesResponse:
"""
- Upsert the open draft on this branch. If no draft is open, one is created automatically. The request body is a free-form agent config partial and must contain at least one recognized field (`prompt`, `voice`, `model`, `tools`, `post_call_analytics`, etc.); the server merges it into the existing draft and returns the resulting draft as a revision-shaped snapshot.
+ Upsert the open draft on this branch. If no draft is open, one is created automatically. The request body is an agent config partial in the same camelCase shape as `GET /agent/{id}` (`globalPrompt`, `firstMessage`, `synthesizer`, `language`, `voiceDetectionConfig`, `smartTurnConfig`, ...) and must contain at least one recognized field; the server merges it into the existing draft and returns the resulting draft as a revision-shaped snapshot.
Parameters
----------
diff --git a/src/smallestai/atoms/agent_versioning_branches/raw_client.py b/src/smallestai/atoms/agent_versioning_branches/raw_client.py
index 9e2f754d..c69c5c1c 100644
--- a/src/smallestai/atoms/agent_versioning_branches/raw_client.py
+++ b/src/smallestai/atoms/agent_versioning_branches/raw_client.py
@@ -874,7 +874,7 @@ def update_draft(
request_options: typing.Optional[RequestOptions] = None,
) -> HttpResponse[UpdateDraftAgentVersioningBranchesResponse]:
"""
- Upsert the open draft on this branch. If no draft is open, one is created automatically. The request body is a free-form agent config partial and must contain at least one recognized field (`prompt`, `voice`, `model`, `tools`, `post_call_analytics`, etc.); the server merges it into the existing draft and returns the resulting draft as a revision-shaped snapshot.
+ Upsert the open draft on this branch. If no draft is open, one is created automatically. The request body is an agent config partial in the same camelCase shape as `GET /agent/{id}` (`globalPrompt`, `firstMessage`, `synthesizer`, `language`, `voiceDetectionConfig`, `smartTurnConfig`, ...) and must contain at least one recognized field; the server merges it into the existing draft and returns the resulting draft as a revision-shaped snapshot.
Parameters
----------
@@ -1047,6 +1047,17 @@ def update_draft(
),
),
)
+ if _response.status_code == 409:
+ raise ConflictError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
if _response.status_code == 423:
raise LockedError(
headers=dict(_response.headers),
@@ -2405,7 +2416,7 @@ async def update_draft(
request_options: typing.Optional[RequestOptions] = None,
) -> AsyncHttpResponse[UpdateDraftAgentVersioningBranchesResponse]:
"""
- Upsert the open draft on this branch. If no draft is open, one is created automatically. The request body is a free-form agent config partial and must contain at least one recognized field (`prompt`, `voice`, `model`, `tools`, `post_call_analytics`, etc.); the server merges it into the existing draft and returns the resulting draft as a revision-shaped snapshot.
+ Upsert the open draft on this branch. If no draft is open, one is created automatically. The request body is an agent config partial in the same camelCase shape as `GET /agent/{id}` (`globalPrompt`, `firstMessage`, `synthesizer`, `language`, `voiceDetectionConfig`, `smartTurnConfig`, ...) and must contain at least one recognized field; the server merges it into the existing draft and returns the resulting draft as a revision-shaped snapshot.
Parameters
----------
@@ -2578,6 +2589,17 @@ async def update_draft(
),
),
)
+ if _response.status_code == 409:
+ raise ConflictError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
if _response.status_code == 423:
raise LockedError(
headers=dict(_response.headers),
diff --git a/src/smallestai/atoms/compliance/raw_client.py b/src/smallestai/atoms/compliance/raw_client.py
index 273dea96..eeeb844a 100644
--- a/src/smallestai/atoms/compliance/raw_client.py
+++ b/src/smallestai/atoms/compliance/raw_client.py
@@ -17,7 +17,6 @@
from ..errors.internal_server_error import InternalServerError
from ..errors.not_found_error import NotFoundError
from ..errors.unauthorized_error import UnauthorizedError
-from ..types.bad_request_error_response import BadRequestErrorResponse
from .types.get_compliance_requirements_request_number_type import GetComplianceRequirementsRequestNumberType
from .types.get_compliance_requirements_request_user_type import GetComplianceRequirementsRequestUserType
from .types.get_compliance_requirements_response import GetComplianceRequirementsResponse
@@ -129,9 +128,9 @@ def get_compliance_status(
raise BadGatewayError(
headers=dict(_response.headers),
body=typing.cast(
- BadRequestErrorResponse,
+ typing.Any,
construct_type(
- type_=BadRequestErrorResponse, # type: ignore
+ type_=typing.Any, # type: ignore
object_=_response.json(),
),
),
@@ -235,9 +234,9 @@ def get_compliance_requirements(
raise BadGatewayError(
headers=dict(_response.headers),
body=typing.cast(
- BadRequestErrorResponse,
+ typing.Any,
construct_type(
- type_=BadRequestErrorResponse, # type: ignore
+ type_=typing.Any, # type: ignore
object_=_response.json(),
),
),
@@ -390,9 +389,9 @@ def submit(
raise BadGatewayError(
headers=dict(_response.headers),
body=typing.cast(
- BadRequestErrorResponse,
+ typing.Any,
construct_type(
- type_=BadRequestErrorResponse, # type: ignore
+ type_=typing.Any, # type: ignore
object_=_response.json(),
),
),
@@ -513,9 +512,9 @@ def resubmit(
raise BadGatewayError(
headers=dict(_response.headers),
body=typing.cast(
- BadRequestErrorResponse,
+ typing.Any,
construct_type(
- type_=BadRequestErrorResponse, # type: ignore
+ type_=typing.Any, # type: ignore
object_=_response.json(),
),
),
@@ -603,9 +602,9 @@ def refresh_compliance_application_status(
raise BadGatewayError(
headers=dict(_response.headers),
body=typing.cast(
- BadRequestErrorResponse,
+ typing.Any,
construct_type(
- type_=BadRequestErrorResponse, # type: ignore
+ type_=typing.Any, # type: ignore
object_=_response.json(),
),
),
@@ -714,9 +713,9 @@ async def get_compliance_status(
raise BadGatewayError(
headers=dict(_response.headers),
body=typing.cast(
- BadRequestErrorResponse,
+ typing.Any,
construct_type(
- type_=BadRequestErrorResponse, # type: ignore
+ type_=typing.Any, # type: ignore
object_=_response.json(),
),
),
@@ -820,9 +819,9 @@ async def get_compliance_requirements(
raise BadGatewayError(
headers=dict(_response.headers),
body=typing.cast(
- BadRequestErrorResponse,
+ typing.Any,
construct_type(
- type_=BadRequestErrorResponse, # type: ignore
+ type_=typing.Any, # type: ignore
object_=_response.json(),
),
),
@@ -975,9 +974,9 @@ async def submit(
raise BadGatewayError(
headers=dict(_response.headers),
body=typing.cast(
- BadRequestErrorResponse,
+ typing.Any,
construct_type(
- type_=BadRequestErrorResponse, # type: ignore
+ type_=typing.Any, # type: ignore
object_=_response.json(),
),
),
@@ -1098,9 +1097,9 @@ async def resubmit(
raise BadGatewayError(
headers=dict(_response.headers),
body=typing.cast(
- BadRequestErrorResponse,
+ typing.Any,
construct_type(
- type_=BadRequestErrorResponse, # type: ignore
+ type_=typing.Any, # type: ignore
object_=_response.json(),
),
),
@@ -1188,9 +1187,9 @@ async def refresh_compliance_application_status(
raise BadGatewayError(
headers=dict(_response.headers),
body=typing.cast(
- BadRequestErrorResponse,
+ typing.Any,
construct_type(
- type_=BadRequestErrorResponse, # type: ignore
+ type_=typing.Any, # type: ignore
object_=_response.json(),
),
),
diff --git a/src/smallestai/atoms/errors/bad_gateway_error.py b/src/smallestai/atoms/errors/bad_gateway_error.py
index 3a81e2cd..dc20174a 100644
--- a/src/smallestai/atoms/errors/bad_gateway_error.py
+++ b/src/smallestai/atoms/errors/bad_gateway_error.py
@@ -3,9 +3,8 @@
import typing
from ...core.api_error import ApiError
-from ..types.bad_request_error_response import BadRequestErrorResponse
class BadGatewayError(ApiError):
- def __init__(self, body: BadRequestErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None):
+ def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None):
super().__init__(status_code=502, headers=headers, body=body)
diff --git a/src/smallestai/atoms/errors/too_many_requests_error.py b/src/smallestai/atoms/errors/too_many_requests_error.py
index 4d4cfe48..7f18fc2c 100644
--- a/src/smallestai/atoms/errors/too_many_requests_error.py
+++ b/src/smallestai/atoms/errors/too_many_requests_error.py
@@ -3,9 +3,8 @@
import typing
from ...core.api_error import ApiError
-from ..types.too_many_requests_error_body import TooManyRequestsErrorBody
class TooManyRequestsError(ApiError):
- def __init__(self, body: TooManyRequestsErrorBody, headers: typing.Optional[typing.Dict[str, str]] = None):
+ def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None):
super().__init__(status_code=429, headers=headers, body=body)
diff --git a/src/smallestai/atoms/prompt_scoring/raw_client.py b/src/smallestai/atoms/prompt_scoring/raw_client.py
index 385116f3..121931cb 100644
--- a/src/smallestai/atoms/prompt_scoring/raw_client.py
+++ b/src/smallestai/atoms/prompt_scoring/raw_client.py
@@ -16,7 +16,6 @@
from ..errors.not_found_error import NotFoundError
from ..errors.too_many_requests_error import TooManyRequestsError
from ..errors.unauthorized_error import UnauthorizedError
-from ..types.too_many_requests_error_body import TooManyRequestsErrorBody
from .types.post_prompt_scoring_score_request import PostPromptScoringScoreRequest
from .types.post_prompt_scoring_score_response import PostPromptScoringScoreResponse
from pydantic import ValidationError
@@ -144,9 +143,9 @@ def score_a_prompt(
raise TooManyRequestsError(
headers=dict(_response.headers),
body=typing.cast(
- TooManyRequestsErrorBody,
+ typing.Any,
construct_type(
- type_=TooManyRequestsErrorBody, # type: ignore
+ type_=typing.Any, # type: ignore
object_=_response.json(),
),
),
@@ -291,9 +290,9 @@ async def score_a_prompt(
raise TooManyRequestsError(
headers=dict(_response.headers),
body=typing.cast(
- TooManyRequestsErrorBody,
+ typing.Any,
construct_type(
- type_=TooManyRequestsErrorBody, # type: ignore
+ type_=typing.Any, # type: ignore
object_=_response.json(),
),
),
diff --git a/src/smallestai/atoms/types/__init__.py b/src/smallestai/atoms/types/__init__.py
index 1d14373f..b7ed31ee 100644
--- a/src/smallestai/atoms/types/__init__.py
+++ b/src/smallestai/atoms/types/__init__.py
@@ -68,6 +68,7 @@
from .compliance_application_status import ComplianceApplicationStatus
from .compliance_application_user_type import ComplianceApplicationUserType
from .compliance_requirement import ComplianceRequirement
+ from .conflict_error_body import ConflictErrorBody
from .conflict_error_response import ConflictErrorResponse
from .diff_change import DiffChange
from .diff_result import DiffResult
@@ -240,6 +241,7 @@
"ComplianceApplicationStatus": ".compliance_application_status",
"ComplianceApplicationUserType": ".compliance_application_user_type",
"ComplianceRequirement": ".compliance_requirement",
+ "ConflictErrorBody": ".conflict_error_body",
"ConflictErrorResponse": ".conflict_error_response",
"DiffChange": ".diff_change",
"DiffResult": ".diff_result",
@@ -434,6 +436,7 @@ def __dir__():
"ComplianceApplicationStatus",
"ComplianceApplicationUserType",
"ComplianceRequirement",
+ "ConflictErrorBody",
"ConflictErrorResponse",
"DiffChange",
"DiffResult",
diff --git a/src/smallestai/atoms/types/conflict_error_body.py b/src/smallestai/atoms/types/conflict_error_body.py
new file mode 100644
index 00000000..178c39c9
--- /dev/null
+++ b/src/smallestai/atoms/types/conflict_error_body.py
@@ -0,0 +1,9 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+from .conflict_error_response import ConflictErrorResponse
+from .draft_conflict_error import DraftConflictError
+from .versioning_v2migration_required_response import VersioningV2MigrationRequiredResponse
+
+ConflictErrorBody = typing.Union[VersioningV2MigrationRequiredResponse, DraftConflictError, ConflictErrorResponse]
diff --git a/src/smallestai/atoms/types/webhook.py b/src/smallestai/atoms/types/webhook.py
index 660a25bb..833dcdef 100644
--- a/src/smallestai/atoms/types/webhook.py
+++ b/src/smallestai/atoms/types/webhook.py
@@ -53,7 +53,7 @@ class Webhook(UncheckedBaseModel):
FieldMetadata(alias="decryptedSecretKey"),
pydantic.Field(
alias="decryptedSecretKey",
- description="The decrypted signing secret for the webhook. This is only returned when fetching a single webhook by ID.",
+ description="The decrypted signing secret for the webhook. Returned by both `GET /webhook` (list) and `GET /webhook?webhookId=` (single). Use as the HMAC-SHA256 key when verifying the `X-Signature` header on incoming webhook deliveries.",
),
] = None
created_at: typing_extensions.Annotated[
diff --git a/src/smallestai/atoms/webhooks/__init__.py b/src/smallestai/atoms/webhooks/__init__.py
index 308f6a7b..ff8200ee 100644
--- a/src/smallestai/atoms/webhooks/__init__.py
+++ b/src/smallestai/atoms/webhooks/__init__.py
@@ -17,6 +17,7 @@
GetWebhookResponseData,
PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem,
PostAgentAgentIdWebhookSubscriptionsResponse,
+ UpdateWebhooksResponse,
)
_dynamic_imports: typing.Dict[str, str] = {
"CreateWebhooksRequestEventsItem": ".types",
@@ -29,6 +30,7 @@
"GetWebhookResponseData": ".types",
"PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem": ".types",
"PostAgentAgentIdWebhookSubscriptionsResponse": ".types",
+ "UpdateWebhooksResponse": ".types",
}
@@ -64,4 +66,5 @@ def __dir__():
"GetWebhookResponseData",
"PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem",
"PostAgentAgentIdWebhookSubscriptionsResponse",
+ "UpdateWebhooksResponse",
]
diff --git a/src/smallestai/atoms/webhooks/client.py b/src/smallestai/atoms/webhooks/client.py
index 675c97c9..c44a814a 100644
--- a/src/smallestai/atoms/webhooks/client.py
+++ b/src/smallestai/atoms/webhooks/client.py
@@ -15,6 +15,7 @@
PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem,
)
from .types.post_agent_agent_id_webhook_subscriptions_response import PostAgentAgentIdWebhookSubscriptionsResponse
+from .types.update_webhooks_response import UpdateWebhooksResponse
# this is used as the default value for optional parameters
OMIT = typing.cast(typing.Any, ...)
@@ -161,6 +162,72 @@ def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] =
_response = self._raw_client.delete(id, request_options=request_options)
return _response.data
+ def update(
+ self,
+ id: str,
+ *,
+ endpoint: typing.Optional[str] = OMIT,
+ description: typing.Optional[str] = OMIT,
+ headers: typing.Optional[typing.Dict[str, str]] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> UpdateWebhooksResponse:
+ """
+ Update a webhook's endpoint URL, description, or custom headers. At
+ least one of the three fields must be present in the request body.
+
+ **Event subscriptions cannot be changed here.** To add or remove an
+ agent's subscription to this webhook, use `POST /agent/{agentId}/webhook-subscriptions`
+ and `DELETE /agent/{agentId}/webhook-subscriptions`.
+
+ **Custom `headers` behavior**
+ - Send a non-empty object to replace all custom headers on the webhook.
+ - Send an empty object (`{}`) to clear all custom headers.
+ - Omit the field to leave existing custom headers untouched.
+
+ Custom header limits: at most 10 headers per webhook, values up to
+ 1024 characters, header names must match RFC 7230 token syntax. The
+ following names are reserved and rejected: `x-signature`, `host`,
+ `content-length`, `content-type`, `connection`, `transfer-encoding`.
+
+ Parameters
+ ----------
+ id : str
+ The ID of the webhook to update.
+
+ endpoint : typing.Optional[str]
+ New endpoint URL. Must be a valid URL.
+
+ description : typing.Optional[str]
+ New human-readable label.
+
+ headers : typing.Optional[typing.Dict[str, str]]
+ Map of custom header names to values. Non-empty object replaces
+ all existing headers; empty object clears them.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ UpdateWebhooksResponse
+ Webhook updated successfully.
+
+ Examples
+ --------
+ from smallestai import SmallestAI
+
+ client = SmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+ client.atoms.webhooks.update(
+ id="id",
+ )
+ """
+ _response = self._raw_client.update(
+ id, endpoint=endpoint, description=description, headers=headers, request_options=request_options
+ )
+ return _response.data
+
def get_webhook_subscriptions_for_an_agent(
self, agent_id: str, *, request_options: typing.Optional[RequestOptions] = None
) -> GetAgentAgentIdWebhookSubscriptionsResponse:
@@ -448,6 +515,80 @@ async def main() -> None:
_response = await self._raw_client.delete(id, request_options=request_options)
return _response.data
+ async def update(
+ self,
+ id: str,
+ *,
+ endpoint: typing.Optional[str] = OMIT,
+ description: typing.Optional[str] = OMIT,
+ headers: typing.Optional[typing.Dict[str, str]] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> UpdateWebhooksResponse:
+ """
+ Update a webhook's endpoint URL, description, or custom headers. At
+ least one of the three fields must be present in the request body.
+
+ **Event subscriptions cannot be changed here.** To add or remove an
+ agent's subscription to this webhook, use `POST /agent/{agentId}/webhook-subscriptions`
+ and `DELETE /agent/{agentId}/webhook-subscriptions`.
+
+ **Custom `headers` behavior**
+ - Send a non-empty object to replace all custom headers on the webhook.
+ - Send an empty object (`{}`) to clear all custom headers.
+ - Omit the field to leave existing custom headers untouched.
+
+ Custom header limits: at most 10 headers per webhook, values up to
+ 1024 characters, header names must match RFC 7230 token syntax. The
+ following names are reserved and rejected: `x-signature`, `host`,
+ `content-length`, `content-type`, `connection`, `transfer-encoding`.
+
+ Parameters
+ ----------
+ id : str
+ The ID of the webhook to update.
+
+ endpoint : typing.Optional[str]
+ New endpoint URL. Must be a valid URL.
+
+ description : typing.Optional[str]
+ New human-readable label.
+
+ headers : typing.Optional[typing.Dict[str, str]]
+ Map of custom header names to values. Non-empty object replaces
+ all existing headers; empty object clears them.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ UpdateWebhooksResponse
+ Webhook updated successfully.
+
+ Examples
+ --------
+ import asyncio
+
+ from smallestai import AsyncSmallestAI
+
+ client = AsyncSmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+
+
+ async def main() -> None:
+ await client.atoms.webhooks.update(
+ id="id",
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.update(
+ id, endpoint=endpoint, description=description, headers=headers, request_options=request_options
+ )
+ return _response.data
+
async def get_webhook_subscriptions_for_an_agent(
self, agent_id: str, *, request_options: typing.Optional[RequestOptions] = None
) -> GetAgentAgentIdWebhookSubscriptionsResponse:
diff --git a/src/smallestai/atoms/webhooks/raw_client.py b/src/smallestai/atoms/webhooks/raw_client.py
index ddcf57ed..2d4f217d 100644
--- a/src/smallestai/atoms/webhooks/raw_client.py
+++ b/src/smallestai/atoms/webhooks/raw_client.py
@@ -26,6 +26,7 @@
PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem,
)
from .types.post_agent_agent_id_webhook_subscriptions_response import PostAgentAgentIdWebhookSubscriptionsResponse
+from .types.update_webhooks_response import UpdateWebhooksResponse
from pydantic import ValidationError
# this is used as the default value for optional parameters
@@ -313,6 +314,134 @@ def delete(
)
raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+ def update(
+ self,
+ id: str,
+ *,
+ endpoint: typing.Optional[str] = OMIT,
+ description: typing.Optional[str] = OMIT,
+ headers: typing.Optional[typing.Dict[str, str]] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[UpdateWebhooksResponse]:
+ """
+ Update a webhook's endpoint URL, description, or custom headers. At
+ least one of the three fields must be present in the request body.
+
+ **Event subscriptions cannot be changed here.** To add or remove an
+ agent's subscription to this webhook, use `POST /agent/{agentId}/webhook-subscriptions`
+ and `DELETE /agent/{agentId}/webhook-subscriptions`.
+
+ **Custom `headers` behavior**
+ - Send a non-empty object to replace all custom headers on the webhook.
+ - Send an empty object (`{}`) to clear all custom headers.
+ - Omit the field to leave existing custom headers untouched.
+
+ Custom header limits: at most 10 headers per webhook, values up to
+ 1024 characters, header names must match RFC 7230 token syntax. The
+ following names are reserved and rejected: `x-signature`, `host`,
+ `content-length`, `content-type`, `connection`, `transfer-encoding`.
+
+ Parameters
+ ----------
+ id : str
+ The ID of the webhook to update.
+
+ endpoint : typing.Optional[str]
+ New endpoint URL. Must be a valid URL.
+
+ description : typing.Optional[str]
+ New human-readable label.
+
+ headers : typing.Optional[typing.Dict[str, str]]
+ Map of custom header names to values. Non-empty object replaces
+ all existing headers; empty object clears them.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[UpdateWebhooksResponse]
+ Webhook updated successfully.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ f"webhook/{encode_path_param(id)}",
+ base_url=self._client_wrapper.get_environment().atoms,
+ method="PATCH",
+ json={
+ "endpoint": endpoint,
+ "description": description,
+ "headers": headers,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ UpdateWebhooksResponse,
+ construct_type(
+ type_=UpdateWebhooksResponse, # 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,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # 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 get_webhook_subscriptions_for_an_agent(
self, agent_id: str, *, request_options: typing.Optional[RequestOptions] = None
) -> HttpResponse[GetAgentAgentIdWebhookSubscriptionsResponse]:
@@ -914,6 +1043,134 @@ async def delete(
)
raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+ async def update(
+ self,
+ id: str,
+ *,
+ endpoint: typing.Optional[str] = OMIT,
+ description: typing.Optional[str] = OMIT,
+ headers: typing.Optional[typing.Dict[str, str]] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> AsyncHttpResponse[UpdateWebhooksResponse]:
+ """
+ Update a webhook's endpoint URL, description, or custom headers. At
+ least one of the three fields must be present in the request body.
+
+ **Event subscriptions cannot be changed here.** To add or remove an
+ agent's subscription to this webhook, use `POST /agent/{agentId}/webhook-subscriptions`
+ and `DELETE /agent/{agentId}/webhook-subscriptions`.
+
+ **Custom `headers` behavior**
+ - Send a non-empty object to replace all custom headers on the webhook.
+ - Send an empty object (`{}`) to clear all custom headers.
+ - Omit the field to leave existing custom headers untouched.
+
+ Custom header limits: at most 10 headers per webhook, values up to
+ 1024 characters, header names must match RFC 7230 token syntax. The
+ following names are reserved and rejected: `x-signature`, `host`,
+ `content-length`, `content-type`, `connection`, `transfer-encoding`.
+
+ Parameters
+ ----------
+ id : str
+ The ID of the webhook to update.
+
+ endpoint : typing.Optional[str]
+ New endpoint URL. Must be a valid URL.
+
+ description : typing.Optional[str]
+ New human-readable label.
+
+ headers : typing.Optional[typing.Dict[str, str]]
+ Map of custom header names to values. Non-empty object replaces
+ all existing headers; empty object clears them.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[UpdateWebhooksResponse]
+ Webhook updated successfully.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ f"webhook/{encode_path_param(id)}",
+ base_url=self._client_wrapper.get_environment().atoms,
+ method="PATCH",
+ json={
+ "endpoint": endpoint,
+ "description": description,
+ "headers": headers,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ UpdateWebhooksResponse,
+ construct_type(
+ type_=UpdateWebhooksResponse, # 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,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # 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 get_webhook_subscriptions_for_an_agent(
self, agent_id: str, *, request_options: typing.Optional[RequestOptions] = None
) -> AsyncHttpResponse[GetAgentAgentIdWebhookSubscriptionsResponse]:
diff --git a/src/smallestai/atoms/webhooks/types/__init__.py b/src/smallestai/atoms/webhooks/types/__init__.py
index d9cbbbd8..85f72017 100644
--- a/src/smallestai/atoms/webhooks/types/__init__.py
+++ b/src/smallestai/atoms/webhooks/types/__init__.py
@@ -18,6 +18,7 @@
PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem,
)
from .post_agent_agent_id_webhook_subscriptions_response import PostAgentAgentIdWebhookSubscriptionsResponse
+ from .update_webhooks_response import UpdateWebhooksResponse
_dynamic_imports: typing.Dict[str, str] = {
"CreateWebhooksRequestEventsItem": ".create_webhooks_request_events_item",
"CreateWebhooksRequestEventsItemEventType": ".create_webhooks_request_events_item_event_type",
@@ -29,6 +30,7 @@
"GetWebhookResponseData": ".get_webhook_response_data",
"PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem": ".post_agent_agent_id_webhook_subscriptions_request_event_types_item",
"PostAgentAgentIdWebhookSubscriptionsResponse": ".post_agent_agent_id_webhook_subscriptions_response",
+ "UpdateWebhooksResponse": ".update_webhooks_response",
}
@@ -64,4 +66,5 @@ def __dir__():
"GetWebhookResponseData",
"PostAgentAgentIdWebhookSubscriptionsRequestEventTypesItem",
"PostAgentAgentIdWebhookSubscriptionsResponse",
+ "UpdateWebhooksResponse",
]
diff --git a/src/smallestai/waves/pulse_stt_streaming/types/pulse_finalize_signal_message.py b/src/smallestai/atoms/webhooks/types/update_webhooks_response.py
similarity index 62%
rename from src/smallestai/waves/pulse_stt_streaming/types/pulse_finalize_signal_message.py
rename to src/smallestai/atoms/webhooks/types/update_webhooks_response.py
index edcb9179..c52d8056 100644
--- a/src/smallestai/waves/pulse_stt_streaming/types/pulse_finalize_signal_message.py
+++ b/src/smallestai/atoms/webhooks/types/update_webhooks_response.py
@@ -5,13 +5,13 @@
import pydantic
from ....core.pydantic_utilities import IS_PYDANTIC_V2
from ....core.unchecked_base_model import UncheckedBaseModel
-from .pulse_finalize_signal_message_type import PulseFinalizeSignalMessageType
-class PulseFinalizeSignalMessage(UncheckedBaseModel):
- type: PulseFinalizeSignalMessageType = pydantic.Field()
+class UpdateWebhooksResponse(UncheckedBaseModel):
+ status: typing.Optional[bool] = None
+ data: typing.Optional[str] = pydantic.Field(default=None)
"""
- Flush current audio buffer and force an immediate is_final transcript. The session remains open for more audio.
+ The ID of the updated webhook.
"""
if IS_PYDANTIC_V2:
diff --git a/src/smallestai/waves/client.py b/src/smallestai/waves/client.py
index cdb261f6..cb439329 100644
--- a/src/smallestai/waves/client.py
+++ b/src/smallestai/waves/client.py
@@ -4,12 +4,16 @@
import typing
+from .. import core
from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
from ..core.request_options import RequestOptions
from .raw_client import AsyncRawWavesClient, RawWavesClient
+from .types.create_voice_clone_waves_request_model import CreateVoiceCloneWavesRequestModel
+from .types.create_voice_clone_waves_response import CreateVoiceCloneWavesResponse
from .types.delete_pronunciation_dict_response import DeletePronunciationDictResponse
from .types.get_voices_waves_request_model import GetVoicesWavesRequestModel
from .types.get_voices_waves_response import GetVoicesWavesResponse
+from .types.list_voice_clones_waves_response import ListVoiceClonesWavesResponse
from .types.pronunciation_dict import PronunciationDict
from .types.pronunciation_item import PronunciationItem
from .types.synthesize_lightning_large_waves_request_output_format import (
@@ -23,14 +27,6 @@
from .types.synthesize_sse_lightning_v2waves_request_output_format import (
SynthesizeSseLightningV2WavesRequestOutputFormat,
)
-from .types.transcribe_pulse_waves_request_capitalize import TranscribePulseWavesRequestCapitalize
-from .types.transcribe_pulse_waves_request_emotion_detection import TranscribePulseWavesRequestEmotionDetection
-from .types.transcribe_pulse_waves_request_encoding import TranscribePulseWavesRequestEncoding
-from .types.transcribe_pulse_waves_request_format import TranscribePulseWavesRequestFormat
-from .types.transcribe_pulse_waves_request_gender_detection import TranscribePulseWavesRequestGenderDetection
-from .types.transcribe_pulse_waves_request_language import TranscribePulseWavesRequestLanguage
-from .types.transcribe_pulse_waves_request_punctuate import TranscribePulseWavesRequestPunctuate
-from .types.transcribe_pulse_waves_response import TranscribePulseWavesResponse
from .types.tts_request_language import TtsRequestLanguage
from .types.tts_request_model import TtsRequestModel
from .types.tts_request_number_pronunciation_language import TtsRequestNumberPronunciationLanguage
@@ -38,8 +34,9 @@
from .types.update_pronunciation_dict_response import UpdatePronunciationDictResponse
if typing.TYPE_CHECKING:
- from .pulse_stt_streaming.client import AsyncPulseSttStreamingClient, PulseSttStreamingClient
+ from .electron.client import AsyncElectronClient, ElectronClient
from .speech_to_speech.client import AsyncSpeechToSpeechClient, SpeechToSpeechClient
+ from .speech_to_text.client import AsyncSpeechToTextClient, SpeechToTextClient
from .streaming_tts.client import AsyncStreamingTtsClient, StreamingTtsClient
from .tts.client import AsyncTtsClient, TtsClient
# this is used as the default value for optional parameters
@@ -50,9 +47,10 @@ class WavesClient:
def __init__(self, *, client_wrapper: SyncClientWrapper):
self._raw_client = RawWavesClient(client_wrapper=client_wrapper)
self._client_wrapper = client_wrapper
+ self._speech_to_text: typing.Optional[SpeechToTextClient] = None
+ self._electron: typing.Optional[ElectronClient] = None
self._tts: typing.Optional[TtsClient] = None
self._streaming_tts: typing.Optional[StreamingTtsClient] = None
- self._pulse_stt_streaming: typing.Optional[PulseSttStreamingClient] = None
self._speech_to_speech: typing.Optional[SpeechToSpeechClient] = None
@property
@@ -892,190 +890,131 @@ def synthesize_sse_tts(
) as r:
yield from r.data
- def transcribe_pulse(
+ def list_voice_clones(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> ListVoiceClonesWavesResponse:
+ """
+ Retrieve all voice clones in your organization.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ ListVoiceClonesWavesResponse
+ List of voice clones.
+
+ Examples
+ --------
+ from smallestai import SmallestAI
+
+ client = SmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+ client.waves.list_voice_clones()
+ """
+ _response = self._raw_client.list_voice_clones(request_options=request_options)
+ return _response.data
+
+ def create_voice_clone(
self,
*,
- request: typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]],
- language: typing.Optional[TranscribePulseWavesRequestLanguage] = None,
- encoding: typing.Optional[TranscribePulseWavesRequestEncoding] = None,
- webhook_url: typing.Optional[str] = None,
- webhook_extra: typing.Optional[str] = None,
- word_timestamps: typing.Optional[bool] = None,
- diarize: typing.Optional[bool] = None,
- gender_detection: typing.Optional[TranscribePulseWavesRequestGenderDetection] = None,
- emotion_detection: typing.Optional[TranscribePulseWavesRequestEmotionDetection] = None,
- format: typing.Optional[TranscribePulseWavesRequestFormat] = None,
- punctuate: typing.Optional[TranscribePulseWavesRequestPunctuate] = None,
- capitalize: typing.Optional[TranscribePulseWavesRequestCapitalize] = None,
+ display_name: str,
+ file: core.File,
+ description: typing.Optional[str] = OMIT,
+ accent: typing.Optional[str] = OMIT,
+ tags: typing.Optional[str] = OMIT,
+ language: typing.Optional[str] = OMIT,
+ model: typing.Optional[CreateVoiceCloneWavesRequestModel] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
- ) -> TranscribePulseWavesResponse:
+ ) -> CreateVoiceCloneWavesResponse:
"""
- Transcribe an audio file to text using the Pulse model. The fastest way to get a transcript when you already have a recording — pass either the raw bytes or a URL.
-
- ## When to use this
-
- Use this endpoint when you have a complete audio file (call recording, voicemail, podcast episode) and want the transcript back in one response. For live transcription as audio arrives, use the realtime WebSocket endpoint (`WSS /waves/v1/pulse/get_text`) instead.
-
- ## Input methods
-
- Send the audio in one of two ways:
-
- 1. **Raw bytes** — `Content-Type: application/octet-stream` with the audio in the body. All knobs (`language`, `word_timestamps`, etc.) are query parameters.
- 2. **URL** — `Content-Type: application/json` with `{"url": "..."}` in the body. Useful when the audio already lives in object storage. Same query parameters apply.
-
- Pulse autodetects the language across 30+ supported locales. Pass `language` explicitly when you already know it — detection is fast but skipping it is faster.
-
- ## Examples
-
- **cURL** (raw bytes)
- ```bash
- curl -X POST "https://api.smallest.ai/waves/v1/pulse/get_text?language=en&word_timestamps=true" \\
- -H "Authorization: Bearer $SMALLEST_API_KEY" \\
- -H "Content-Type: application/octet-stream" \\
- --data-binary "@./call.wav"
- ```
-
- **cURL** (URL)
- ```bash
- curl -X POST "https://api.smallest.ai/waves/v1/pulse/get_text?language=en" \\
- -H "Authorization: Bearer $SMALLEST_API_KEY" \\
- -H "Content-Type: application/json" \\
- -d '{"url": "https://your-bucket.s3.amazonaws.com/call.wav"}'
- ```
-
- **Python** (`pip install smallestai>=4.4.0`)
- ```python
- from smallestai import SmallestAI
-
- client = SmallestAI(api_key="YOUR_API_KEY")
- with open("./call.wav", "rb") as f:
- result = client.waves.transcribe_pulse(
- request=f.read(),
- language="en",
- word_timestamps=True,
- diarize=True,
- )
- print(result.status) # "success"
- print(result.transcription) # the transcript string
- ```
-
- **JavaScript / TypeScript** (using `fetch`)
- ```typescript
- import { readFileSync } from "node:fs";
-
- const audio = readFileSync("./call.wav");
- const params = new URLSearchParams({ language: "en", word_timestamps: "true", diarize: "true" });
-
- const res = await fetch(`https://api.smallest.ai/waves/v1/pulse/get_text?${params}`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${process.env.SMALLEST_API_KEY}`,
- "Content-Type": "application/octet-stream",
- },
- body: audio,
- });
- const result = await res.json();
- console.log(result.transcription);
- ```
-
- ## Common gotchas
-
- - **Max file size is 250 MB.** Larger files return HTTP `400` with `{errors: "Audio data too large", status: "error", message: "Error handling audio data"}`. Compress to mono 16 kHz PCM if you're close to the limit; quality is unaffected.
- - **Formatting flags (`format`, `punctuate`, `capitalize`)** are accepted at the wire level and exposed in the Python SDK as of `smallestai>=4.4.0`. Today they currently return the same transcript regardless of value — pass them in your integration so it works as the behavior changes.
- - **Webhook-driven flow**: pass `webhook_url` to receive the transcript asynchronously. The endpoint returns immediately; the transcript hits your webhook when ready. Useful for long files where you don't want to hold an HTTP connection open.
- - **Speaker diarization** (`diarize=true`) adds latency. Skip it if you only need the words.
- - **JavaScript / TypeScript**: the official `smallestai` npm package predates the Pulse model, so call this endpoint with `fetch` or `axios` as shown above.
-
+ Create an instant voice clone in a single call. Defaults to `lightning-v3.1`.
+
Parameters
----------
- request : typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]
-
- language : typing.Optional[TranscribePulseWavesRequestLanguage]
- Language of the audio file. Set explicitly to the known language for best accuracy.
-
- **26 single-language codes** on this endpoint: `en`, `hi`, `de`, `es`, `ru`, `it`, `fr`, `nl`, `pt`, `uk`, `pl`, `cs`, `sk`, `lv`, `et`, `ro`, `fi`, `sv`, `bg`, `hu`, `da`, `lt`, `mt`, `zh`, `ja`, `ko`.
-
- **Regional auto-detect aggregators** for unknown audio:
- - `multi-eu` (default) — auto-detects across all 21 European codes above plus `en`.
- - `multi-asian` — auto-detects across `zh`, `ko`, `ja`, `en`.
- - `multi-indic`: auto-detects across `en`, `hi`, `gu`, `mr`, `bn`, `or`. India region only.
-
- Omitting `language` routes to `multi-eu`. See the [Pulse model card](/models/model-cards/speech-to-text/pulse) for the full table.
-
- encoding : typing.Optional[TranscribePulseWavesRequestEncoding]
- Audio encoding of the bytes you upload. Mirrors the `encoding`
- parameter on the realtime WS endpoint.
-
- - `linear16`, `linear32` — raw PCM (16-bit and 32-bit)
- - `alaw`, `mulaw` — 8 kHz telephony codecs
- - `opus`, `ogg_opus` — Opus compressed audio (raw and Ogg container)
-
- When omitted, the server detects the format from the file's
- container header (works for `.wav`, `.mp3`, `.flac`, `.ogg`,
- `.m4a`, `.webm`).
-
- webhook_url : typing.Optional[str]
-
- webhook_extra : typing.Optional[str]
-
- word_timestamps : typing.Optional[bool]
- Whether to include word and utterance level timestamps in the response
-
- diarize : typing.Optional[bool]
- Whether to perform speaker diarization
-
- gender_detection : typing.Optional[TranscribePulseWavesRequestGenderDetection]
- Whether to predict the gender of the speaker
-
- emotion_detection : typing.Optional[TranscribePulseWavesRequestEmotionDetection]
- Whether to predict speaker emotions
-
- format : typing.Optional[TranscribePulseWavesRequestFormat]
- Master formatting switch for the transcript. When `false`, forces
- `punctuate=false`, `capitalize=false`, and also disables Inverse
- Text Normalization (ITN) so it cannot silently reintroduce
- punctuation or casing.
-
- When `true`, the `punctuate` and `capitalize` params take effect
- independently. Leave `format=true` and use those two to fine-tune.
-
- punctuate : typing.Optional[TranscribePulseWavesRequestPunctuate]
- When `false`, strips end-of-sentence punctuation (`.`, `,`, `?`, `!`)
- from the transcript, `words[].word`, and
- `utterances[].transcript`. Does not affect casing — use
- `capitalize` for that. Overridden to `false` when `format=false`.
-
- capitalize : typing.Optional[TranscribePulseWavesRequestCapitalize]
- When `false`, lowercases the entire transcript output (transcript,
- `words[].word`, and `utterances[].transcript`). Does not affect
- punctuation — use `punctuate` for that. Overridden to `false`
- when `format=false`.
-
+ display_name : str
+ Human-readable name for the voice clone.
+
+ file : core.File
+ See core.File for more documentation
+
+ description : typing.Optional[str]
+ Optional longer description for the voice clone.
+
+ accent : typing.Optional[str]
+ Optional accent tag (e.g. "general", "indian").
+
+ tags : typing.Optional[str]
+ Optional comma-separated list of tags. Server splits on
+ commas and trims whitespace (`"en, tone-test"` → `["en", "tone-test"]`).
+
+ language : typing.Optional[str]
+ Primary language the clone will be used for. Optional, but
+ **strongly recommended** — set it to the language of your
+ reference audio. The TTS request's `language` should also
+ match this code; setting it now avoids silent language
+ mismatches at inference time.
+
+ Must be one of the languages supported by `lightning-v3.1`
+ (e.g. `en`, `hi`). The server validates and rejects
+ unsupported codes with a 400.
+
+ model : typing.Optional[CreateVoiceCloneWavesRequestModel]
+ Voice cloning model. Defaults to `lightning-v3.1`.
+ `lightning-v2` is accepted by the schema for historical
+ reasons but is deprecated — the server returns 400 with
+ `"Voice cloning for lightning-v2 is deprecated. Please use lightning-v3.1"`.
+
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
-
+
Returns
-------
- TranscribePulseWavesResponse
- Speech transcribed successfully
+ CreateVoiceCloneWavesResponse
+ Voice clone created. Includes pre-generated sample clips of the new voice.
+
+ Examples
+ --------
+ from smallestai import SmallestAI
+
+ client = SmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+ client.waves.create_voice_clone(
+ display_name="displayName",
+ )
"""
- _response = self._raw_client.transcribe_pulse(
- request=request,
+ _response = self._raw_client.create_voice_clone(
+ display_name=display_name,
+ file=file,
+ description=description,
+ accent=accent,
+ tags=tags,
language=language,
- encoding=encoding,
- webhook_url=webhook_url,
- webhook_extra=webhook_extra,
- word_timestamps=word_timestamps,
- diarize=diarize,
- gender_detection=gender_detection,
- emotion_detection=emotion_detection,
- format=format,
- punctuate=punctuate,
- capitalize=capitalize,
+ model=model,
request_options=request_options,
)
return _response.data
+ @property
+ def speech_to_text(self):
+ if self._speech_to_text is None:
+ from .speech_to_text.client import SpeechToTextClient # noqa: E402
+
+ self._speech_to_text = SpeechToTextClient(client_wrapper=self._client_wrapper)
+ return self._speech_to_text
+
+ @property
+ def electron(self):
+ if self._electron is None:
+ from .electron.client import ElectronClient # noqa: E402
+
+ self._electron = ElectronClient(client_wrapper=self._client_wrapper)
+ return self._electron
+
@property
def tts(self):
if self._tts is None:
@@ -1092,14 +1031,6 @@ def streaming_tts(self):
self._streaming_tts = StreamingTtsClient(client_wrapper=self._client_wrapper)
return self._streaming_tts
- @property
- def pulse_stt_streaming(self):
- if self._pulse_stt_streaming is None:
- from .pulse_stt_streaming.client import PulseSttStreamingClient # noqa: E402
-
- self._pulse_stt_streaming = PulseSttStreamingClient(client_wrapper=self._client_wrapper)
- return self._pulse_stt_streaming
-
@property
def speech_to_speech(self):
if self._speech_to_speech is None:
@@ -1113,9 +1044,10 @@ class AsyncWavesClient:
def __init__(self, *, client_wrapper: AsyncClientWrapper):
self._raw_client = AsyncRawWavesClient(client_wrapper=client_wrapper)
self._client_wrapper = client_wrapper
+ self._speech_to_text: typing.Optional[AsyncSpeechToTextClient] = None
+ self._electron: typing.Optional[AsyncElectronClient] = None
self._tts: typing.Optional[AsyncTtsClient] = None
self._streaming_tts: typing.Optional[AsyncStreamingTtsClient] = None
- self._pulse_stt_streaming: typing.Optional[AsyncPulseSttStreamingClient] = None
self._speech_to_speech: typing.Optional[AsyncSpeechToSpeechClient] = None
@property
@@ -2057,190 +1989,147 @@ async def main() -> None:
async for _chunk in r.data:
yield _chunk
- async def transcribe_pulse(
+ async def list_voice_clones(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> ListVoiceClonesWavesResponse:
+ """
+ Retrieve all voice clones in your organization.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ ListVoiceClonesWavesResponse
+ List of voice clones.
+
+ Examples
+ --------
+ import asyncio
+
+ from smallestai import AsyncSmallestAI
+
+ client = AsyncSmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+
+
+ async def main() -> None:
+ await client.waves.list_voice_clones()
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.list_voice_clones(request_options=request_options)
+ return _response.data
+
+ async def create_voice_clone(
self,
*,
- request: typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]],
- language: typing.Optional[TranscribePulseWavesRequestLanguage] = None,
- encoding: typing.Optional[TranscribePulseWavesRequestEncoding] = None,
- webhook_url: typing.Optional[str] = None,
- webhook_extra: typing.Optional[str] = None,
- word_timestamps: typing.Optional[bool] = None,
- diarize: typing.Optional[bool] = None,
- gender_detection: typing.Optional[TranscribePulseWavesRequestGenderDetection] = None,
- emotion_detection: typing.Optional[TranscribePulseWavesRequestEmotionDetection] = None,
- format: typing.Optional[TranscribePulseWavesRequestFormat] = None,
- punctuate: typing.Optional[TranscribePulseWavesRequestPunctuate] = None,
- capitalize: typing.Optional[TranscribePulseWavesRequestCapitalize] = None,
+ display_name: str,
+ file: core.File,
+ description: typing.Optional[str] = OMIT,
+ accent: typing.Optional[str] = OMIT,
+ tags: typing.Optional[str] = OMIT,
+ language: typing.Optional[str] = OMIT,
+ model: typing.Optional[CreateVoiceCloneWavesRequestModel] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
- ) -> TranscribePulseWavesResponse:
+ ) -> CreateVoiceCloneWavesResponse:
"""
- Transcribe an audio file to text using the Pulse model. The fastest way to get a transcript when you already have a recording — pass either the raw bytes or a URL.
-
- ## When to use this
-
- Use this endpoint when you have a complete audio file (call recording, voicemail, podcast episode) and want the transcript back in one response. For live transcription as audio arrives, use the realtime WebSocket endpoint (`WSS /waves/v1/pulse/get_text`) instead.
-
- ## Input methods
-
- Send the audio in one of two ways:
-
- 1. **Raw bytes** — `Content-Type: application/octet-stream` with the audio in the body. All knobs (`language`, `word_timestamps`, etc.) are query parameters.
- 2. **URL** — `Content-Type: application/json` with `{"url": "..."}` in the body. Useful when the audio already lives in object storage. Same query parameters apply.
-
- Pulse autodetects the language across 30+ supported locales. Pass `language` explicitly when you already know it — detection is fast but skipping it is faster.
-
- ## Examples
-
- **cURL** (raw bytes)
- ```bash
- curl -X POST "https://api.smallest.ai/waves/v1/pulse/get_text?language=en&word_timestamps=true" \\
- -H "Authorization: Bearer $SMALLEST_API_KEY" \\
- -H "Content-Type: application/octet-stream" \\
- --data-binary "@./call.wav"
- ```
-
- **cURL** (URL)
- ```bash
- curl -X POST "https://api.smallest.ai/waves/v1/pulse/get_text?language=en" \\
- -H "Authorization: Bearer $SMALLEST_API_KEY" \\
- -H "Content-Type: application/json" \\
- -d '{"url": "https://your-bucket.s3.amazonaws.com/call.wav"}'
- ```
-
- **Python** (`pip install smallestai>=4.4.0`)
- ```python
- from smallestai import SmallestAI
-
- client = SmallestAI(api_key="YOUR_API_KEY")
- with open("./call.wav", "rb") as f:
- result = client.waves.transcribe_pulse(
- request=f.read(),
- language="en",
- word_timestamps=True,
- diarize=True,
- )
- print(result.status) # "success"
- print(result.transcription) # the transcript string
- ```
-
- **JavaScript / TypeScript** (using `fetch`)
- ```typescript
- import { readFileSync } from "node:fs";
-
- const audio = readFileSync("./call.wav");
- const params = new URLSearchParams({ language: "en", word_timestamps: "true", diarize: "true" });
-
- const res = await fetch(`https://api.smallest.ai/waves/v1/pulse/get_text?${params}`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${process.env.SMALLEST_API_KEY}`,
- "Content-Type": "application/octet-stream",
- },
- body: audio,
- });
- const result = await res.json();
- console.log(result.transcription);
- ```
-
- ## Common gotchas
-
- - **Max file size is 250 MB.** Larger files return HTTP `400` with `{errors: "Audio data too large", status: "error", message: "Error handling audio data"}`. Compress to mono 16 kHz PCM if you're close to the limit; quality is unaffected.
- - **Formatting flags (`format`, `punctuate`, `capitalize`)** are accepted at the wire level and exposed in the Python SDK as of `smallestai>=4.4.0`. Today they currently return the same transcript regardless of value — pass them in your integration so it works as the behavior changes.
- - **Webhook-driven flow**: pass `webhook_url` to receive the transcript asynchronously. The endpoint returns immediately; the transcript hits your webhook when ready. Useful for long files where you don't want to hold an HTTP connection open.
- - **Speaker diarization** (`diarize=true`) adds latency. Skip it if you only need the words.
- - **JavaScript / TypeScript**: the official `smallestai` npm package predates the Pulse model, so call this endpoint with `fetch` or `axios` as shown above.
-
+ Create an instant voice clone in a single call. Defaults to `lightning-v3.1`.
+
Parameters
----------
- request : typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]
-
- language : typing.Optional[TranscribePulseWavesRequestLanguage]
- Language of the audio file. Set explicitly to the known language for best accuracy.
-
- **26 single-language codes** on this endpoint: `en`, `hi`, `de`, `es`, `ru`, `it`, `fr`, `nl`, `pt`, `uk`, `pl`, `cs`, `sk`, `lv`, `et`, `ro`, `fi`, `sv`, `bg`, `hu`, `da`, `lt`, `mt`, `zh`, `ja`, `ko`.
-
- **Regional auto-detect aggregators** for unknown audio:
- - `multi-eu` (default) — auto-detects across all 21 European codes above plus `en`.
- - `multi-asian` — auto-detects across `zh`, `ko`, `ja`, `en`.
- - `multi-indic`: auto-detects across `en`, `hi`, `gu`, `mr`, `bn`, `or`. India region only.
-
- Omitting `language` routes to `multi-eu`. See the [Pulse model card](/models/model-cards/speech-to-text/pulse) for the full table.
-
- encoding : typing.Optional[TranscribePulseWavesRequestEncoding]
- Audio encoding of the bytes you upload. Mirrors the `encoding`
- parameter on the realtime WS endpoint.
-
- - `linear16`, `linear32` — raw PCM (16-bit and 32-bit)
- - `alaw`, `mulaw` — 8 kHz telephony codecs
- - `opus`, `ogg_opus` — Opus compressed audio (raw and Ogg container)
-
- When omitted, the server detects the format from the file's
- container header (works for `.wav`, `.mp3`, `.flac`, `.ogg`,
- `.m4a`, `.webm`).
-
- webhook_url : typing.Optional[str]
-
- webhook_extra : typing.Optional[str]
-
- word_timestamps : typing.Optional[bool]
- Whether to include word and utterance level timestamps in the response
-
- diarize : typing.Optional[bool]
- Whether to perform speaker diarization
-
- gender_detection : typing.Optional[TranscribePulseWavesRequestGenderDetection]
- Whether to predict the gender of the speaker
-
- emotion_detection : typing.Optional[TranscribePulseWavesRequestEmotionDetection]
- Whether to predict speaker emotions
-
- format : typing.Optional[TranscribePulseWavesRequestFormat]
- Master formatting switch for the transcript. When `false`, forces
- `punctuate=false`, `capitalize=false`, and also disables Inverse
- Text Normalization (ITN) so it cannot silently reintroduce
- punctuation or casing.
-
- When `true`, the `punctuate` and `capitalize` params take effect
- independently. Leave `format=true` and use those two to fine-tune.
-
- punctuate : typing.Optional[TranscribePulseWavesRequestPunctuate]
- When `false`, strips end-of-sentence punctuation (`.`, `,`, `?`, `!`)
- from the transcript, `words[].word`, and
- `utterances[].transcript`. Does not affect casing — use
- `capitalize` for that. Overridden to `false` when `format=false`.
-
- capitalize : typing.Optional[TranscribePulseWavesRequestCapitalize]
- When `false`, lowercases the entire transcript output (transcript,
- `words[].word`, and `utterances[].transcript`). Does not affect
- punctuation — use `punctuate` for that. Overridden to `false`
- when `format=false`.
-
+ display_name : str
+ Human-readable name for the voice clone.
+
+ file : core.File
+ See core.File for more documentation
+
+ description : typing.Optional[str]
+ Optional longer description for the voice clone.
+
+ accent : typing.Optional[str]
+ Optional accent tag (e.g. "general", "indian").
+
+ tags : typing.Optional[str]
+ Optional comma-separated list of tags. Server splits on
+ commas and trims whitespace (`"en, tone-test"` → `["en", "tone-test"]`).
+
+ language : typing.Optional[str]
+ Primary language the clone will be used for. Optional, but
+ **strongly recommended** — set it to the language of your
+ reference audio. The TTS request's `language` should also
+ match this code; setting it now avoids silent language
+ mismatches at inference time.
+
+ Must be one of the languages supported by `lightning-v3.1`
+ (e.g. `en`, `hi`). The server validates and rejects
+ unsupported codes with a 400.
+
+ model : typing.Optional[CreateVoiceCloneWavesRequestModel]
+ Voice cloning model. Defaults to `lightning-v3.1`.
+ `lightning-v2` is accepted by the schema for historical
+ reasons but is deprecated — the server returns 400 with
+ `"Voice cloning for lightning-v2 is deprecated. Please use lightning-v3.1"`.
+
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
-
+
Returns
-------
- TranscribePulseWavesResponse
- Speech transcribed successfully
+ CreateVoiceCloneWavesResponse
+ Voice clone created. Includes pre-generated sample clips of the new voice.
+
+ Examples
+ --------
+ import asyncio
+
+ from smallestai import AsyncSmallestAI
+
+ client = AsyncSmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+
+
+ async def main() -> None:
+ await client.waves.create_voice_clone(
+ display_name="displayName",
+ )
+
+
+ asyncio.run(main())
"""
- _response = await self._raw_client.transcribe_pulse(
- request=request,
+ _response = await self._raw_client.create_voice_clone(
+ display_name=display_name,
+ file=file,
+ description=description,
+ accent=accent,
+ tags=tags,
language=language,
- encoding=encoding,
- webhook_url=webhook_url,
- webhook_extra=webhook_extra,
- word_timestamps=word_timestamps,
- diarize=diarize,
- gender_detection=gender_detection,
- emotion_detection=emotion_detection,
- format=format,
- punctuate=punctuate,
- capitalize=capitalize,
+ model=model,
request_options=request_options,
)
return _response.data
+ @property
+ def speech_to_text(self):
+ if self._speech_to_text is None:
+ from .speech_to_text.client import AsyncSpeechToTextClient # noqa: E402
+
+ self._speech_to_text = AsyncSpeechToTextClient(client_wrapper=self._client_wrapper)
+ return self._speech_to_text
+
+ @property
+ def electron(self):
+ if self._electron is None:
+ from .electron.client import AsyncElectronClient # noqa: E402
+
+ self._electron = AsyncElectronClient(client_wrapper=self._client_wrapper)
+ return self._electron
+
@property
def tts(self):
if self._tts is None:
@@ -2257,14 +2146,6 @@ def streaming_tts(self):
self._streaming_tts = AsyncStreamingTtsClient(client_wrapper=self._client_wrapper)
return self._streaming_tts
- @property
- def pulse_stt_streaming(self):
- if self._pulse_stt_streaming is None:
- from .pulse_stt_streaming.client import AsyncPulseSttStreamingClient # noqa: E402
-
- self._pulse_stt_streaming = AsyncPulseSttStreamingClient(client_wrapper=self._client_wrapper)
- return self._pulse_stt_streaming
-
@property
def speech_to_speech(self):
if self._speech_to_speech is None:
diff --git a/src/smallestai/waves/electron/__init__.py b/src/smallestai/waves/electron/__init__.py
new file mode 100644
index 00000000..4dc8793a
--- /dev/null
+++ b/src/smallestai/waves/electron/__init__.py
@@ -0,0 +1,64 @@
+# This file was auto-generated by Fern from our API Definition.
+
+# isort: skip_file
+
+import typing
+from importlib import import_module
+
+if typing.TYPE_CHECKING:
+ from .types import (
+ ChatCompletionRequestResponseFormat,
+ ChatCompletionRequestResponseFormatType,
+ ChatCompletionRequestStop,
+ ChatCompletionRequestStreamOptions,
+ ChatCompletionRequestToolChoice,
+ ChatCompletionRequestToolChoiceFunction,
+ ChatCompletionRequestToolChoiceFunctionFunction,
+ ChatCompletionRequestToolChoiceFunctionType,
+ ChatCompletionRequestToolChoiceZero,
+ )
+_dynamic_imports: typing.Dict[str, str] = {
+ "ChatCompletionRequestResponseFormat": ".types",
+ "ChatCompletionRequestResponseFormatType": ".types",
+ "ChatCompletionRequestStop": ".types",
+ "ChatCompletionRequestStreamOptions": ".types",
+ "ChatCompletionRequestToolChoice": ".types",
+ "ChatCompletionRequestToolChoiceFunction": ".types",
+ "ChatCompletionRequestToolChoiceFunctionFunction": ".types",
+ "ChatCompletionRequestToolChoiceFunctionType": ".types",
+ "ChatCompletionRequestToolChoiceZero": ".types",
+}
+
+
+def __getattr__(attr_name: str) -> typing.Any:
+ module_name = _dynamic_imports.get(attr_name)
+ if module_name is None:
+ raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}")
+ try:
+ module = import_module(module_name, __package__)
+ if module_name == f".{attr_name}":
+ return module
+ else:
+ return getattr(module, attr_name)
+ except ImportError as e:
+ raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e
+ except AttributeError as e:
+ raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e
+
+
+def __dir__():
+ lazy_attrs = list(_dynamic_imports.keys())
+ return sorted(lazy_attrs)
+
+
+__all__ = [
+ "ChatCompletionRequestResponseFormat",
+ "ChatCompletionRequestResponseFormatType",
+ "ChatCompletionRequestStop",
+ "ChatCompletionRequestStreamOptions",
+ "ChatCompletionRequestToolChoice",
+ "ChatCompletionRequestToolChoiceFunction",
+ "ChatCompletionRequestToolChoiceFunctionFunction",
+ "ChatCompletionRequestToolChoiceFunctionType",
+ "ChatCompletionRequestToolChoiceZero",
+]
diff --git a/src/smallestai/waves/electron/client.py b/src/smallestai/waves/electron/client.py
new file mode 100644
index 00000000..38223f28
--- /dev/null
+++ b/src/smallestai/waves/electron/client.py
@@ -0,0 +1,504 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ...core.request_options import RequestOptions
+from ..types.chat_completion import ChatCompletion
+from ..types.electron_message import ElectronMessage
+from .raw_client import AsyncRawElectronClient, RawElectronClient
+from .types.chat_completion_request_response_format import ChatCompletionRequestResponseFormat
+from .types.chat_completion_request_stop import ChatCompletionRequestStop
+from .types.chat_completion_request_stream_options import ChatCompletionRequestStreamOptions
+from .types.chat_completion_request_tool_choice import ChatCompletionRequestToolChoice
+
+# this is used as the default value for optional parameters
+OMIT = typing.cast(typing.Any, ...)
+
+
+class ElectronClient:
+ def __init__(self, *, client_wrapper: SyncClientWrapper):
+ self._raw_client = RawElectronClient(client_wrapper=client_wrapper)
+
+ @property
+ def with_raw_response(self) -> RawElectronClient:
+ """
+ Retrieves a raw implementation of this client that returns raw responses.
+
+ Returns
+ -------
+ RawElectronClient
+ """
+ return self._raw_client
+
+ def complete(
+ self,
+ *,
+ model: str,
+ messages: typing.Sequence[ElectronMessage],
+ temperature: typing.Optional[float] = OMIT,
+ top_p: typing.Optional[float] = OMIT,
+ max_tokens: typing.Optional[int] = OMIT,
+ stream: typing.Optional[bool] = OMIT,
+ stream_options: typing.Optional[ChatCompletionRequestStreamOptions] = OMIT,
+ tools: typing.Optional[typing.Sequence[typing.Dict[str, typing.Any]]] = OMIT,
+ tool_choice: typing.Optional[ChatCompletionRequestToolChoice] = OMIT,
+ response_format: typing.Optional[ChatCompletionRequestResponseFormat] = OMIT,
+ stop: typing.Optional[ChatCompletionRequestStop] = OMIT,
+ seed: typing.Optional[int] = OMIT,
+ logit_bias: typing.Optional[typing.Dict[str, float]] = OMIT,
+ logprobs: typing.Optional[bool] = OMIT,
+ top_logprobs: typing.Optional[int] = OMIT,
+ presence_penalty: typing.Optional[float] = OMIT,
+ frequency_penalty: typing.Optional[float] = OMIT,
+ user: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> ChatCompletion:
+ """
+ Generate a chat completion with Electron. OpenAI-compatible
+ request/response shape — point any OpenAI SDK at
+ `https://api.smallest.ai/waves/v1` and it just works.
+
+ Set `stream: true` to receive tokens via Server-Sent Events. With
+ `stream_options: { include_usage: true }`, the final SSE chunk
+ carries the `usage` block so token accounting is exact even on
+ client disconnects.
+
+ Tool calling follows OpenAI's `tools` array convention. When you
+ provide a voice-agent-style system prompt, Electron emits a short
+ filler phrase in the assistant message `content` field alongside
+ `tool_calls` — see the [Tool Calling guide](/models/documentation/llm-electron/tool-function-calling)
+ for the voice-agent pattern.
+
+ ## Examples
+
+ **cURL**
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/chat/completions" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/json" \\
+ -d '{
+ "model": "electron",
+ "messages": [
+ {"role": "user", "content": "Write one sentence about why the sky is blue."}
+ ]
+ }'
+ ```
+
+ **Python** (`pip install openai`)
+ ```python
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(
+ base_url="https://api.smallest.ai/waves/v1",
+ api_key=os.environ["SMALLEST_API_KEY"],
+ )
+
+ response = client.chat.completions.create(
+ model="electron",
+ messages=[
+ {"role": "user", "content": "Write one sentence about why the sky is blue."}
+ ],
+ )
+
+ print(response.choices[0].message.content)
+ ```
+
+ **JavaScript / TypeScript** (`npm install openai`)
+ ```typescript
+ import OpenAI from "openai";
+
+ const client = new OpenAI({
+ baseURL: "https://api.smallest.ai/waves/v1",
+ apiKey: process.env.SMALLEST_API_KEY,
+ });
+
+ const response = await client.chat.completions.create({
+ model: "electron",
+ messages: [
+ { role: "user", content: "Write one sentence about why the sky is blue." },
+ ],
+ });
+
+ console.log(response.choices[0].message.content);
+ ```
+
+ **Streaming with usage** (Python)
+ ```python
+ stream = client.chat.completions.create(
+ model="electron",
+ messages=[{"role": "user", "content": "Tell me a one-sentence fun fact."}],
+ stream=True,
+ stream_options={"include_usage": True},
+ )
+ for chunk in stream:
+ if chunk.choices and chunk.choices[0].delta.content:
+ print(chunk.choices[0].delta.content, end="", flush=True)
+ if chunk.usage:
+ print(f"\\n\\nTokens: {chunk.usage.total_tokens}")
+ ```
+
+ ## Common gotchas
+
+ - **Base URL is `/waves/v1`**, not `/v1`. The OpenAI SDK appends `/chat/completions` for you.
+ - **`stream_options.include_usage: true`** is required for exact token accounting on streaming calls — the final SSE chunk carries the `usage` block.
+ - **`n > 1` and `prompt_logprobs` are rejected.** Use multiple requests if you need parallel completions.
+ - **Auth header is `Authorization: Bearer $SMALLEST_API_KEY`** — get the key from the [Smallest AI Console](https://app.smallest.ai/dashboard/api-keys).
+
+ Parameters
+ ----------
+ model : str
+ Model ID. Currently only `"electron"`.
+
+ messages : typing.Sequence[ElectronMessage]
+ Chat history. Standard OpenAI message array.
+
+ temperature : typing.Optional[float]
+ Sampling temperature.
+
+ top_p : typing.Optional[float]
+ Nucleus sampling.
+
+ max_tokens : typing.Optional[int]
+ Maximum output tokens. Combined input + output context ceiling
+ is 32,768.
+
+ stream : typing.Optional[bool]
+ When true, response is `text/event-stream`. See the
+ [Streaming guide](/models/documentation/llm-electron/streaming).
+
+ stream_options : typing.Optional[ChatCompletionRequestStreamOptions]
+
+ tools : typing.Optional[typing.Sequence[typing.Dict[str, typing.Any]]]
+ Tool / function calling definitions. Forwarded verbatim to the
+ OpenAI-compatible upstream, so the standard OpenAI shape
+ (`{type: "function", function: {name, description, parameters}}`)
+ is the recommended form and is what the examples below use.
+ The wire schema is permissive (`array`) — any tools payload
+ the upstream accepts will work. See [Tool Calling](/models/documentation/llm-electron/tool-function-calling)
+ for details.
+
+ tool_choice : typing.Optional[ChatCompletionRequestToolChoice]
+
+ response_format : typing.Optional[ChatCompletionRequestResponseFormat]
+ Output shape. `{type: "text"}` (default) or `{type: "json_object"}`.
+
+ stop : typing.Optional[ChatCompletionRequestStop]
+
+ seed : typing.Optional[int]
+ Best-effort determinism.
+
+ logit_bias : typing.Optional[typing.Dict[str, float]]
+
+ logprobs : typing.Optional[bool]
+
+ top_logprobs : typing.Optional[int]
+
+ presence_penalty : typing.Optional[float]
+
+ frequency_penalty : typing.Optional[float]
+
+ user : typing.Optional[str]
+ Opaque end-user identifier. Not interpreted by Electron.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ ChatCompletion
+ Non-streaming: standard OpenAI `chat.completion` object.
+
+ Streaming (`stream: true`): `text/event-stream` SSE — each
+ event is a `chat.completion.chunk` delta, terminated by
+ `data: [DONE]`.
+
+ Examples
+ --------
+ from smallestai import SmallestAI
+ from smallestai.waves import ElectronMessage
+
+ client = SmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+ client.waves.electron.complete(
+ model="electron",
+ messages=[
+ ElectronMessage(
+ role="user",
+ content="Hello!",
+ )
+ ],
+ )
+ """
+ _response = self._raw_client.complete(
+ model=model,
+ messages=messages,
+ temperature=temperature,
+ top_p=top_p,
+ max_tokens=max_tokens,
+ stream=stream,
+ stream_options=stream_options,
+ tools=tools,
+ tool_choice=tool_choice,
+ response_format=response_format,
+ stop=stop,
+ seed=seed,
+ logit_bias=logit_bias,
+ logprobs=logprobs,
+ top_logprobs=top_logprobs,
+ presence_penalty=presence_penalty,
+ frequency_penalty=frequency_penalty,
+ user=user,
+ request_options=request_options,
+ )
+ return _response.data
+
+
+class AsyncElectronClient:
+ def __init__(self, *, client_wrapper: AsyncClientWrapper):
+ self._raw_client = AsyncRawElectronClient(client_wrapper=client_wrapper)
+
+ @property
+ def with_raw_response(self) -> AsyncRawElectronClient:
+ """
+ Retrieves a raw implementation of this client that returns raw responses.
+
+ Returns
+ -------
+ AsyncRawElectronClient
+ """
+ return self._raw_client
+
+ async def complete(
+ self,
+ *,
+ model: str,
+ messages: typing.Sequence[ElectronMessage],
+ temperature: typing.Optional[float] = OMIT,
+ top_p: typing.Optional[float] = OMIT,
+ max_tokens: typing.Optional[int] = OMIT,
+ stream: typing.Optional[bool] = OMIT,
+ stream_options: typing.Optional[ChatCompletionRequestStreamOptions] = OMIT,
+ tools: typing.Optional[typing.Sequence[typing.Dict[str, typing.Any]]] = OMIT,
+ tool_choice: typing.Optional[ChatCompletionRequestToolChoice] = OMIT,
+ response_format: typing.Optional[ChatCompletionRequestResponseFormat] = OMIT,
+ stop: typing.Optional[ChatCompletionRequestStop] = OMIT,
+ seed: typing.Optional[int] = OMIT,
+ logit_bias: typing.Optional[typing.Dict[str, float]] = OMIT,
+ logprobs: typing.Optional[bool] = OMIT,
+ top_logprobs: typing.Optional[int] = OMIT,
+ presence_penalty: typing.Optional[float] = OMIT,
+ frequency_penalty: typing.Optional[float] = OMIT,
+ user: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> ChatCompletion:
+ """
+ Generate a chat completion with Electron. OpenAI-compatible
+ request/response shape — point any OpenAI SDK at
+ `https://api.smallest.ai/waves/v1` and it just works.
+
+ Set `stream: true` to receive tokens via Server-Sent Events. With
+ `stream_options: { include_usage: true }`, the final SSE chunk
+ carries the `usage` block so token accounting is exact even on
+ client disconnects.
+
+ Tool calling follows OpenAI's `tools` array convention. When you
+ provide a voice-agent-style system prompt, Electron emits a short
+ filler phrase in the assistant message `content` field alongside
+ `tool_calls` — see the [Tool Calling guide](/models/documentation/llm-electron/tool-function-calling)
+ for the voice-agent pattern.
+
+ ## Examples
+
+ **cURL**
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/chat/completions" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/json" \\
+ -d '{
+ "model": "electron",
+ "messages": [
+ {"role": "user", "content": "Write one sentence about why the sky is blue."}
+ ]
+ }'
+ ```
+
+ **Python** (`pip install openai`)
+ ```python
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(
+ base_url="https://api.smallest.ai/waves/v1",
+ api_key=os.environ["SMALLEST_API_KEY"],
+ )
+
+ response = client.chat.completions.create(
+ model="electron",
+ messages=[
+ {"role": "user", "content": "Write one sentence about why the sky is blue."}
+ ],
+ )
+
+ print(response.choices[0].message.content)
+ ```
+
+ **JavaScript / TypeScript** (`npm install openai`)
+ ```typescript
+ import OpenAI from "openai";
+
+ const client = new OpenAI({
+ baseURL: "https://api.smallest.ai/waves/v1",
+ apiKey: process.env.SMALLEST_API_KEY,
+ });
+
+ const response = await client.chat.completions.create({
+ model: "electron",
+ messages: [
+ { role: "user", content: "Write one sentence about why the sky is blue." },
+ ],
+ });
+
+ console.log(response.choices[0].message.content);
+ ```
+
+ **Streaming with usage** (Python)
+ ```python
+ stream = client.chat.completions.create(
+ model="electron",
+ messages=[{"role": "user", "content": "Tell me a one-sentence fun fact."}],
+ stream=True,
+ stream_options={"include_usage": True},
+ )
+ for chunk in stream:
+ if chunk.choices and chunk.choices[0].delta.content:
+ print(chunk.choices[0].delta.content, end="", flush=True)
+ if chunk.usage:
+ print(f"\\n\\nTokens: {chunk.usage.total_tokens}")
+ ```
+
+ ## Common gotchas
+
+ - **Base URL is `/waves/v1`**, not `/v1`. The OpenAI SDK appends `/chat/completions` for you.
+ - **`stream_options.include_usage: true`** is required for exact token accounting on streaming calls — the final SSE chunk carries the `usage` block.
+ - **`n > 1` and `prompt_logprobs` are rejected.** Use multiple requests if you need parallel completions.
+ - **Auth header is `Authorization: Bearer $SMALLEST_API_KEY`** — get the key from the [Smallest AI Console](https://app.smallest.ai/dashboard/api-keys).
+
+ Parameters
+ ----------
+ model : str
+ Model ID. Currently only `"electron"`.
+
+ messages : typing.Sequence[ElectronMessage]
+ Chat history. Standard OpenAI message array.
+
+ temperature : typing.Optional[float]
+ Sampling temperature.
+
+ top_p : typing.Optional[float]
+ Nucleus sampling.
+
+ max_tokens : typing.Optional[int]
+ Maximum output tokens. Combined input + output context ceiling
+ is 32,768.
+
+ stream : typing.Optional[bool]
+ When true, response is `text/event-stream`. See the
+ [Streaming guide](/models/documentation/llm-electron/streaming).
+
+ stream_options : typing.Optional[ChatCompletionRequestStreamOptions]
+
+ tools : typing.Optional[typing.Sequence[typing.Dict[str, typing.Any]]]
+ Tool / function calling definitions. Forwarded verbatim to the
+ OpenAI-compatible upstream, so the standard OpenAI shape
+ (`{type: "function", function: {name, description, parameters}}`)
+ is the recommended form and is what the examples below use.
+ The wire schema is permissive (`array`) — any tools payload
+ the upstream accepts will work. See [Tool Calling](/models/documentation/llm-electron/tool-function-calling)
+ for details.
+
+ tool_choice : typing.Optional[ChatCompletionRequestToolChoice]
+
+ response_format : typing.Optional[ChatCompletionRequestResponseFormat]
+ Output shape. `{type: "text"}` (default) or `{type: "json_object"}`.
+
+ stop : typing.Optional[ChatCompletionRequestStop]
+
+ seed : typing.Optional[int]
+ Best-effort determinism.
+
+ logit_bias : typing.Optional[typing.Dict[str, float]]
+
+ logprobs : typing.Optional[bool]
+
+ top_logprobs : typing.Optional[int]
+
+ presence_penalty : typing.Optional[float]
+
+ frequency_penalty : typing.Optional[float]
+
+ user : typing.Optional[str]
+ Opaque end-user identifier. Not interpreted by Electron.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ ChatCompletion
+ Non-streaming: standard OpenAI `chat.completion` object.
+
+ Streaming (`stream: true`): `text/event-stream` SSE — each
+ event is a `chat.completion.chunk` delta, terminated by
+ `data: [DONE]`.
+
+ Examples
+ --------
+ import asyncio
+
+ from smallestai import AsyncSmallestAI
+ from smallestai.waves import ElectronMessage
+
+ client = AsyncSmallestAI(
+ api_key="YOUR_API_KEY",
+ )
+
+
+ async def main() -> None:
+ await client.waves.electron.complete(
+ model="electron",
+ messages=[
+ ElectronMessage(
+ role="user",
+ content="Hello!",
+ )
+ ],
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.complete(
+ model=model,
+ messages=messages,
+ temperature=temperature,
+ top_p=top_p,
+ max_tokens=max_tokens,
+ stream=stream,
+ stream_options=stream_options,
+ tools=tools,
+ tool_choice=tool_choice,
+ response_format=response_format,
+ stop=stop,
+ seed=seed,
+ logit_bias=logit_bias,
+ logprobs=logprobs,
+ top_logprobs=top_logprobs,
+ presence_penalty=presence_penalty,
+ frequency_penalty=frequency_penalty,
+ user=user,
+ request_options=request_options,
+ )
+ return _response.data
diff --git a/src/smallestai/waves/electron/raw_client.py b/src/smallestai/waves/electron/raw_client.py
new file mode 100644
index 00000000..a18c217c
--- /dev/null
+++ b/src/smallestai/waves/electron/raw_client.py
@@ -0,0 +1,654 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+from json.decoder import JSONDecodeError
+
+from ...core.api_error import ApiError
+from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ...core.http_response import AsyncHttpResponse, HttpResponse
+from ...core.parse_error import ParsingError
+from ...core.request_options import RequestOptions
+from ...core.serialization import convert_and_respect_annotation_metadata
+from ...core.unchecked_base_model import construct_type
+from ..errors.bad_gateway_error import BadGatewayError
+from ..errors.bad_request_error import BadRequestError
+from ..errors.forbidden_error import ForbiddenError
+from ..errors.service_unavailable_error import ServiceUnavailableError
+from ..errors.too_many_requests_error import TooManyRequestsError
+from ..errors.unauthorized_error import UnauthorizedError
+from ..types.chat_completion import ChatCompletion
+from ..types.electron_message import ElectronMessage
+from .types.chat_completion_request_response_format import ChatCompletionRequestResponseFormat
+from .types.chat_completion_request_stop import ChatCompletionRequestStop
+from .types.chat_completion_request_stream_options import ChatCompletionRequestStreamOptions
+from .types.chat_completion_request_tool_choice import ChatCompletionRequestToolChoice
+from pydantic import ValidationError
+
+# this is used as the default value for optional parameters
+OMIT = typing.cast(typing.Any, ...)
+
+
+class RawElectronClient:
+ def __init__(self, *, client_wrapper: SyncClientWrapper):
+ self._client_wrapper = client_wrapper
+
+ def complete(
+ self,
+ *,
+ model: str,
+ messages: typing.Sequence[ElectronMessage],
+ temperature: typing.Optional[float] = OMIT,
+ top_p: typing.Optional[float] = OMIT,
+ max_tokens: typing.Optional[int] = OMIT,
+ stream: typing.Optional[bool] = OMIT,
+ stream_options: typing.Optional[ChatCompletionRequestStreamOptions] = OMIT,
+ tools: typing.Optional[typing.Sequence[typing.Dict[str, typing.Any]]] = OMIT,
+ tool_choice: typing.Optional[ChatCompletionRequestToolChoice] = OMIT,
+ response_format: typing.Optional[ChatCompletionRequestResponseFormat] = OMIT,
+ stop: typing.Optional[ChatCompletionRequestStop] = OMIT,
+ seed: typing.Optional[int] = OMIT,
+ logit_bias: typing.Optional[typing.Dict[str, float]] = OMIT,
+ logprobs: typing.Optional[bool] = OMIT,
+ top_logprobs: typing.Optional[int] = OMIT,
+ presence_penalty: typing.Optional[float] = OMIT,
+ frequency_penalty: typing.Optional[float] = OMIT,
+ user: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[ChatCompletion]:
+ """
+ Generate a chat completion with Electron. OpenAI-compatible
+ request/response shape — point any OpenAI SDK at
+ `https://api.smallest.ai/waves/v1` and it just works.
+
+ Set `stream: true` to receive tokens via Server-Sent Events. With
+ `stream_options: { include_usage: true }`, the final SSE chunk
+ carries the `usage` block so token accounting is exact even on
+ client disconnects.
+
+ Tool calling follows OpenAI's `tools` array convention. When you
+ provide a voice-agent-style system prompt, Electron emits a short
+ filler phrase in the assistant message `content` field alongside
+ `tool_calls` — see the [Tool Calling guide](/models/documentation/llm-electron/tool-function-calling)
+ for the voice-agent pattern.
+
+ ## Examples
+
+ **cURL**
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/chat/completions" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/json" \\
+ -d '{
+ "model": "electron",
+ "messages": [
+ {"role": "user", "content": "Write one sentence about why the sky is blue."}
+ ]
+ }'
+ ```
+
+ **Python** (`pip install openai`)
+ ```python
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(
+ base_url="https://api.smallest.ai/waves/v1",
+ api_key=os.environ["SMALLEST_API_KEY"],
+ )
+
+ response = client.chat.completions.create(
+ model="electron",
+ messages=[
+ {"role": "user", "content": "Write one sentence about why the sky is blue."}
+ ],
+ )
+
+ print(response.choices[0].message.content)
+ ```
+
+ **JavaScript / TypeScript** (`npm install openai`)
+ ```typescript
+ import OpenAI from "openai";
+
+ const client = new OpenAI({
+ baseURL: "https://api.smallest.ai/waves/v1",
+ apiKey: process.env.SMALLEST_API_KEY,
+ });
+
+ const response = await client.chat.completions.create({
+ model: "electron",
+ messages: [
+ { role: "user", content: "Write one sentence about why the sky is blue." },
+ ],
+ });
+
+ console.log(response.choices[0].message.content);
+ ```
+
+ **Streaming with usage** (Python)
+ ```python
+ stream = client.chat.completions.create(
+ model="electron",
+ messages=[{"role": "user", "content": "Tell me a one-sentence fun fact."}],
+ stream=True,
+ stream_options={"include_usage": True},
+ )
+ for chunk in stream:
+ if chunk.choices and chunk.choices[0].delta.content:
+ print(chunk.choices[0].delta.content, end="", flush=True)
+ if chunk.usage:
+ print(f"\\n\\nTokens: {chunk.usage.total_tokens}")
+ ```
+
+ ## Common gotchas
+
+ - **Base URL is `/waves/v1`**, not `/v1`. The OpenAI SDK appends `/chat/completions` for you.
+ - **`stream_options.include_usage: true`** is required for exact token accounting on streaming calls — the final SSE chunk carries the `usage` block.
+ - **`n > 1` and `prompt_logprobs` are rejected.** Use multiple requests if you need parallel completions.
+ - **Auth header is `Authorization: Bearer $SMALLEST_API_KEY`** — get the key from the [Smallest AI Console](https://app.smallest.ai/dashboard/api-keys).
+
+ Parameters
+ ----------
+ model : str
+ Model ID. Currently only `"electron"`.
+
+ messages : typing.Sequence[ElectronMessage]
+ Chat history. Standard OpenAI message array.
+
+ temperature : typing.Optional[float]
+ Sampling temperature.
+
+ top_p : typing.Optional[float]
+ Nucleus sampling.
+
+ max_tokens : typing.Optional[int]
+ Maximum output tokens. Combined input + output context ceiling
+ is 32,768.
+
+ stream : typing.Optional[bool]
+ When true, response is `text/event-stream`. See the
+ [Streaming guide](/models/documentation/llm-electron/streaming).
+
+ stream_options : typing.Optional[ChatCompletionRequestStreamOptions]
+
+ tools : typing.Optional[typing.Sequence[typing.Dict[str, typing.Any]]]
+ Tool / function calling definitions. Forwarded verbatim to the
+ OpenAI-compatible upstream, so the standard OpenAI shape
+ (`{type: "function", function: {name, description, parameters}}`)
+ is the recommended form and is what the examples below use.
+ The wire schema is permissive (`array`) — any tools payload
+ the upstream accepts will work. See [Tool Calling](/models/documentation/llm-electron/tool-function-calling)
+ for details.
+
+ tool_choice : typing.Optional[ChatCompletionRequestToolChoice]
+
+ response_format : typing.Optional[ChatCompletionRequestResponseFormat]
+ Output shape. `{type: "text"}` (default) or `{type: "json_object"}`.
+
+ stop : typing.Optional[ChatCompletionRequestStop]
+
+ seed : typing.Optional[int]
+ Best-effort determinism.
+
+ logit_bias : typing.Optional[typing.Dict[str, float]]
+
+ logprobs : typing.Optional[bool]
+
+ top_logprobs : typing.Optional[int]
+
+ presence_penalty : typing.Optional[float]
+
+ frequency_penalty : typing.Optional[float]
+
+ user : typing.Optional[str]
+ Opaque end-user identifier. Not interpreted by Electron.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[ChatCompletion]
+ Non-streaming: standard OpenAI `chat.completion` object.
+
+ Streaming (`stream: true`): `text/event-stream` SSE — each
+ event is a `chat.completion.chunk` delta, terminated by
+ `data: [DONE]`.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "waves/v1/chat/completions",
+ base_url=self._client_wrapper.get_environment().waves,
+ method="POST",
+ json={
+ "model": model,
+ "messages": convert_and_respect_annotation_metadata(
+ object_=messages, annotation=typing.Sequence[ElectronMessage], direction="write"
+ ),
+ "temperature": temperature,
+ "top_p": top_p,
+ "max_tokens": max_tokens,
+ "stream": stream,
+ "stream_options": convert_and_respect_annotation_metadata(
+ object_=stream_options, annotation=ChatCompletionRequestStreamOptions, direction="write"
+ ),
+ "tools": tools,
+ "tool_choice": convert_and_respect_annotation_metadata(
+ object_=tool_choice, annotation=ChatCompletionRequestToolChoice, direction="write"
+ ),
+ "response_format": convert_and_respect_annotation_metadata(
+ object_=response_format, annotation=ChatCompletionRequestResponseFormat, direction="write"
+ ),
+ "stop": convert_and_respect_annotation_metadata(
+ object_=stop, annotation=ChatCompletionRequestStop, direction="write"
+ ),
+ "seed": seed,
+ "logit_bias": logit_bias,
+ "logprobs": logprobs,
+ "top_logprobs": top_logprobs,
+ "presence_penalty": presence_penalty,
+ "frequency_penalty": frequency_penalty,
+ "user": user,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ ChatCompletion,
+ construct_type(
+ type_=ChatCompletion, # 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,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 502:
+ raise BadGatewayError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 503:
+ raise ServiceUnavailableError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # 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)
+
+
+class AsyncRawElectronClient:
+ def __init__(self, *, client_wrapper: AsyncClientWrapper):
+ self._client_wrapper = client_wrapper
+
+ async def complete(
+ self,
+ *,
+ model: str,
+ messages: typing.Sequence[ElectronMessage],
+ temperature: typing.Optional[float] = OMIT,
+ top_p: typing.Optional[float] = OMIT,
+ max_tokens: typing.Optional[int] = OMIT,
+ stream: typing.Optional[bool] = OMIT,
+ stream_options: typing.Optional[ChatCompletionRequestStreamOptions] = OMIT,
+ tools: typing.Optional[typing.Sequence[typing.Dict[str, typing.Any]]] = OMIT,
+ tool_choice: typing.Optional[ChatCompletionRequestToolChoice] = OMIT,
+ response_format: typing.Optional[ChatCompletionRequestResponseFormat] = OMIT,
+ stop: typing.Optional[ChatCompletionRequestStop] = OMIT,
+ seed: typing.Optional[int] = OMIT,
+ logit_bias: typing.Optional[typing.Dict[str, float]] = OMIT,
+ logprobs: typing.Optional[bool] = OMIT,
+ top_logprobs: typing.Optional[int] = OMIT,
+ presence_penalty: typing.Optional[float] = OMIT,
+ frequency_penalty: typing.Optional[float] = OMIT,
+ user: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> AsyncHttpResponse[ChatCompletion]:
+ """
+ Generate a chat completion with Electron. OpenAI-compatible
+ request/response shape — point any OpenAI SDK at
+ `https://api.smallest.ai/waves/v1` and it just works.
+
+ Set `stream: true` to receive tokens via Server-Sent Events. With
+ `stream_options: { include_usage: true }`, the final SSE chunk
+ carries the `usage` block so token accounting is exact even on
+ client disconnects.
+
+ Tool calling follows OpenAI's `tools` array convention. When you
+ provide a voice-agent-style system prompt, Electron emits a short
+ filler phrase in the assistant message `content` field alongside
+ `tool_calls` — see the [Tool Calling guide](/models/documentation/llm-electron/tool-function-calling)
+ for the voice-agent pattern.
+
+ ## Examples
+
+ **cURL**
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/chat/completions" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/json" \\
+ -d '{
+ "model": "electron",
+ "messages": [
+ {"role": "user", "content": "Write one sentence about why the sky is blue."}
+ ]
+ }'
+ ```
+
+ **Python** (`pip install openai`)
+ ```python
+ import os
+ from openai import OpenAI
+
+ client = OpenAI(
+ base_url="https://api.smallest.ai/waves/v1",
+ api_key=os.environ["SMALLEST_API_KEY"],
+ )
+
+ response = client.chat.completions.create(
+ model="electron",
+ messages=[
+ {"role": "user", "content": "Write one sentence about why the sky is blue."}
+ ],
+ )
+
+ print(response.choices[0].message.content)
+ ```
+
+ **JavaScript / TypeScript** (`npm install openai`)
+ ```typescript
+ import OpenAI from "openai";
+
+ const client = new OpenAI({
+ baseURL: "https://api.smallest.ai/waves/v1",
+ apiKey: process.env.SMALLEST_API_KEY,
+ });
+
+ const response = await client.chat.completions.create({
+ model: "electron",
+ messages: [
+ { role: "user", content: "Write one sentence about why the sky is blue." },
+ ],
+ });
+
+ console.log(response.choices[0].message.content);
+ ```
+
+ **Streaming with usage** (Python)
+ ```python
+ stream = client.chat.completions.create(
+ model="electron",
+ messages=[{"role": "user", "content": "Tell me a one-sentence fun fact."}],
+ stream=True,
+ stream_options={"include_usage": True},
+ )
+ for chunk in stream:
+ if chunk.choices and chunk.choices[0].delta.content:
+ print(chunk.choices[0].delta.content, end="", flush=True)
+ if chunk.usage:
+ print(f"\\n\\nTokens: {chunk.usage.total_tokens}")
+ ```
+
+ ## Common gotchas
+
+ - **Base URL is `/waves/v1`**, not `/v1`. The OpenAI SDK appends `/chat/completions` for you.
+ - **`stream_options.include_usage: true`** is required for exact token accounting on streaming calls — the final SSE chunk carries the `usage` block.
+ - **`n > 1` and `prompt_logprobs` are rejected.** Use multiple requests if you need parallel completions.
+ - **Auth header is `Authorization: Bearer $SMALLEST_API_KEY`** — get the key from the [Smallest AI Console](https://app.smallest.ai/dashboard/api-keys).
+
+ Parameters
+ ----------
+ model : str
+ Model ID. Currently only `"electron"`.
+
+ messages : typing.Sequence[ElectronMessage]
+ Chat history. Standard OpenAI message array.
+
+ temperature : typing.Optional[float]
+ Sampling temperature.
+
+ top_p : typing.Optional[float]
+ Nucleus sampling.
+
+ max_tokens : typing.Optional[int]
+ Maximum output tokens. Combined input + output context ceiling
+ is 32,768.
+
+ stream : typing.Optional[bool]
+ When true, response is `text/event-stream`. See the
+ [Streaming guide](/models/documentation/llm-electron/streaming).
+
+ stream_options : typing.Optional[ChatCompletionRequestStreamOptions]
+
+ tools : typing.Optional[typing.Sequence[typing.Dict[str, typing.Any]]]
+ Tool / function calling definitions. Forwarded verbatim to the
+ OpenAI-compatible upstream, so the standard OpenAI shape
+ (`{type: "function", function: {name, description, parameters}}`)
+ is the recommended form and is what the examples below use.
+ The wire schema is permissive (`array`) — any tools payload
+ the upstream accepts will work. See [Tool Calling](/models/documentation/llm-electron/tool-function-calling)
+ for details.
+
+ tool_choice : typing.Optional[ChatCompletionRequestToolChoice]
+
+ response_format : typing.Optional[ChatCompletionRequestResponseFormat]
+ Output shape. `{type: "text"}` (default) or `{type: "json_object"}`.
+
+ stop : typing.Optional[ChatCompletionRequestStop]
+
+ seed : typing.Optional[int]
+ Best-effort determinism.
+
+ logit_bias : typing.Optional[typing.Dict[str, float]]
+
+ logprobs : typing.Optional[bool]
+
+ top_logprobs : typing.Optional[int]
+
+ presence_penalty : typing.Optional[float]
+
+ frequency_penalty : typing.Optional[float]
+
+ user : typing.Optional[str]
+ Opaque end-user identifier. Not interpreted by Electron.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[ChatCompletion]
+ Non-streaming: standard OpenAI `chat.completion` object.
+
+ Streaming (`stream: true`): `text/event-stream` SSE — each
+ event is a `chat.completion.chunk` delta, terminated by
+ `data: [DONE]`.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "waves/v1/chat/completions",
+ base_url=self._client_wrapper.get_environment().waves,
+ method="POST",
+ json={
+ "model": model,
+ "messages": convert_and_respect_annotation_metadata(
+ object_=messages, annotation=typing.Sequence[ElectronMessage], direction="write"
+ ),
+ "temperature": temperature,
+ "top_p": top_p,
+ "max_tokens": max_tokens,
+ "stream": stream,
+ "stream_options": convert_and_respect_annotation_metadata(
+ object_=stream_options, annotation=ChatCompletionRequestStreamOptions, direction="write"
+ ),
+ "tools": tools,
+ "tool_choice": convert_and_respect_annotation_metadata(
+ object_=tool_choice, annotation=ChatCompletionRequestToolChoice, direction="write"
+ ),
+ "response_format": convert_and_respect_annotation_metadata(
+ object_=response_format, annotation=ChatCompletionRequestResponseFormat, direction="write"
+ ),
+ "stop": convert_and_respect_annotation_metadata(
+ object_=stop, annotation=ChatCompletionRequestStop, direction="write"
+ ),
+ "seed": seed,
+ "logit_bias": logit_bias,
+ "logprobs": logprobs,
+ "top_logprobs": top_logprobs,
+ "presence_penalty": presence_penalty,
+ "frequency_penalty": frequency_penalty,
+ "user": user,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ ChatCompletion,
+ construct_type(
+ type_=ChatCompletion, # 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,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 502:
+ raise BadGatewayError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 503:
+ raise ServiceUnavailableError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # 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)
diff --git a/src/smallestai/waves/electron/types/__init__.py b/src/smallestai/waves/electron/types/__init__.py
new file mode 100644
index 00000000..84172092
--- /dev/null
+++ b/src/smallestai/waves/electron/types/__init__.py
@@ -0,0 +1,62 @@
+# This file was auto-generated by Fern from our API Definition.
+
+# isort: skip_file
+
+import typing
+from importlib import import_module
+
+if typing.TYPE_CHECKING:
+ from .chat_completion_request_response_format import ChatCompletionRequestResponseFormat
+ from .chat_completion_request_response_format_type import ChatCompletionRequestResponseFormatType
+ from .chat_completion_request_stop import ChatCompletionRequestStop
+ from .chat_completion_request_stream_options import ChatCompletionRequestStreamOptions
+ from .chat_completion_request_tool_choice import ChatCompletionRequestToolChoice
+ from .chat_completion_request_tool_choice_function import ChatCompletionRequestToolChoiceFunction
+ from .chat_completion_request_tool_choice_function_function import ChatCompletionRequestToolChoiceFunctionFunction
+ from .chat_completion_request_tool_choice_function_type import ChatCompletionRequestToolChoiceFunctionType
+ from .chat_completion_request_tool_choice_zero import ChatCompletionRequestToolChoiceZero
+_dynamic_imports: typing.Dict[str, str] = {
+ "ChatCompletionRequestResponseFormat": ".chat_completion_request_response_format",
+ "ChatCompletionRequestResponseFormatType": ".chat_completion_request_response_format_type",
+ "ChatCompletionRequestStop": ".chat_completion_request_stop",
+ "ChatCompletionRequestStreamOptions": ".chat_completion_request_stream_options",
+ "ChatCompletionRequestToolChoice": ".chat_completion_request_tool_choice",
+ "ChatCompletionRequestToolChoiceFunction": ".chat_completion_request_tool_choice_function",
+ "ChatCompletionRequestToolChoiceFunctionFunction": ".chat_completion_request_tool_choice_function_function",
+ "ChatCompletionRequestToolChoiceFunctionType": ".chat_completion_request_tool_choice_function_type",
+ "ChatCompletionRequestToolChoiceZero": ".chat_completion_request_tool_choice_zero",
+}
+
+
+def __getattr__(attr_name: str) -> typing.Any:
+ module_name = _dynamic_imports.get(attr_name)
+ if module_name is None:
+ raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}")
+ try:
+ module = import_module(module_name, __package__)
+ if module_name == f".{attr_name}":
+ return module
+ else:
+ return getattr(module, attr_name)
+ except ImportError as e:
+ raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e
+ except AttributeError as e:
+ raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e
+
+
+def __dir__():
+ lazy_attrs = list(_dynamic_imports.keys())
+ return sorted(lazy_attrs)
+
+
+__all__ = [
+ "ChatCompletionRequestResponseFormat",
+ "ChatCompletionRequestResponseFormatType",
+ "ChatCompletionRequestStop",
+ "ChatCompletionRequestStreamOptions",
+ "ChatCompletionRequestToolChoice",
+ "ChatCompletionRequestToolChoiceFunction",
+ "ChatCompletionRequestToolChoiceFunctionFunction",
+ "ChatCompletionRequestToolChoiceFunctionType",
+ "ChatCompletionRequestToolChoiceZero",
+]
diff --git a/src/smallestai/waves/pulse_stt_streaming/types/pulse_close_stream_signal_message.py b/src/smallestai/waves/electron/types/chat_completion_request_response_format.py
similarity index 60%
rename from src/smallestai/waves/pulse_stt_streaming/types/pulse_close_stream_signal_message.py
rename to src/smallestai/waves/electron/types/chat_completion_request_response_format.py
index 2506aee4..67f87c68 100644
--- a/src/smallestai/waves/pulse_stt_streaming/types/pulse_close_stream_signal_message.py
+++ b/src/smallestai/waves/electron/types/chat_completion_request_response_format.py
@@ -5,15 +5,16 @@
import pydantic
from ....core.pydantic_utilities import IS_PYDANTIC_V2
from ....core.unchecked_base_model import UncheckedBaseModel
-from .pulse_close_stream_signal_message_type import PulseCloseStreamSignalMessageType
+from .chat_completion_request_response_format_type import ChatCompletionRequestResponseFormatType
-class PulseCloseStreamSignalMessage(UncheckedBaseModel):
- type: PulseCloseStreamSignalMessageType = pydantic.Field()
+class ChatCompletionRequestResponseFormat(UncheckedBaseModel):
"""
- Signal the end of the audio stream. The server flushes remaining audio, delivers final transcripts, and responds with is_last=true.
+ Output shape. `{type: "text"}` (default) or `{type: "json_object"}`.
"""
+ type: typing.Optional[ChatCompletionRequestResponseFormatType] = None
+
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/smallestai/waves/electron/types/chat_completion_request_response_format_type.py b/src/smallestai/waves/electron/types/chat_completion_request_response_format_type.py
new file mode 100644
index 00000000..5d16a0d9
--- /dev/null
+++ b/src/smallestai/waves/electron/types/chat_completion_request_response_format_type.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+ChatCompletionRequestResponseFormatType = typing.Union[typing.Literal["text", "json_object"], typing.Any]
diff --git a/src/smallestai/waves/electron/types/chat_completion_request_stop.py b/src/smallestai/waves/electron/types/chat_completion_request_stop.py
new file mode 100644
index 00000000..7ac495c1
--- /dev/null
+++ b/src/smallestai/waves/electron/types/chat_completion_request_stop.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+ChatCompletionRequestStop = typing.Union[str, typing.List[str]]
diff --git a/src/smallestai/waves/electron/types/chat_completion_request_stream_options.py b/src/smallestai/waves/electron/types/chat_completion_request_stream_options.py
new file mode 100644
index 00000000..af458f8d
--- /dev/null
+++ b/src/smallestai/waves/electron/types/chat_completion_request_stream_options.py
@@ -0,0 +1,24 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ....core.pydantic_utilities import IS_PYDANTIC_V2
+from ....core.unchecked_base_model import UncheckedBaseModel
+
+
+class ChatCompletionRequestStreamOptions(UncheckedBaseModel):
+ include_usage: typing.Optional[bool] = pydantic.Field(default=None)
+ """
+ Append a final SSE chunk with the `usage` block. Strongly
+ recommended for any caller that tracks token consumption.
+ """
+
+ 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/smallestai/waves/electron/types/chat_completion_request_tool_choice.py b/src/smallestai/waves/electron/types/chat_completion_request_tool_choice.py
new file mode 100644
index 00000000..05a7b27a
--- /dev/null
+++ b/src/smallestai/waves/electron/types/chat_completion_request_tool_choice.py
@@ -0,0 +1,10 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+from .chat_completion_request_tool_choice_function import ChatCompletionRequestToolChoiceFunction
+from .chat_completion_request_tool_choice_zero import ChatCompletionRequestToolChoiceZero
+
+ChatCompletionRequestToolChoice = typing.Union[
+ ChatCompletionRequestToolChoiceZero, ChatCompletionRequestToolChoiceFunction
+]
diff --git a/src/smallestai/waves/electron/types/chat_completion_request_tool_choice_function.py b/src/smallestai/waves/electron/types/chat_completion_request_tool_choice_function.py
new file mode 100644
index 00000000..8661189d
--- /dev/null
+++ b/src/smallestai/waves/electron/types/chat_completion_request_tool_choice_function.py
@@ -0,0 +1,23 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ....core.pydantic_utilities import IS_PYDANTIC_V2
+from ....core.unchecked_base_model import UncheckedBaseModel
+from .chat_completion_request_tool_choice_function_function import ChatCompletionRequestToolChoiceFunctionFunction
+from .chat_completion_request_tool_choice_function_type import ChatCompletionRequestToolChoiceFunctionType
+
+
+class ChatCompletionRequestToolChoiceFunction(UncheckedBaseModel):
+ type: ChatCompletionRequestToolChoiceFunctionType
+ function: ChatCompletionRequestToolChoiceFunctionFunction
+
+ 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/smallestai/waves/electron/types/chat_completion_request_tool_choice_function_function.py b/src/smallestai/waves/electron/types/chat_completion_request_tool_choice_function_function.py
new file mode 100644
index 00000000..b90572de
--- /dev/null
+++ b/src/smallestai/waves/electron/types/chat_completion_request_tool_choice_function_function.py
@@ -0,0 +1,20 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ....core.pydantic_utilities import IS_PYDANTIC_V2
+from ....core.unchecked_base_model import UncheckedBaseModel
+
+
+class ChatCompletionRequestToolChoiceFunctionFunction(UncheckedBaseModel):
+ name: str
+
+ 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/smallestai/waves/electron/types/chat_completion_request_tool_choice_function_type.py b/src/smallestai/waves/electron/types/chat_completion_request_tool_choice_function_type.py
new file mode 100644
index 00000000..15a25b2f
--- /dev/null
+++ b/src/smallestai/waves/electron/types/chat_completion_request_tool_choice_function_type.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+ChatCompletionRequestToolChoiceFunctionType = typing.Union[typing.Literal["function"], typing.Any]
diff --git a/src/smallestai/waves/electron/types/chat_completion_request_tool_choice_zero.py b/src/smallestai/waves/electron/types/chat_completion_request_tool_choice_zero.py
new file mode 100644
index 00000000..df81c5c1
--- /dev/null
+++ b/src/smallestai/waves/electron/types/chat_completion_request_tool_choice_zero.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+ChatCompletionRequestToolChoiceZero = typing.Union[typing.Literal["auto", "required", "none"], typing.Any]
diff --git a/src/smallestai/waves/errors/__init__.py b/src/smallestai/waves/errors/__init__.py
index 3d0326c1..6c70f0be 100644
--- a/src/smallestai/waves/errors/__init__.py
+++ b/src/smallestai/waves/errors/__init__.py
@@ -6,12 +6,22 @@
from importlib import import_module
if typing.TYPE_CHECKING:
+ from .bad_gateway_error import BadGatewayError
from .bad_request_error import BadRequestError
+ from .content_too_large_error import ContentTooLargeError
+ from .forbidden_error import ForbiddenError
from .internal_server_error import InternalServerError
+ from .service_unavailable_error import ServiceUnavailableError
+ from .too_many_requests_error import TooManyRequestsError
from .unauthorized_error import UnauthorizedError
_dynamic_imports: typing.Dict[str, str] = {
+ "BadGatewayError": ".bad_gateway_error",
"BadRequestError": ".bad_request_error",
+ "ContentTooLargeError": ".content_too_large_error",
+ "ForbiddenError": ".forbidden_error",
"InternalServerError": ".internal_server_error",
+ "ServiceUnavailableError": ".service_unavailable_error",
+ "TooManyRequestsError": ".too_many_requests_error",
"UnauthorizedError": ".unauthorized_error",
}
@@ -37,4 +47,13 @@ def __dir__():
return sorted(lazy_attrs)
-__all__ = ["BadRequestError", "InternalServerError", "UnauthorizedError"]
+__all__ = [
+ "BadGatewayError",
+ "BadRequestError",
+ "ContentTooLargeError",
+ "ForbiddenError",
+ "InternalServerError",
+ "ServiceUnavailableError",
+ "TooManyRequestsError",
+ "UnauthorizedError",
+]
diff --git a/src/smallestai/waves/errors/bad_gateway_error.py b/src/smallestai/waves/errors/bad_gateway_error.py
new file mode 100644
index 00000000..dc20174a
--- /dev/null
+++ b/src/smallestai/waves/errors/bad_gateway_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 BadGatewayError(ApiError):
+ def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None):
+ super().__init__(status_code=502, headers=headers, body=body)
diff --git a/src/smallestai/waves/errors/content_too_large_error.py b/src/smallestai/waves/errors/content_too_large_error.py
new file mode 100644
index 00000000..52542b4f
--- /dev/null
+++ b/src/smallestai/waves/errors/content_too_large_error.py
@@ -0,0 +1,11 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+from ...core.api_error import ApiError
+from ..types.stt_error_response import SttErrorResponse
+
+
+class ContentTooLargeError(ApiError):
+ def __init__(self, body: SttErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None):
+ super().__init__(status_code=413, headers=headers, body=body)
diff --git a/src/smallestai/waves/errors/forbidden_error.py b/src/smallestai/waves/errors/forbidden_error.py
new file mode 100644
index 00000000..f3562469
--- /dev/null
+++ b/src/smallestai/waves/errors/forbidden_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 ForbiddenError(ApiError):
+ def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None):
+ super().__init__(status_code=403, headers=headers, body=body)
diff --git a/src/smallestai/waves/errors/service_unavailable_error.py b/src/smallestai/waves/errors/service_unavailable_error.py
new file mode 100644
index 00000000..1e7c99e9
--- /dev/null
+++ b/src/smallestai/waves/errors/service_unavailable_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 ServiceUnavailableError(ApiError):
+ def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None):
+ super().__init__(status_code=503, headers=headers, body=body)
diff --git a/src/smallestai/waves/errors/too_many_requests_error.py b/src/smallestai/waves/errors/too_many_requests_error.py
new file mode 100644
index 00000000..7f18fc2c
--- /dev/null
+++ b/src/smallestai/waves/errors/too_many_requests_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 TooManyRequestsError(ApiError):
+ def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None):
+ super().__init__(status_code=429, headers=headers, body=body)
diff --git a/src/smallestai/waves/pulse_stt_streaming/__init__.py b/src/smallestai/waves/pulse_stt_streaming/__init__.py
deleted file mode 100644
index 218aa8fa..00000000
--- a/src/smallestai/waves/pulse_stt_streaming/__init__.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-# isort: skip_file
-
-import typing
-from importlib import import_module
-
-if typing.TYPE_CHECKING:
- from .types import (
- PulseCloseStreamSignalMessage,
- PulseCloseStreamSignalMessageType,
- PulseFinalizeSignalMessage,
- PulseFinalizeSignalMessageType,
- PulseSpeechEndedEventMessage,
- PulseSpeechEndedEventMessageType,
- PulseSpeechStartedEventMessage,
- PulseSpeechStartedEventMessageType,
- PulseTranscriptionResponseMessage,
- PulseTranscriptionResponseMessageUtterancesItem,
- PulseTranscriptionResponseMessageWordsItem,
- )
-_dynamic_imports: typing.Dict[str, str] = {
- "PulseCloseStreamSignalMessage": ".types",
- "PulseCloseStreamSignalMessageType": ".types",
- "PulseFinalizeSignalMessage": ".types",
- "PulseFinalizeSignalMessageType": ".types",
- "PulseSpeechEndedEventMessage": ".types",
- "PulseSpeechEndedEventMessageType": ".types",
- "PulseSpeechStartedEventMessage": ".types",
- "PulseSpeechStartedEventMessageType": ".types",
- "PulseTranscriptionResponseMessage": ".types",
- "PulseTranscriptionResponseMessageUtterancesItem": ".types",
- "PulseTranscriptionResponseMessageWordsItem": ".types",
-}
-
-
-def __getattr__(attr_name: str) -> typing.Any:
- module_name = _dynamic_imports.get(attr_name)
- if module_name is None:
- raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}")
- try:
- module = import_module(module_name, __package__)
- if module_name == f".{attr_name}":
- return module
- else:
- return getattr(module, attr_name)
- except ImportError as e:
- raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e
- except AttributeError as e:
- raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e
-
-
-def __dir__():
- lazy_attrs = list(_dynamic_imports.keys())
- return sorted(lazy_attrs)
-
-
-__all__ = [
- "PulseCloseStreamSignalMessage",
- "PulseCloseStreamSignalMessageType",
- "PulseFinalizeSignalMessage",
- "PulseFinalizeSignalMessageType",
- "PulseSpeechEndedEventMessage",
- "PulseSpeechEndedEventMessageType",
- "PulseSpeechStartedEventMessage",
- "PulseSpeechStartedEventMessageType",
- "PulseTranscriptionResponseMessage",
- "PulseTranscriptionResponseMessageUtterancesItem",
- "PulseTranscriptionResponseMessageWordsItem",
-]
diff --git a/src/smallestai/waves/pulse_stt_streaming/client.py b/src/smallestai/waves/pulse_stt_streaming/client.py
deleted file mode 100644
index 0dd05b29..00000000
--- a/src/smallestai/waves/pulse_stt_streaming/client.py
+++ /dev/null
@@ -1,307 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-import urllib.parse
-from contextlib import asynccontextmanager, contextmanager
-
-import websockets.sync.client as websockets_sync_client
-from ...core.api_error import ApiError
-from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
-from ...core.jsonable_encoder import jsonable_encoder
-from ...core.query_encoder import encode_query
-from ...core.remove_none_from_dict import remove_none_from_dict
-from ...core.request_options import RequestOptions
-from ...core.websocket_compat import InvalidWebSocketStatus, get_status_code
-from .raw_client import AsyncRawPulseSttStreamingClient, RawPulseSttStreamingClient
-from .socket_client import AsyncPulseSttStreamingSocketClient, PulseSttStreamingSocketClient
-
-try:
- from websockets.legacy.client import connect as websockets_client_connect # type: ignore
-except ImportError:
- from websockets import connect as websockets_client_connect # type: ignore
-
-
-class PulseSttStreamingClient:
- def __init__(self, *, client_wrapper: SyncClientWrapper):
- self._raw_client = RawPulseSttStreamingClient(client_wrapper=client_wrapper)
-
- @property
- def with_raw_response(self) -> RawPulseSttStreamingClient:
- """
- Retrieves a raw implementation of this client that returns raw responses.
-
- Returns
- -------
- RawPulseSttStreamingClient
- """
- return self._raw_client
-
- @contextmanager
- def connect(
- self, *, request_options: typing.Optional[RequestOptions] = None
- ) -> typing.Iterator[PulseSttStreamingSocketClient]:
- """
- Transcribe audio in real time over a persistent WebSocket. The fit-for-purpose path for live captioning, voice agents, and any flow where you need partial transcripts as the user is still speaking.
-
- ## When to use this
-
- - **Use this** for live audio: microphone input, voice-agent turns, simultaneous interpretation, low-latency captioning. Partial results stream back while audio is still arriving.
- - **Use the pre-recorded REST endpoint** (`POST /waves/v1/pulse/get_text`) when you have a complete file. Single request, single response, less plumbing.
-
- ## How it works
-
- 1. Open a WebSocket to `wss://api.smallest.ai/waves/v1/pulse/get_text` with `Authorization: Bearer ` and the session params (`language`, `sample_rate`, `encoding`, etc.) as query string.
- 2. Stream raw PCM (or your chosen `encoding`) over the socket as binary frames.
- 3. The server pushes back JSON `transcriptionResponse` messages — partial results (`is_final: false`) as you speak, finalized text (`is_final: true`) when an utterance closes.
- 4. Send a `finalize` message to force end-of-utterance, or `close_stream` to end the session.
-
- ## Examples
-
- **Python** (real-time mic input)
- ```python
- import asyncio, json, websockets, pyaudio
-
- URL = "wss://api.smallest.ai/waves/v1/pulse/get_text?language=en&sample_rate=16000&encoding=linear16&word_timestamps=true"
- HEADERS = {"Authorization": f"Bearer {API_KEY}"}
-
- async def stream_mic():
- pa = pyaudio.PyAudio()
- stream = pa.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True, frames_per_buffer=320)
- async with websockets.connect(URL, additional_headers=HEADERS) as ws:
- async def send_audio():
- while True:
- await ws.send(stream.read(320))
- async def recv_transcripts():
- async for msg in ws:
- data = json.loads(msg)
- tag = "FINAL " if data.get("is_final") else "partial"
- print(f"[{tag}] {data.get('transcript')}")
- await asyncio.gather(send_audio(), recv_transcripts())
-
- asyncio.run(stream_mic())
- ```
-
- **JavaScript / TypeScript** (using `ws`)
- ```typescript
- import WebSocket from "ws";
- import { readFileSync } from "node:fs";
-
- const params = new URLSearchParams({
- language: "en", sample_rate: "16000", encoding: "linear16", word_timestamps: "true",
- });
- const ws = new WebSocket(`wss://api.smallest.ai/waves/v1/pulse/get_text?${params}`, {
- headers: { Authorization: `Bearer ${process.env.SMALLEST_API_KEY}` },
- });
-
- ws.on("open", () => {
- const audio = readFileSync("./call.pcm"); // 16-bit mono PCM at 16 kHz
- for (let i = 0; i < audio.length; i += 3200) {
- ws.send(audio.subarray(i, i + 3200));
- }
- ws.send(JSON.stringify({ type: "finalize" }));
- });
-
- ws.on("message", (raw) => {
- const data = JSON.parse(raw.toString());
- const tag = data.is_final ? "FINAL " : "partial";
- console.log(`[${tag}] ${data.transcript}`);
- if (data.is_last) ws.close();
- });
- ```
-
- ## Common gotchas
-
- - **Match `sample_rate` to your audio.** The server will not resample for you — sending 44.1 kHz audio with `sample_rate=16000` produces garbage transcripts.
- - **`finalize` vs `close_stream`**: `finalize` ends the current utterance and triggers a final transcript without closing the session. `close_stream` ends the session entirely.
- - **`keywords` is WebSocket-only.** Pass them on connect for proper-noun / jargon boosting; not available on the REST endpoint.
- - **`format`/`punctuate`/`capitalize`** are accepted at the wire level today. They currently return the same transcript regardless of value — pass them in your integration so it works as the behavior changes.
- - **PII/PCI redaction (`redact_pii`, `redact_pci`)** runs server-side on finalized transcripts only — partials may show the unredacted text briefly before being replaced.
- - **JavaScript / TypeScript**: the official `smallestai` npm package predates the Pulse model, so connect with the `ws` library directly as shown above.
-
- Parameters
- ----------
- request_options : typing.Optional[RequestOptions]
- Request-specific configuration.
-
- Returns
- -------
- PulseSttStreamingSocketClient
- """
- ws_url = self._raw_client._client_wrapper.get_environment().waves_ws + "/waves/v1/pulse/get_text"
- _encoded_query_params = encode_query(
- jsonable_encoder(
- remove_none_from_dict(
- {
- **(
- request_options.get("additional_query_parameters", {}) or {}
- if request_options is not None
- else {}
- ),
- }
- )
- )
- )
- if _encoded_query_params:
- ws_url = ws_url + "?" + urllib.parse.urlencode(_encoded_query_params)
- headers = self._raw_client._client_wrapper.get_headers()
- if request_options and "additional_headers" in request_options:
- headers.update(request_options["additional_headers"])
- try:
- with websockets_sync_client.connect(ws_url, additional_headers=headers) as protocol:
- yield PulseSttStreamingSocketClient(websocket=protocol)
- except InvalidWebSocketStatus as exc:
- status_code: int = get_status_code(exc)
- if status_code == 401:
- raise ApiError(
- status_code=status_code,
- headers=dict(headers),
- body="Websocket initialized with invalid credentials.",
- )
- raise ApiError(
- status_code=status_code,
- headers=dict(headers),
- body="Unexpected error when initializing websocket connection.",
- )
-
-
-class AsyncPulseSttStreamingClient:
- def __init__(self, *, client_wrapper: AsyncClientWrapper):
- self._raw_client = AsyncRawPulseSttStreamingClient(client_wrapper=client_wrapper)
-
- @property
- def with_raw_response(self) -> AsyncRawPulseSttStreamingClient:
- """
- Retrieves a raw implementation of this client that returns raw responses.
-
- Returns
- -------
- AsyncRawPulseSttStreamingClient
- """
- return self._raw_client
-
- @asynccontextmanager
- async def connect(
- self, *, request_options: typing.Optional[RequestOptions] = None
- ) -> typing.AsyncIterator[AsyncPulseSttStreamingSocketClient]:
- """
- Transcribe audio in real time over a persistent WebSocket. The fit-for-purpose path for live captioning, voice agents, and any flow where you need partial transcripts as the user is still speaking.
-
- ## When to use this
-
- - **Use this** for live audio: microphone input, voice-agent turns, simultaneous interpretation, low-latency captioning. Partial results stream back while audio is still arriving.
- - **Use the pre-recorded REST endpoint** (`POST /waves/v1/pulse/get_text`) when you have a complete file. Single request, single response, less plumbing.
-
- ## How it works
-
- 1. Open a WebSocket to `wss://api.smallest.ai/waves/v1/pulse/get_text` with `Authorization: Bearer ` and the session params (`language`, `sample_rate`, `encoding`, etc.) as query string.
- 2. Stream raw PCM (or your chosen `encoding`) over the socket as binary frames.
- 3. The server pushes back JSON `transcriptionResponse` messages — partial results (`is_final: false`) as you speak, finalized text (`is_final: true`) when an utterance closes.
- 4. Send a `finalize` message to force end-of-utterance, or `close_stream` to end the session.
-
- ## Examples
-
- **Python** (real-time mic input)
- ```python
- import asyncio, json, websockets, pyaudio
-
- URL = "wss://api.smallest.ai/waves/v1/pulse/get_text?language=en&sample_rate=16000&encoding=linear16&word_timestamps=true"
- HEADERS = {"Authorization": f"Bearer {API_KEY}"}
-
- async def stream_mic():
- pa = pyaudio.PyAudio()
- stream = pa.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True, frames_per_buffer=320)
- async with websockets.connect(URL, additional_headers=HEADERS) as ws:
- async def send_audio():
- while True:
- await ws.send(stream.read(320))
- async def recv_transcripts():
- async for msg in ws:
- data = json.loads(msg)
- tag = "FINAL " if data.get("is_final") else "partial"
- print(f"[{tag}] {data.get('transcript')}")
- await asyncio.gather(send_audio(), recv_transcripts())
-
- asyncio.run(stream_mic())
- ```
-
- **JavaScript / TypeScript** (using `ws`)
- ```typescript
- import WebSocket from "ws";
- import { readFileSync } from "node:fs";
-
- const params = new URLSearchParams({
- language: "en", sample_rate: "16000", encoding: "linear16", word_timestamps: "true",
- });
- const ws = new WebSocket(`wss://api.smallest.ai/waves/v1/pulse/get_text?${params}`, {
- headers: { Authorization: `Bearer ${process.env.SMALLEST_API_KEY}` },
- });
-
- ws.on("open", () => {
- const audio = readFileSync("./call.pcm"); // 16-bit mono PCM at 16 kHz
- for (let i = 0; i < audio.length; i += 3200) {
- ws.send(audio.subarray(i, i + 3200));
- }
- ws.send(JSON.stringify({ type: "finalize" }));
- });
-
- ws.on("message", (raw) => {
- const data = JSON.parse(raw.toString());
- const tag = data.is_final ? "FINAL " : "partial";
- console.log(`[${tag}] ${data.transcript}`);
- if (data.is_last) ws.close();
- });
- ```
-
- ## Common gotchas
-
- - **Match `sample_rate` to your audio.** The server will not resample for you — sending 44.1 kHz audio with `sample_rate=16000` produces garbage transcripts.
- - **`finalize` vs `close_stream`**: `finalize` ends the current utterance and triggers a final transcript without closing the session. `close_stream` ends the session entirely.
- - **`keywords` is WebSocket-only.** Pass them on connect for proper-noun / jargon boosting; not available on the REST endpoint.
- - **`format`/`punctuate`/`capitalize`** are accepted at the wire level today. They currently return the same transcript regardless of value — pass them in your integration so it works as the behavior changes.
- - **PII/PCI redaction (`redact_pii`, `redact_pci`)** runs server-side on finalized transcripts only — partials may show the unredacted text briefly before being replaced.
- - **JavaScript / TypeScript**: the official `smallestai` npm package predates the Pulse model, so connect with the `ws` library directly as shown above.
-
- Parameters
- ----------
- request_options : typing.Optional[RequestOptions]
- Request-specific configuration.
-
- Returns
- -------
- AsyncPulseSttStreamingSocketClient
- """
- ws_url = self._raw_client._client_wrapper.get_environment().waves_ws + "/waves/v1/pulse/get_text"
- _encoded_query_params = encode_query(
- jsonable_encoder(
- remove_none_from_dict(
- {
- **(
- request_options.get("additional_query_parameters", {}) or {}
- if request_options is not None
- else {}
- ),
- }
- )
- )
- )
- if _encoded_query_params:
- ws_url = ws_url + "?" + urllib.parse.urlencode(_encoded_query_params)
- headers = self._raw_client._client_wrapper.get_headers()
- if request_options and "additional_headers" in request_options:
- headers.update(request_options["additional_headers"])
- try:
- async with websockets_client_connect(ws_url, extra_headers=headers) as protocol:
- yield AsyncPulseSttStreamingSocketClient(websocket=protocol)
- except InvalidWebSocketStatus as exc:
- status_code: int = get_status_code(exc)
- if status_code == 401:
- raise ApiError(
- status_code=status_code,
- headers=dict(headers),
- body="Websocket initialized with invalid credentials.",
- )
- raise ApiError(
- status_code=status_code,
- headers=dict(headers),
- body="Unexpected error when initializing websocket connection.",
- )
diff --git a/src/smallestai/waves/pulse_stt_streaming/raw_client.py b/src/smallestai/waves/pulse_stt_streaming/raw_client.py
deleted file mode 100644
index 9a973ae3..00000000
--- a/src/smallestai/waves/pulse_stt_streaming/raw_client.py
+++ /dev/null
@@ -1,284 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-import urllib.parse
-from contextlib import asynccontextmanager, contextmanager
-
-import websockets.sync.client as websockets_sync_client
-from ...core.api_error import ApiError
-from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
-from ...core.jsonable_encoder import jsonable_encoder
-from ...core.query_encoder import encode_query
-from ...core.remove_none_from_dict import remove_none_from_dict
-from ...core.request_options import RequestOptions
-from ...core.websocket_compat import InvalidWebSocketStatus, get_status_code
-from .socket_client import AsyncPulseSttStreamingSocketClient, PulseSttStreamingSocketClient
-
-try:
- from websockets.legacy.client import connect as websockets_client_connect # type: ignore
-except ImportError:
- from websockets import connect as websockets_client_connect # type: ignore
-
-
-class RawPulseSttStreamingClient:
- def __init__(self, *, client_wrapper: SyncClientWrapper):
- self._client_wrapper = client_wrapper
-
- @contextmanager
- def connect(
- self, *, request_options: typing.Optional[RequestOptions] = None
- ) -> typing.Iterator[PulseSttStreamingSocketClient]:
- """
- Transcribe audio in real time over a persistent WebSocket. The fit-for-purpose path for live captioning, voice agents, and any flow where you need partial transcripts as the user is still speaking.
-
- ## When to use this
-
- - **Use this** for live audio: microphone input, voice-agent turns, simultaneous interpretation, low-latency captioning. Partial results stream back while audio is still arriving.
- - **Use the pre-recorded REST endpoint** (`POST /waves/v1/pulse/get_text`) when you have a complete file. Single request, single response, less plumbing.
-
- ## How it works
-
- 1. Open a WebSocket to `wss://api.smallest.ai/waves/v1/pulse/get_text` with `Authorization: Bearer ` and the session params (`language`, `sample_rate`, `encoding`, etc.) as query string.
- 2. Stream raw PCM (or your chosen `encoding`) over the socket as binary frames.
- 3. The server pushes back JSON `transcriptionResponse` messages — partial results (`is_final: false`) as you speak, finalized text (`is_final: true`) when an utterance closes.
- 4. Send a `finalize` message to force end-of-utterance, or `close_stream` to end the session.
-
- ## Examples
-
- **Python** (real-time mic input)
- ```python
- import asyncio, json, websockets, pyaudio
-
- URL = "wss://api.smallest.ai/waves/v1/pulse/get_text?language=en&sample_rate=16000&encoding=linear16&word_timestamps=true"
- HEADERS = {"Authorization": f"Bearer {API_KEY}"}
-
- async def stream_mic():
- pa = pyaudio.PyAudio()
- stream = pa.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True, frames_per_buffer=320)
- async with websockets.connect(URL, additional_headers=HEADERS) as ws:
- async def send_audio():
- while True:
- await ws.send(stream.read(320))
- async def recv_transcripts():
- async for msg in ws:
- data = json.loads(msg)
- tag = "FINAL " if data.get("is_final") else "partial"
- print(f"[{tag}] {data.get('transcript')}")
- await asyncio.gather(send_audio(), recv_transcripts())
-
- asyncio.run(stream_mic())
- ```
-
- **JavaScript / TypeScript** (using `ws`)
- ```typescript
- import WebSocket from "ws";
- import { readFileSync } from "node:fs";
-
- const params = new URLSearchParams({
- language: "en", sample_rate: "16000", encoding: "linear16", word_timestamps: "true",
- });
- const ws = new WebSocket(`wss://api.smallest.ai/waves/v1/pulse/get_text?${params}`, {
- headers: { Authorization: `Bearer ${process.env.SMALLEST_API_KEY}` },
- });
-
- ws.on("open", () => {
- const audio = readFileSync("./call.pcm"); // 16-bit mono PCM at 16 kHz
- for (let i = 0; i < audio.length; i += 3200) {
- ws.send(audio.subarray(i, i + 3200));
- }
- ws.send(JSON.stringify({ type: "finalize" }));
- });
-
- ws.on("message", (raw) => {
- const data = JSON.parse(raw.toString());
- const tag = data.is_final ? "FINAL " : "partial";
- console.log(`[${tag}] ${data.transcript}`);
- if (data.is_last) ws.close();
- });
- ```
-
- ## Common gotchas
-
- - **Match `sample_rate` to your audio.** The server will not resample for you — sending 44.1 kHz audio with `sample_rate=16000` produces garbage transcripts.
- - **`finalize` vs `close_stream`**: `finalize` ends the current utterance and triggers a final transcript without closing the session. `close_stream` ends the session entirely.
- - **`keywords` is WebSocket-only.** Pass them on connect for proper-noun / jargon boosting; not available on the REST endpoint.
- - **`format`/`punctuate`/`capitalize`** are accepted at the wire level today. They currently return the same transcript regardless of value — pass them in your integration so it works as the behavior changes.
- - **PII/PCI redaction (`redact_pii`, `redact_pci`)** runs server-side on finalized transcripts only — partials may show the unredacted text briefly before being replaced.
- - **JavaScript / TypeScript**: the official `smallestai` npm package predates the Pulse model, so connect with the `ws` library directly as shown above.
-
- Parameters
- ----------
- request_options : typing.Optional[RequestOptions]
- Request-specific configuration.
-
- Returns
- -------
- PulseSttStreamingSocketClient
- """
- ws_url = self._client_wrapper.get_environment().waves_ws + "/waves/v1/pulse/get_text"
- _encoded_query_params = encode_query(
- jsonable_encoder(
- remove_none_from_dict(
- {
- **(
- request_options.get("additional_query_parameters", {}) or {}
- if request_options is not None
- else {}
- ),
- }
- )
- )
- )
- if _encoded_query_params:
- ws_url = ws_url + "?" + urllib.parse.urlencode(_encoded_query_params)
- headers = self._client_wrapper.get_headers()
- if request_options and "additional_headers" in request_options:
- headers.update(request_options["additional_headers"])
- try:
- with websockets_sync_client.connect(ws_url, additional_headers=headers) as protocol:
- yield PulseSttStreamingSocketClient(websocket=protocol)
- except InvalidWebSocketStatus as exc:
- status_code: int = get_status_code(exc)
- if status_code == 401:
- raise ApiError(
- status_code=status_code,
- headers=dict(headers),
- body="Websocket initialized with invalid credentials.",
- )
- raise ApiError(
- status_code=status_code,
- headers=dict(headers),
- body="Unexpected error when initializing websocket connection.",
- )
-
-
-class AsyncRawPulseSttStreamingClient:
- def __init__(self, *, client_wrapper: AsyncClientWrapper):
- self._client_wrapper = client_wrapper
-
- @asynccontextmanager
- async def connect(
- self, *, request_options: typing.Optional[RequestOptions] = None
- ) -> typing.AsyncIterator[AsyncPulseSttStreamingSocketClient]:
- """
- Transcribe audio in real time over a persistent WebSocket. The fit-for-purpose path for live captioning, voice agents, and any flow where you need partial transcripts as the user is still speaking.
-
- ## When to use this
-
- - **Use this** for live audio: microphone input, voice-agent turns, simultaneous interpretation, low-latency captioning. Partial results stream back while audio is still arriving.
- - **Use the pre-recorded REST endpoint** (`POST /waves/v1/pulse/get_text`) when you have a complete file. Single request, single response, less plumbing.
-
- ## How it works
-
- 1. Open a WebSocket to `wss://api.smallest.ai/waves/v1/pulse/get_text` with `Authorization: Bearer ` and the session params (`language`, `sample_rate`, `encoding`, etc.) as query string.
- 2. Stream raw PCM (or your chosen `encoding`) over the socket as binary frames.
- 3. The server pushes back JSON `transcriptionResponse` messages — partial results (`is_final: false`) as you speak, finalized text (`is_final: true`) when an utterance closes.
- 4. Send a `finalize` message to force end-of-utterance, or `close_stream` to end the session.
-
- ## Examples
-
- **Python** (real-time mic input)
- ```python
- import asyncio, json, websockets, pyaudio
-
- URL = "wss://api.smallest.ai/waves/v1/pulse/get_text?language=en&sample_rate=16000&encoding=linear16&word_timestamps=true"
- HEADERS = {"Authorization": f"Bearer {API_KEY}"}
-
- async def stream_mic():
- pa = pyaudio.PyAudio()
- stream = pa.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True, frames_per_buffer=320)
- async with websockets.connect(URL, additional_headers=HEADERS) as ws:
- async def send_audio():
- while True:
- await ws.send(stream.read(320))
- async def recv_transcripts():
- async for msg in ws:
- data = json.loads(msg)
- tag = "FINAL " if data.get("is_final") else "partial"
- print(f"[{tag}] {data.get('transcript')}")
- await asyncio.gather(send_audio(), recv_transcripts())
-
- asyncio.run(stream_mic())
- ```
-
- **JavaScript / TypeScript** (using `ws`)
- ```typescript
- import WebSocket from "ws";
- import { readFileSync } from "node:fs";
-
- const params = new URLSearchParams({
- language: "en", sample_rate: "16000", encoding: "linear16", word_timestamps: "true",
- });
- const ws = new WebSocket(`wss://api.smallest.ai/waves/v1/pulse/get_text?${params}`, {
- headers: { Authorization: `Bearer ${process.env.SMALLEST_API_KEY}` },
- });
-
- ws.on("open", () => {
- const audio = readFileSync("./call.pcm"); // 16-bit mono PCM at 16 kHz
- for (let i = 0; i < audio.length; i += 3200) {
- ws.send(audio.subarray(i, i + 3200));
- }
- ws.send(JSON.stringify({ type: "finalize" }));
- });
-
- ws.on("message", (raw) => {
- const data = JSON.parse(raw.toString());
- const tag = data.is_final ? "FINAL " : "partial";
- console.log(`[${tag}] ${data.transcript}`);
- if (data.is_last) ws.close();
- });
- ```
-
- ## Common gotchas
-
- - **Match `sample_rate` to your audio.** The server will not resample for you — sending 44.1 kHz audio with `sample_rate=16000` produces garbage transcripts.
- - **`finalize` vs `close_stream`**: `finalize` ends the current utterance and triggers a final transcript without closing the session. `close_stream` ends the session entirely.
- - **`keywords` is WebSocket-only.** Pass them on connect for proper-noun / jargon boosting; not available on the REST endpoint.
- - **`format`/`punctuate`/`capitalize`** are accepted at the wire level today. They currently return the same transcript regardless of value — pass them in your integration so it works as the behavior changes.
- - **PII/PCI redaction (`redact_pii`, `redact_pci`)** runs server-side on finalized transcripts only — partials may show the unredacted text briefly before being replaced.
- - **JavaScript / TypeScript**: the official `smallestai` npm package predates the Pulse model, so connect with the `ws` library directly as shown above.
-
- Parameters
- ----------
- request_options : typing.Optional[RequestOptions]
- Request-specific configuration.
-
- Returns
- -------
- AsyncPulseSttStreamingSocketClient
- """
- ws_url = self._client_wrapper.get_environment().waves_ws + "/waves/v1/pulse/get_text"
- _encoded_query_params = encode_query(
- jsonable_encoder(
- remove_none_from_dict(
- {
- **(
- request_options.get("additional_query_parameters", {}) or {}
- if request_options is not None
- else {}
- ),
- }
- )
- )
- )
- if _encoded_query_params:
- ws_url = ws_url + "?" + urllib.parse.urlencode(_encoded_query_params)
- headers = self._client_wrapper.get_headers()
- if request_options and "additional_headers" in request_options:
- headers.update(request_options["additional_headers"])
- try:
- async with websockets_client_connect(ws_url, extra_headers=headers) as protocol:
- yield AsyncPulseSttStreamingSocketClient(websocket=protocol)
- except InvalidWebSocketStatus as exc:
- status_code: int = get_status_code(exc)
- if status_code == 401:
- raise ApiError(
- status_code=status_code,
- headers=dict(headers),
- body="Websocket initialized with invalid credentials.",
- )
- raise ApiError(
- status_code=status_code,
- headers=dict(headers),
- body="Unexpected error when initializing websocket connection.",
- )
diff --git a/src/smallestai/waves/pulse_stt_streaming/types/__init__.py b/src/smallestai/waves/pulse_stt_streaming/types/__init__.py
deleted file mode 100644
index dbe16f17..00000000
--- a/src/smallestai/waves/pulse_stt_streaming/types/__init__.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-# isort: skip_file
-
-import typing
-from importlib import import_module
-
-if typing.TYPE_CHECKING:
- from .pulse_close_stream_signal_message import PulseCloseStreamSignalMessage
- from .pulse_close_stream_signal_message_type import PulseCloseStreamSignalMessageType
- from .pulse_finalize_signal_message import PulseFinalizeSignalMessage
- from .pulse_finalize_signal_message_type import PulseFinalizeSignalMessageType
- from .pulse_speech_ended_event_message import PulseSpeechEndedEventMessage
- from .pulse_speech_ended_event_message_type import PulseSpeechEndedEventMessageType
- from .pulse_speech_started_event_message import PulseSpeechStartedEventMessage
- from .pulse_speech_started_event_message_type import PulseSpeechStartedEventMessageType
- from .pulse_transcription_response_message import PulseTranscriptionResponseMessage
- from .pulse_transcription_response_message_utterances_item import PulseTranscriptionResponseMessageUtterancesItem
- from .pulse_transcription_response_message_words_item import PulseTranscriptionResponseMessageWordsItem
-_dynamic_imports: typing.Dict[str, str] = {
- "PulseCloseStreamSignalMessage": ".pulse_close_stream_signal_message",
- "PulseCloseStreamSignalMessageType": ".pulse_close_stream_signal_message_type",
- "PulseFinalizeSignalMessage": ".pulse_finalize_signal_message",
- "PulseFinalizeSignalMessageType": ".pulse_finalize_signal_message_type",
- "PulseSpeechEndedEventMessage": ".pulse_speech_ended_event_message",
- "PulseSpeechEndedEventMessageType": ".pulse_speech_ended_event_message_type",
- "PulseSpeechStartedEventMessage": ".pulse_speech_started_event_message",
- "PulseSpeechStartedEventMessageType": ".pulse_speech_started_event_message_type",
- "PulseTranscriptionResponseMessage": ".pulse_transcription_response_message",
- "PulseTranscriptionResponseMessageUtterancesItem": ".pulse_transcription_response_message_utterances_item",
- "PulseTranscriptionResponseMessageWordsItem": ".pulse_transcription_response_message_words_item",
-}
-
-
-def __getattr__(attr_name: str) -> typing.Any:
- module_name = _dynamic_imports.get(attr_name)
- if module_name is None:
- raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}")
- try:
- module = import_module(module_name, __package__)
- if module_name == f".{attr_name}":
- return module
- else:
- return getattr(module, attr_name)
- except ImportError as e:
- raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e
- except AttributeError as e:
- raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e
-
-
-def __dir__():
- lazy_attrs = list(_dynamic_imports.keys())
- return sorted(lazy_attrs)
-
-
-__all__ = [
- "PulseCloseStreamSignalMessage",
- "PulseCloseStreamSignalMessageType",
- "PulseFinalizeSignalMessage",
- "PulseFinalizeSignalMessageType",
- "PulseSpeechEndedEventMessage",
- "PulseSpeechEndedEventMessageType",
- "PulseSpeechStartedEventMessage",
- "PulseSpeechStartedEventMessageType",
- "PulseTranscriptionResponseMessage",
- "PulseTranscriptionResponseMessageUtterancesItem",
- "PulseTranscriptionResponseMessageWordsItem",
-]
diff --git a/src/smallestai/waves/pulse_stt_streaming/types/pulse_close_stream_signal_message_type.py b/src/smallestai/waves/pulse_stt_streaming/types/pulse_close_stream_signal_message_type.py
deleted file mode 100644
index 43aba18f..00000000
--- a/src/smallestai/waves/pulse_stt_streaming/types/pulse_close_stream_signal_message_type.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-
-PulseCloseStreamSignalMessageType = typing.Union[typing.Literal["close_stream"], typing.Any]
diff --git a/src/smallestai/waves/pulse_stt_streaming/types/pulse_finalize_signal_message_type.py b/src/smallestai/waves/pulse_stt_streaming/types/pulse_finalize_signal_message_type.py
deleted file mode 100644
index a7010898..00000000
--- a/src/smallestai/waves/pulse_stt_streaming/types/pulse_finalize_signal_message_type.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-
-PulseFinalizeSignalMessageType = typing.Union[typing.Literal["finalize"], typing.Any]
diff --git a/src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_ended_event_message.py b/src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_ended_event_message.py
deleted file mode 100644
index a9baa868..00000000
--- a/src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_ended_event_message.py
+++ /dev/null
@@ -1,34 +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
-from ....core.unchecked_base_model import UncheckedBaseModel
-from .pulse_speech_ended_event_message_type import PulseSpeechEndedEventMessageType
-
-
-class PulseSpeechEndedEventMessage(UncheckedBaseModel):
- type: PulseSpeechEndedEventMessageType = pydantic.Field()
- """
- Discriminator. Always `speech_ended` for this event.
- """
-
- session_id: str = pydantic.Field()
- """
- Same session identifier returned on the `transcription` messages.
- """
-
- timestamp: float = pydantic.Field()
- """
- Acoustic offset of speech (end of a continuous voiced region), in seconds from the first audio frame.
- """
-
- 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/smallestai/waves/pulse_stt_streaming/types/pulse_speech_ended_event_message_type.py b/src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_ended_event_message_type.py
deleted file mode 100644
index 2b13aa8e..00000000
--- a/src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_ended_event_message_type.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-
-PulseSpeechEndedEventMessageType = typing.Union[typing.Literal["speech_ended"], typing.Any]
diff --git a/src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_started_event_message.py b/src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_started_event_message.py
deleted file mode 100644
index d9b24fa9..00000000
--- a/src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_started_event_message.py
+++ /dev/null
@@ -1,34 +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
-from ....core.unchecked_base_model import UncheckedBaseModel
-from .pulse_speech_started_event_message_type import PulseSpeechStartedEventMessageType
-
-
-class PulseSpeechStartedEventMessage(UncheckedBaseModel):
- type: PulseSpeechStartedEventMessageType = pydantic.Field()
- """
- Discriminator. Always `speech_started` for this event.
- """
-
- session_id: str = pydantic.Field()
- """
- Matches the `session_id` returned on `transcription` messages from the same connection. Use it to correlate VAD events with transcript turns.
- """
-
- timestamp: float = pydantic.Field()
- """
- Acoustic onset of speech, in seconds from the first audio frame sent on this connection.
- """
-
- 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/smallestai/waves/pulse_stt_streaming/types/pulse_speech_started_event_message_type.py b/src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_started_event_message_type.py
deleted file mode 100644
index b594f852..00000000
--- a/src/smallestai/waves/pulse_stt_streaming/types/pulse_speech_started_event_message_type.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-
-PulseSpeechStartedEventMessageType = typing.Union[typing.Literal["speech_started"], typing.Any]
diff --git a/src/smallestai/waves/pulse_stt_streaming/types/pulse_transcription_response_message.py b/src/smallestai/waves/pulse_stt_streaming/types/pulse_transcription_response_message.py
deleted file mode 100644
index 51d10f2c..00000000
--- a/src/smallestai/waves/pulse_stt_streaming/types/pulse_transcription_response_message.py
+++ /dev/null
@@ -1,72 +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
-from ....core.unchecked_base_model import UncheckedBaseModel
-from .pulse_transcription_response_message_utterances_item import PulseTranscriptionResponseMessageUtterancesItem
-from .pulse_transcription_response_message_words_item import PulseTranscriptionResponseMessageWordsItem
-
-
-class PulseTranscriptionResponseMessage(UncheckedBaseModel):
- session_id: str = pydantic.Field()
- """
- Unique identifier for the transcription session
- """
-
- transcript: typing.Optional[str] = pydantic.Field(default=None)
- """
- Partial or complete transcription text for the current segment
- """
-
- is_final: typing.Optional[bool] = pydantic.Field(default=None)
- """
- Indicates if this is the final transcription for the current segment
- """
-
- is_last: typing.Optional[bool] = pydantic.Field(default=None)
- """
- Indicates if this is the last transcription in the session
- """
-
- from_finalize: typing.Optional[bool] = pydantic.Field(default=None)
- """
- True when this final transcript was triggered by a manual finalize message rather than automatic finalization
- """
-
- words: typing.Optional[typing.List[PulseTranscriptionResponseMessageWordsItem]] = pydantic.Field(default=None)
- """
- Word-level timestamps (when word_timestamps=true)
- """
-
- utterances: typing.Optional[typing.List[PulseTranscriptionResponseMessageUtterancesItem]] = pydantic.Field(
- default=None
- )
- """
- Sentence-level timestamps (when sentence_timestamps=true)
- """
-
- language: typing.Optional[str] = pydantic.Field(default=None)
- """
- Detected primary language code, only returned when `is_final=True`
- """
-
- languages: typing.Optional[typing.List[str]] = pydantic.Field(default=None)
- """
- List of codes of languages detected in the audio, only returned when `is_final=True`
- """
-
- redacted_entities: typing.Optional[typing.List[str]] = pydantic.Field(default=None)
- """
- List of redacted entity placeholders (when redact_pii or redact_pci is enabled)
- """
-
- 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/smallestai/waves/raw_client.py b/src/smallestai/waves/raw_client.py
index 3dbc6148..70dd4576 100644
--- a/src/smallestai/waves/raw_client.py
+++ b/src/smallestai/waves/raw_client.py
@@ -5,6 +5,7 @@
from json.decoder import JSONDecodeError
from logging import error, warning
+from .. import core
from ..core.api_error import ApiError
from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
from ..core.http_response import AsyncHttpResponse, HttpResponse
@@ -18,9 +19,12 @@
from .errors.bad_request_error import BadRequestError
from .errors.internal_server_error import InternalServerError
from .errors.unauthorized_error import UnauthorizedError
+from .types.create_voice_clone_waves_request_model import CreateVoiceCloneWavesRequestModel
+from .types.create_voice_clone_waves_response import CreateVoiceCloneWavesResponse
from .types.delete_pronunciation_dict_response import DeletePronunciationDictResponse
from .types.get_voices_waves_request_model import GetVoicesWavesRequestModel
from .types.get_voices_waves_response import GetVoicesWavesResponse
+from .types.list_voice_clones_waves_response import ListVoiceClonesWavesResponse
from .types.pronunciation_dict import PronunciationDict
from .types.pronunciation_item import PronunciationItem
from .types.synthesize_lightning_large_waves_request_output_format import (
@@ -34,14 +38,6 @@
from .types.synthesize_sse_lightning_v2waves_request_output_format import (
SynthesizeSseLightningV2WavesRequestOutputFormat,
)
-from .types.transcribe_pulse_waves_request_capitalize import TranscribePulseWavesRequestCapitalize
-from .types.transcribe_pulse_waves_request_emotion_detection import TranscribePulseWavesRequestEmotionDetection
-from .types.transcribe_pulse_waves_request_encoding import TranscribePulseWavesRequestEncoding
-from .types.transcribe_pulse_waves_request_format import TranscribePulseWavesRequestFormat
-from .types.transcribe_pulse_waves_request_gender_detection import TranscribePulseWavesRequestGenderDetection
-from .types.transcribe_pulse_waves_request_language import TranscribePulseWavesRequestLanguage
-from .types.transcribe_pulse_waves_request_punctuate import TranscribePulseWavesRequestPunctuate
-from .types.transcribe_pulse_waves_response import TranscribePulseWavesResponse
from .types.tts_request_language import TtsRequestLanguage
from .types.tts_request_model import TtsRequestModel
from .types.tts_request_number_pronunciation_language import TtsRequestNumberPronunciationLanguage
@@ -1291,203 +1287,152 @@ def _iter():
yield _stream()
- def transcribe_pulse(
+ def list_voice_clones(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[ListVoiceClonesWavesResponse]:
+ """
+ Retrieve all voice clones in your organization.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[ListVoiceClonesWavesResponse]
+ List of voice clones.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "waves/v1/voice-cloning",
+ base_url=self._client_wrapper.get_environment().waves,
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ ListVoiceClonesWavesResponse,
+ construct_type(
+ type_=ListVoiceClonesWavesResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # 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 create_voice_clone(
self,
*,
- request: typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]],
- language: typing.Optional[TranscribePulseWavesRequestLanguage] = None,
- encoding: typing.Optional[TranscribePulseWavesRequestEncoding] = None,
- webhook_url: typing.Optional[str] = None,
- webhook_extra: typing.Optional[str] = None,
- word_timestamps: typing.Optional[bool] = None,
- diarize: typing.Optional[bool] = None,
- gender_detection: typing.Optional[TranscribePulseWavesRequestGenderDetection] = None,
- emotion_detection: typing.Optional[TranscribePulseWavesRequestEmotionDetection] = None,
- format: typing.Optional[TranscribePulseWavesRequestFormat] = None,
- punctuate: typing.Optional[TranscribePulseWavesRequestPunctuate] = None,
- capitalize: typing.Optional[TranscribePulseWavesRequestCapitalize] = None,
+ display_name: str,
+ file: core.File,
+ description: typing.Optional[str] = OMIT,
+ accent: typing.Optional[str] = OMIT,
+ tags: typing.Optional[str] = OMIT,
+ language: typing.Optional[str] = OMIT,
+ model: typing.Optional[CreateVoiceCloneWavesRequestModel] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
- ) -> HttpResponse[TranscribePulseWavesResponse]:
+ ) -> HttpResponse[CreateVoiceCloneWavesResponse]:
"""
- Transcribe an audio file to text using the Pulse model. The fastest way to get a transcript when you already have a recording — pass either the raw bytes or a URL.
-
- ## When to use this
-
- Use this endpoint when you have a complete audio file (call recording, voicemail, podcast episode) and want the transcript back in one response. For live transcription as audio arrives, use the realtime WebSocket endpoint (`WSS /waves/v1/pulse/get_text`) instead.
-
- ## Input methods
-
- Send the audio in one of two ways:
-
- 1. **Raw bytes** — `Content-Type: application/octet-stream` with the audio in the body. All knobs (`language`, `word_timestamps`, etc.) are query parameters.
- 2. **URL** — `Content-Type: application/json` with `{"url": "..."}` in the body. Useful when the audio already lives in object storage. Same query parameters apply.
-
- Pulse autodetects the language across 30+ supported locales. Pass `language` explicitly when you already know it — detection is fast but skipping it is faster.
-
- ## Examples
-
- **cURL** (raw bytes)
- ```bash
- curl -X POST "https://api.smallest.ai/waves/v1/pulse/get_text?language=en&word_timestamps=true" \\
- -H "Authorization: Bearer $SMALLEST_API_KEY" \\
- -H "Content-Type: application/octet-stream" \\
- --data-binary "@./call.wav"
- ```
-
- **cURL** (URL)
- ```bash
- curl -X POST "https://api.smallest.ai/waves/v1/pulse/get_text?language=en" \\
- -H "Authorization: Bearer $SMALLEST_API_KEY" \\
- -H "Content-Type: application/json" \\
- -d '{"url": "https://your-bucket.s3.amazonaws.com/call.wav"}'
- ```
-
- **Python** (`pip install smallestai>=4.4.0`)
- ```python
- from smallestai import SmallestAI
-
- client = SmallestAI(api_key="YOUR_API_KEY")
- with open("./call.wav", "rb") as f:
- result = client.waves.transcribe_pulse(
- request=f.read(),
- language="en",
- word_timestamps=True,
- diarize=True,
- )
- print(result.status) # "success"
- print(result.transcription) # the transcript string
- ```
-
- **JavaScript / TypeScript** (using `fetch`)
- ```typescript
- import { readFileSync } from "node:fs";
-
- const audio = readFileSync("./call.wav");
- const params = new URLSearchParams({ language: "en", word_timestamps: "true", diarize: "true" });
-
- const res = await fetch(`https://api.smallest.ai/waves/v1/pulse/get_text?${params}`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${process.env.SMALLEST_API_KEY}`,
- "Content-Type": "application/octet-stream",
- },
- body: audio,
- });
- const result = await res.json();
- console.log(result.transcription);
- ```
-
- ## Common gotchas
-
- - **Max file size is 250 MB.** Larger files return HTTP `400` with `{errors: "Audio data too large", status: "error", message: "Error handling audio data"}`. Compress to mono 16 kHz PCM if you're close to the limit; quality is unaffected.
- - **Formatting flags (`format`, `punctuate`, `capitalize`)** are accepted at the wire level and exposed in the Python SDK as of `smallestai>=4.4.0`. Today they currently return the same transcript regardless of value — pass them in your integration so it works as the behavior changes.
- - **Webhook-driven flow**: pass `webhook_url` to receive the transcript asynchronously. The endpoint returns immediately; the transcript hits your webhook when ready. Useful for long files where you don't want to hold an HTTP connection open.
- - **Speaker diarization** (`diarize=true`) adds latency. Skip it if you only need the words.
- - **JavaScript / TypeScript**: the official `smallestai` npm package predates the Pulse model, so call this endpoint with `fetch` or `axios` as shown above.
-
+ Create an instant voice clone in a single call. Defaults to `lightning-v3.1`.
+
Parameters
----------
- request : typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]
-
- language : typing.Optional[TranscribePulseWavesRequestLanguage]
- Language of the audio file. Set explicitly to the known language for best accuracy.
-
- **26 single-language codes** on this endpoint: `en`, `hi`, `de`, `es`, `ru`, `it`, `fr`, `nl`, `pt`, `uk`, `pl`, `cs`, `sk`, `lv`, `et`, `ro`, `fi`, `sv`, `bg`, `hu`, `da`, `lt`, `mt`, `zh`, `ja`, `ko`.
-
- **Regional auto-detect aggregators** for unknown audio:
- - `multi-eu` (default) — auto-detects across all 21 European codes above plus `en`.
- - `multi-asian` — auto-detects across `zh`, `ko`, `ja`, `en`.
- - `multi-indic`: auto-detects across `en`, `hi`, `gu`, `mr`, `bn`, `or`. India region only.
-
- Omitting `language` routes to `multi-eu`. See the [Pulse model card](/models/model-cards/speech-to-text/pulse) for the full table.
-
- encoding : typing.Optional[TranscribePulseWavesRequestEncoding]
- Audio encoding of the bytes you upload. Mirrors the `encoding`
- parameter on the realtime WS endpoint.
-
- - `linear16`, `linear32` — raw PCM (16-bit and 32-bit)
- - `alaw`, `mulaw` — 8 kHz telephony codecs
- - `opus`, `ogg_opus` — Opus compressed audio (raw and Ogg container)
-
- When omitted, the server detects the format from the file's
- container header (works for `.wav`, `.mp3`, `.flac`, `.ogg`,
- `.m4a`, `.webm`).
-
- webhook_url : typing.Optional[str]
-
- webhook_extra : typing.Optional[str]
-
- word_timestamps : typing.Optional[bool]
- Whether to include word and utterance level timestamps in the response
-
- diarize : typing.Optional[bool]
- Whether to perform speaker diarization
-
- gender_detection : typing.Optional[TranscribePulseWavesRequestGenderDetection]
- Whether to predict the gender of the speaker
-
- emotion_detection : typing.Optional[TranscribePulseWavesRequestEmotionDetection]
- Whether to predict speaker emotions
-
- format : typing.Optional[TranscribePulseWavesRequestFormat]
- Master formatting switch for the transcript. When `false`, forces
- `punctuate=false`, `capitalize=false`, and also disables Inverse
- Text Normalization (ITN) so it cannot silently reintroduce
- punctuation or casing.
-
- When `true`, the `punctuate` and `capitalize` params take effect
- independently. Leave `format=true` and use those two to fine-tune.
-
- punctuate : typing.Optional[TranscribePulseWavesRequestPunctuate]
- When `false`, strips end-of-sentence punctuation (`.`, `,`, `?`, `!`)
- from the transcript, `words[].word`, and
- `utterances[].transcript`. Does not affect casing — use
- `capitalize` for that. Overridden to `false` when `format=false`.
-
- capitalize : typing.Optional[TranscribePulseWavesRequestCapitalize]
- When `false`, lowercases the entire transcript output (transcript,
- `words[].word`, and `utterances[].transcript`). Does not affect
- punctuation — use `punctuate` for that. Overridden to `false`
- when `format=false`.
-
+ display_name : str
+ Human-readable name for the voice clone.
+
+ file : core.File
+ See core.File for more documentation
+
+ description : typing.Optional[str]
+ Optional longer description for the voice clone.
+
+ accent : typing.Optional[str]
+ Optional accent tag (e.g. "general", "indian").
+
+ tags : typing.Optional[str]
+ Optional comma-separated list of tags. Server splits on
+ commas and trims whitespace (`"en, tone-test"` → `["en", "tone-test"]`).
+
+ language : typing.Optional[str]
+ Primary language the clone will be used for. Optional, but
+ **strongly recommended** — set it to the language of your
+ reference audio. The TTS request's `language` should also
+ match this code; setting it now avoids silent language
+ mismatches at inference time.
+
+ Must be one of the languages supported by `lightning-v3.1`
+ (e.g. `en`, `hi`). The server validates and rejects
+ unsupported codes with a 400.
+
+ model : typing.Optional[CreateVoiceCloneWavesRequestModel]
+ Voice cloning model. Defaults to `lightning-v3.1`.
+ `lightning-v2` is accepted by the schema for historical
+ reasons but is deprecated — the server returns 400 with
+ `"Voice cloning for lightning-v2 is deprecated. Please use lightning-v3.1"`.
+
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
-
+
Returns
-------
- HttpResponse[TranscribePulseWavesResponse]
- Speech transcribed successfully
+ HttpResponse[CreateVoiceCloneWavesResponse]
+ Voice clone created. Includes pre-generated sample clips of the new voice.
"""
_response = self._client_wrapper.httpx_client.request(
- "waves/v1/pulse/get_text",
+ "waves/v1/voice-cloning",
base_url=self._client_wrapper.get_environment().waves,
method="POST",
- params={
+ data={
+ "displayName": display_name,
+ "description": description,
+ "accent": accent,
+ "tags": tags,
"language": language,
- "encoding": encoding,
- "webhook_url": webhook_url,
- "webhook_extra": webhook_extra,
- "word_timestamps": word_timestamps,
- "diarize": diarize,
- "gender_detection": gender_detection,
- "emotion_detection": emotion_detection,
- "format": format,
- "punctuate": punctuate,
- "capitalize": capitalize,
+ "model": model,
},
- content=request,
- headers={
- "content-type": "application/octet-stream",
+ files={
+ "file": file,
},
request_options=request_options,
omit=OMIT,
+ force_multipart=True,
)
try:
if 200 <= _response.status_code < 300:
_data = typing.cast(
- TranscribePulseWavesResponse,
+ CreateVoiceCloneWavesResponse,
construct_type(
- type_=TranscribePulseWavesResponse, # type: ignore
+ type_=CreateVoiceCloneWavesResponse, # type: ignore
object_=_response.json(),
),
)
@@ -2774,203 +2719,152 @@ async def _iter():
yield await _stream()
- async def transcribe_pulse(
+ async def list_voice_clones(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> AsyncHttpResponse[ListVoiceClonesWavesResponse]:
+ """
+ Retrieve all voice clones in your organization.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[ListVoiceClonesWavesResponse]
+ List of voice clones.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "waves/v1/voice-cloning",
+ base_url=self._client_wrapper.get_environment().waves,
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ ListVoiceClonesWavesResponse,
+ construct_type(
+ type_=ListVoiceClonesWavesResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # 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 create_voice_clone(
self,
*,
- request: typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]],
- language: typing.Optional[TranscribePulseWavesRequestLanguage] = None,
- encoding: typing.Optional[TranscribePulseWavesRequestEncoding] = None,
- webhook_url: typing.Optional[str] = None,
- webhook_extra: typing.Optional[str] = None,
- word_timestamps: typing.Optional[bool] = None,
- diarize: typing.Optional[bool] = None,
- gender_detection: typing.Optional[TranscribePulseWavesRequestGenderDetection] = None,
- emotion_detection: typing.Optional[TranscribePulseWavesRequestEmotionDetection] = None,
- format: typing.Optional[TranscribePulseWavesRequestFormat] = None,
- punctuate: typing.Optional[TranscribePulseWavesRequestPunctuate] = None,
- capitalize: typing.Optional[TranscribePulseWavesRequestCapitalize] = None,
+ display_name: str,
+ file: core.File,
+ description: typing.Optional[str] = OMIT,
+ accent: typing.Optional[str] = OMIT,
+ tags: typing.Optional[str] = OMIT,
+ language: typing.Optional[str] = OMIT,
+ model: typing.Optional[CreateVoiceCloneWavesRequestModel] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
- ) -> AsyncHttpResponse[TranscribePulseWavesResponse]:
+ ) -> AsyncHttpResponse[CreateVoiceCloneWavesResponse]:
"""
- Transcribe an audio file to text using the Pulse model. The fastest way to get a transcript when you already have a recording — pass either the raw bytes or a URL.
-
- ## When to use this
-
- Use this endpoint when you have a complete audio file (call recording, voicemail, podcast episode) and want the transcript back in one response. For live transcription as audio arrives, use the realtime WebSocket endpoint (`WSS /waves/v1/pulse/get_text`) instead.
-
- ## Input methods
-
- Send the audio in one of two ways:
-
- 1. **Raw bytes** — `Content-Type: application/octet-stream` with the audio in the body. All knobs (`language`, `word_timestamps`, etc.) are query parameters.
- 2. **URL** — `Content-Type: application/json` with `{"url": "..."}` in the body. Useful when the audio already lives in object storage. Same query parameters apply.
-
- Pulse autodetects the language across 30+ supported locales. Pass `language` explicitly when you already know it — detection is fast but skipping it is faster.
-
- ## Examples
-
- **cURL** (raw bytes)
- ```bash
- curl -X POST "https://api.smallest.ai/waves/v1/pulse/get_text?language=en&word_timestamps=true" \\
- -H "Authorization: Bearer $SMALLEST_API_KEY" \\
- -H "Content-Type: application/octet-stream" \\
- --data-binary "@./call.wav"
- ```
-
- **cURL** (URL)
- ```bash
- curl -X POST "https://api.smallest.ai/waves/v1/pulse/get_text?language=en" \\
- -H "Authorization: Bearer $SMALLEST_API_KEY" \\
- -H "Content-Type: application/json" \\
- -d '{"url": "https://your-bucket.s3.amazonaws.com/call.wav"}'
- ```
-
- **Python** (`pip install smallestai>=4.4.0`)
- ```python
- from smallestai import SmallestAI
-
- client = SmallestAI(api_key="YOUR_API_KEY")
- with open("./call.wav", "rb") as f:
- result = client.waves.transcribe_pulse(
- request=f.read(),
- language="en",
- word_timestamps=True,
- diarize=True,
- )
- print(result.status) # "success"
- print(result.transcription) # the transcript string
- ```
-
- **JavaScript / TypeScript** (using `fetch`)
- ```typescript
- import { readFileSync } from "node:fs";
-
- const audio = readFileSync("./call.wav");
- const params = new URLSearchParams({ language: "en", word_timestamps: "true", diarize: "true" });
-
- const res = await fetch(`https://api.smallest.ai/waves/v1/pulse/get_text?${params}`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${process.env.SMALLEST_API_KEY}`,
- "Content-Type": "application/octet-stream",
- },
- body: audio,
- });
- const result = await res.json();
- console.log(result.transcription);
- ```
-
- ## Common gotchas
-
- - **Max file size is 250 MB.** Larger files return HTTP `400` with `{errors: "Audio data too large", status: "error", message: "Error handling audio data"}`. Compress to mono 16 kHz PCM if you're close to the limit; quality is unaffected.
- - **Formatting flags (`format`, `punctuate`, `capitalize`)** are accepted at the wire level and exposed in the Python SDK as of `smallestai>=4.4.0`. Today they currently return the same transcript regardless of value — pass them in your integration so it works as the behavior changes.
- - **Webhook-driven flow**: pass `webhook_url` to receive the transcript asynchronously. The endpoint returns immediately; the transcript hits your webhook when ready. Useful for long files where you don't want to hold an HTTP connection open.
- - **Speaker diarization** (`diarize=true`) adds latency. Skip it if you only need the words.
- - **JavaScript / TypeScript**: the official `smallestai` npm package predates the Pulse model, so call this endpoint with `fetch` or `axios` as shown above.
-
+ Create an instant voice clone in a single call. Defaults to `lightning-v3.1`.
+
Parameters
----------
- request : typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]
-
- language : typing.Optional[TranscribePulseWavesRequestLanguage]
- Language of the audio file. Set explicitly to the known language for best accuracy.
-
- **26 single-language codes** on this endpoint: `en`, `hi`, `de`, `es`, `ru`, `it`, `fr`, `nl`, `pt`, `uk`, `pl`, `cs`, `sk`, `lv`, `et`, `ro`, `fi`, `sv`, `bg`, `hu`, `da`, `lt`, `mt`, `zh`, `ja`, `ko`.
-
- **Regional auto-detect aggregators** for unknown audio:
- - `multi-eu` (default) — auto-detects across all 21 European codes above plus `en`.
- - `multi-asian` — auto-detects across `zh`, `ko`, `ja`, `en`.
- - `multi-indic`: auto-detects across `en`, `hi`, `gu`, `mr`, `bn`, `or`. India region only.
-
- Omitting `language` routes to `multi-eu`. See the [Pulse model card](/models/model-cards/speech-to-text/pulse) for the full table.
-
- encoding : typing.Optional[TranscribePulseWavesRequestEncoding]
- Audio encoding of the bytes you upload. Mirrors the `encoding`
- parameter on the realtime WS endpoint.
-
- - `linear16`, `linear32` — raw PCM (16-bit and 32-bit)
- - `alaw`, `mulaw` — 8 kHz telephony codecs
- - `opus`, `ogg_opus` — Opus compressed audio (raw and Ogg container)
-
- When omitted, the server detects the format from the file's
- container header (works for `.wav`, `.mp3`, `.flac`, `.ogg`,
- `.m4a`, `.webm`).
-
- webhook_url : typing.Optional[str]
-
- webhook_extra : typing.Optional[str]
-
- word_timestamps : typing.Optional[bool]
- Whether to include word and utterance level timestamps in the response
-
- diarize : typing.Optional[bool]
- Whether to perform speaker diarization
-
- gender_detection : typing.Optional[TranscribePulseWavesRequestGenderDetection]
- Whether to predict the gender of the speaker
-
- emotion_detection : typing.Optional[TranscribePulseWavesRequestEmotionDetection]
- Whether to predict speaker emotions
-
- format : typing.Optional[TranscribePulseWavesRequestFormat]
- Master formatting switch for the transcript. When `false`, forces
- `punctuate=false`, `capitalize=false`, and also disables Inverse
- Text Normalization (ITN) so it cannot silently reintroduce
- punctuation or casing.
-
- When `true`, the `punctuate` and `capitalize` params take effect
- independently. Leave `format=true` and use those two to fine-tune.
-
- punctuate : typing.Optional[TranscribePulseWavesRequestPunctuate]
- When `false`, strips end-of-sentence punctuation (`.`, `,`, `?`, `!`)
- from the transcript, `words[].word`, and
- `utterances[].transcript`. Does not affect casing — use
- `capitalize` for that. Overridden to `false` when `format=false`.
-
- capitalize : typing.Optional[TranscribePulseWavesRequestCapitalize]
- When `false`, lowercases the entire transcript output (transcript,
- `words[].word`, and `utterances[].transcript`). Does not affect
- punctuation — use `punctuate` for that. Overridden to `false`
- when `format=false`.
-
+ display_name : str
+ Human-readable name for the voice clone.
+
+ file : core.File
+ See core.File for more documentation
+
+ description : typing.Optional[str]
+ Optional longer description for the voice clone.
+
+ accent : typing.Optional[str]
+ Optional accent tag (e.g. "general", "indian").
+
+ tags : typing.Optional[str]
+ Optional comma-separated list of tags. Server splits on
+ commas and trims whitespace (`"en, tone-test"` → `["en", "tone-test"]`).
+
+ language : typing.Optional[str]
+ Primary language the clone will be used for. Optional, but
+ **strongly recommended** — set it to the language of your
+ reference audio. The TTS request's `language` should also
+ match this code; setting it now avoids silent language
+ mismatches at inference time.
+
+ Must be one of the languages supported by `lightning-v3.1`
+ (e.g. `en`, `hi`). The server validates and rejects
+ unsupported codes with a 400.
+
+ model : typing.Optional[CreateVoiceCloneWavesRequestModel]
+ Voice cloning model. Defaults to `lightning-v3.1`.
+ `lightning-v2` is accepted by the schema for historical
+ reasons but is deprecated — the server returns 400 with
+ `"Voice cloning for lightning-v2 is deprecated. Please use lightning-v3.1"`.
+
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
-
+
Returns
-------
- AsyncHttpResponse[TranscribePulseWavesResponse]
- Speech transcribed successfully
+ AsyncHttpResponse[CreateVoiceCloneWavesResponse]
+ Voice clone created. Includes pre-generated sample clips of the new voice.
"""
_response = await self._client_wrapper.httpx_client.request(
- "waves/v1/pulse/get_text",
+ "waves/v1/voice-cloning",
base_url=self._client_wrapper.get_environment().waves,
method="POST",
- params={
+ data={
+ "displayName": display_name,
+ "description": description,
+ "accent": accent,
+ "tags": tags,
"language": language,
- "encoding": encoding,
- "webhook_url": webhook_url,
- "webhook_extra": webhook_extra,
- "word_timestamps": word_timestamps,
- "diarize": diarize,
- "gender_detection": gender_detection,
- "emotion_detection": emotion_detection,
- "format": format,
- "punctuate": punctuate,
- "capitalize": capitalize,
+ "model": model,
},
- content=request,
- headers={
- "content-type": "application/octet-stream",
+ files={
+ "file": file,
},
request_options=request_options,
omit=OMIT,
+ force_multipart=True,
)
try:
if 200 <= _response.status_code < 300:
_data = typing.cast(
- TranscribePulseWavesResponse,
+ CreateVoiceCloneWavesResponse,
construct_type(
- type_=TranscribePulseWavesResponse, # type: ignore
+ type_=CreateVoiceCloneWavesResponse, # type: ignore
object_=_response.json(),
),
)
diff --git a/src/smallestai/waves/speech_to_text/__init__.py b/src/smallestai/waves/speech_to_text/__init__.py
new file mode 100644
index 00000000..41d51f66
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/__init__.py
@@ -0,0 +1,91 @@
+# This file was auto-generated by Fern from our API Definition.
+
+# isort: skip_file
+
+import typing
+from importlib import import_module
+
+if typing.TYPE_CHECKING:
+ from .types import (
+ CloseStream,
+ CloseStreamType,
+ FinalizeSignal,
+ FinalizeSignalType,
+ TranscribeRequestEmotionDetection,
+ TranscribeRequestGenderDetection,
+ TranscribeRequestLanguage,
+ TranscribeRequestModel,
+ TranscribeRequestRedactPci,
+ TranscribeRequestRedactPii,
+ TranscribeRequestWebhookMethod,
+ TranscribeResponse,
+ TranscriptionErrorEvent,
+ TranscriptionErrorEventType,
+ TranscriptionEvent,
+ TranscriptionEventType,
+ TranscriptionEventUtterancesItem,
+ TranscriptionEventWordsItem,
+ )
+_dynamic_imports: typing.Dict[str, str] = {
+ "CloseStream": ".types",
+ "CloseStreamType": ".types",
+ "FinalizeSignal": ".types",
+ "FinalizeSignalType": ".types",
+ "TranscribeRequestEmotionDetection": ".types",
+ "TranscribeRequestGenderDetection": ".types",
+ "TranscribeRequestLanguage": ".types",
+ "TranscribeRequestModel": ".types",
+ "TranscribeRequestRedactPci": ".types",
+ "TranscribeRequestRedactPii": ".types",
+ "TranscribeRequestWebhookMethod": ".types",
+ "TranscribeResponse": ".types",
+ "TranscriptionErrorEvent": ".types",
+ "TranscriptionErrorEventType": ".types",
+ "TranscriptionEvent": ".types",
+ "TranscriptionEventType": ".types",
+ "TranscriptionEventUtterancesItem": ".types",
+ "TranscriptionEventWordsItem": ".types",
+}
+
+
+def __getattr__(attr_name: str) -> typing.Any:
+ module_name = _dynamic_imports.get(attr_name)
+ if module_name is None:
+ raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}")
+ try:
+ module = import_module(module_name, __package__)
+ if module_name == f".{attr_name}":
+ return module
+ else:
+ return getattr(module, attr_name)
+ except ImportError as e:
+ raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e
+ except AttributeError as e:
+ raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e
+
+
+def __dir__():
+ lazy_attrs = list(_dynamic_imports.keys())
+ return sorted(lazy_attrs)
+
+
+__all__ = [
+ "CloseStream",
+ "CloseStreamType",
+ "FinalizeSignal",
+ "FinalizeSignalType",
+ "TranscribeRequestEmotionDetection",
+ "TranscribeRequestGenderDetection",
+ "TranscribeRequestLanguage",
+ "TranscribeRequestModel",
+ "TranscribeRequestRedactPci",
+ "TranscribeRequestRedactPii",
+ "TranscribeRequestWebhookMethod",
+ "TranscribeResponse",
+ "TranscriptionErrorEvent",
+ "TranscriptionErrorEventType",
+ "TranscriptionEvent",
+ "TranscriptionEventType",
+ "TranscriptionEventUtterancesItem",
+ "TranscriptionEventWordsItem",
+]
diff --git a/src/smallestai/waves/speech_to_text/client.py b/src/smallestai/waves/speech_to_text/client.py
new file mode 100644
index 00000000..b56e85f3
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/client.py
@@ -0,0 +1,742 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+import urllib.parse
+from contextlib import asynccontextmanager, contextmanager
+
+import websockets.sync.client as websockets_sync_client
+from ...core.api_error import ApiError
+from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ...core.jsonable_encoder import jsonable_encoder
+from ...core.query_encoder import encode_query
+from ...core.remove_none_from_dict import remove_none_from_dict
+from ...core.request_options import RequestOptions
+from ...core.websocket_compat import InvalidWebSocketStatus, get_status_code
+from .raw_client import AsyncRawSpeechToTextClient, RawSpeechToTextClient
+from .socket_client import AsyncSpeechToTextSocketClient, SpeechToTextSocketClient
+from .types.transcribe_request_emotion_detection import TranscribeRequestEmotionDetection
+from .types.transcribe_request_gender_detection import TranscribeRequestGenderDetection
+from .types.transcribe_request_language import TranscribeRequestLanguage
+from .types.transcribe_request_model import TranscribeRequestModel
+from .types.transcribe_request_redact_pci import TranscribeRequestRedactPci
+from .types.transcribe_request_redact_pii import TranscribeRequestRedactPii
+from .types.transcribe_request_webhook_method import TranscribeRequestWebhookMethod
+from .types.transcribe_response import TranscribeResponse
+
+try:
+ from websockets.legacy.client import connect as websockets_client_connect # type: ignore
+except ImportError:
+ from websockets import connect as websockets_client_connect # type: ignore
+
+# this is used as the default value for optional parameters
+OMIT = typing.cast(typing.Any, ...)
+
+
+class SpeechToTextClient:
+ def __init__(self, *, client_wrapper: SyncClientWrapper):
+ self._raw_client = RawSpeechToTextClient(client_wrapper=client_wrapper)
+
+ @property
+ def with_raw_response(self) -> RawSpeechToTextClient:
+ """
+ Retrieves a raw implementation of this client that returns raw responses.
+
+ Returns
+ -------
+ RawSpeechToTextClient
+ """
+ return self._raw_client
+
+ def transcribe(
+ self,
+ *,
+ model: TranscribeRequestModel,
+ language: TranscribeRequestLanguage,
+ request: typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]],
+ word_timestamps: typing.Optional[bool] = None,
+ diarize: typing.Optional[bool] = None,
+ webhook_url: typing.Optional[str] = None,
+ webhook_method: typing.Optional[TranscribeRequestWebhookMethod] = None,
+ webhook_extra: typing.Optional[str] = None,
+ redact_pii: typing.Optional[TranscribeRequestRedactPii] = None,
+ redact_pci: typing.Optional[TranscribeRequestRedactPci] = None,
+ emotion_detection: typing.Optional[TranscribeRequestEmotionDetection] = None,
+ gender_detection: typing.Optional[TranscribeRequestGenderDetection] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> TranscribeResponse:
+ """
+ Transcribe an audio file. The model is chosen via `?model=`:
+
+ - `?model=pulse-pro`: English-only, leaderboard-ranked accuracy. Raw bytes only; pass `webhook_url` to receive transcription asynchronously on long files.
+ - `?model=pulse`: multilingual transcription (21 streaming + 26 pre-recorded languages), supports both raw bytes and audio-by-URL.
+
+ ## When to use this
+
+ Use this endpoint when you have a complete audio file (call recording, voicemail, podcast episode) and want the transcript back in one response. For live transcription as audio arrives, use the realtime WebSocket endpoint (`WS /waves/v1/stt/live`) instead.
+
+ Pulse Pro has no streaming worker today; calls to `WS /waves/v1/stt/live?model=pulse-pro` return `400` before the WebSocket upgrades.
+
+ ## Input methods
+
+ - **Raw bytes**: `Content-Type: application/octet-stream` with the audio in the body. All knobs are query parameters.
+ - **URL (`?model=pulse` only)**: `Content-Type: application/json` with `{"url": "..."}` in the body.
+
+ ## Examples
+
+ **cURL**: Pulse Pro, sync
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse-pro&language=en&word_timestamps=true" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/octet-stream" \\
+ --data-binary "@./call.wav"
+ ```
+
+ **cURL**: Pulse Pro, async via webhook
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse-pro&language=en&webhook_url=https://your.app/cb" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/octet-stream" \\
+ --data-binary "@./call.wav"
+ ```
+ Returns `200 { "status": "processing", "request_id": "..." }` immediately. The webhook receives the full transcription when ready.
+
+ **cURL**: Pulse, audio-by-URL
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse&language=en" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/json" \\
+ -d '{"url": "https://your-bucket.s3.amazonaws.com/call.wav"}'
+ ```
+
+ **Python**
+ ```python
+ import requests
+
+ with open("./call.wav", "rb") as f:
+ audio = f.read()
+
+ r = requests.post(
+ "https://api.smallest.ai/waves/v1/stt/",
+ params={"model": "pulse-pro", "language": "en", "word_timestamps": "true"},
+ headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/octet-stream"},
+ data=audio,
+ )
+ r.raise_for_status()
+ print(r.json()["transcription"])
+ ```
+
+ **JavaScript / TypeScript**
+ ```typescript
+ import { readFileSync } from "node:fs";
+
+ const audio = readFileSync("./call.wav");
+ const params = new URLSearchParams({ model: "pulse-pro", language: "en", word_timestamps: "true" });
+
+ const res = await fetch(`https://api.smallest.ai/waves/v1/stt/?${params}`, {
+ method: "POST",
+ headers: { Authorization: `Bearer ${process.env.SMALLEST_API_KEY}`, "Content-Type": "application/octet-stream" },
+ body: audio,
+ });
+ console.log((await res.json()).transcription);
+ ```
+
+ ## Common gotchas
+
+ - **`model` is required.** Missing or invalid values return `400` with an enum-validation error.
+ - **Pulse Pro is English only.** Pass `language=en`. Other language codes are accepted at the wire level but produce unpredictable output.
+ - **Pulse Pro does not support audio-by-URL.** Send raw bytes or use `?model=pulse` for the URL flow.
+ - **Async (webhook) mode is Pulse Pro only.** Pulse runs sync only on this endpoint.
+ - **Max payload 250 MB.** Larger requests return `413`. Compress to mono 16 kHz PCM if you are close to the limit; quality is unaffected.
+
+ Parameters
+ ----------
+ model : TranscribeRequestModel
+ Selects which ASR model handles the request. Required; missing or invalid values return `400`.
+
+ - `pulse-pro`: English only, leaderboard-ranked accuracy, raw bytes only; supports async via `webhook_url`.
+ - `pulse`: multilingual (39 languages), raw bytes OR URL.
+
+ language : TranscribeRequestLanguage
+ Language of the audio file. This endpoint is **Pre-Recorded (HTTP)** — for streaming, switch to `WSS /waves/v1/stt/live` (different supported language set).
+
+ **26 single-language codes:** `en`, `hi`, `de`, `es`, `ru`, `it`, `fr`, `nl`, `pt`, `uk`, `pl`, `cs`, `sk`, `lv`, `et`, `ro`, `fi`, `sv`, `bg`, `hu`, `da`, `lt`, `mt`, `zh`, `ja`, `ko`.
+
+ **Regional auto-detect aggregators** for unknown audio:
+ - `multi-eu` — auto-detects across all 21 European codes plus `en`.
+ - `multi-asian` — auto-detects across `zh`, `ko`, `ja`, `en`.
+ - `multi-indic`: auto-detects across `en`, `hi`, `gu`, `mr`, `bn`, `or`. India region only.
+
+ - **Pulse Pro**: pass `en`.
+ - **Pulse**: pass any of the single-language codes above, or use the `multi-eu` / `multi-asian` / `multi-indic` aggregator for unknown audio. See the [Pulse model card](/models/model-cards/speech-to-text/pulse) for the full table with language names.
+
+ request : typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]
+
+ word_timestamps : typing.Optional[bool]
+ Include the per-word `words[]` array in the response — each entry carries the recognized `word`, its `start`/`end` timestamps, and a per-word `confidence` score (0.0–1.0). With `diarize=true`, entries also include `speaker`. On Pulse Pro this costs roughly one-third of throughput.
+
+ diarize : typing.Optional[bool]
+ Multi-speaker identification; adds per-word and per-utterance speaker labels.
+
+ webhook_url : typing.Optional[str]
+ Pulse Pro only. If set, the response is `200` with `{"status": "processing", "request_id": "..."}` immediately, and the full transcription is delivered to this URL when ready. Use for long files where you do not want to hold an HTTP connection open.
+
+ webhook_method : typing.Optional[TranscribeRequestWebhookMethod]
+ HTTP method to use when calling the webhook. Pulse Pro only.
+
+ webhook_extra : typing.Optional[str]
+ Arbitrary metadata returned to the webhook in addition to the transcription payload. Pulse Pro only.
+
+ redact_pii : typing.Optional[TranscribeRequestRedactPii]
+ Redact personally identifiable information from the transcript.
+ Names → `[FIRSTNAME_*]` / `[LASTNAME_*]`, phone numbers →
+ `[PHONENUMBER_*]`, addresses → `[ADDRESS_*]`, etc. The redaction
+ tokens use sequential indices so multiple occurrences of the same
+ entity get distinct labels (`[FIRSTNAME_1]`, `[FIRSTNAME_2]`).
+
+ **Language support:** currently effective only on `en` and `hi`.
+ Setting `redact_pii=true` on other language codes is accepted
+ but does not redact.
+
+ redact_pci : typing.Optional[TranscribeRequestRedactPci]
+ Redact payment card information (credit-card numbers, CVV, account
+ numbers, etc.). Replaces matches with `[ACCOUNTNUMBER_*]` tokens.
+ Use alongside `redact_pii=true` for full PCI-compliant transcript
+ handling.
+
+ **Language support:** currently effective only on `en` and `hi`.
+ Setting `redact_pci=true` on other language codes is accepted
+ but does not redact.
+
+ emotion_detection : typing.Optional[TranscribeRequestEmotionDetection]
+ When `true`, the response adds an `emotions` object mapping detected
+ emotion labels to confidence scores. Useful for voice-of-customer
+ analytics on call recordings.
+
+ gender_detection : typing.Optional[TranscribeRequestGenderDetection]
+ When `true`, the response adds a `gender` field with the detected
+ speaker gender label. Pulse pre-recorded only.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ TranscribeResponse
+ Transcription succeeded. The response body has two shapes:
+
+ - **Sync**: full `TranscriptionResponse` with `transcription`, `words`, `metadata`, etc. Returned when `webhook_url` is not set (all `?model=pulse` requests, and `?model=pulse-pro` requests without a webhook).
+ - **Async**: `{ "status": "processing", "request_id": "..." }`. Returned when `?model=pulse-pro` is paired with `webhook_url`. The full `TranscriptionResponse` then arrives on the webhook when ready.
+ """
+ _response = self._raw_client.transcribe(
+ model=model,
+ language=language,
+ request=request,
+ word_timestamps=word_timestamps,
+ diarize=diarize,
+ webhook_url=webhook_url,
+ webhook_method=webhook_method,
+ webhook_extra=webhook_extra,
+ redact_pii=redact_pii,
+ redact_pci=redact_pci,
+ emotion_detection=emotion_detection,
+ gender_detection=gender_detection,
+ request_options=request_options,
+ )
+ return _response.data
+
+ @contextmanager
+ def stream(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> typing.Iterator[SpeechToTextSocketClient]:
+ """
+ Real-time speech-to-text over a persistent WebSocket. The fit-for-purpose path for live captioning, voice agents, and any flow where you need partial transcripts as the user is still speaking.
+
+ ## When to use this
+
+ - **Use this** for live audio: microphone input, voice-agent turns, simultaneous interpretation, low-latency captioning. Partial results stream back while audio is still arriving.
+ - **Use `POST /waves/v1/stt/`** when you have a complete file. Single request, single response, less plumbing.
+
+ ## Model selection
+
+ Today only `?model=pulse` is supported on the streaming endpoint. `?model=pulse-pro` is rejected with `400` before WebSocket upgrade because Pulse Pro has no streaming worker; use `POST /waves/v1/stt/?model=pulse-pro` (HTTP) instead.
+
+ ## How it works
+
+ 1. Open a WebSocket to `wss://api.smallest.ai/waves/v1/stt/live` with `Authorization: Bearer ` and the session params (`model`, `language`, `sample_rate`, `encoding`, etc.) as query string.
+ 2. Stream raw PCM (or your chosen `encoding`) over the socket as binary frames.
+ 3. The server pushes back JSON `transcription` messages with `is_final: false` partial results as audio streams and `is_final: true` when an utterance closes.
+ 4. Send a control message when the user pauses or the session ends:
+ - **`{"type":"finalize"}`** — *turn-boundary signal*. Flushes the current audio buffer, emits one `is_final: true` transcript for that turn, and **keeps the WebSocket open** for the next user turn. Use this once per turn in a multi-turn voice agent.
+ - **`{"type":"close_stream"}`** — *session-end signal*. Flushes remaining audio, emits the terminal `is_final: true` + `is_last: true` transcript, then closes the WebSocket. Use this once, at the actual end of the session (call end, app shutdown, or after a single-shot transcription buffer is fully streamed).
+
+ A multi-turn voice agent typically fires many `finalize` messages and exactly one `close_stream`. A one-off transcription of a fixed audio buffer fires only `close_stream`.
+
+ ## Examples
+
+ **Python — multi-turn voice agent (recommended for Voice AI)**
+
+ Send `finalize` per user turn so the WebSocket stays open across the whole call — you pay the connection cost once, not per turn:
+
+ ```python
+ import asyncio, json, websockets
+
+ URL = "wss://api.smallest.ai/waves/v1/stt/live?model=pulse&language=en&sample_rate=16000&encoding=linear16&itn_normalize=true&finalize_on_words=false&eou_timeout_ms=1000"
+ HEADERS = {"Authorization": f"Bearer {API_KEY}"}
+
+ async def run_voice_agent(audio_source, llm_reply, stop_event):
+ async with websockets.connect(URL, additional_headers=HEADERS) as ws:
+ async def stream_audio():
+ async for frame in audio_source:
+ if stop_event.is_set(): return
+ await ws.send(frame)
+
+ # Call this when your VAD detects end-of-turn (user paused)
+ async def end_of_turn():
+ await ws.send(json.dumps({"type": "finalize"}))
+
+ async def consume():
+ async for msg in ws:
+ data = json.loads(msg)
+ if data.get("is_last"): break # only fires after close_stream
+ if data.get("is_final"):
+ await llm_reply(data["transcript"]) # ITN-normalized full turn
+
+ producer = asyncio.create_task(stream_audio())
+ consumer = asyncio.create_task(consume())
+ await stop_event.wait() # end of call
+
+ await ws.send(json.dumps({"type": "close_stream"}))
+ await consumer
+ producer.cancel()
+ ```
+
+ **Python — single-shot transcription**
+
+ For one-off transcription of a complete audio buffer (file, single utterance) where no further audio is coming, send `close_stream` directly after the last chunk:
+
+ ```python
+ import asyncio, json, websockets
+
+ URL = "wss://api.smallest.ai/waves/v1/stt/live?model=pulse&language=en&sample_rate=16000&encoding=linear16"
+ HEADERS = {"Authorization": f"Bearer {API_KEY}"}
+
+ async def transcribe_once(audio_bytes):
+ async with websockets.connect(URL, additional_headers=HEADERS) as ws:
+ for i in range(0, len(audio_bytes), 4096):
+ await ws.send(audio_bytes[i:i+4096])
+ await ws.send(json.dumps({"type": "close_stream"}))
+ async for msg in ws:
+ data = json.loads(msg)
+ if data.get("is_final"):
+ print(data["transcript"])
+ if data.get("is_last"):
+ break
+
+ asyncio.run(transcribe_once(open("audio.pcm", "rb").read()))
+ ```
+
+ ## Common gotchas
+
+ - **`model` is required.** Missing or invalid values return `400` before the WebSocket upgrades.
+ - **Match `sample_rate` to your audio.** The server does not resample; mismatched rates produce garbage transcripts.
+ - **Existing clients** on `wss://api.smallest.ai/waves/v1/pulse/get_text` continue to work alongside this unified path.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ SpeechToTextSocketClient
+ """
+ ws_url = self._raw_client._client_wrapper.get_environment().waves_ws + "/waves/v1/stt/live"
+ _encoded_query_params = encode_query(
+ jsonable_encoder(
+ remove_none_from_dict(
+ {
+ **(
+ request_options.get("additional_query_parameters", {}) or {}
+ if request_options is not None
+ else {}
+ ),
+ }
+ )
+ )
+ )
+ if _encoded_query_params:
+ ws_url = ws_url + "?" + urllib.parse.urlencode(_encoded_query_params)
+ headers = self._raw_client._client_wrapper.get_headers()
+ if request_options and "additional_headers" in request_options:
+ headers.update(request_options["additional_headers"])
+ try:
+ with websockets_sync_client.connect(ws_url, additional_headers=headers) as protocol:
+ yield SpeechToTextSocketClient(websocket=protocol)
+ except InvalidWebSocketStatus as exc:
+ status_code: int = get_status_code(exc)
+ if status_code == 401:
+ raise ApiError(
+ status_code=status_code,
+ headers=dict(headers),
+ body="Websocket initialized with invalid credentials.",
+ )
+ raise ApiError(
+ status_code=status_code,
+ headers=dict(headers),
+ body="Unexpected error when initializing websocket connection.",
+ )
+
+
+class AsyncSpeechToTextClient:
+ def __init__(self, *, client_wrapper: AsyncClientWrapper):
+ self._raw_client = AsyncRawSpeechToTextClient(client_wrapper=client_wrapper)
+
+ @property
+ def with_raw_response(self) -> AsyncRawSpeechToTextClient:
+ """
+ Retrieves a raw implementation of this client that returns raw responses.
+
+ Returns
+ -------
+ AsyncRawSpeechToTextClient
+ """
+ return self._raw_client
+
+ async def transcribe(
+ self,
+ *,
+ model: TranscribeRequestModel,
+ language: TranscribeRequestLanguage,
+ request: typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]],
+ word_timestamps: typing.Optional[bool] = None,
+ diarize: typing.Optional[bool] = None,
+ webhook_url: typing.Optional[str] = None,
+ webhook_method: typing.Optional[TranscribeRequestWebhookMethod] = None,
+ webhook_extra: typing.Optional[str] = None,
+ redact_pii: typing.Optional[TranscribeRequestRedactPii] = None,
+ redact_pci: typing.Optional[TranscribeRequestRedactPci] = None,
+ emotion_detection: typing.Optional[TranscribeRequestEmotionDetection] = None,
+ gender_detection: typing.Optional[TranscribeRequestGenderDetection] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> TranscribeResponse:
+ """
+ Transcribe an audio file. The model is chosen via `?model=`:
+
+ - `?model=pulse-pro`: English-only, leaderboard-ranked accuracy. Raw bytes only; pass `webhook_url` to receive transcription asynchronously on long files.
+ - `?model=pulse`: multilingual transcription (21 streaming + 26 pre-recorded languages), supports both raw bytes and audio-by-URL.
+
+ ## When to use this
+
+ Use this endpoint when you have a complete audio file (call recording, voicemail, podcast episode) and want the transcript back in one response. For live transcription as audio arrives, use the realtime WebSocket endpoint (`WS /waves/v1/stt/live`) instead.
+
+ Pulse Pro has no streaming worker today; calls to `WS /waves/v1/stt/live?model=pulse-pro` return `400` before the WebSocket upgrades.
+
+ ## Input methods
+
+ - **Raw bytes**: `Content-Type: application/octet-stream` with the audio in the body. All knobs are query parameters.
+ - **URL (`?model=pulse` only)**: `Content-Type: application/json` with `{"url": "..."}` in the body.
+
+ ## Examples
+
+ **cURL**: Pulse Pro, sync
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse-pro&language=en&word_timestamps=true" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/octet-stream" \\
+ --data-binary "@./call.wav"
+ ```
+
+ **cURL**: Pulse Pro, async via webhook
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse-pro&language=en&webhook_url=https://your.app/cb" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/octet-stream" \\
+ --data-binary "@./call.wav"
+ ```
+ Returns `200 { "status": "processing", "request_id": "..." }` immediately. The webhook receives the full transcription when ready.
+
+ **cURL**: Pulse, audio-by-URL
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse&language=en" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/json" \\
+ -d '{"url": "https://your-bucket.s3.amazonaws.com/call.wav"}'
+ ```
+
+ **Python**
+ ```python
+ import requests
+
+ with open("./call.wav", "rb") as f:
+ audio = f.read()
+
+ r = requests.post(
+ "https://api.smallest.ai/waves/v1/stt/",
+ params={"model": "pulse-pro", "language": "en", "word_timestamps": "true"},
+ headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/octet-stream"},
+ data=audio,
+ )
+ r.raise_for_status()
+ print(r.json()["transcription"])
+ ```
+
+ **JavaScript / TypeScript**
+ ```typescript
+ import { readFileSync } from "node:fs";
+
+ const audio = readFileSync("./call.wav");
+ const params = new URLSearchParams({ model: "pulse-pro", language: "en", word_timestamps: "true" });
+
+ const res = await fetch(`https://api.smallest.ai/waves/v1/stt/?${params}`, {
+ method: "POST",
+ headers: { Authorization: `Bearer ${process.env.SMALLEST_API_KEY}`, "Content-Type": "application/octet-stream" },
+ body: audio,
+ });
+ console.log((await res.json()).transcription);
+ ```
+
+ ## Common gotchas
+
+ - **`model` is required.** Missing or invalid values return `400` with an enum-validation error.
+ - **Pulse Pro is English only.** Pass `language=en`. Other language codes are accepted at the wire level but produce unpredictable output.
+ - **Pulse Pro does not support audio-by-URL.** Send raw bytes or use `?model=pulse` for the URL flow.
+ - **Async (webhook) mode is Pulse Pro only.** Pulse runs sync only on this endpoint.
+ - **Max payload 250 MB.** Larger requests return `413`. Compress to mono 16 kHz PCM if you are close to the limit; quality is unaffected.
+
+ Parameters
+ ----------
+ model : TranscribeRequestModel
+ Selects which ASR model handles the request. Required; missing or invalid values return `400`.
+
+ - `pulse-pro`: English only, leaderboard-ranked accuracy, raw bytes only; supports async via `webhook_url`.
+ - `pulse`: multilingual (39 languages), raw bytes OR URL.
+
+ language : TranscribeRequestLanguage
+ Language of the audio file. This endpoint is **Pre-Recorded (HTTP)** — for streaming, switch to `WSS /waves/v1/stt/live` (different supported language set).
+
+ **26 single-language codes:** `en`, `hi`, `de`, `es`, `ru`, `it`, `fr`, `nl`, `pt`, `uk`, `pl`, `cs`, `sk`, `lv`, `et`, `ro`, `fi`, `sv`, `bg`, `hu`, `da`, `lt`, `mt`, `zh`, `ja`, `ko`.
+
+ **Regional auto-detect aggregators** for unknown audio:
+ - `multi-eu` — auto-detects across all 21 European codes plus `en`.
+ - `multi-asian` — auto-detects across `zh`, `ko`, `ja`, `en`.
+ - `multi-indic`: auto-detects across `en`, `hi`, `gu`, `mr`, `bn`, `or`. India region only.
+
+ - **Pulse Pro**: pass `en`.
+ - **Pulse**: pass any of the single-language codes above, or use the `multi-eu` / `multi-asian` / `multi-indic` aggregator for unknown audio. See the [Pulse model card](/models/model-cards/speech-to-text/pulse) for the full table with language names.
+
+ request : typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]
+
+ word_timestamps : typing.Optional[bool]
+ Include the per-word `words[]` array in the response — each entry carries the recognized `word`, its `start`/`end` timestamps, and a per-word `confidence` score (0.0–1.0). With `diarize=true`, entries also include `speaker`. On Pulse Pro this costs roughly one-third of throughput.
+
+ diarize : typing.Optional[bool]
+ Multi-speaker identification; adds per-word and per-utterance speaker labels.
+
+ webhook_url : typing.Optional[str]
+ Pulse Pro only. If set, the response is `200` with `{"status": "processing", "request_id": "..."}` immediately, and the full transcription is delivered to this URL when ready. Use for long files where you do not want to hold an HTTP connection open.
+
+ webhook_method : typing.Optional[TranscribeRequestWebhookMethod]
+ HTTP method to use when calling the webhook. Pulse Pro only.
+
+ webhook_extra : typing.Optional[str]
+ Arbitrary metadata returned to the webhook in addition to the transcription payload. Pulse Pro only.
+
+ redact_pii : typing.Optional[TranscribeRequestRedactPii]
+ Redact personally identifiable information from the transcript.
+ Names → `[FIRSTNAME_*]` / `[LASTNAME_*]`, phone numbers →
+ `[PHONENUMBER_*]`, addresses → `[ADDRESS_*]`, etc. The redaction
+ tokens use sequential indices so multiple occurrences of the same
+ entity get distinct labels (`[FIRSTNAME_1]`, `[FIRSTNAME_2]`).
+
+ **Language support:** currently effective only on `en` and `hi`.
+ Setting `redact_pii=true` on other language codes is accepted
+ but does not redact.
+
+ redact_pci : typing.Optional[TranscribeRequestRedactPci]
+ Redact payment card information (credit-card numbers, CVV, account
+ numbers, etc.). Replaces matches with `[ACCOUNTNUMBER_*]` tokens.
+ Use alongside `redact_pii=true` for full PCI-compliant transcript
+ handling.
+
+ **Language support:** currently effective only on `en` and `hi`.
+ Setting `redact_pci=true` on other language codes is accepted
+ but does not redact.
+
+ emotion_detection : typing.Optional[TranscribeRequestEmotionDetection]
+ When `true`, the response adds an `emotions` object mapping detected
+ emotion labels to confidence scores. Useful for voice-of-customer
+ analytics on call recordings.
+
+ gender_detection : typing.Optional[TranscribeRequestGenderDetection]
+ When `true`, the response adds a `gender` field with the detected
+ speaker gender label. Pulse pre-recorded only.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ TranscribeResponse
+ Transcription succeeded. The response body has two shapes:
+
+ - **Sync**: full `TranscriptionResponse` with `transcription`, `words`, `metadata`, etc. Returned when `webhook_url` is not set (all `?model=pulse` requests, and `?model=pulse-pro` requests without a webhook).
+ - **Async**: `{ "status": "processing", "request_id": "..." }`. Returned when `?model=pulse-pro` is paired with `webhook_url`. The full `TranscriptionResponse` then arrives on the webhook when ready.
+ """
+ _response = await self._raw_client.transcribe(
+ model=model,
+ language=language,
+ request=request,
+ word_timestamps=word_timestamps,
+ diarize=diarize,
+ webhook_url=webhook_url,
+ webhook_method=webhook_method,
+ webhook_extra=webhook_extra,
+ redact_pii=redact_pii,
+ redact_pci=redact_pci,
+ emotion_detection=emotion_detection,
+ gender_detection=gender_detection,
+ request_options=request_options,
+ )
+ return _response.data
+
+ @asynccontextmanager
+ async def stream(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> typing.AsyncIterator[AsyncSpeechToTextSocketClient]:
+ """
+ Real-time speech-to-text over a persistent WebSocket. The fit-for-purpose path for live captioning, voice agents, and any flow where you need partial transcripts as the user is still speaking.
+
+ ## When to use this
+
+ - **Use this** for live audio: microphone input, voice-agent turns, simultaneous interpretation, low-latency captioning. Partial results stream back while audio is still arriving.
+ - **Use `POST /waves/v1/stt/`** when you have a complete file. Single request, single response, less plumbing.
+
+ ## Model selection
+
+ Today only `?model=pulse` is supported on the streaming endpoint. `?model=pulse-pro` is rejected with `400` before WebSocket upgrade because Pulse Pro has no streaming worker; use `POST /waves/v1/stt/?model=pulse-pro` (HTTP) instead.
+
+ ## How it works
+
+ 1. Open a WebSocket to `wss://api.smallest.ai/waves/v1/stt/live` with `Authorization: Bearer ` and the session params (`model`, `language`, `sample_rate`, `encoding`, etc.) as query string.
+ 2. Stream raw PCM (or your chosen `encoding`) over the socket as binary frames.
+ 3. The server pushes back JSON `transcription` messages with `is_final: false` partial results as audio streams and `is_final: true` when an utterance closes.
+ 4. Send a control message when the user pauses or the session ends:
+ - **`{"type":"finalize"}`** — *turn-boundary signal*. Flushes the current audio buffer, emits one `is_final: true` transcript for that turn, and **keeps the WebSocket open** for the next user turn. Use this once per turn in a multi-turn voice agent.
+ - **`{"type":"close_stream"}`** — *session-end signal*. Flushes remaining audio, emits the terminal `is_final: true` + `is_last: true` transcript, then closes the WebSocket. Use this once, at the actual end of the session (call end, app shutdown, or after a single-shot transcription buffer is fully streamed).
+
+ A multi-turn voice agent typically fires many `finalize` messages and exactly one `close_stream`. A one-off transcription of a fixed audio buffer fires only `close_stream`.
+
+ ## Examples
+
+ **Python — multi-turn voice agent (recommended for Voice AI)**
+
+ Send `finalize` per user turn so the WebSocket stays open across the whole call — you pay the connection cost once, not per turn:
+
+ ```python
+ import asyncio, json, websockets
+
+ URL = "wss://api.smallest.ai/waves/v1/stt/live?model=pulse&language=en&sample_rate=16000&encoding=linear16&itn_normalize=true&finalize_on_words=false&eou_timeout_ms=1000"
+ HEADERS = {"Authorization": f"Bearer {API_KEY}"}
+
+ async def run_voice_agent(audio_source, llm_reply, stop_event):
+ async with websockets.connect(URL, additional_headers=HEADERS) as ws:
+ async def stream_audio():
+ async for frame in audio_source:
+ if stop_event.is_set(): return
+ await ws.send(frame)
+
+ # Call this when your VAD detects end-of-turn (user paused)
+ async def end_of_turn():
+ await ws.send(json.dumps({"type": "finalize"}))
+
+ async def consume():
+ async for msg in ws:
+ data = json.loads(msg)
+ if data.get("is_last"): break # only fires after close_stream
+ if data.get("is_final"):
+ await llm_reply(data["transcript"]) # ITN-normalized full turn
+
+ producer = asyncio.create_task(stream_audio())
+ consumer = asyncio.create_task(consume())
+ await stop_event.wait() # end of call
+
+ await ws.send(json.dumps({"type": "close_stream"}))
+ await consumer
+ producer.cancel()
+ ```
+
+ **Python — single-shot transcription**
+
+ For one-off transcription of a complete audio buffer (file, single utterance) where no further audio is coming, send `close_stream` directly after the last chunk:
+
+ ```python
+ import asyncio, json, websockets
+
+ URL = "wss://api.smallest.ai/waves/v1/stt/live?model=pulse&language=en&sample_rate=16000&encoding=linear16"
+ HEADERS = {"Authorization": f"Bearer {API_KEY}"}
+
+ async def transcribe_once(audio_bytes):
+ async with websockets.connect(URL, additional_headers=HEADERS) as ws:
+ for i in range(0, len(audio_bytes), 4096):
+ await ws.send(audio_bytes[i:i+4096])
+ await ws.send(json.dumps({"type": "close_stream"}))
+ async for msg in ws:
+ data = json.loads(msg)
+ if data.get("is_final"):
+ print(data["transcript"])
+ if data.get("is_last"):
+ break
+
+ asyncio.run(transcribe_once(open("audio.pcm", "rb").read()))
+ ```
+
+ ## Common gotchas
+
+ - **`model` is required.** Missing or invalid values return `400` before the WebSocket upgrades.
+ - **Match `sample_rate` to your audio.** The server does not resample; mismatched rates produce garbage transcripts.
+ - **Existing clients** on `wss://api.smallest.ai/waves/v1/pulse/get_text` continue to work alongside this unified path.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncSpeechToTextSocketClient
+ """
+ ws_url = self._raw_client._client_wrapper.get_environment().waves_ws + "/waves/v1/stt/live"
+ _encoded_query_params = encode_query(
+ jsonable_encoder(
+ remove_none_from_dict(
+ {
+ **(
+ request_options.get("additional_query_parameters", {}) or {}
+ if request_options is not None
+ else {}
+ ),
+ }
+ )
+ )
+ )
+ if _encoded_query_params:
+ ws_url = ws_url + "?" + urllib.parse.urlencode(_encoded_query_params)
+ headers = self._raw_client._client_wrapper.get_headers()
+ if request_options and "additional_headers" in request_options:
+ headers.update(request_options["additional_headers"])
+ try:
+ async with websockets_client_connect(ws_url, extra_headers=headers) as protocol:
+ yield AsyncSpeechToTextSocketClient(websocket=protocol)
+ except InvalidWebSocketStatus as exc:
+ status_code: int = get_status_code(exc)
+ if status_code == 401:
+ raise ApiError(
+ status_code=status_code,
+ headers=dict(headers),
+ body="Websocket initialized with invalid credentials.",
+ )
+ raise ApiError(
+ status_code=status_code,
+ headers=dict(headers),
+ body="Unexpected error when initializing websocket connection.",
+ )
diff --git a/src/smallestai/waves/speech_to_text/raw_client.py b/src/smallestai/waves/speech_to_text/raw_client.py
new file mode 100644
index 00000000..38e3237d
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/raw_client.py
@@ -0,0 +1,915 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+import urllib.parse
+from contextlib import asynccontextmanager, contextmanager
+from json.decoder import JSONDecodeError
+
+import websockets.sync.client as websockets_sync_client
+from ...core.api_error import ApiError
+from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ...core.http_response import AsyncHttpResponse, HttpResponse
+from ...core.jsonable_encoder import jsonable_encoder
+from ...core.parse_error import ParsingError
+from ...core.query_encoder import encode_query
+from ...core.remove_none_from_dict import remove_none_from_dict
+from ...core.request_options import RequestOptions
+from ...core.unchecked_base_model import construct_type
+from ...core.websocket_compat import InvalidWebSocketStatus, get_status_code
+from ..errors.bad_request_error import BadRequestError
+from ..errors.content_too_large_error import ContentTooLargeError
+from ..errors.forbidden_error import ForbiddenError
+from ..errors.service_unavailable_error import ServiceUnavailableError
+from ..errors.too_many_requests_error import TooManyRequestsError
+from ..errors.unauthorized_error import UnauthorizedError
+from ..types.stt_error_response import SttErrorResponse
+from .socket_client import AsyncSpeechToTextSocketClient, SpeechToTextSocketClient
+from .types.transcribe_request_emotion_detection import TranscribeRequestEmotionDetection
+from .types.transcribe_request_gender_detection import TranscribeRequestGenderDetection
+from .types.transcribe_request_language import TranscribeRequestLanguage
+from .types.transcribe_request_model import TranscribeRequestModel
+from .types.transcribe_request_redact_pci import TranscribeRequestRedactPci
+from .types.transcribe_request_redact_pii import TranscribeRequestRedactPii
+from .types.transcribe_request_webhook_method import TranscribeRequestWebhookMethod
+from .types.transcribe_response import TranscribeResponse
+from pydantic import ValidationError
+
+try:
+ from websockets.legacy.client import connect as websockets_client_connect # type: ignore
+except ImportError:
+ from websockets import connect as websockets_client_connect # type: ignore
+
+# this is used as the default value for optional parameters
+OMIT = typing.cast(typing.Any, ...)
+
+
+class RawSpeechToTextClient:
+ def __init__(self, *, client_wrapper: SyncClientWrapper):
+ self._client_wrapper = client_wrapper
+
+ def transcribe(
+ self,
+ *,
+ model: TranscribeRequestModel,
+ language: TranscribeRequestLanguage,
+ request: typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]],
+ word_timestamps: typing.Optional[bool] = None,
+ diarize: typing.Optional[bool] = None,
+ webhook_url: typing.Optional[str] = None,
+ webhook_method: typing.Optional[TranscribeRequestWebhookMethod] = None,
+ webhook_extra: typing.Optional[str] = None,
+ redact_pii: typing.Optional[TranscribeRequestRedactPii] = None,
+ redact_pci: typing.Optional[TranscribeRequestRedactPci] = None,
+ emotion_detection: typing.Optional[TranscribeRequestEmotionDetection] = None,
+ gender_detection: typing.Optional[TranscribeRequestGenderDetection] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[TranscribeResponse]:
+ """
+ Transcribe an audio file. The model is chosen via `?model=`:
+
+ - `?model=pulse-pro`: English-only, leaderboard-ranked accuracy. Raw bytes only; pass `webhook_url` to receive transcription asynchronously on long files.
+ - `?model=pulse`: multilingual transcription (21 streaming + 26 pre-recorded languages), supports both raw bytes and audio-by-URL.
+
+ ## When to use this
+
+ Use this endpoint when you have a complete audio file (call recording, voicemail, podcast episode) and want the transcript back in one response. For live transcription as audio arrives, use the realtime WebSocket endpoint (`WS /waves/v1/stt/live`) instead.
+
+ Pulse Pro has no streaming worker today; calls to `WS /waves/v1/stt/live?model=pulse-pro` return `400` before the WebSocket upgrades.
+
+ ## Input methods
+
+ - **Raw bytes**: `Content-Type: application/octet-stream` with the audio in the body. All knobs are query parameters.
+ - **URL (`?model=pulse` only)**: `Content-Type: application/json` with `{"url": "..."}` in the body.
+
+ ## Examples
+
+ **cURL**: Pulse Pro, sync
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse-pro&language=en&word_timestamps=true" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/octet-stream" \\
+ --data-binary "@./call.wav"
+ ```
+
+ **cURL**: Pulse Pro, async via webhook
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse-pro&language=en&webhook_url=https://your.app/cb" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/octet-stream" \\
+ --data-binary "@./call.wav"
+ ```
+ Returns `200 { "status": "processing", "request_id": "..." }` immediately. The webhook receives the full transcription when ready.
+
+ **cURL**: Pulse, audio-by-URL
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse&language=en" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/json" \\
+ -d '{"url": "https://your-bucket.s3.amazonaws.com/call.wav"}'
+ ```
+
+ **Python**
+ ```python
+ import requests
+
+ with open("./call.wav", "rb") as f:
+ audio = f.read()
+
+ r = requests.post(
+ "https://api.smallest.ai/waves/v1/stt/",
+ params={"model": "pulse-pro", "language": "en", "word_timestamps": "true"},
+ headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/octet-stream"},
+ data=audio,
+ )
+ r.raise_for_status()
+ print(r.json()["transcription"])
+ ```
+
+ **JavaScript / TypeScript**
+ ```typescript
+ import { readFileSync } from "node:fs";
+
+ const audio = readFileSync("./call.wav");
+ const params = new URLSearchParams({ model: "pulse-pro", language: "en", word_timestamps: "true" });
+
+ const res = await fetch(`https://api.smallest.ai/waves/v1/stt/?${params}`, {
+ method: "POST",
+ headers: { Authorization: `Bearer ${process.env.SMALLEST_API_KEY}`, "Content-Type": "application/octet-stream" },
+ body: audio,
+ });
+ console.log((await res.json()).transcription);
+ ```
+
+ ## Common gotchas
+
+ - **`model` is required.** Missing or invalid values return `400` with an enum-validation error.
+ - **Pulse Pro is English only.** Pass `language=en`. Other language codes are accepted at the wire level but produce unpredictable output.
+ - **Pulse Pro does not support audio-by-URL.** Send raw bytes or use `?model=pulse` for the URL flow.
+ - **Async (webhook) mode is Pulse Pro only.** Pulse runs sync only on this endpoint.
+ - **Max payload 250 MB.** Larger requests return `413`. Compress to mono 16 kHz PCM if you are close to the limit; quality is unaffected.
+
+ Parameters
+ ----------
+ model : TranscribeRequestModel
+ Selects which ASR model handles the request. Required; missing or invalid values return `400`.
+
+ - `pulse-pro`: English only, leaderboard-ranked accuracy, raw bytes only; supports async via `webhook_url`.
+ - `pulse`: multilingual (39 languages), raw bytes OR URL.
+
+ language : TranscribeRequestLanguage
+ Language of the audio file. This endpoint is **Pre-Recorded (HTTP)** — for streaming, switch to `WSS /waves/v1/stt/live` (different supported language set).
+
+ **26 single-language codes:** `en`, `hi`, `de`, `es`, `ru`, `it`, `fr`, `nl`, `pt`, `uk`, `pl`, `cs`, `sk`, `lv`, `et`, `ro`, `fi`, `sv`, `bg`, `hu`, `da`, `lt`, `mt`, `zh`, `ja`, `ko`.
+
+ **Regional auto-detect aggregators** for unknown audio:
+ - `multi-eu` — auto-detects across all 21 European codes plus `en`.
+ - `multi-asian` — auto-detects across `zh`, `ko`, `ja`, `en`.
+ - `multi-indic`: auto-detects across `en`, `hi`, `gu`, `mr`, `bn`, `or`. India region only.
+
+ - **Pulse Pro**: pass `en`.
+ - **Pulse**: pass any of the single-language codes above, or use the `multi-eu` / `multi-asian` / `multi-indic` aggregator for unknown audio. See the [Pulse model card](/models/model-cards/speech-to-text/pulse) for the full table with language names.
+
+ request : typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]
+
+ word_timestamps : typing.Optional[bool]
+ Include the per-word `words[]` array in the response — each entry carries the recognized `word`, its `start`/`end` timestamps, and a per-word `confidence` score (0.0–1.0). With `diarize=true`, entries also include `speaker`. On Pulse Pro this costs roughly one-third of throughput.
+
+ diarize : typing.Optional[bool]
+ Multi-speaker identification; adds per-word and per-utterance speaker labels.
+
+ webhook_url : typing.Optional[str]
+ Pulse Pro only. If set, the response is `200` with `{"status": "processing", "request_id": "..."}` immediately, and the full transcription is delivered to this URL when ready. Use for long files where you do not want to hold an HTTP connection open.
+
+ webhook_method : typing.Optional[TranscribeRequestWebhookMethod]
+ HTTP method to use when calling the webhook. Pulse Pro only.
+
+ webhook_extra : typing.Optional[str]
+ Arbitrary metadata returned to the webhook in addition to the transcription payload. Pulse Pro only.
+
+ redact_pii : typing.Optional[TranscribeRequestRedactPii]
+ Redact personally identifiable information from the transcript.
+ Names → `[FIRSTNAME_*]` / `[LASTNAME_*]`, phone numbers →
+ `[PHONENUMBER_*]`, addresses → `[ADDRESS_*]`, etc. The redaction
+ tokens use sequential indices so multiple occurrences of the same
+ entity get distinct labels (`[FIRSTNAME_1]`, `[FIRSTNAME_2]`).
+
+ **Language support:** currently effective only on `en` and `hi`.
+ Setting `redact_pii=true` on other language codes is accepted
+ but does not redact.
+
+ redact_pci : typing.Optional[TranscribeRequestRedactPci]
+ Redact payment card information (credit-card numbers, CVV, account
+ numbers, etc.). Replaces matches with `[ACCOUNTNUMBER_*]` tokens.
+ Use alongside `redact_pii=true` for full PCI-compliant transcript
+ handling.
+
+ **Language support:** currently effective only on `en` and `hi`.
+ Setting `redact_pci=true` on other language codes is accepted
+ but does not redact.
+
+ emotion_detection : typing.Optional[TranscribeRequestEmotionDetection]
+ When `true`, the response adds an `emotions` object mapping detected
+ emotion labels to confidence scores. Useful for voice-of-customer
+ analytics on call recordings.
+
+ gender_detection : typing.Optional[TranscribeRequestGenderDetection]
+ When `true`, the response adds a `gender` field with the detected
+ speaker gender label. Pulse pre-recorded only.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[TranscribeResponse]
+ Transcription succeeded. The response body has two shapes:
+
+ - **Sync**: full `TranscriptionResponse` with `transcription`, `words`, `metadata`, etc. Returned when `webhook_url` is not set (all `?model=pulse` requests, and `?model=pulse-pro` requests without a webhook).
+ - **Async**: `{ "status": "processing", "request_id": "..." }`. Returned when `?model=pulse-pro` is paired with `webhook_url`. The full `TranscriptionResponse` then arrives on the webhook when ready.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "waves/v1/stt/",
+ base_url=self._client_wrapper.get_environment().waves,
+ method="POST",
+ params={
+ "model": model,
+ "language": language,
+ "word_timestamps": word_timestamps,
+ "diarize": diarize,
+ "webhook_url": webhook_url,
+ "webhook_method": webhook_method,
+ "webhook_extra": webhook_extra,
+ "redact_pii": redact_pii,
+ "redact_pci": redact_pci,
+ "emotion_detection": emotion_detection,
+ "gender_detection": gender_detection,
+ },
+ content=request,
+ headers={
+ "content-type": "application/octet-stream",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ TranscribeResponse,
+ construct_type(
+ type_=TranscribeResponse, # 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,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 413:
+ raise ContentTooLargeError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ SttErrorResponse,
+ construct_type(
+ type_=SttErrorResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 503:
+ raise ServiceUnavailableError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # 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)
+
+ @contextmanager
+ def stream(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> typing.Iterator[SpeechToTextSocketClient]:
+ """
+ Real-time speech-to-text over a persistent WebSocket. The fit-for-purpose path for live captioning, voice agents, and any flow where you need partial transcripts as the user is still speaking.
+
+ ## When to use this
+
+ - **Use this** for live audio: microphone input, voice-agent turns, simultaneous interpretation, low-latency captioning. Partial results stream back while audio is still arriving.
+ - **Use `POST /waves/v1/stt/`** when you have a complete file. Single request, single response, less plumbing.
+
+ ## Model selection
+
+ Today only `?model=pulse` is supported on the streaming endpoint. `?model=pulse-pro` is rejected with `400` before WebSocket upgrade because Pulse Pro has no streaming worker; use `POST /waves/v1/stt/?model=pulse-pro` (HTTP) instead.
+
+ ## How it works
+
+ 1. Open a WebSocket to `wss://api.smallest.ai/waves/v1/stt/live` with `Authorization: Bearer ` and the session params (`model`, `language`, `sample_rate`, `encoding`, etc.) as query string.
+ 2. Stream raw PCM (or your chosen `encoding`) over the socket as binary frames.
+ 3. The server pushes back JSON `transcription` messages with `is_final: false` partial results as audio streams and `is_final: true` when an utterance closes.
+ 4. Send a control message when the user pauses or the session ends:
+ - **`{"type":"finalize"}`** — *turn-boundary signal*. Flushes the current audio buffer, emits one `is_final: true` transcript for that turn, and **keeps the WebSocket open** for the next user turn. Use this once per turn in a multi-turn voice agent.
+ - **`{"type":"close_stream"}`** — *session-end signal*. Flushes remaining audio, emits the terminal `is_final: true` + `is_last: true` transcript, then closes the WebSocket. Use this once, at the actual end of the session (call end, app shutdown, or after a single-shot transcription buffer is fully streamed).
+
+ A multi-turn voice agent typically fires many `finalize` messages and exactly one `close_stream`. A one-off transcription of a fixed audio buffer fires only `close_stream`.
+
+ ## Examples
+
+ **Python — multi-turn voice agent (recommended for Voice AI)**
+
+ Send `finalize` per user turn so the WebSocket stays open across the whole call — you pay the connection cost once, not per turn:
+
+ ```python
+ import asyncio, json, websockets
+
+ URL = "wss://api.smallest.ai/waves/v1/stt/live?model=pulse&language=en&sample_rate=16000&encoding=linear16&itn_normalize=true&finalize_on_words=false&eou_timeout_ms=1000"
+ HEADERS = {"Authorization": f"Bearer {API_KEY}"}
+
+ async def run_voice_agent(audio_source, llm_reply, stop_event):
+ async with websockets.connect(URL, additional_headers=HEADERS) as ws:
+ async def stream_audio():
+ async for frame in audio_source:
+ if stop_event.is_set(): return
+ await ws.send(frame)
+
+ # Call this when your VAD detects end-of-turn (user paused)
+ async def end_of_turn():
+ await ws.send(json.dumps({"type": "finalize"}))
+
+ async def consume():
+ async for msg in ws:
+ data = json.loads(msg)
+ if data.get("is_last"): break # only fires after close_stream
+ if data.get("is_final"):
+ await llm_reply(data["transcript"]) # ITN-normalized full turn
+
+ producer = asyncio.create_task(stream_audio())
+ consumer = asyncio.create_task(consume())
+ await stop_event.wait() # end of call
+
+ await ws.send(json.dumps({"type": "close_stream"}))
+ await consumer
+ producer.cancel()
+ ```
+
+ **Python — single-shot transcription**
+
+ For one-off transcription of a complete audio buffer (file, single utterance) where no further audio is coming, send `close_stream` directly after the last chunk:
+
+ ```python
+ import asyncio, json, websockets
+
+ URL = "wss://api.smallest.ai/waves/v1/stt/live?model=pulse&language=en&sample_rate=16000&encoding=linear16"
+ HEADERS = {"Authorization": f"Bearer {API_KEY}"}
+
+ async def transcribe_once(audio_bytes):
+ async with websockets.connect(URL, additional_headers=HEADERS) as ws:
+ for i in range(0, len(audio_bytes), 4096):
+ await ws.send(audio_bytes[i:i+4096])
+ await ws.send(json.dumps({"type": "close_stream"}))
+ async for msg in ws:
+ data = json.loads(msg)
+ if data.get("is_final"):
+ print(data["transcript"])
+ if data.get("is_last"):
+ break
+
+ asyncio.run(transcribe_once(open("audio.pcm", "rb").read()))
+ ```
+
+ ## Common gotchas
+
+ - **`model` is required.** Missing or invalid values return `400` before the WebSocket upgrades.
+ - **Match `sample_rate` to your audio.** The server does not resample; mismatched rates produce garbage transcripts.
+ - **Existing clients** on `wss://api.smallest.ai/waves/v1/pulse/get_text` continue to work alongside this unified path.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ SpeechToTextSocketClient
+ """
+ ws_url = self._client_wrapper.get_environment().waves_ws + "/waves/v1/stt/live"
+ _encoded_query_params = encode_query(
+ jsonable_encoder(
+ remove_none_from_dict(
+ {
+ **(
+ request_options.get("additional_query_parameters", {}) or {}
+ if request_options is not None
+ else {}
+ ),
+ }
+ )
+ )
+ )
+ if _encoded_query_params:
+ ws_url = ws_url + "?" + urllib.parse.urlencode(_encoded_query_params)
+ headers = self._client_wrapper.get_headers()
+ if request_options and "additional_headers" in request_options:
+ headers.update(request_options["additional_headers"])
+ try:
+ with websockets_sync_client.connect(ws_url, additional_headers=headers) as protocol:
+ yield SpeechToTextSocketClient(websocket=protocol)
+ except InvalidWebSocketStatus as exc:
+ status_code: int = get_status_code(exc)
+ if status_code == 401:
+ raise ApiError(
+ status_code=status_code,
+ headers=dict(headers),
+ body="Websocket initialized with invalid credentials.",
+ )
+ raise ApiError(
+ status_code=status_code,
+ headers=dict(headers),
+ body="Unexpected error when initializing websocket connection.",
+ )
+
+
+class AsyncRawSpeechToTextClient:
+ def __init__(self, *, client_wrapper: AsyncClientWrapper):
+ self._client_wrapper = client_wrapper
+
+ async def transcribe(
+ self,
+ *,
+ model: TranscribeRequestModel,
+ language: TranscribeRequestLanguage,
+ request: typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]],
+ word_timestamps: typing.Optional[bool] = None,
+ diarize: typing.Optional[bool] = None,
+ webhook_url: typing.Optional[str] = None,
+ webhook_method: typing.Optional[TranscribeRequestWebhookMethod] = None,
+ webhook_extra: typing.Optional[str] = None,
+ redact_pii: typing.Optional[TranscribeRequestRedactPii] = None,
+ redact_pci: typing.Optional[TranscribeRequestRedactPci] = None,
+ emotion_detection: typing.Optional[TranscribeRequestEmotionDetection] = None,
+ gender_detection: typing.Optional[TranscribeRequestGenderDetection] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> AsyncHttpResponse[TranscribeResponse]:
+ """
+ Transcribe an audio file. The model is chosen via `?model=`:
+
+ - `?model=pulse-pro`: English-only, leaderboard-ranked accuracy. Raw bytes only; pass `webhook_url` to receive transcription asynchronously on long files.
+ - `?model=pulse`: multilingual transcription (21 streaming + 26 pre-recorded languages), supports both raw bytes and audio-by-URL.
+
+ ## When to use this
+
+ Use this endpoint when you have a complete audio file (call recording, voicemail, podcast episode) and want the transcript back in one response. For live transcription as audio arrives, use the realtime WebSocket endpoint (`WS /waves/v1/stt/live`) instead.
+
+ Pulse Pro has no streaming worker today; calls to `WS /waves/v1/stt/live?model=pulse-pro` return `400` before the WebSocket upgrades.
+
+ ## Input methods
+
+ - **Raw bytes**: `Content-Type: application/octet-stream` with the audio in the body. All knobs are query parameters.
+ - **URL (`?model=pulse` only)**: `Content-Type: application/json` with `{"url": "..."}` in the body.
+
+ ## Examples
+
+ **cURL**: Pulse Pro, sync
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse-pro&language=en&word_timestamps=true" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/octet-stream" \\
+ --data-binary "@./call.wav"
+ ```
+
+ **cURL**: Pulse Pro, async via webhook
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse-pro&language=en&webhook_url=https://your.app/cb" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/octet-stream" \\
+ --data-binary "@./call.wav"
+ ```
+ Returns `200 { "status": "processing", "request_id": "..." }` immediately. The webhook receives the full transcription when ready.
+
+ **cURL**: Pulse, audio-by-URL
+ ```bash
+ curl -X POST "https://api.smallest.ai/waves/v1/stt/?model=pulse&language=en" \\
+ -H "Authorization: Bearer $SMALLEST_API_KEY" \\
+ -H "Content-Type: application/json" \\
+ -d '{"url": "https://your-bucket.s3.amazonaws.com/call.wav"}'
+ ```
+
+ **Python**
+ ```python
+ import requests
+
+ with open("./call.wav", "rb") as f:
+ audio = f.read()
+
+ r = requests.post(
+ "https://api.smallest.ai/waves/v1/stt/",
+ params={"model": "pulse-pro", "language": "en", "word_timestamps": "true"},
+ headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/octet-stream"},
+ data=audio,
+ )
+ r.raise_for_status()
+ print(r.json()["transcription"])
+ ```
+
+ **JavaScript / TypeScript**
+ ```typescript
+ import { readFileSync } from "node:fs";
+
+ const audio = readFileSync("./call.wav");
+ const params = new URLSearchParams({ model: "pulse-pro", language: "en", word_timestamps: "true" });
+
+ const res = await fetch(`https://api.smallest.ai/waves/v1/stt/?${params}`, {
+ method: "POST",
+ headers: { Authorization: `Bearer ${process.env.SMALLEST_API_KEY}`, "Content-Type": "application/octet-stream" },
+ body: audio,
+ });
+ console.log((await res.json()).transcription);
+ ```
+
+ ## Common gotchas
+
+ - **`model` is required.** Missing or invalid values return `400` with an enum-validation error.
+ - **Pulse Pro is English only.** Pass `language=en`. Other language codes are accepted at the wire level but produce unpredictable output.
+ - **Pulse Pro does not support audio-by-URL.** Send raw bytes or use `?model=pulse` for the URL flow.
+ - **Async (webhook) mode is Pulse Pro only.** Pulse runs sync only on this endpoint.
+ - **Max payload 250 MB.** Larger requests return `413`. Compress to mono 16 kHz PCM if you are close to the limit; quality is unaffected.
+
+ Parameters
+ ----------
+ model : TranscribeRequestModel
+ Selects which ASR model handles the request. Required; missing or invalid values return `400`.
+
+ - `pulse-pro`: English only, leaderboard-ranked accuracy, raw bytes only; supports async via `webhook_url`.
+ - `pulse`: multilingual (39 languages), raw bytes OR URL.
+
+ language : TranscribeRequestLanguage
+ Language of the audio file. This endpoint is **Pre-Recorded (HTTP)** — for streaming, switch to `WSS /waves/v1/stt/live` (different supported language set).
+
+ **26 single-language codes:** `en`, `hi`, `de`, `es`, `ru`, `it`, `fr`, `nl`, `pt`, `uk`, `pl`, `cs`, `sk`, `lv`, `et`, `ro`, `fi`, `sv`, `bg`, `hu`, `da`, `lt`, `mt`, `zh`, `ja`, `ko`.
+
+ **Regional auto-detect aggregators** for unknown audio:
+ - `multi-eu` — auto-detects across all 21 European codes plus `en`.
+ - `multi-asian` — auto-detects across `zh`, `ko`, `ja`, `en`.
+ - `multi-indic`: auto-detects across `en`, `hi`, `gu`, `mr`, `bn`, `or`. India region only.
+
+ - **Pulse Pro**: pass `en`.
+ - **Pulse**: pass any of the single-language codes above, or use the `multi-eu` / `multi-asian` / `multi-indic` aggregator for unknown audio. See the [Pulse model card](/models/model-cards/speech-to-text/pulse) for the full table with language names.
+
+ request : typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]
+
+ word_timestamps : typing.Optional[bool]
+ Include the per-word `words[]` array in the response — each entry carries the recognized `word`, its `start`/`end` timestamps, and a per-word `confidence` score (0.0–1.0). With `diarize=true`, entries also include `speaker`. On Pulse Pro this costs roughly one-third of throughput.
+
+ diarize : typing.Optional[bool]
+ Multi-speaker identification; adds per-word and per-utterance speaker labels.
+
+ webhook_url : typing.Optional[str]
+ Pulse Pro only. If set, the response is `200` with `{"status": "processing", "request_id": "..."}` immediately, and the full transcription is delivered to this URL when ready. Use for long files where you do not want to hold an HTTP connection open.
+
+ webhook_method : typing.Optional[TranscribeRequestWebhookMethod]
+ HTTP method to use when calling the webhook. Pulse Pro only.
+
+ webhook_extra : typing.Optional[str]
+ Arbitrary metadata returned to the webhook in addition to the transcription payload. Pulse Pro only.
+
+ redact_pii : typing.Optional[TranscribeRequestRedactPii]
+ Redact personally identifiable information from the transcript.
+ Names → `[FIRSTNAME_*]` / `[LASTNAME_*]`, phone numbers →
+ `[PHONENUMBER_*]`, addresses → `[ADDRESS_*]`, etc. The redaction
+ tokens use sequential indices so multiple occurrences of the same
+ entity get distinct labels (`[FIRSTNAME_1]`, `[FIRSTNAME_2]`).
+
+ **Language support:** currently effective only on `en` and `hi`.
+ Setting `redact_pii=true` on other language codes is accepted
+ but does not redact.
+
+ redact_pci : typing.Optional[TranscribeRequestRedactPci]
+ Redact payment card information (credit-card numbers, CVV, account
+ numbers, etc.). Replaces matches with `[ACCOUNTNUMBER_*]` tokens.
+ Use alongside `redact_pii=true` for full PCI-compliant transcript
+ handling.
+
+ **Language support:** currently effective only on `en` and `hi`.
+ Setting `redact_pci=true` on other language codes is accepted
+ but does not redact.
+
+ emotion_detection : typing.Optional[TranscribeRequestEmotionDetection]
+ When `true`, the response adds an `emotions` object mapping detected
+ emotion labels to confidence scores. Useful for voice-of-customer
+ analytics on call recordings.
+
+ gender_detection : typing.Optional[TranscribeRequestGenderDetection]
+ When `true`, the response adds a `gender` field with the detected
+ speaker gender label. Pulse pre-recorded only.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[TranscribeResponse]
+ Transcription succeeded. The response body has two shapes:
+
+ - **Sync**: full `TranscriptionResponse` with `transcription`, `words`, `metadata`, etc. Returned when `webhook_url` is not set (all `?model=pulse` requests, and `?model=pulse-pro` requests without a webhook).
+ - **Async**: `{ "status": "processing", "request_id": "..." }`. Returned when `?model=pulse-pro` is paired with `webhook_url`. The full `TranscriptionResponse` then arrives on the webhook when ready.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "waves/v1/stt/",
+ base_url=self._client_wrapper.get_environment().waves,
+ method="POST",
+ params={
+ "model": model,
+ "language": language,
+ "word_timestamps": word_timestamps,
+ "diarize": diarize,
+ "webhook_url": webhook_url,
+ "webhook_method": webhook_method,
+ "webhook_extra": webhook_extra,
+ "redact_pii": redact_pii,
+ "redact_pci": redact_pci,
+ "emotion_detection": emotion_detection,
+ "gender_detection": gender_detection,
+ },
+ content=request,
+ headers={
+ "content-type": "application/octet-stream",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ TranscribeResponse,
+ construct_type(
+ type_=TranscribeResponse, # 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,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 413:
+ raise ContentTooLargeError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ SttErrorResponse,
+ construct_type(
+ type_=SttErrorResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 503:
+ raise ServiceUnavailableError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ construct_type(
+ type_=typing.Any, # 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)
+
+ @asynccontextmanager
+ async def stream(
+ self, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> typing.AsyncIterator[AsyncSpeechToTextSocketClient]:
+ """
+ Real-time speech-to-text over a persistent WebSocket. The fit-for-purpose path for live captioning, voice agents, and any flow where you need partial transcripts as the user is still speaking.
+
+ ## When to use this
+
+ - **Use this** for live audio: microphone input, voice-agent turns, simultaneous interpretation, low-latency captioning. Partial results stream back while audio is still arriving.
+ - **Use `POST /waves/v1/stt/`** when you have a complete file. Single request, single response, less plumbing.
+
+ ## Model selection
+
+ Today only `?model=pulse` is supported on the streaming endpoint. `?model=pulse-pro` is rejected with `400` before WebSocket upgrade because Pulse Pro has no streaming worker; use `POST /waves/v1/stt/?model=pulse-pro` (HTTP) instead.
+
+ ## How it works
+
+ 1. Open a WebSocket to `wss://api.smallest.ai/waves/v1/stt/live` with `Authorization: Bearer ` and the session params (`model`, `language`, `sample_rate`, `encoding`, etc.) as query string.
+ 2. Stream raw PCM (or your chosen `encoding`) over the socket as binary frames.
+ 3. The server pushes back JSON `transcription` messages with `is_final: false` partial results as audio streams and `is_final: true` when an utterance closes.
+ 4. Send a control message when the user pauses or the session ends:
+ - **`{"type":"finalize"}`** — *turn-boundary signal*. Flushes the current audio buffer, emits one `is_final: true` transcript for that turn, and **keeps the WebSocket open** for the next user turn. Use this once per turn in a multi-turn voice agent.
+ - **`{"type":"close_stream"}`** — *session-end signal*. Flushes remaining audio, emits the terminal `is_final: true` + `is_last: true` transcript, then closes the WebSocket. Use this once, at the actual end of the session (call end, app shutdown, or after a single-shot transcription buffer is fully streamed).
+
+ A multi-turn voice agent typically fires many `finalize` messages and exactly one `close_stream`. A one-off transcription of a fixed audio buffer fires only `close_stream`.
+
+ ## Examples
+
+ **Python — multi-turn voice agent (recommended for Voice AI)**
+
+ Send `finalize` per user turn so the WebSocket stays open across the whole call — you pay the connection cost once, not per turn:
+
+ ```python
+ import asyncio, json, websockets
+
+ URL = "wss://api.smallest.ai/waves/v1/stt/live?model=pulse&language=en&sample_rate=16000&encoding=linear16&itn_normalize=true&finalize_on_words=false&eou_timeout_ms=1000"
+ HEADERS = {"Authorization": f"Bearer {API_KEY}"}
+
+ async def run_voice_agent(audio_source, llm_reply, stop_event):
+ async with websockets.connect(URL, additional_headers=HEADERS) as ws:
+ async def stream_audio():
+ async for frame in audio_source:
+ if stop_event.is_set(): return
+ await ws.send(frame)
+
+ # Call this when your VAD detects end-of-turn (user paused)
+ async def end_of_turn():
+ await ws.send(json.dumps({"type": "finalize"}))
+
+ async def consume():
+ async for msg in ws:
+ data = json.loads(msg)
+ if data.get("is_last"): break # only fires after close_stream
+ if data.get("is_final"):
+ await llm_reply(data["transcript"]) # ITN-normalized full turn
+
+ producer = asyncio.create_task(stream_audio())
+ consumer = asyncio.create_task(consume())
+ await stop_event.wait() # end of call
+
+ await ws.send(json.dumps({"type": "close_stream"}))
+ await consumer
+ producer.cancel()
+ ```
+
+ **Python — single-shot transcription**
+
+ For one-off transcription of a complete audio buffer (file, single utterance) where no further audio is coming, send `close_stream` directly after the last chunk:
+
+ ```python
+ import asyncio, json, websockets
+
+ URL = "wss://api.smallest.ai/waves/v1/stt/live?model=pulse&language=en&sample_rate=16000&encoding=linear16"
+ HEADERS = {"Authorization": f"Bearer {API_KEY}"}
+
+ async def transcribe_once(audio_bytes):
+ async with websockets.connect(URL, additional_headers=HEADERS) as ws:
+ for i in range(0, len(audio_bytes), 4096):
+ await ws.send(audio_bytes[i:i+4096])
+ await ws.send(json.dumps({"type": "close_stream"}))
+ async for msg in ws:
+ data = json.loads(msg)
+ if data.get("is_final"):
+ print(data["transcript"])
+ if data.get("is_last"):
+ break
+
+ asyncio.run(transcribe_once(open("audio.pcm", "rb").read()))
+ ```
+
+ ## Common gotchas
+
+ - **`model` is required.** Missing or invalid values return `400` before the WebSocket upgrades.
+ - **Match `sample_rate` to your audio.** The server does not resample; mismatched rates produce garbage transcripts.
+ - **Existing clients** on `wss://api.smallest.ai/waves/v1/pulse/get_text` continue to work alongside this unified path.
+
+ Parameters
+ ----------
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncSpeechToTextSocketClient
+ """
+ ws_url = self._client_wrapper.get_environment().waves_ws + "/waves/v1/stt/live"
+ _encoded_query_params = encode_query(
+ jsonable_encoder(
+ remove_none_from_dict(
+ {
+ **(
+ request_options.get("additional_query_parameters", {}) or {}
+ if request_options is not None
+ else {}
+ ),
+ }
+ )
+ )
+ )
+ if _encoded_query_params:
+ ws_url = ws_url + "?" + urllib.parse.urlencode(_encoded_query_params)
+ headers = self._client_wrapper.get_headers()
+ if request_options and "additional_headers" in request_options:
+ headers.update(request_options["additional_headers"])
+ try:
+ async with websockets_client_connect(ws_url, extra_headers=headers) as protocol:
+ yield AsyncSpeechToTextSocketClient(websocket=protocol)
+ except InvalidWebSocketStatus as exc:
+ status_code: int = get_status_code(exc)
+ if status_code == 401:
+ raise ApiError(
+ status_code=status_code,
+ headers=dict(headers),
+ body="Websocket initialized with invalid credentials.",
+ )
+ raise ApiError(
+ status_code=status_code,
+ headers=dict(headers),
+ body="Unexpected error when initializing websocket connection.",
+ )
diff --git a/src/smallestai/waves/pulse_stt_streaming/socket_client.py b/src/smallestai/waves/speech_to_text/socket_client.py
similarity index 72%
rename from src/smallestai/waves/pulse_stt_streaming/socket_client.py
rename to src/smallestai/waves/speech_to_text/socket_client.py
index 7a66c6a8..e7417345 100644
--- a/src/smallestai/waves/pulse_stt_streaming/socket_client.py
+++ b/src/smallestai/waves/speech_to_text/socket_client.py
@@ -9,11 +9,10 @@
import websockets.sync.connection as websockets_sync_connection
from ...core.events import EventEmitterMixin, EventType
from ...core.unchecked_base_model import construct_type
-from .types.pulse_close_stream_signal_message import PulseCloseStreamSignalMessage
-from .types.pulse_finalize_signal_message import PulseFinalizeSignalMessage
-from .types.pulse_speech_ended_event_message import PulseSpeechEndedEventMessage
-from .types.pulse_speech_started_event_message import PulseSpeechStartedEventMessage
-from .types.pulse_transcription_response_message import PulseTranscriptionResponseMessage
+from .types.close_stream import CloseStream
+from .types.finalize_signal import FinalizeSignal
+from .types.transcription_error_event import TranscriptionErrorEvent
+from .types.transcription_event import TranscriptionEvent
try:
from websockets.legacy.client import WebSocketClientProtocol # type: ignore
@@ -21,12 +20,10 @@
from websockets import WebSocketClientProtocol # type: ignore
_logger = logging.getLogger(__name__)
-PulseSttStreamingSocketClientResponse = typing.Union[
- PulseTranscriptionResponseMessage, PulseSpeechStartedEventMessage, PulseSpeechEndedEventMessage
-]
+SpeechToTextSocketClientResponse = typing.Union[TranscriptionEvent, TranscriptionErrorEvent]
-class AsyncPulseSttStreamingSocketClient(EventEmitterMixin):
+class AsyncSpeechToTextSocketClient(EventEmitterMixin):
def __init__(self, *, websocket: WebSocketClientProtocol):
super().__init__()
self._websocket = websocket
@@ -37,7 +34,7 @@ async def __aiter__(self):
yield message
else:
try:
- yield construct_type(type_=PulseSttStreamingSocketClientResponse, object_=json.loads(message)) # type: ignore
+ yield construct_type(type_=SpeechToTextSocketClientResponse, object_=json.loads(message)) # type: ignore
except Exception:
_logger.warning(
"Skipping unknown WebSocket message; update your SDK version to support new message types."
@@ -62,7 +59,7 @@ async def start_listening(self):
else:
json_data = json.loads(raw_message)
try:
- parsed = construct_type(type_=PulseSttStreamingSocketClientResponse, object_=json_data) # type: ignore
+ parsed = construct_type(type_=SpeechToTextSocketClientResponse, object_=json_data) # type: ignore
except Exception:
_logger.warning(
"Skipping unknown WebSocket message; update your SDK version to support new message types."
@@ -74,28 +71,28 @@ async def start_listening(self):
finally:
await self._emit_async(EventType.CLOSE, None)
- async def transcribe_streaming_pulse_send_audio(self, message: bytes) -> None:
+ async def send_audio_chunk_out(self, message: bytes) -> None:
"""
Send a message to the websocket connection.
The message will be sent as a bytes.
"""
await self._send(message)
- async def transcribe_streaming_pulse_send_finalize(self, message: PulseFinalizeSignalMessage) -> None:
+ async def send_finalize_signal(self, message: FinalizeSignal) -> None:
"""
Send a message to the websocket connection.
- The message will be sent as a PulseFinalizeSignalMessage.
+ The message will be sent as a FinalizeSignal.
"""
await self._send_model(message)
- async def transcribe_streaming_pulse_send_close_stream(self, message: PulseCloseStreamSignalMessage) -> None:
+ async def send_close_stream(self, message: CloseStream) -> None:
"""
Send a message to the websocket connection.
- The message will be sent as a PulseCloseStreamSignalMessage.
+ The message will be sent as a CloseStream.
"""
await self._send_model(message)
- async def recv(self) -> PulseSttStreamingSocketClientResponse:
+ async def recv(self) -> SpeechToTextSocketClientResponse:
"""
Receive a message from the websocket connection.
"""
@@ -104,7 +101,7 @@ async def recv(self) -> PulseSttStreamingSocketClientResponse:
return data # type: ignore
json_data = json.loads(data)
try:
- return construct_type(type_=PulseSttStreamingSocketClientResponse, object_=json_data) # type: ignore
+ return construct_type(type_=SpeechToTextSocketClientResponse, object_=json_data) # type: ignore
except Exception:
_logger.warning("Skipping unknown WebSocket message; update your SDK version to support new message types.")
return json_data # type: ignore
@@ -124,7 +121,7 @@ async def _send_model(self, data: typing.Any) -> None:
await self._send(data.dict())
-class PulseSttStreamingSocketClient(EventEmitterMixin):
+class SpeechToTextSocketClient(EventEmitterMixin):
def __init__(self, *, websocket: websockets_sync_connection.Connection):
super().__init__()
self._websocket = websocket
@@ -135,7 +132,7 @@ def __iter__(self):
yield message
else:
try:
- yield construct_type(type_=PulseSttStreamingSocketClientResponse, object_=json.loads(message)) # type: ignore
+ yield construct_type(type_=SpeechToTextSocketClientResponse, object_=json.loads(message)) # type: ignore
except Exception:
_logger.warning(
"Skipping unknown WebSocket message; update your SDK version to support new message types."
@@ -160,7 +157,7 @@ def start_listening(self):
else:
json_data = json.loads(raw_message)
try:
- parsed = construct_type(type_=PulseSttStreamingSocketClientResponse, object_=json_data) # type: ignore
+ parsed = construct_type(type_=SpeechToTextSocketClientResponse, object_=json_data) # type: ignore
except Exception:
_logger.warning(
"Skipping unknown WebSocket message; update your SDK version to support new message types."
@@ -172,28 +169,28 @@ def start_listening(self):
finally:
self._emit(EventType.CLOSE, None)
- def transcribe_streaming_pulse_send_audio(self, message: bytes) -> None:
+ def send_audio_chunk_out(self, message: bytes) -> None:
"""
Send a message to the websocket connection.
The message will be sent as a bytes.
"""
self._send(message)
- def transcribe_streaming_pulse_send_finalize(self, message: PulseFinalizeSignalMessage) -> None:
+ def send_finalize_signal(self, message: FinalizeSignal) -> None:
"""
Send a message to the websocket connection.
- The message will be sent as a PulseFinalizeSignalMessage.
+ The message will be sent as a FinalizeSignal.
"""
self._send_model(message)
- def transcribe_streaming_pulse_send_close_stream(self, message: PulseCloseStreamSignalMessage) -> None:
+ def send_close_stream(self, message: CloseStream) -> None:
"""
Send a message to the websocket connection.
- The message will be sent as a PulseCloseStreamSignalMessage.
+ The message will be sent as a CloseStream.
"""
self._send_model(message)
- def recv(self) -> PulseSttStreamingSocketClientResponse:
+ def recv(self) -> SpeechToTextSocketClientResponse:
"""
Receive a message from the websocket connection.
"""
@@ -202,7 +199,7 @@ def recv(self) -> PulseSttStreamingSocketClientResponse:
return data # type: ignore
json_data = json.loads(data)
try:
- return construct_type(type_=PulseSttStreamingSocketClientResponse, object_=json_data) # type: ignore
+ return construct_type(type_=SpeechToTextSocketClientResponse, object_=json_data) # type: ignore
except Exception:
_logger.warning("Skipping unknown WebSocket message; update your SDK version to support new message types.")
return json_data # type: ignore
diff --git a/src/smallestai/waves/speech_to_text/types/__init__.py b/src/smallestai/waves/speech_to_text/types/__init__.py
new file mode 100644
index 00000000..a5674b46
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/__init__.py
@@ -0,0 +1,89 @@
+# This file was auto-generated by Fern from our API Definition.
+
+# isort: skip_file
+
+import typing
+from importlib import import_module
+
+if typing.TYPE_CHECKING:
+ from .close_stream import CloseStream
+ from .close_stream_type import CloseStreamType
+ from .finalize_signal import FinalizeSignal
+ from .finalize_signal_type import FinalizeSignalType
+ from .transcribe_request_emotion_detection import TranscribeRequestEmotionDetection
+ from .transcribe_request_gender_detection import TranscribeRequestGenderDetection
+ from .transcribe_request_language import TranscribeRequestLanguage
+ from .transcribe_request_model import TranscribeRequestModel
+ from .transcribe_request_redact_pci import TranscribeRequestRedactPci
+ from .transcribe_request_redact_pii import TranscribeRequestRedactPii
+ from .transcribe_request_webhook_method import TranscribeRequestWebhookMethod
+ from .transcribe_response import TranscribeResponse
+ from .transcription_error_event import TranscriptionErrorEvent
+ from .transcription_error_event_type import TranscriptionErrorEventType
+ from .transcription_event import TranscriptionEvent
+ from .transcription_event_type import TranscriptionEventType
+ from .transcription_event_utterances_item import TranscriptionEventUtterancesItem
+ from .transcription_event_words_item import TranscriptionEventWordsItem
+_dynamic_imports: typing.Dict[str, str] = {
+ "CloseStream": ".close_stream",
+ "CloseStreamType": ".close_stream_type",
+ "FinalizeSignal": ".finalize_signal",
+ "FinalizeSignalType": ".finalize_signal_type",
+ "TranscribeRequestEmotionDetection": ".transcribe_request_emotion_detection",
+ "TranscribeRequestGenderDetection": ".transcribe_request_gender_detection",
+ "TranscribeRequestLanguage": ".transcribe_request_language",
+ "TranscribeRequestModel": ".transcribe_request_model",
+ "TranscribeRequestRedactPci": ".transcribe_request_redact_pci",
+ "TranscribeRequestRedactPii": ".transcribe_request_redact_pii",
+ "TranscribeRequestWebhookMethod": ".transcribe_request_webhook_method",
+ "TranscribeResponse": ".transcribe_response",
+ "TranscriptionErrorEvent": ".transcription_error_event",
+ "TranscriptionErrorEventType": ".transcription_error_event_type",
+ "TranscriptionEvent": ".transcription_event",
+ "TranscriptionEventType": ".transcription_event_type",
+ "TranscriptionEventUtterancesItem": ".transcription_event_utterances_item",
+ "TranscriptionEventWordsItem": ".transcription_event_words_item",
+}
+
+
+def __getattr__(attr_name: str) -> typing.Any:
+ module_name = _dynamic_imports.get(attr_name)
+ if module_name is None:
+ raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}")
+ try:
+ module = import_module(module_name, __package__)
+ if module_name == f".{attr_name}":
+ return module
+ else:
+ return getattr(module, attr_name)
+ except ImportError as e:
+ raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e
+ except AttributeError as e:
+ raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e
+
+
+def __dir__():
+ lazy_attrs = list(_dynamic_imports.keys())
+ return sorted(lazy_attrs)
+
+
+__all__ = [
+ "CloseStream",
+ "CloseStreamType",
+ "FinalizeSignal",
+ "FinalizeSignalType",
+ "TranscribeRequestEmotionDetection",
+ "TranscribeRequestGenderDetection",
+ "TranscribeRequestLanguage",
+ "TranscribeRequestModel",
+ "TranscribeRequestRedactPci",
+ "TranscribeRequestRedactPii",
+ "TranscribeRequestWebhookMethod",
+ "TranscribeResponse",
+ "TranscriptionErrorEvent",
+ "TranscriptionErrorEventType",
+ "TranscriptionEvent",
+ "TranscriptionEventType",
+ "TranscriptionEventUtterancesItem",
+ "TranscriptionEventWordsItem",
+]
diff --git a/src/smallestai/waves/speech_to_text/types/close_stream.py b/src/smallestai/waves/speech_to_text/types/close_stream.py
new file mode 100644
index 00000000..d661bb6b
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/close_stream.py
@@ -0,0 +1,24 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ....core.pydantic_utilities import IS_PYDANTIC_V2
+from ....core.unchecked_base_model import UncheckedBaseModel
+from .close_stream_type import CloseStreamType
+
+
+class CloseStream(UncheckedBaseModel):
+ type: CloseStreamType = pydantic.Field()
+ """
+ Flush remaining buffered audio, emit the terminal `is_final` + `is_last` transcript, then close the WebSocket. Send exactly once at the end of the session.
+ """
+
+ 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/smallestai/waves/speech_to_text/types/close_stream_type.py b/src/smallestai/waves/speech_to_text/types/close_stream_type.py
new file mode 100644
index 00000000..fd987dd5
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/close_stream_type.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+CloseStreamType = typing.Union[typing.Literal["close_stream"], typing.Any]
diff --git a/src/smallestai/waves/speech_to_text/types/finalize_signal.py b/src/smallestai/waves/speech_to_text/types/finalize_signal.py
new file mode 100644
index 00000000..1017456e
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/finalize_signal.py
@@ -0,0 +1,24 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ....core.pydantic_utilities import IS_PYDANTIC_V2
+from ....core.unchecked_base_model import UncheckedBaseModel
+from .finalize_signal_type import FinalizeSignalType
+
+
+class FinalizeSignal(UncheckedBaseModel):
+ type: FinalizeSignalType = pydantic.Field()
+ """
+ Flush the current audio buffer and emit one `is_final: true` transcript. The WebSocket stays open and accepts audio for the next user turn.
+ """
+
+ 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/smallestai/waves/speech_to_text/types/finalize_signal_type.py b/src/smallestai/waves/speech_to_text/types/finalize_signal_type.py
new file mode 100644
index 00000000..0dc39e85
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/finalize_signal_type.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+FinalizeSignalType = typing.Union[typing.Literal["finalize"], typing.Any]
diff --git a/src/smallestai/waves/types/transcribe_pulse_waves_request_format.py b/src/smallestai/waves/speech_to_text/types/transcribe_request_emotion_detection.py
similarity index 63%
rename from src/smallestai/waves/types/transcribe_pulse_waves_request_format.py
rename to src/smallestai/waves/speech_to_text/types/transcribe_request_emotion_detection.py
index 5a985bf2..14c17659 100644
--- a/src/smallestai/waves/types/transcribe_pulse_waves_request_format.py
+++ b/src/smallestai/waves/speech_to_text/types/transcribe_request_emotion_detection.py
@@ -2,4 +2,4 @@
import typing
-TranscribePulseWavesRequestFormat = typing.Union[typing.Literal["true", "false"], typing.Any]
+TranscribeRequestEmotionDetection = typing.Union[typing.Literal["true", "false"], typing.Any]
diff --git a/src/smallestai/waves/speech_to_text/types/transcribe_request_gender_detection.py b/src/smallestai/waves/speech_to_text/types/transcribe_request_gender_detection.py
new file mode 100644
index 00000000..0384d9bf
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/transcribe_request_gender_detection.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+TranscribeRequestGenderDetection = typing.Union[typing.Literal["true", "false"], typing.Any]
diff --git a/src/smallestai/waves/types/transcribe_pulse_waves_request_language.py b/src/smallestai/waves/speech_to_text/types/transcribe_request_language.py
similarity index 91%
rename from src/smallestai/waves/types/transcribe_pulse_waves_request_language.py
rename to src/smallestai/waves/speech_to_text/types/transcribe_request_language.py
index a49c533c..162bd95c 100644
--- a/src/smallestai/waves/types/transcribe_pulse_waves_request_language.py
+++ b/src/smallestai/waves/speech_to_text/types/transcribe_request_language.py
@@ -2,7 +2,7 @@
import typing
-TranscribePulseWavesRequestLanguage = typing.Union[
+TranscribeRequestLanguage = typing.Union[
typing.Literal[
"en",
"hi",
diff --git a/src/smallestai/waves/speech_to_text/types/transcribe_request_model.py b/src/smallestai/waves/speech_to_text/types/transcribe_request_model.py
new file mode 100644
index 00000000..2fef5d0d
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/transcribe_request_model.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+TranscribeRequestModel = typing.Union[typing.Literal["pulse-pro", "pulse"], typing.Any]
diff --git a/src/smallestai/waves/speech_to_text/types/transcribe_request_redact_pci.py b/src/smallestai/waves/speech_to_text/types/transcribe_request_redact_pci.py
new file mode 100644
index 00000000..bf873377
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/transcribe_request_redact_pci.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+TranscribeRequestRedactPci = typing.Union[typing.Literal["true", "false"], typing.Any]
diff --git a/src/smallestai/waves/speech_to_text/types/transcribe_request_redact_pii.py b/src/smallestai/waves/speech_to_text/types/transcribe_request_redact_pii.py
new file mode 100644
index 00000000..16f57adc
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/transcribe_request_redact_pii.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+TranscribeRequestRedactPii = typing.Union[typing.Literal["true", "false"], typing.Any]
diff --git a/src/smallestai/waves/speech_to_text/types/transcribe_request_webhook_method.py b/src/smallestai/waves/speech_to_text/types/transcribe_request_webhook_method.py
new file mode 100644
index 00000000..62ebd49c
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/transcribe_request_webhook_method.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+TranscribeRequestWebhookMethod = typing.Union[typing.Literal["GET", "POST"], typing.Any]
diff --git a/src/smallestai/waves/speech_to_text/types/transcribe_response.py b/src/smallestai/waves/speech_to_text/types/transcribe_response.py
new file mode 100644
index 00000000..0ad34ca3
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/transcribe_response.py
@@ -0,0 +1,8 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+from ...types.async_accepted import AsyncAccepted
+from ...types.transcription_response import TranscriptionResponse
+
+TranscribeResponse = typing.Union[TranscriptionResponse, AsyncAccepted]
diff --git a/src/smallestai/waves/speech_to_text/types/transcription_error_event.py b/src/smallestai/waves/speech_to_text/types/transcription_error_event.py
new file mode 100644
index 00000000..1599718e
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/transcription_error_event.py
@@ -0,0 +1,23 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ....core.pydantic_utilities import IS_PYDANTIC_V2
+from ....core.unchecked_base_model import UncheckedBaseModel
+from .transcription_error_event_type import TranscriptionErrorEventType
+
+
+class TranscriptionErrorEvent(UncheckedBaseModel):
+ type: typing.Optional[TranscriptionErrorEventType] = None
+ status: typing.Optional[str] = None
+ message: 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/smallestai/waves/types/pulse_stt_error_response_status.py b/src/smallestai/waves/speech_to_text/types/transcription_error_event_type.py
similarity index 60%
rename from src/smallestai/waves/types/pulse_stt_error_response_status.py
rename to src/smallestai/waves/speech_to_text/types/transcription_error_event_type.py
index ed4a2670..834c4af2 100644
--- a/src/smallestai/waves/types/pulse_stt_error_response_status.py
+++ b/src/smallestai/waves/speech_to_text/types/transcription_error_event_type.py
@@ -2,4 +2,4 @@
import typing
-PulseSttErrorResponseStatus = typing.Union[typing.Literal["error"], typing.Any]
+TranscriptionErrorEventType = typing.Union[typing.Literal["error"], typing.Any]
diff --git a/src/smallestai/waves/speech_to_text/types/transcription_event.py b/src/smallestai/waves/speech_to_text/types/transcription_event.py
new file mode 100644
index 00000000..44c002c6
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/transcription_event.py
@@ -0,0 +1,66 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ....core.pydantic_utilities import IS_PYDANTIC_V2
+from ....core.unchecked_base_model import UncheckedBaseModel
+from .transcription_event_type import TranscriptionEventType
+from .transcription_event_utterances_item import TranscriptionEventUtterancesItem
+from .transcription_event_words_item import TranscriptionEventWordsItem
+
+
+class TranscriptionEvent(UncheckedBaseModel):
+ type: typing.Optional[TranscriptionEventType] = None
+ status: typing.Optional[str] = None
+ transcription: typing.Optional[str] = None
+ transcript: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Same content as `transcription`; the WebSocket DTO emits both fields for SDK compatibility.
+ """
+
+ full_transcript: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Cumulative transcript across all utterances in this session, when `full_transcript=true` is set on connect.
+ """
+
+ is_final: typing.Optional[bool] = pydantic.Field(default=None)
+ """
+ True when this segment will not be revised.
+ """
+
+ is_last: typing.Optional[bool] = pydantic.Field(default=None)
+ """
+ True on the terminal segment, fires only after the client sends `{"type":"close_stream"}`.
+ """
+
+ from_finalize: typing.Optional[bool] = pydantic.Field(default=None)
+ """
+ True when this `is_final` was produced by a client-sent `{"type":"finalize"}` rather than the server's automatic finalizer. Useful in multi-turn flows for per-turn latency measurement.
+ """
+
+ session_id: typing.Optional[str] = None
+ language: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ The language code Pulse detected or was pinned to for this segment.
+ """
+
+ languages: typing.Optional[typing.List[str]] = pydantic.Field(default=None)
+ """
+ Populated when language detection emits multiple candidates.
+ """
+
+ words: typing.Optional[typing.List[TranscriptionEventWordsItem]] = None
+ utterances: typing.Optional[typing.List[TranscriptionEventUtterancesItem]] = pydantic.Field(default=None)
+ """
+ Sentence-level timestamps. Present when `sentence_timestamps=true` is set on connect.
+ """
+
+ 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/smallestai/waves/speech_to_text/types/transcription_event_type.py b/src/smallestai/waves/speech_to_text/types/transcription_event_type.py
new file mode 100644
index 00000000..df6288bd
--- /dev/null
+++ b/src/smallestai/waves/speech_to_text/types/transcription_event_type.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+TranscriptionEventType = typing.Union[typing.Literal["transcription"], typing.Any]
diff --git a/src/smallestai/waves/pulse_stt_streaming/types/pulse_transcription_response_message_utterances_item.py b/src/smallestai/waves/speech_to_text/types/transcription_event_utterances_item.py
similarity index 74%
rename from src/smallestai/waves/pulse_stt_streaming/types/pulse_transcription_response_message_utterances_item.py
rename to src/smallestai/waves/speech_to_text/types/transcription_event_utterances_item.py
index 296d04bc..647cfe38 100644
--- a/src/smallestai/waves/pulse_stt_streaming/types/pulse_transcription_response_message_utterances_item.py
+++ b/src/smallestai/waves/speech_to_text/types/transcription_event_utterances_item.py
@@ -7,25 +7,25 @@
from ....core.unchecked_base_model import UncheckedBaseModel
-class PulseTranscriptionResponseMessageUtterancesItem(UncheckedBaseModel):
+class TranscriptionEventUtterancesItem(UncheckedBaseModel):
text: typing.Optional[str] = pydantic.Field(default=None)
"""
- The transcribed sentence
+ The sentence text.
"""
start: typing.Optional[float] = pydantic.Field(default=None)
"""
- Start time in seconds
+ Start time in seconds.
"""
end: typing.Optional[float] = pydantic.Field(default=None)
"""
- End time in seconds
+ End time in seconds.
"""
- speaker: typing.Optional[int] = pydantic.Field(default=None)
+ speaker: typing.Optional[str] = pydantic.Field(default=None)
"""
- Speaker label (when diarization is enabled)
+ Speaker label. Present when `diarize=true`.
"""
if IS_PYDANTIC_V2:
diff --git a/src/smallestai/waves/pulse_stt_streaming/types/pulse_transcription_response_message_words_item.py b/src/smallestai/waves/speech_to_text/types/transcription_event_words_item.py
similarity index 53%
rename from src/smallestai/waves/pulse_stt_streaming/types/pulse_transcription_response_message_words_item.py
rename to src/smallestai/waves/speech_to_text/types/transcription_event_words_item.py
index dbe1df7a..10aea06f 100644
--- a/src/smallestai/waves/pulse_stt_streaming/types/pulse_transcription_response_message_words_item.py
+++ b/src/smallestai/waves/speech_to_text/types/transcription_event_words_item.py
@@ -7,35 +7,23 @@
from ....core.unchecked_base_model import UncheckedBaseModel
-class PulseTranscriptionResponseMessageWordsItem(UncheckedBaseModel):
- word: typing.Optional[str] = pydantic.Field(default=None)
- """
- The transcribed word
- """
-
- start: typing.Optional[float] = pydantic.Field(default=None)
- """
- Start time in seconds
- """
-
- end: typing.Optional[float] = pydantic.Field(default=None)
- """
- End time in seconds
- """
-
+class TranscriptionEventWordsItem(UncheckedBaseModel):
+ word: typing.Optional[str] = None
+ start: typing.Optional[float] = None
+ end: typing.Optional[float] = None
confidence: typing.Optional[float] = pydantic.Field(default=None)
"""
- Confidence score for the word (0.0 to 1.0)
+ Per-word confidence score, from 0.0 to 1.0.
"""
- speaker: typing.Optional[int] = pydantic.Field(default=None)
+ speaker: typing.Optional[str] = pydantic.Field(default=None)
"""
- Speaker label (when diarization is enabled)
+ Present when `diarize=true`.
"""
speaker_confidence: typing.Optional[float] = pydantic.Field(default=None)
"""
- Confidence score for the speaker assignment (0.0 to 1.0)
+ Confidence of the speaker assignment, from 0.0 to 1.0. Present when `diarize=true`.
"""
if IS_PYDANTIC_V2:
diff --git a/src/smallestai/waves/types/__init__.py b/src/smallestai/waves/types/__init__.py
index 390ab068..28f21ea4 100644
--- a/src/smallestai/waves/types/__init__.py
+++ b/src/smallestai/waves/types/__init__.py
@@ -6,10 +6,15 @@
from importlib import import_module
if typing.TYPE_CHECKING:
+ from .async_accepted import AsyncAccepted
from .audio_chunk import AudioChunk
from .audio_chunk_data import AudioChunkData
from .audio_chunk_status import AudioChunkStatus
from .bad_request_error_body import BadRequestErrorBody
+ from .chat_completion import ChatCompletion
+ from .chat_completion_choices_item import ChatCompletionChoicesItem
+ from .chat_completion_choices_item_finish_reason import ChatCompletionChoicesItemFinishReason
+ from .chat_completion_object import ChatCompletionObject
from .completion_status import CompletionStatus
from .completion_status_status import CompletionStatusStatus
from .conversation_item import ConversationItem
@@ -18,7 +23,19 @@
from .conversation_item_role import ConversationItemRole
from .conversation_item_status import ConversationItemStatus
from .conversation_item_type import ConversationItemType
+ from .create_voice_clone_waves_request_model import CreateVoiceCloneWavesRequestModel
+ from .create_voice_clone_waves_response import CreateVoiceCloneWavesResponse
+ from .create_voice_clone_waves_response_data import CreateVoiceCloneWavesResponseData
+ from .create_voice_clone_waves_response_data_samples_item import CreateVoiceCloneWavesResponseDataSamplesItem
+ from .create_voice_clone_waves_response_data_status import CreateVoiceCloneWavesResponseDataStatus
from .delete_pronunciation_dict_response import DeletePronunciationDictResponse
+ from .electron_message import ElectronMessage
+ from .electron_tool_call import ElectronToolCall
+ from .electron_tool_call_function import ElectronToolCallFunction
+ from .electron_tool_call_type import ElectronToolCallType
+ from .error import Error
+ from .error_error import ErrorError
+ from .error_error_details_item import ErrorErrorDetailsItem
from .error_response import ErrorResponse
from .error_response_error import ErrorResponseError
from .error_response_status import ErrorResponseStatus
@@ -27,6 +44,7 @@
from .get_voices_waves_response_voices_item import GetVoicesWavesResponseVoicesItem
from .get_voices_waves_response_voices_item_tags import GetVoicesWavesResponseVoicesItemTags
from .internal_server_error_body import InternalServerErrorBody
+ from .internal_server_error_body_error_code import InternalServerErrorBodyErrorCode
from .lightning_large_request import LightningLargeRequest
from .lightning_large_request_language import LightningLargeRequestLanguage
from .lightning_large_request_output_format import LightningLargeRequestOutputFormat
@@ -36,15 +54,16 @@
from .lightningv2request import Lightningv2Request
from .lightningv2request_language import Lightningv2RequestLanguage
from .lightningv2request_output_format import Lightningv2RequestOutputFormat
+ from .list_voice_clones_waves_response import ListVoiceClonesWavesResponse
+ from .list_voice_clones_waves_response_data_item import ListVoiceClonesWavesResponseDataItem
+ from .list_voice_clones_waves_response_data_item_cloning_type import ListVoiceClonesWavesResponseDataItemCloningType
+ from .list_voice_clones_waves_response_data_item_status import ListVoiceClonesWavesResponseDataItemStatus
from .pronunciation_dict import PronunciationDict
from .pronunciation_item import PronunciationItem
- from .pulse_stt_auth_missing_response import PulseSttAuthMissingResponse
- from .pulse_stt_error_response import PulseSttErrorResponse
- from .pulse_stt_error_response_legacy import PulseSttErrorResponseLegacy
- from .pulse_stt_error_response_status import PulseSttErrorResponseStatus
from .session_config import SessionConfig
from .session_config_voice import SessionConfigVoice
from .streaming_tts_config import StreamingTtsConfig
+ from .stt_error_response import SttErrorResponse
from .synthesize_lightning_large_waves_request_output_format import SynthesizeLightningLargeWavesRequestOutputFormat
from .synthesize_lightning_v2waves_request_output_format import SynthesizeLightningV2WavesRequestOutputFormat
from .synthesize_lightning_waves_request_output_format import SynthesizeLightningWavesRequestOutputFormat
@@ -54,20 +73,8 @@
from .synthesize_sse_lightning_v2waves_request_output_format import SynthesizeSseLightningV2WavesRequestOutputFormat
from .tool import Tool
from .tool_type import ToolType
- from .transcribe_pulse_waves_request_capitalize import TranscribePulseWavesRequestCapitalize
- from .transcribe_pulse_waves_request_emotion_detection import TranscribePulseWavesRequestEmotionDetection
- from .transcribe_pulse_waves_request_encoding import TranscribePulseWavesRequestEncoding
- from .transcribe_pulse_waves_request_format import TranscribePulseWavesRequestFormat
- from .transcribe_pulse_waves_request_gender_detection import TranscribePulseWavesRequestGenderDetection
- from .transcribe_pulse_waves_request_language import TranscribePulseWavesRequestLanguage
- from .transcribe_pulse_waves_request_punctuate import TranscribePulseWavesRequestPunctuate
- from .transcribe_pulse_waves_response import TranscribePulseWavesResponse
- from .transcribe_pulse_waves_response_emotions import TranscribePulseWavesResponseEmotions
- from .transcribe_pulse_waves_response_gender import TranscribePulseWavesResponseGender
- from .transcribe_pulse_waves_response_metadata import TranscribePulseWavesResponseMetadata
- from .transcribe_pulse_waves_response_utterances_item import TranscribePulseWavesResponseUtterancesItem
- from .transcribe_pulse_waves_response_words_item import TranscribePulseWavesResponseWordsItem
- from .transcription_result import TranscriptionResult
+ from .transcription_response import TranscriptionResponse
+ from .transcription_response_metadata import TranscriptionResponseMetadata
from .tts_error import TtsError
from .tts_request import TtsRequest
from .tts_request_language import TtsRequestLanguage
@@ -76,11 +83,20 @@
from .tts_request_output_format import TtsRequestOutputFormat
from .unauthorized_error_body import UnauthorizedErrorBody
from .update_pronunciation_dict_response import UpdatePronunciationDictResponse
+ from .usage import Usage
+ from .usage_prompt_tokens_details import UsagePromptTokensDetails
+ from .utterance import Utterance
+ from .word import Word
_dynamic_imports: typing.Dict[str, str] = {
+ "AsyncAccepted": ".async_accepted",
"AudioChunk": ".audio_chunk",
"AudioChunkData": ".audio_chunk_data",
"AudioChunkStatus": ".audio_chunk_status",
"BadRequestErrorBody": ".bad_request_error_body",
+ "ChatCompletion": ".chat_completion",
+ "ChatCompletionChoicesItem": ".chat_completion_choices_item",
+ "ChatCompletionChoicesItemFinishReason": ".chat_completion_choices_item_finish_reason",
+ "ChatCompletionObject": ".chat_completion_object",
"CompletionStatus": ".completion_status",
"CompletionStatusStatus": ".completion_status_status",
"ConversationItem": ".conversation_item",
@@ -89,7 +105,19 @@
"ConversationItemRole": ".conversation_item_role",
"ConversationItemStatus": ".conversation_item_status",
"ConversationItemType": ".conversation_item_type",
+ "CreateVoiceCloneWavesRequestModel": ".create_voice_clone_waves_request_model",
+ "CreateVoiceCloneWavesResponse": ".create_voice_clone_waves_response",
+ "CreateVoiceCloneWavesResponseData": ".create_voice_clone_waves_response_data",
+ "CreateVoiceCloneWavesResponseDataSamplesItem": ".create_voice_clone_waves_response_data_samples_item",
+ "CreateVoiceCloneWavesResponseDataStatus": ".create_voice_clone_waves_response_data_status",
"DeletePronunciationDictResponse": ".delete_pronunciation_dict_response",
+ "ElectronMessage": ".electron_message",
+ "ElectronToolCall": ".electron_tool_call",
+ "ElectronToolCallFunction": ".electron_tool_call_function",
+ "ElectronToolCallType": ".electron_tool_call_type",
+ "Error": ".error",
+ "ErrorError": ".error_error",
+ "ErrorErrorDetailsItem": ".error_error_details_item",
"ErrorResponse": ".error_response",
"ErrorResponseError": ".error_response_error",
"ErrorResponseStatus": ".error_response_status",
@@ -98,6 +126,7 @@
"GetVoicesWavesResponseVoicesItem": ".get_voices_waves_response_voices_item",
"GetVoicesWavesResponseVoicesItemTags": ".get_voices_waves_response_voices_item_tags",
"InternalServerErrorBody": ".internal_server_error_body",
+ "InternalServerErrorBodyErrorCode": ".internal_server_error_body_error_code",
"LightningLargeRequest": ".lightning_large_request",
"LightningLargeRequestLanguage": ".lightning_large_request_language",
"LightningLargeRequestOutputFormat": ".lightning_large_request_output_format",
@@ -107,15 +136,16 @@
"Lightningv2Request": ".lightningv2request",
"Lightningv2RequestLanguage": ".lightningv2request_language",
"Lightningv2RequestOutputFormat": ".lightningv2request_output_format",
+ "ListVoiceClonesWavesResponse": ".list_voice_clones_waves_response",
+ "ListVoiceClonesWavesResponseDataItem": ".list_voice_clones_waves_response_data_item",
+ "ListVoiceClonesWavesResponseDataItemCloningType": ".list_voice_clones_waves_response_data_item_cloning_type",
+ "ListVoiceClonesWavesResponseDataItemStatus": ".list_voice_clones_waves_response_data_item_status",
"PronunciationDict": ".pronunciation_dict",
"PronunciationItem": ".pronunciation_item",
- "PulseSttAuthMissingResponse": ".pulse_stt_auth_missing_response",
- "PulseSttErrorResponse": ".pulse_stt_error_response",
- "PulseSttErrorResponseLegacy": ".pulse_stt_error_response_legacy",
- "PulseSttErrorResponseStatus": ".pulse_stt_error_response_status",
"SessionConfig": ".session_config",
"SessionConfigVoice": ".session_config_voice",
"StreamingTtsConfig": ".streaming_tts_config",
+ "SttErrorResponse": ".stt_error_response",
"SynthesizeLightningLargeWavesRequestOutputFormat": ".synthesize_lightning_large_waves_request_output_format",
"SynthesizeLightningV2WavesRequestOutputFormat": ".synthesize_lightning_v2waves_request_output_format",
"SynthesizeLightningWavesRequestOutputFormat": ".synthesize_lightning_waves_request_output_format",
@@ -123,20 +153,8 @@
"SynthesizeSseLightningV2WavesRequestOutputFormat": ".synthesize_sse_lightning_v2waves_request_output_format",
"Tool": ".tool",
"ToolType": ".tool_type",
- "TranscribePulseWavesRequestCapitalize": ".transcribe_pulse_waves_request_capitalize",
- "TranscribePulseWavesRequestEmotionDetection": ".transcribe_pulse_waves_request_emotion_detection",
- "TranscribePulseWavesRequestEncoding": ".transcribe_pulse_waves_request_encoding",
- "TranscribePulseWavesRequestFormat": ".transcribe_pulse_waves_request_format",
- "TranscribePulseWavesRequestGenderDetection": ".transcribe_pulse_waves_request_gender_detection",
- "TranscribePulseWavesRequestLanguage": ".transcribe_pulse_waves_request_language",
- "TranscribePulseWavesRequestPunctuate": ".transcribe_pulse_waves_request_punctuate",
- "TranscribePulseWavesResponse": ".transcribe_pulse_waves_response",
- "TranscribePulseWavesResponseEmotions": ".transcribe_pulse_waves_response_emotions",
- "TranscribePulseWavesResponseGender": ".transcribe_pulse_waves_response_gender",
- "TranscribePulseWavesResponseMetadata": ".transcribe_pulse_waves_response_metadata",
- "TranscribePulseWavesResponseUtterancesItem": ".transcribe_pulse_waves_response_utterances_item",
- "TranscribePulseWavesResponseWordsItem": ".transcribe_pulse_waves_response_words_item",
- "TranscriptionResult": ".transcription_result",
+ "TranscriptionResponse": ".transcription_response",
+ "TranscriptionResponseMetadata": ".transcription_response_metadata",
"TtsError": ".tts_error",
"TtsRequest": ".tts_request",
"TtsRequestLanguage": ".tts_request_language",
@@ -145,6 +163,10 @@
"TtsRequestOutputFormat": ".tts_request_output_format",
"UnauthorizedErrorBody": ".unauthorized_error_body",
"UpdatePronunciationDictResponse": ".update_pronunciation_dict_response",
+ "Usage": ".usage",
+ "UsagePromptTokensDetails": ".usage_prompt_tokens_details",
+ "Utterance": ".utterance",
+ "Word": ".word",
}
@@ -170,10 +192,15 @@ def __dir__():
__all__ = [
+ "AsyncAccepted",
"AudioChunk",
"AudioChunkData",
"AudioChunkStatus",
"BadRequestErrorBody",
+ "ChatCompletion",
+ "ChatCompletionChoicesItem",
+ "ChatCompletionChoicesItemFinishReason",
+ "ChatCompletionObject",
"CompletionStatus",
"CompletionStatusStatus",
"ConversationItem",
@@ -182,7 +209,19 @@ def __dir__():
"ConversationItemRole",
"ConversationItemStatus",
"ConversationItemType",
+ "CreateVoiceCloneWavesRequestModel",
+ "CreateVoiceCloneWavesResponse",
+ "CreateVoiceCloneWavesResponseData",
+ "CreateVoiceCloneWavesResponseDataSamplesItem",
+ "CreateVoiceCloneWavesResponseDataStatus",
"DeletePronunciationDictResponse",
+ "ElectronMessage",
+ "ElectronToolCall",
+ "ElectronToolCallFunction",
+ "ElectronToolCallType",
+ "Error",
+ "ErrorError",
+ "ErrorErrorDetailsItem",
"ErrorResponse",
"ErrorResponseError",
"ErrorResponseStatus",
@@ -191,6 +230,7 @@ def __dir__():
"GetVoicesWavesResponseVoicesItem",
"GetVoicesWavesResponseVoicesItemTags",
"InternalServerErrorBody",
+ "InternalServerErrorBodyErrorCode",
"LightningLargeRequest",
"LightningLargeRequestLanguage",
"LightningLargeRequestOutputFormat",
@@ -200,15 +240,16 @@ def __dir__():
"Lightningv2Request",
"Lightningv2RequestLanguage",
"Lightningv2RequestOutputFormat",
+ "ListVoiceClonesWavesResponse",
+ "ListVoiceClonesWavesResponseDataItem",
+ "ListVoiceClonesWavesResponseDataItemCloningType",
+ "ListVoiceClonesWavesResponseDataItemStatus",
"PronunciationDict",
"PronunciationItem",
- "PulseSttAuthMissingResponse",
- "PulseSttErrorResponse",
- "PulseSttErrorResponseLegacy",
- "PulseSttErrorResponseStatus",
"SessionConfig",
"SessionConfigVoice",
"StreamingTtsConfig",
+ "SttErrorResponse",
"SynthesizeLightningLargeWavesRequestOutputFormat",
"SynthesizeLightningV2WavesRequestOutputFormat",
"SynthesizeLightningWavesRequestOutputFormat",
@@ -216,20 +257,8 @@ def __dir__():
"SynthesizeSseLightningV2WavesRequestOutputFormat",
"Tool",
"ToolType",
- "TranscribePulseWavesRequestCapitalize",
- "TranscribePulseWavesRequestEmotionDetection",
- "TranscribePulseWavesRequestEncoding",
- "TranscribePulseWavesRequestFormat",
- "TranscribePulseWavesRequestGenderDetection",
- "TranscribePulseWavesRequestLanguage",
- "TranscribePulseWavesRequestPunctuate",
- "TranscribePulseWavesResponse",
- "TranscribePulseWavesResponseEmotions",
- "TranscribePulseWavesResponseGender",
- "TranscribePulseWavesResponseMetadata",
- "TranscribePulseWavesResponseUtterancesItem",
- "TranscribePulseWavesResponseWordsItem",
- "TranscriptionResult",
+ "TranscriptionResponse",
+ "TranscriptionResponseMetadata",
"TtsError",
"TtsRequest",
"TtsRequestLanguage",
@@ -238,4 +267,8 @@ def __dir__():
"TtsRequestOutputFormat",
"UnauthorizedErrorBody",
"UpdatePronunciationDictResponse",
+ "Usage",
+ "UsagePromptTokensDetails",
+ "Utterance",
+ "Word",
]
diff --git a/src/smallestai/waves/types/transcribe_pulse_waves_response_emotions.py b/src/smallestai/waves/types/async_accepted.py
similarity index 61%
rename from src/smallestai/waves/types/transcribe_pulse_waves_response_emotions.py
rename to src/smallestai/waves/types/async_accepted.py
index 4401c965..e9aa05a7 100644
--- a/src/smallestai/waves/types/transcribe_pulse_waves_response_emotions.py
+++ b/src/smallestai/waves/types/async_accepted.py
@@ -7,16 +7,13 @@
from ...core.unchecked_base_model import UncheckedBaseModel
-class TranscribePulseWavesResponseEmotions(UncheckedBaseModel):
+class AsyncAccepted(UncheckedBaseModel):
"""
- Predicted emotions of the speaker if requested
+ Returned by Pulse Pro when `webhook_url` is set. The transcription arrives on the webhook when ready.
"""
- happiness: typing.Optional[float] = None
- sadness: typing.Optional[float] = None
- disgust: typing.Optional[float] = None
- fear: typing.Optional[float] = None
- anger: typing.Optional[float] = None
+ status: str
+ request_id: str
if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
diff --git a/src/smallestai/waves/types/bad_request_error_body.py b/src/smallestai/waves/types/bad_request_error_body.py
index 75c34cb6..d37c7f1a 100644
--- a/src/smallestai/waves/types/bad_request_error_body.py
+++ b/src/smallestai/waves/types/bad_request_error_body.py
@@ -10,12 +10,7 @@
class BadRequestErrorBody(UncheckedBaseModel):
error: typing.Optional[str] = pydantic.Field(default=None)
"""
- Error type
- """
-
- message: typing.Optional[str] = pydantic.Field(default=None)
- """
- Error message
+ Error message.
"""
if IS_PYDANTIC_V2:
diff --git a/src/smallestai/waves/types/chat_completion.py b/src/smallestai/waves/types/chat_completion.py
new file mode 100644
index 00000000..0c6aee5f
--- /dev/null
+++ b/src/smallestai/waves/types/chat_completion.py
@@ -0,0 +1,28 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .chat_completion_choices_item import ChatCompletionChoicesItem
+from .chat_completion_object import ChatCompletionObject
+from .usage import Usage
+
+
+class ChatCompletion(UncheckedBaseModel):
+ id: typing.Optional[str] = None
+ object: typing.Optional[ChatCompletionObject] = None
+ created: typing.Optional[int] = None
+ model: typing.Optional[str] = None
+ choices: typing.Optional[typing.List[ChatCompletionChoicesItem]] = None
+ usage: typing.Optional[Usage] = 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/smallestai/waves/types/chat_completion_choices_item.py b/src/smallestai/waves/types/chat_completion_choices_item.py
new file mode 100644
index 00000000..9dd60ac2
--- /dev/null
+++ b/src/smallestai/waves/types/chat_completion_choices_item.py
@@ -0,0 +1,24 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .chat_completion_choices_item_finish_reason import ChatCompletionChoicesItemFinishReason
+from .electron_message import ElectronMessage
+
+
+class ChatCompletionChoicesItem(UncheckedBaseModel):
+ index: typing.Optional[int] = None
+ message: typing.Optional[ElectronMessage] = None
+ finish_reason: typing.Optional[ChatCompletionChoicesItemFinishReason] = 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/smallestai/waves/types/chat_completion_choices_item_finish_reason.py b/src/smallestai/waves/types/chat_completion_choices_item_finish_reason.py
new file mode 100644
index 00000000..1d962e52
--- /dev/null
+++ b/src/smallestai/waves/types/chat_completion_choices_item_finish_reason.py
@@ -0,0 +1,7 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+ChatCompletionChoicesItemFinishReason = typing.Union[
+ typing.Literal["stop", "length", "tool_calls", "content_filter"], typing.Any
+]
diff --git a/src/smallestai/waves/types/chat_completion_object.py b/src/smallestai/waves/types/chat_completion_object.py
new file mode 100644
index 00000000..2cdf859a
--- /dev/null
+++ b/src/smallestai/waves/types/chat_completion_object.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+ChatCompletionObject = typing.Union[typing.Literal["chat.completion"], typing.Any]
diff --git a/src/smallestai/waves/types/create_voice_clone_waves_request_model.py b/src/smallestai/waves/types/create_voice_clone_waves_request_model.py
new file mode 100644
index 00000000..5cf0e204
--- /dev/null
+++ b/src/smallestai/waves/types/create_voice_clone_waves_request_model.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+CreateVoiceCloneWavesRequestModel = typing.Union[typing.Literal["lightning-v3.1"], typing.Any]
diff --git a/src/smallestai/waves/types/create_voice_clone_waves_response.py b/src/smallestai/waves/types/create_voice_clone_waves_response.py
new file mode 100644
index 00000000..b8463797
--- /dev/null
+++ b/src/smallestai/waves/types/create_voice_clone_waves_response.py
@@ -0,0 +1,22 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .create_voice_clone_waves_response_data import CreateVoiceCloneWavesResponseData
+
+
+class CreateVoiceCloneWavesResponse(UncheckedBaseModel):
+ message: typing.Optional[str] = None
+ data: typing.Optional[CreateVoiceCloneWavesResponseData] = 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/smallestai/waves/types/create_voice_clone_waves_response_data.py b/src/smallestai/waves/types/create_voice_clone_waves_response_data.py
new file mode 100644
index 00000000..7636f659
--- /dev/null
+++ b/src/smallestai/waves/types/create_voice_clone_waves_response_data.py
@@ -0,0 +1,52 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import datetime as dt
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.serialization import FieldMetadata
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .create_voice_clone_waves_response_data_samples_item import CreateVoiceCloneWavesResponseDataSamplesItem
+from .create_voice_clone_waves_response_data_status import CreateVoiceCloneWavesResponseDataStatus
+
+
+class CreateVoiceCloneWavesResponseData(UncheckedBaseModel):
+ voice_id: typing_extensions.Annotated[
+ typing.Optional[str],
+ FieldMetadata(alias="voiceId"),
+ pydantic.Field(alias="voiceId", description="Unique voice ID. Pass this as `voice_id` in TTS requests."),
+ ] = None
+ display_name: typing_extensions.Annotated[
+ typing.Optional[str], FieldMetadata(alias="displayName"), pydantic.Field(alias="displayName")
+ ] = None
+ model: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Internal model document for the cloned voice.
+ """
+
+ status: typing.Optional[CreateVoiceCloneWavesResponseDataStatus] = None
+ language: typing.Optional[str] = None
+ audio_file_names: typing_extensions.Annotated[
+ typing.Optional[typing.List[str]], FieldMetadata(alias="audioFileNames"), pydantic.Field(alias="audioFileNames")
+ ] = None
+ created_at: typing_extensions.Annotated[
+ typing.Optional[dt.datetime], FieldMetadata(alias="createdAt"), pydantic.Field(alias="createdAt")
+ ] = None
+ organization_id: typing_extensions.Annotated[
+ typing.Optional[str], FieldMetadata(alias="organizationId"), pydantic.Field(alias="organizationId")
+ ] = None
+ samples: typing.Optional[typing.List[CreateVoiceCloneWavesResponseDataSamplesItem]] = pydantic.Field(default=None)
+ """
+ Pre-generated sample audio clips in the cloned voice.
+ """
+
+ 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/smallestai/waves/types/create_voice_clone_waves_response_data_samples_item.py b/src/smallestai/waves/types/create_voice_clone_waves_response_data_samples_item.py
new file mode 100644
index 00000000..967f7c85
--- /dev/null
+++ b/src/smallestai/waves/types/create_voice_clone_waves_response_data_samples_item.py
@@ -0,0 +1,31 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.serialization import FieldMetadata
+from ...core.unchecked_base_model import UncheckedBaseModel
+
+
+class CreateVoiceCloneWavesResponseDataSamplesItem(UncheckedBaseModel):
+ text: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Text that was synthesized.
+ """
+
+ audio_url: typing_extensions.Annotated[
+ typing.Optional[str],
+ FieldMetadata(alias="audioUrl"),
+ pydantic.Field(alias="audioUrl", description="Signed URL to the generated sample audio."),
+ ] = 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/smallestai/waves/types/create_voice_clone_waves_response_data_status.py b/src/smallestai/waves/types/create_voice_clone_waves_response_data_status.py
new file mode 100644
index 00000000..cfc2769e
--- /dev/null
+++ b/src/smallestai/waves/types/create_voice_clone_waves_response_data_status.py
@@ -0,0 +1,7 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+CreateVoiceCloneWavesResponseDataStatus = typing.Union[
+ typing.Literal["pending", "processing", "completed", "failed"], typing.Any
+]
diff --git a/src/smallestai/waves/types/electron_message.py b/src/smallestai/waves/types/electron_message.py
new file mode 100644
index 00000000..90a27165
--- /dev/null
+++ b/src/smallestai/waves/types/electron_message.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
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .electron_tool_call import ElectronToolCall
+
+
+class ElectronMessage(UncheckedBaseModel):
+ role: str = pydantic.Field()
+ """
+ Message role — one of `system`, `user`, `assistant`, or `tool`.
+ `tool` is used to feed a function-call result back to the model on the next turn.
+ """
+
+ content: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Text content for the message. `null` is permitted on assistant messages that carry only `tool_calls`.
+ """
+
+ tool_calls: typing.Optional[typing.List[ElectronToolCall]] = None
+ tool_call_id: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Required when `role` is `"tool"`.
+ """
+
+ 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/smallestai/waves/types/electron_tool_call.py b/src/smallestai/waves/types/electron_tool_call.py
new file mode 100644
index 00000000..8c583188
--- /dev/null
+++ b/src/smallestai/waves/types/electron_tool_call.py
@@ -0,0 +1,24 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .electron_tool_call_function import ElectronToolCallFunction
+from .electron_tool_call_type import ElectronToolCallType
+
+
+class ElectronToolCall(UncheckedBaseModel):
+ id: str
+ type: ElectronToolCallType
+ function: ElectronToolCallFunction
+
+ 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/smallestai/waves/types/pulse_stt_auth_missing_response.py b/src/smallestai/waves/types/electron_tool_call_function.py
similarity index 60%
rename from src/smallestai/waves/types/pulse_stt_auth_missing_response.py
rename to src/smallestai/waves/types/electron_tool_call_function.py
index 6ebed01e..7850939a 100644
--- a/src/smallestai/waves/types/pulse_stt_auth_missing_response.py
+++ b/src/smallestai/waves/types/electron_tool_call_function.py
@@ -7,16 +7,11 @@
from ...core.unchecked_base_model import UncheckedBaseModel
-class PulseSttAuthMissingResponse(UncheckedBaseModel):
+class ElectronToolCallFunction(UncheckedBaseModel):
+ name: str
+ arguments: str = pydantic.Field()
"""
- Shape returned when the request omits the `Authorization` header
- entirely. Distinct from invalid-token (which uses `PulseSttErrorResponseLegacy`).
- Live-probed 2026-06-16.
- """
-
- message: str = pydantic.Field()
- """
- Human-readable explanation of why auth failed.
+ JSON-encoded argument object.
"""
if IS_PYDANTIC_V2:
diff --git a/src/smallestai/waves/types/electron_tool_call_type.py b/src/smallestai/waves/types/electron_tool_call_type.py
new file mode 100644
index 00000000..455da712
--- /dev/null
+++ b/src/smallestai/waves/types/electron_tool_call_type.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+ElectronToolCallType = typing.Union[typing.Literal["function"], typing.Any]
diff --git a/src/smallestai/waves/types/error.py b/src/smallestai/waves/types/error.py
new file mode 100644
index 00000000..61c51070
--- /dev/null
+++ b/src/smallestai/waves/types/error.py
@@ -0,0 +1,21 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .error_error import ErrorError
+
+
+class Error(UncheckedBaseModel):
+ error: typing.Optional[ErrorError] = 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/smallestai/waves/types/error_error.py b/src/smallestai/waves/types/error_error.py
new file mode 100644
index 00000000..38e5fe6f
--- /dev/null
+++ b/src/smallestai/waves/types/error_error.py
@@ -0,0 +1,37 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .error_error_details_item import ErrorErrorDetailsItem
+
+
+class ErrorError(UncheckedBaseModel):
+ message: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Human-readable error message.
+ """
+
+ type: typing.Optional[str] = None
+ details: typing.Optional[typing.List[ErrorErrorDetailsItem]] = pydantic.Field(default=None)
+ """
+ Validation issues, when applicable. Each entry includes the JSON
+ path to the offending field plus a short reason. Present on
+ schema-validation failures (e.g. `n > 1`, `prompt_logprobs`).
+ """
+
+ request_id: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Echo in support tickets so the request can be traced.
+ """
+
+ 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/smallestai/waves/types/error_error_details_item.py b/src/smallestai/waves/types/error_error_details_item.py
new file mode 100644
index 00000000..e0241fe2
--- /dev/null
+++ b/src/smallestai/waves/types/error_error_details_item.py
@@ -0,0 +1,22 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+
+
+class ErrorErrorDetailsItem(UncheckedBaseModel):
+ code: typing.Optional[str] = None
+ message: typing.Optional[str] = None
+ path: typing.Optional[typing.List[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/smallestai/waves/types/error_response.py b/src/smallestai/waves/types/error_response.py
index 8bc5d0dd..a175802f 100644
--- a/src/smallestai/waves/types/error_response.py
+++ b/src/smallestai/waves/types/error_response.py
@@ -10,8 +10,13 @@
class ErrorResponse(UncheckedBaseModel):
- status: typing.Optional[ErrorResponseStatus] = None
- error: typing.Optional[ErrorResponseError] = None
+ request_id: str = pydantic.Field()
+ """
+ Unique identifier for the failed request
+ """
+
+ status: ErrorResponseStatus
+ error: ErrorResponseError
if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
diff --git a/src/smallestai/waves/types/error_response_error.py b/src/smallestai/waves/types/error_response_error.py
index ca2daa87..f94a59a0 100644
--- a/src/smallestai/waves/types/error_response_error.py
+++ b/src/smallestai/waves/types/error_response_error.py
@@ -8,8 +8,15 @@
class ErrorResponseError(UncheckedBaseModel):
- message: typing.Optional[str] = None
- code: typing.Optional[str] = None
+ message: str = pydantic.Field()
+ """
+ Human-readable error message
+ """
+
+ code: str = pydantic.Field()
+ """
+ Machine-readable error code
+ """
if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
diff --git a/src/smallestai/waves/types/internal_server_error_body.py b/src/smallestai/waves/types/internal_server_error_body.py
index a7c82eb7..61fad64a 100644
--- a/src/smallestai/waves/types/internal_server_error_body.py
+++ b/src/smallestai/waves/types/internal_server_error_body.py
@@ -5,17 +5,14 @@
import pydantic
from ...core.pydantic_utilities import IS_PYDANTIC_V2
from ...core.unchecked_base_model import UncheckedBaseModel
+from .internal_server_error_body_error_code import InternalServerErrorBodyErrorCode
class InternalServerErrorBody(UncheckedBaseModel):
- error: typing.Optional[str] = pydantic.Field(default=None)
+ error: typing.Optional[str] = None
+ error_code: typing.Optional[InternalServerErrorBodyErrorCode] = pydantic.Field(default=None)
"""
- Error type
- """
-
- message: typing.Optional[str] = pydantic.Field(default=None)
- """
- Error message
+ Present when a known failure mode occurred.
"""
if IS_PYDANTIC_V2:
diff --git a/src/smallestai/waves/types/internal_server_error_body_error_code.py b/src/smallestai/waves/types/internal_server_error_body_error_code.py
new file mode 100644
index 00000000..b8c79587
--- /dev/null
+++ b/src/smallestai/waves/types/internal_server_error_body_error_code.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+InternalServerErrorBodyErrorCode = typing.Union[typing.Literal["voice_clone_timeout", "voice_clone_error"], typing.Any]
diff --git a/src/smallestai/waves/types/list_voice_clones_waves_response.py b/src/smallestai/waves/types/list_voice_clones_waves_response.py
new file mode 100644
index 00000000..4986a2f1
--- /dev/null
+++ b/src/smallestai/waves/types/list_voice_clones_waves_response.py
@@ -0,0 +1,21 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .list_voice_clones_waves_response_data_item import ListVoiceClonesWavesResponseDataItem
+
+
+class ListVoiceClonesWavesResponse(UncheckedBaseModel):
+ data: typing.Optional[typing.List[ListVoiceClonesWavesResponseDataItem]] = 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/smallestai/waves/types/list_voice_clones_waves_response_data_item.py b/src/smallestai/waves/types/list_voice_clones_waves_response_data_item.py
new file mode 100644
index 00000000..1a47f801
--- /dev/null
+++ b/src/smallestai/waves/types/list_voice_clones_waves_response_data_item.py
@@ -0,0 +1,54 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import datetime as dt
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.serialization import FieldMetadata
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .list_voice_clones_waves_response_data_item_cloning_type import ListVoiceClonesWavesResponseDataItemCloningType
+from .list_voice_clones_waves_response_data_item_status import ListVoiceClonesWavesResponseDataItemStatus
+
+
+class ListVoiceClonesWavesResponseDataItem(UncheckedBaseModel):
+ id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="_id"), pydantic.Field(alias="_id")] = (
+ None
+ )
+ voice_id: typing_extensions.Annotated[
+ typing.Optional[str], FieldMetadata(alias="voiceId"), pydantic.Field(alias="voiceId")
+ ] = None
+ display_name: typing_extensions.Annotated[
+ typing.Optional[str], FieldMetadata(alias="displayName"), pydantic.Field(alias="displayName")
+ ] = None
+ description: typing.Optional[str] = None
+ accent: typing.Optional[str] = None
+ tags: typing.Optional[typing.List[str]] = None
+ language: typing.Optional[str] = None
+ status: typing.Optional[ListVoiceClonesWavesResponseDataItemStatus] = None
+ cloning_type: typing_extensions.Annotated[
+ typing.Optional[ListVoiceClonesWavesResponseDataItemCloningType],
+ FieldMetadata(alias="cloningType"),
+ pydantic.Field(alias="cloningType"),
+ ] = None
+ model_ids: typing_extensions.Annotated[
+ typing.Optional[typing.List[str]],
+ FieldMetadata(alias="modelIds"),
+ pydantic.Field(
+ alias="modelIds",
+ description="Models this clone is compatible with. `lightning-v3.1`\nis the current default. Older entries may list\n`lightning-large`.",
+ ),
+ ] = None
+ created_at: typing_extensions.Annotated[
+ typing.Optional[dt.datetime], FieldMetadata(alias="createdAt"), pydantic.Field(alias="createdAt")
+ ] = 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/smallestai/waves/types/list_voice_clones_waves_response_data_item_cloning_type.py b/src/smallestai/waves/types/list_voice_clones_waves_response_data_item_cloning_type.py
new file mode 100644
index 00000000..24e40a4b
--- /dev/null
+++ b/src/smallestai/waves/types/list_voice_clones_waves_response_data_item_cloning_type.py
@@ -0,0 +1,5 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+ListVoiceClonesWavesResponseDataItemCloningType = typing.Union[typing.Literal["instant", "professional"], typing.Any]
diff --git a/src/smallestai/waves/types/list_voice_clones_waves_response_data_item_status.py b/src/smallestai/waves/types/list_voice_clones_waves_response_data_item_status.py
new file mode 100644
index 00000000..492e8e0f
--- /dev/null
+++ b/src/smallestai/waves/types/list_voice_clones_waves_response_data_item_status.py
@@ -0,0 +1,7 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+ListVoiceClonesWavesResponseDataItemStatus = typing.Union[
+ typing.Literal["pending", "processing", "completed", "failed"], typing.Any
+]
diff --git a/src/smallestai/waves/types/pulse_stt_error_response.py b/src/smallestai/waves/types/pulse_stt_error_response.py
deleted file mode 100644
index 2d2e2d0b..00000000
--- a/src/smallestai/waves/types/pulse_stt_error_response.py
+++ /dev/null
@@ -1,67 +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
-from ...core.unchecked_base_model import UncheckedBaseModel
-from .pulse_stt_error_response_status import PulseSttErrorResponseStatus
-
-
-class PulseSttErrorResponse(UncheckedBaseModel):
- """
- Canonical error shape for 400-class responses from the Pulse STT REST
- endpoint. Live-probed against prod 2026-06-16.
-
- Two variants share the same envelope:
- - Generic validation: `{errors, status, message}` (e.g. invalid params,
- malformed body, oversized upload).
- - Discriminated: `{status, error_code, message, language?, region?}`
- for known classes such as `LANGUAGE_NOT_ENABLED_IN_REGION`.
-
- Clients should treat `errors` and `error_code` as mutually exclusive —
- exactly one will be present. `status` and `message` are always set.
-
- Note: the platform's field naming is inconsistent — `errors` (plural)
- on 400-class responses vs `error` (singular) on some 401 responses.
- See `PulseSttErrorResponseLegacy` for the singular-`error` shape.
- """
-
- status: PulseSttErrorResponseStatus = pydantic.Field()
- """
- Always `"error"`.
- """
-
- message: str = pydantic.Field()
- """
- Generic error category (e.g. "Error handling audio data").
- """
-
- errors: typing.Optional[str] = pydantic.Field(default=None)
- """
- Human-readable error message (validation variant).
- """
-
- error_code: typing.Optional[str] = pydantic.Field(default=None)
- """
- Machine-readable error code (discriminated variant, e.g. `LANGUAGE_NOT_ENABLED_IN_REGION`).
- """
-
- language: typing.Optional[str] = pydantic.Field(default=None)
- """
- Echo of the requested language (present on `LANGUAGE_NOT_ENABLED_IN_REGION`).
- """
-
- region: typing.Optional[str] = pydantic.Field(default=None)
- """
- Region the request hit (present on `LANGUAGE_NOT_ENABLED_IN_REGION`).
- """
-
- 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/smallestai/waves/types/pulse_stt_error_response_legacy.py b/src/smallestai/waves/types/stt_error_response.py
similarity index 59%
rename from src/smallestai/waves/types/pulse_stt_error_response_legacy.py
rename to src/smallestai/waves/types/stt_error_response.py
index ede51cb1..80cf20fa 100644
--- a/src/smallestai/waves/types/pulse_stt_error_response_legacy.py
+++ b/src/smallestai/waves/types/stt_error_response.py
@@ -7,17 +7,20 @@
from ...core.unchecked_base_model import UncheckedBaseModel
-class PulseSttErrorResponseLegacy(UncheckedBaseModel):
+class SttErrorResponse(UncheckedBaseModel):
+ error: str = pydantic.Field()
"""
- Legacy singular-`error` shape returned by 401 bad-key and 500
- responses. Distinct from `PulseSttErrorResponse` (the plural `errors`
- shape used by 400-class responses) — the platform is inconsistent across
- error paths. Live-probed 2026-06-16; see verifications/ log.
+ Error message.
"""
- error: str = pydantic.Field()
+ request_id: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Correlation ID for support / logs
+ """
+
+ details: typing.Optional[typing.List[typing.Dict[str, typing.Any]]] = pydantic.Field(default=None)
"""
- Error message describing what went wrong.
+ Additional error details (validation errors).
"""
if IS_PYDANTIC_V2:
diff --git a/src/smallestai/waves/types/transcribe_pulse_waves_request_capitalize.py b/src/smallestai/waves/types/transcribe_pulse_waves_request_capitalize.py
deleted file mode 100644
index a3c4c4e8..00000000
--- a/src/smallestai/waves/types/transcribe_pulse_waves_request_capitalize.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-
-TranscribePulseWavesRequestCapitalize = typing.Union[typing.Literal["true", "false"], typing.Any]
diff --git a/src/smallestai/waves/types/transcribe_pulse_waves_request_emotion_detection.py b/src/smallestai/waves/types/transcribe_pulse_waves_request_emotion_detection.py
deleted file mode 100644
index 50b222c5..00000000
--- a/src/smallestai/waves/types/transcribe_pulse_waves_request_emotion_detection.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-
-TranscribePulseWavesRequestEmotionDetection = typing.Union[typing.Literal["true", "false"], typing.Any]
diff --git a/src/smallestai/waves/types/transcribe_pulse_waves_request_encoding.py b/src/smallestai/waves/types/transcribe_pulse_waves_request_encoding.py
deleted file mode 100644
index 53e42ac1..00000000
--- a/src/smallestai/waves/types/transcribe_pulse_waves_request_encoding.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-
-TranscribePulseWavesRequestEncoding = typing.Union[
- typing.Literal["linear16", "linear32", "alaw", "mulaw", "opus", "ogg_opus"], typing.Any
-]
diff --git a/src/smallestai/waves/types/transcribe_pulse_waves_request_gender_detection.py b/src/smallestai/waves/types/transcribe_pulse_waves_request_gender_detection.py
deleted file mode 100644
index 6b1d4f28..00000000
--- a/src/smallestai/waves/types/transcribe_pulse_waves_request_gender_detection.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-
-TranscribePulseWavesRequestGenderDetection = typing.Union[typing.Literal["true", "false"], typing.Any]
diff --git a/src/smallestai/waves/types/transcribe_pulse_waves_request_punctuate.py b/src/smallestai/waves/types/transcribe_pulse_waves_request_punctuate.py
deleted file mode 100644
index 70752720..00000000
--- a/src/smallestai/waves/types/transcribe_pulse_waves_request_punctuate.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-
-TranscribePulseWavesRequestPunctuate = typing.Union[typing.Literal["true", "false"], typing.Any]
diff --git a/src/smallestai/waves/types/transcribe_pulse_waves_response.py b/src/smallestai/waves/types/transcribe_pulse_waves_response.py
deleted file mode 100644
index a1bb6041..00000000
--- a/src/smallestai/waves/types/transcribe_pulse_waves_response.py
+++ /dev/null
@@ -1,63 +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
-from ...core.unchecked_base_model import UncheckedBaseModel
-from .transcribe_pulse_waves_response_emotions import TranscribePulseWavesResponseEmotions
-from .transcribe_pulse_waves_response_gender import TranscribePulseWavesResponseGender
-from .transcribe_pulse_waves_response_metadata import TranscribePulseWavesResponseMetadata
-from .transcribe_pulse_waves_response_utterances_item import TranscribePulseWavesResponseUtterancesItem
-from .transcribe_pulse_waves_response_words_item import TranscribePulseWavesResponseWordsItem
-
-
-class TranscribePulseWavesResponse(UncheckedBaseModel):
- status: typing.Optional[str] = pydantic.Field(default=None)
- """
- Status of the transcription request
- """
-
- transcription: typing.Optional[str] = pydantic.Field(default=None)
- """
- The transcribed text from the audio file
- """
-
- audio_length: typing.Optional[float] = pydantic.Field(default=None)
- """
- Duration of the audio file in seconds
- """
-
- words: typing.Optional[typing.List[TranscribePulseWavesResponseWordsItem]] = pydantic.Field(default=None)
- """
- Word-level timestamps in seconds.
- """
-
- utterances: typing.Optional[typing.List[TranscribePulseWavesResponseUtterancesItem]] = pydantic.Field(default=None)
- """
- List of utterances with start and end times
- """
-
- gender: typing.Optional[TranscribePulseWavesResponseGender] = pydantic.Field(default=None)
- """
- Predicted gender of the speaker if requested
- """
-
- emotions: typing.Optional[TranscribePulseWavesResponseEmotions] = pydantic.Field(default=None)
- """
- Predicted emotions of the speaker if requested
- """
-
- metadata: typing.Optional[TranscribePulseWavesResponseMetadata] = pydantic.Field(default=None)
- """
- Metadata about the transcription
- """
-
- 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/smallestai/waves/types/transcribe_pulse_waves_response_gender.py b/src/smallestai/waves/types/transcribe_pulse_waves_response_gender.py
deleted file mode 100644
index f6e59154..00000000
--- a/src/smallestai/waves/types/transcribe_pulse_waves_response_gender.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-
-TranscribePulseWavesResponseGender = typing.Union[typing.Literal["male", "female"], typing.Any]
diff --git a/src/smallestai/waves/types/transcription_response.py b/src/smallestai/waves/types/transcription_response.py
new file mode 100644
index 00000000..7b628c24
--- /dev/null
+++ b/src/smallestai/waves/types/transcription_response.py
@@ -0,0 +1,42 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .transcription_response_metadata import TranscriptionResponseMetadata
+from .utterance import Utterance
+from .word import Word
+
+
+class TranscriptionResponse(UncheckedBaseModel):
+ status: str
+ transcription: str
+ words: typing.Optional[typing.List[Word]] = None
+ utterances: typing.Optional[typing.List[Utterance]] = pydantic.Field(default=None)
+ """
+ Sentence-level segments with optional speaker labels. Returned by `?model=pulse` only; Pulse Pro responses omit this field.
+ """
+
+ language: typing.Optional[str] = None
+ metadata: typing.Optional[TranscriptionResponseMetadata] = None
+ request_id: typing.Optional[str] = None
+ gender: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Detected speaker gender label. Present when `gender_detection=true` was set on the request.
+ """
+
+ emotions: typing.Optional[typing.Dict[str, float]] = pydantic.Field(default=None)
+ """
+ Detected emotion labels mapped to confidence scores. Present when `emotion_detection=true` was set on the request.
+ """
+
+ 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/smallestai/waves/types/transcribe_pulse_waves_response_metadata.py b/src/smallestai/waves/types/transcription_response_metadata.py
similarity index 58%
rename from src/smallestai/waves/types/transcribe_pulse_waves_response_metadata.py
rename to src/smallestai/waves/types/transcription_response_metadata.py
index 35513b1f..a5b12ae7 100644
--- a/src/smallestai/waves/types/transcribe_pulse_waves_response_metadata.py
+++ b/src/smallestai/waves/types/transcription_response_metadata.py
@@ -9,25 +9,36 @@
from ...core.unchecked_base_model import UncheckedBaseModel
-class TranscribePulseWavesResponseMetadata(UncheckedBaseModel):
+class TranscriptionResponseMetadata(UncheckedBaseModel):
+ duration: typing.Optional[float] = pydantic.Field(default=None)
"""
- Metadata about the transcription
+ Audio duration in seconds.
"""
- filename: typing.Optional[str] = pydantic.Field(default=None)
+ processing_time_ms: typing.Optional[float] = pydantic.Field(default=None)
"""
- Name of the audio file
+ Pulse Pro only.
"""
- duration: typing.Optional[float] = pydantic.Field(default=None)
+ rtfx: typing.Optional[float] = pydantic.Field(default=None)
+ """
+ Real-time factor for this request (Pulse Pro only).
+ """
+
+ num_chunks: typing.Optional[float] = pydantic.Field(default=None)
+ """
+ Number of internal chunks the audio was split into (Pulse Pro only).
+ """
+
+ filename: typing.Optional[str] = pydantic.Field(default=None)
"""
- Duration of the audio file in minutes
+ Pulse responses include this when sent via URL.
"""
file_size: typing_extensions.Annotated[
typing.Optional[float],
FieldMetadata(alias="fileSize"),
- pydantic.Field(alias="fileSize", description="Size of the audio file in bytes"),
+ pydantic.Field(alias="fileSize", description="Bytes received (Pulse responses)."),
] = None
if IS_PYDANTIC_V2:
diff --git a/src/smallestai/waves/types/transcription_result.py b/src/smallestai/waves/types/transcription_result.py
deleted file mode 100644
index 134dba9e..00000000
--- a/src/smallestai/waves/types/transcription_result.py
+++ /dev/null
@@ -1,38 +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
-from ...core.unchecked_base_model import UncheckedBaseModel
-
-
-class TranscriptionResult(UncheckedBaseModel):
- session_id: typing.Optional[str] = pydantic.Field(default=None)
- """
- Unique identifier for the transcription session
- """
-
- transcript: typing.Optional[str] = pydantic.Field(default=None)
- """
- Transcribed text
- """
-
- is_final: typing.Optional[bool] = pydantic.Field(default=None)
- """
- Final transcription flag
- """
-
- is_last: typing.Optional[bool] = pydantic.Field(default=None)
- """
- Last transcription flag
- """
-
- 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/smallestai/waves/types/unauthorized_error_body.py b/src/smallestai/waves/types/unauthorized_error_body.py
index cf8df855..3d977693 100644
--- a/src/smallestai/waves/types/unauthorized_error_body.py
+++ b/src/smallestai/waves/types/unauthorized_error_body.py
@@ -2,7 +2,19 @@
import typing
-from .pulse_stt_auth_missing_response import PulseSttAuthMissingResponse
-from .pulse_stt_error_response_legacy import PulseSttErrorResponseLegacy
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
-UnauthorizedErrorBody = typing.Union[PulseSttAuthMissingResponse, PulseSttErrorResponseLegacy]
+
+class UnauthorizedErrorBody(UncheckedBaseModel):
+ error: 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/smallestai/waves/types/usage.py b/src/smallestai/waves/types/usage.py
new file mode 100644
index 00000000..7ebf09ed
--- /dev/null
+++ b/src/smallestai/waves/types/usage.py
@@ -0,0 +1,28 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+from .usage_prompt_tokens_details import UsagePromptTokensDetails
+
+
+class Usage(UncheckedBaseModel):
+ prompt_tokens: typing.Optional[int] = pydantic.Field(default=None)
+ """
+ Total input tokens (cached + uncached).
+ """
+
+ completion_tokens: typing.Optional[int] = None
+ total_tokens: typing.Optional[int] = None
+ prompt_tokens_details: typing.Optional[UsagePromptTokensDetails] = 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/smallestai/waves/types/usage_prompt_tokens_details.py b/src/smallestai/waves/types/usage_prompt_tokens_details.py
new file mode 100644
index 00000000..a104271e
--- /dev/null
+++ b/src/smallestai/waves/types/usage_prompt_tokens_details.py
@@ -0,0 +1,24 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2
+from ...core.unchecked_base_model import UncheckedBaseModel
+
+
+class UsagePromptTokensDetails(UncheckedBaseModel):
+ cached_tokens: typing.Optional[int] = pydantic.Field(default=None)
+ """
+ Subset of `prompt_tokens` served from prefix cache. Billed at
+ the discounted rate ($0.10 / 1M vs $0.40 / 1M for fresh input).
+ """
+
+ 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/smallestai/waves/types/transcribe_pulse_waves_response_utterances_item.py b/src/smallestai/waves/types/utterance.py
similarity index 76%
rename from src/smallestai/waves/types/transcribe_pulse_waves_response_utterances_item.py
rename to src/smallestai/waves/types/utterance.py
index 0be7de83..75d6cdf1 100644
--- a/src/smallestai/waves/types/transcribe_pulse_waves_response_utterances_item.py
+++ b/src/smallestai/waves/types/utterance.py
@@ -7,14 +7,11 @@
from ...core.unchecked_base_model import UncheckedBaseModel
-class TranscribePulseWavesResponseUtterancesItem(UncheckedBaseModel):
+class Utterance(UncheckedBaseModel):
text: typing.Optional[str] = None
start: typing.Optional[float] = None
end: typing.Optional[float] = None
- speaker: typing.Optional[str] = pydantic.Field(default=None)
- """
- Speaker if diarization is enabled
- """
+ speaker: typing.Optional[str] = None
if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
diff --git a/src/smallestai/waves/types/transcribe_pulse_waves_response_words_item.py b/src/smallestai/waves/types/word.py
similarity index 78%
rename from src/smallestai/waves/types/transcribe_pulse_waves_response_words_item.py
rename to src/smallestai/waves/types/word.py
index 9592675f..5547d56a 100644
--- a/src/smallestai/waves/types/transcribe_pulse_waves_response_words_item.py
+++ b/src/smallestai/waves/types/word.py
@@ -7,15 +7,19 @@
from ...core.unchecked_base_model import UncheckedBaseModel
-class TranscribePulseWavesResponseWordsItem(UncheckedBaseModel):
+class Word(UncheckedBaseModel):
+ word: typing.Optional[str] = None
start: typing.Optional[float] = None
end: typing.Optional[float] = None
- speaker: typing.Optional[str] = pydantic.Field(default=None)
+ confidence: typing.Optional[float] = pydantic.Field(default=None)
"""
- Speaker if diarization is enabled
+ Per-word confidence score, from 0.0 to 1.0.
"""
- word: typing.Optional[str] = None
+ speaker: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Present when `diarize=true`.
+ """
if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
diff --git a/tests/wire/test_atoms_webhooks.py b/tests/wire/test_atoms_webhooks.py
index 9f5ca025..3aad0fb4 100644
--- a/tests/wire/test_atoms_webhooks.py
+++ b/tests/wire/test_atoms_webhooks.py
@@ -38,6 +38,16 @@ def test_atoms_webhooks_delete() -> None:
verify_request_count(test_id, "DELETE", "/webhook/id", None, 1)
+def test_atoms_webhooks_update() -> None:
+ """Test update endpoint with WireMock"""
+ test_id = "atoms.webhooks.update.0"
+ client = get_client(test_id)
+ client.atoms.webhooks.update(
+ id="id",
+ )
+ verify_request_count(test_id, "PATCH", "/webhook/id", None, 1)
+
+
def test_atoms_webhooks_get_webhook_subscriptions_for_an_agent() -> None:
"""Test getWebhookSubscriptionsForAnAgent endpoint with WireMock"""
test_id = "atoms.webhooks.get_webhook_subscriptions_for_an_agent.0"
diff --git a/tests/wire/test_waves.py b/tests/wire/test_waves.py
index c42e1124..2add360e 100644
--- a/tests/wire/test_waves.py
+++ b/tests/wire/test_waves.py
@@ -124,3 +124,22 @@ def test_waves_synthesize_sse_tts() -> None:
):
pass
verify_request_count(test_id, "POST", "/waves/v1/tts/live", None, 1)
+
+
+def test_waves_list_voice_clones() -> None:
+ """Test list_voice_clones endpoint with WireMock"""
+ test_id = "waves.list_voice_clones.0"
+ client = get_client(test_id)
+ client.waves.list_voice_clones()
+ verify_request_count(test_id, "GET", "/waves/v1/voice-cloning", None, 1)
+
+
+def test_waves_create_voice_clone() -> None:
+ """Test create_voice_clone endpoint with WireMock"""
+ test_id = "waves.create_voice_clone.0"
+ client = get_client(test_id)
+ client.waves.create_voice_clone(
+ file="example_file",
+ display_name="displayName",
+ )
+ verify_request_count(test_id, "POST", "/waves/v1/voice-cloning", None, 1)
diff --git a/tests/wire/test_waves_electron.py b/tests/wire/test_waves_electron.py
new file mode 100644
index 00000000..1a24592f
--- /dev/null
+++ b/tests/wire/test_waves_electron.py
@@ -0,0 +1,19 @@
+from .conftest import get_client, verify_request_count
+
+from smallestai.waves import ElectronMessage
+
+
+def test_waves_electron_complete() -> None:
+ """Test complete endpoint with WireMock"""
+ test_id = "waves.electron.complete.0"
+ client = get_client(test_id)
+ client.waves.electron.complete(
+ model="electron",
+ messages=[
+ ElectronMessage(
+ role="user",
+ content="Hello!",
+ )
+ ],
+ )
+ verify_request_count(test_id, "POST", "/waves/v1/chat/completions", None, 1)
diff --git a/wiremock/wiremock-mappings.json b/wiremock/wiremock-mappings.json
index 5767c74a..abd0d9e2 100644
--- a/wiremock/wiremock-mappings.json
+++ b/wiremock/wiremock-mappings.json
@@ -2118,6 +2118,42 @@
}
}
},
+ {
+ "id": "d7f75404-4732-4f98-8fc5-4a2101d5cd7f",
+ "name": "Update a webhook - default",
+ "request": {
+ "urlPathTemplate": "/webhook/{id}",
+ "method": "PATCH",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
+ "pathParameters": {
+ "id": {
+ "equalTo": "id"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "body": "{\n \"status\": true,\n \"data\": \"60d0fe4f5311236168a109ca\"\n}",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ },
+ "uuid": "d7f75404-4732-4f98-8fc5-4a2101d5cd7f",
+ "persistent": true,
+ "priority": 3,
+ "metadata": {
+ "mocklab": {
+ "created": {
+ "at": "2020-01-01T00:00:00.000Z",
+ "via": "SYSTEM"
+ }
+ }
+ }
+ },
{
"id": "d78e54f2-cac3-4637-ad04-9d0299e40ffa",
"name": "Get webhook subscriptions for an agent - default",
@@ -5139,9 +5175,103 @@
}
}
}
+ },
+ {
+ "id": "b60f8d75-1c7a-4103-862e-f053097e5c4d",
+ "name": "List Voice Clones - default",
+ "request": {
+ "urlPathTemplate": "/waves/v1/voice-cloning",
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "body": "{\n \"data\": [\n {\n \"_id\": \"_id\",\n \"voiceId\": \"voice_dLP5T67Qw7\",\n \"displayName\": \"displayName\",\n \"description\": \"description\",\n \"accent\": \"accent\",\n \"tags\": [\n \"tags\"\n ],\n \"language\": \"language\",\n \"status\": \"pending\",\n \"cloningType\": \"instant\",\n \"modelIds\": [\n \"modelIds\"\n ],\n \"createdAt\": \"2024-01-15T09:30:00Z\"\n }\n ]\n}",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ },
+ "uuid": "b60f8d75-1c7a-4103-862e-f053097e5c4d",
+ "persistent": true,
+ "priority": 3,
+ "metadata": {
+ "mocklab": {
+ "created": {
+ "at": "2020-01-01T00:00:00.000Z",
+ "via": "SYSTEM"
+ }
+ }
+ },
+ "postServeActions": []
+ },
+ {
+ "id": "9a0e74fb-de4d-47c4-b383-ced90e767ea8",
+ "name": "Create a Voice Clone - default",
+ "request": {
+ "urlPathTemplate": "/waves/v1/voice-cloning",
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "body": "{\n \"message\": \"Voice clone created successfully\",\n \"data\": {\n \"voiceId\": \"voice_dLP5T67Qw7\",\n \"displayName\": \"displayName\",\n \"model\": \"model\",\n \"status\": \"pending\",\n \"language\": \"language\",\n \"audioFileNames\": [\n \"audioFileNames\"\n ],\n \"createdAt\": \"2024-01-15T09:30:00Z\",\n \"organizationId\": \"organizationId\",\n \"samples\": [\n {}\n ]\n }\n}",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ },
+ "uuid": "9a0e74fb-de4d-47c4-b383-ced90e767ea8",
+ "persistent": true,
+ "priority": 3,
+ "metadata": {
+ "mocklab": {
+ "created": {
+ "at": "2020-01-01T00:00:00.000Z",
+ "via": "SYSTEM"
+ }
+ }
+ }
+ },
+ {
+ "id": "0a17450a-d8b0-4413-904a-2917ba8f43ae",
+ "name": "Chat Completions (Electron) - Minimal request",
+ "request": {
+ "urlPathTemplate": "/waves/v1/chat/completions",
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "body": "{\n \"id\": \"id\",\n \"object\": \"chat.completion\",\n \"created\": 1,\n \"model\": \"model\",\n \"choices\": [\n {\n \"index\": 1,\n \"message\": {\n \"role\": \"user\"\n },\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 1,\n \"completion_tokens\": 1,\n \"total_tokens\": 1,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 1\n }\n }\n}",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ },
+ "uuid": "0a17450a-d8b0-4413-904a-2917ba8f43ae",
+ "persistent": true,
+ "priority": 3,
+ "metadata": {
+ "mocklab": {
+ "created": {
+ "at": "2020-01-01T00:00:00.000Z",
+ "via": "SYSTEM"
+ }
+ }
+ }
}
],
"meta": {
- "total": 146
+ "total": 150
}
}
\ No newline at end of file
From 674bc3b6ce4d937f3915da843965f4556691111a Mon Sep 17 00:00:00 2001
From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com>
Date: Tue, 28 Jul 2026 08:57:16 +0000
Subject: [PATCH 2/3] [fern-replay] Applied customizations
Patches applied (1):
- patch-0c9e9b4e: refactor: rename agent-swarm -> agent-crew across SDK + CLI (4.4.2)
---
.fern/replay.lock | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/.fern/replay.lock b/.fern/replay.lock
index 991221ad..ce1e26d2 100644
--- a/.fern/replay.lock
+++ b/.fern/replay.lock
@@ -18,14 +18,20 @@ generations:
cli_version: unknown
generator_versions:
fernapi/fern-python-sdk: 5.12.12
-current_generation: ae36558fb1b1fc953760c7d5064d4c7e523c6113
+ - commit_sha: 58539514affc27c6340e570ffd7f18d6c6806433
+ tree_hash: 58fcba582a25fe11d53122e234311bb95e43b19c
+ timestamp: 2026-07-28T08:57:13.556Z
+ cli_version: unknown
+ generator_versions:
+ fernapi/fern-python-sdk: 5.12.12
+current_generation: 58539514affc27c6340e570ffd7f18d6c6806433
patches:
- id: patch-0c9e9b4e
content_hash: sha256:43cbb1c853a3de0a20a4d1e151169f4039eec0f4d998b32978cb5038f2a3b665
original_commit: 0c9e9b4e2286bd64653a1bdeb91a2b871a56668e
original_message: "refactor: rename agent-swarm -> agent-crew across SDK + CLI (4.4.2)"
original_author: Maharshi Chattopadhyay
- base_generation: ae36558fb1b1fc953760c7d5064d4c7e523c6113
+ base_generation: 58539514affc27c6340e570ffd7f18d6c6806433
files:
- src/smallestai/atoms/swarm/__init__.py
- src/smallestai/atoms/swarm/clients/__init__.py
From f4ee075c4fa0d78ad48ead22167b55e00b94192a Mon Sep 17 00:00:00 2001
From: Abhishek Mishra
Date: Tue, 28 Jul 2026 14:35:17 +0530
Subject: [PATCH 3/3] =?UTF-8?q?release:=205.3.0=20=E2=80=94=20unified=20sp?=
=?UTF-8?q?eech=5Fto=5Ftext,=20typed=20streaming=20helper,=20STT/Electron?=
=?UTF-8?q?=20routing=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Bump version 5.2.0 -> 5.3.0 + changelog.md entry.
- Add hand-written typed streaming-STT helper (smallestai.waves.helpers.stream_speech_to_text)
covering the full STT param set incl. keyword boosting, with unit tests; .fernignore'd.
- Refresh waves/__init__.py to the regenerated export surface (electron, speech_to_text,
voice cloning) while preserving the stream_tts backward-compat shim. Fixes stale
pulse_stt_streaming reference and missing top-level exports (e.g. ElectronMessage).
- Remove tests/custom/test_pulse_rest_format_params.py: it guarded the removed legacy
/waves/v1/pulse/get_text endpoint (transcribe_pulse).
Gauntlet on the built 5.3.0 wheel: mypy clean, scripts/verify.py PASSED, custom suite
41 passed, fresh-install imports + surface OK. STT batch/stream + electron live-verified
against api.smallest.ai.
---
.fernignore | 1 +
changelog.md | 18 +
pyproject.toml | 2 +-
src/smallestai/waves/__init__.py | 569 +++++++++++++++++-
src/smallestai/waves/helpers/__init__.py | 3 +
src/smallestai/waves/helpers/streaming_stt.py | 214 +++++++
tests/custom/test_pulse_rest_format_params.py | 40 --
tests/custom/test_streaming_stt_helper.py | 159 +++++
8 files changed, 952 insertions(+), 54 deletions(-)
create mode 100644 src/smallestai/waves/helpers/__init__.py
create mode 100644 src/smallestai/waves/helpers/streaming_stt.py
delete mode 100644 tests/custom/test_pulse_rest_format_params.py
create mode 100644 tests/custom/test_streaming_stt_helper.py
diff --git a/.fernignore b/.fernignore
index ca415b58..9b54b840 100644
--- a/.fernignore
+++ b/.fernignore
@@ -47,3 +47,4 @@ RELEASE.md
# Pre-release verification harness (hand-written)
scripts/**
Makefile
+src/smallestai/waves/helpers/**
diff --git a/changelog.md b/changelog.md
index 20d865d4..26a301d2 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,21 @@
+## 5.3.0 - 2026-07-28
+
+Unified speech-to-text namespace (breaking), voice cloning, Electron LLM, webhook update.
+
+* **Breaking — unified STT**: legacy `client.waves.transcribe_pulse(...)` and
+ `client.waves.pulse_stt_streaming(...)` are removed. Use `client.waves.speech_to_text.transcribe(...)`
+ (file/batch, typed `model` pulse|pulse-pro + `language`) and `client.waves.speech_to_text.stream(...)`
+ (live WebSocket). The `smallestai.waves.types.transcribe_pulse_*` types are gone. No deprecation alias.
+* **Fix — STT/Electron routing**: `speech_to_text.*` and `electron.complete` were generated against the
+ atoms base and 404'd; both now resolve to the waves base. Live-verified against `api.smallest.ai`.
+* **Typed streaming helper** (`from smallestai.waves.helpers import stream_speech_to_text`): typed
+ handshake params for the full STT param set incl. keyword boosting (`keywords`), redaction, VAD,
+ diarization; booleans and list params serialized for you; passthrough via `additional_query_parameters`.
+* **Additive**: `waves.create_voice_clone` / `list_voice_clones`, `waves.electron.complete`,
+ `atoms.webhooks.update`.
+* Verified live end-to-end against `api.smallest.ai` (batch transcribe, live stream with keyword
+ boosting, electron completion).
+
## 5.2.0 - 2026-07-24
Agent Versioning v2 (branch/revision API) + new namespaces, plus an ergonomic versioning helper.
diff --git a/pyproject.toml b/pyproject.toml
index b536aeee..39a734b7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ dynamic = ["version"]
[tool.poetry]
name = "smallestai"
-version = "5.2.0"
+version = "5.3.0"
description = ""
readme = "README.md"
authors = []
diff --git a/src/smallestai/waves/__init__.py b/src/smallestai/waves/__init__.py
index f202e0b2..3842e4ea 100644
--- a/src/smallestai/waves/__init__.py
+++ b/src/smallestai/waves/__init__.py
@@ -6,22 +6,387 @@
from importlib import import_module
if typing.TYPE_CHECKING:
- from . import pulse_stt_streaming, speech_to_speech, streaming_tts, tts
- from .types.pronunciation_item import PronunciationItem
-
- # Backward-compat shims for the 4.3.1 surface (source in stream_tts.py,
- # protected by .fernignore). For new code use the namespaced Fern client,
- # e.g. `client.waves.tts.connect(...)` for the unified TTS WebSocket.
+ from .types import (
+ AsyncAccepted,
+ AudioChunk,
+ AudioChunkData,
+ AudioChunkStatus,
+ BadRequestErrorBody,
+ ChatCompletion,
+ ChatCompletionChoicesItem,
+ ChatCompletionChoicesItemFinishReason,
+ ChatCompletionObject,
+ CompletionStatus,
+ CompletionStatusStatus,
+ ConversationItem,
+ ConversationItemContentItem,
+ ConversationItemContentItemType,
+ ConversationItemRole,
+ ConversationItemStatus,
+ ConversationItemType,
+ CreateVoiceCloneWavesRequestModel,
+ CreateVoiceCloneWavesResponse,
+ CreateVoiceCloneWavesResponseData,
+ CreateVoiceCloneWavesResponseDataSamplesItem,
+ CreateVoiceCloneWavesResponseDataStatus,
+ DeletePronunciationDictResponse,
+ ElectronMessage,
+ ElectronToolCall,
+ ElectronToolCallFunction,
+ ElectronToolCallType,
+ Error,
+ ErrorError,
+ ErrorErrorDetailsItem,
+ ErrorResponse,
+ ErrorResponseError,
+ ErrorResponseStatus,
+ GetVoicesWavesRequestModel,
+ GetVoicesWavesResponse,
+ GetVoicesWavesResponseVoicesItem,
+ GetVoicesWavesResponseVoicesItemTags,
+ InternalServerErrorBody,
+ InternalServerErrorBodyErrorCode,
+ LightningLargeRequest,
+ LightningLargeRequestLanguage,
+ LightningLargeRequestOutputFormat,
+ LightningRequest,
+ LightningRequestLanguage,
+ LightningRequestOutputFormat,
+ Lightningv2Request,
+ Lightningv2RequestLanguage,
+ Lightningv2RequestOutputFormat,
+ ListVoiceClonesWavesResponse,
+ ListVoiceClonesWavesResponseDataItem,
+ ListVoiceClonesWavesResponseDataItemCloningType,
+ ListVoiceClonesWavesResponseDataItemStatus,
+ PronunciationDict,
+ PronunciationItem,
+ SessionConfig,
+ SessionConfigVoice,
+ StreamingTtsConfig,
+ SttErrorResponse,
+ SynthesizeLightningLargeWavesRequestOutputFormat,
+ SynthesizeLightningV2WavesRequestOutputFormat,
+ SynthesizeLightningWavesRequestOutputFormat,
+ SynthesizeSseLightningLargeWavesRequestOutputFormat,
+ SynthesizeSseLightningV2WavesRequestOutputFormat,
+ Tool,
+ ToolType,
+ TranscriptionResponse,
+ TranscriptionResponseMetadata,
+ TtsError,
+ TtsRequest,
+ TtsRequestLanguage,
+ TtsRequestModel,
+ TtsRequestNumberPronunciationLanguage,
+ TtsRequestOutputFormat,
+ UnauthorizedErrorBody,
+ UpdatePronunciationDictResponse,
+ Usage,
+ UsagePromptTokensDetails,
+ Utterance,
+ Word,
+ )
+ from .errors import (
+ BadGatewayError,
+ BadRequestError,
+ ContentTooLargeError,
+ ForbiddenError,
+ InternalServerError,
+ ServiceUnavailableError,
+ TooManyRequestsError,
+ UnauthorizedError,
+ )
+ from . import electron, speech_to_speech, speech_to_text, streaming_tts, tts
+ # Backward-compat shim for the 4.3.1 surface (source in stream_tts.py, .fernignore'd).
from .stream_tts import TTSConfig, WavesStreamingTTS
+ from .electron import (
+ ChatCompletionRequestResponseFormat,
+ ChatCompletionRequestResponseFormatType,
+ ChatCompletionRequestStop,
+ ChatCompletionRequestStreamOptions,
+ ChatCompletionRequestToolChoice,
+ ChatCompletionRequestToolChoiceFunction,
+ ChatCompletionRequestToolChoiceFunctionFunction,
+ ChatCompletionRequestToolChoiceFunctionType,
+ ChatCompletionRequestToolChoiceZero,
+ )
+ from .speech_to_speech import (
+ ConversationItemAdded,
+ ConversationItemAddedType,
+ ConversationItemCreate,
+ ConversationItemCreateType,
+ ConversationItemDone,
+ ConversationItemDoneType,
+ ErrorEvent,
+ ErrorEventError,
+ ErrorEventErrorCode,
+ ErrorEventType,
+ InputAudioBufferAppend,
+ InputAudioBufferAppendType,
+ InputAudioBufferSpeechStarted,
+ InputAudioBufferSpeechStartedType,
+ InputAudioBufferSpeechStopped,
+ InputAudioBufferSpeechStoppedType,
+ ResponseCancel,
+ ResponseCancelType,
+ ResponseCreate,
+ ResponseCreateType,
+ ResponseCreated,
+ ResponseCreatedResponse,
+ ResponseCreatedType,
+ ResponseDone,
+ ResponseDoneResponse,
+ ResponseDoneResponseStatus,
+ ResponseDoneResponseStatusDetails,
+ ResponseDoneResponseStatusDetailsError,
+ ResponseDoneResponseStatusDetailsReason,
+ ResponseDoneResponseUsage,
+ ResponseDoneType,
+ ResponseFunctionCallArgumentsDelta,
+ ResponseFunctionCallArgumentsDeltaType,
+ ResponseFunctionCallArgumentsDone,
+ ResponseFunctionCallArgumentsDoneType,
+ ResponseOutputAudioDelta,
+ ResponseOutputAudioDeltaType,
+ ResponseOutputAudioDone,
+ ResponseOutputAudioDoneType,
+ SessionConfigure,
+ SessionConfigureType,
+ SessionConfigured,
+ SessionConfiguredSession,
+ SessionConfiguredType,
+ SessionCreated,
+ SessionCreatedType,
+ SessionUpdate,
+ SessionUpdateSession,
+ SessionUpdateType,
+ SessionUpdated,
+ SessionUpdatedType,
+ )
+ from .speech_to_text import (
+ CloseStream,
+ CloseStreamType,
+ FinalizeSignal,
+ FinalizeSignalType,
+ TranscribeRequestEmotionDetection,
+ TranscribeRequestGenderDetection,
+ TranscribeRequestLanguage,
+ TranscribeRequestModel,
+ TranscribeRequestRedactPci,
+ TranscribeRequestRedactPii,
+ TranscribeRequestWebhookMethod,
+ TranscribeResponse,
+ TranscriptionErrorEvent,
+ TranscriptionErrorEventType,
+ TranscriptionEvent,
+ TranscriptionEventType,
+ TranscriptionEventUtterancesItem,
+ TranscriptionEventWordsItem,
+ )
+ from .streaming_tts import (
+ StreamTtsRequestMessage,
+ StreamTtsRequestMessageLanguage,
+ StreamTtsResponseMessage,
+ StreamTtsResponseMessageData,
+ StreamTtsResponseMessageError,
+ StreamTtsResponseMessageStatus,
+ )
+ from .tts import (
+ TtsRequestMessage,
+ TtsRequestMessageLanguage,
+ TtsRequestMessageModel,
+ TtsRequestMessageNumberPronunciationLanguage,
+ TtsResponseMessage,
+ TtsResponseMessageData,
+ TtsResponseMessageStatus,
+ )
_dynamic_imports: typing.Dict[str, str] = {
- "PronunciationItem": ".types.pronunciation_item",
- "pulse_stt_streaming": ".pulse_stt_streaming",
+ "WavesStreamingTTS": ".stream_tts",
+ "TTSConfig": ".stream_tts",
+ "AsyncAccepted": ".types",
+ "AudioChunk": ".types",
+ "AudioChunkData": ".types",
+ "AudioChunkStatus": ".types",
+ "BadGatewayError": ".errors",
+ "BadRequestError": ".errors",
+ "BadRequestErrorBody": ".types",
+ "ChatCompletion": ".types",
+ "ChatCompletionChoicesItem": ".types",
+ "ChatCompletionChoicesItemFinishReason": ".types",
+ "ChatCompletionObject": ".types",
+ "ChatCompletionRequestResponseFormat": ".electron",
+ "ChatCompletionRequestResponseFormatType": ".electron",
+ "ChatCompletionRequestStop": ".electron",
+ "ChatCompletionRequestStreamOptions": ".electron",
+ "ChatCompletionRequestToolChoice": ".electron",
+ "ChatCompletionRequestToolChoiceFunction": ".electron",
+ "ChatCompletionRequestToolChoiceFunctionFunction": ".electron",
+ "ChatCompletionRequestToolChoiceFunctionType": ".electron",
+ "ChatCompletionRequestToolChoiceZero": ".electron",
+ "CloseStream": ".speech_to_text",
+ "CloseStreamType": ".speech_to_text",
+ "CompletionStatus": ".types",
+ "CompletionStatusStatus": ".types",
+ "ContentTooLargeError": ".errors",
+ "ConversationItem": ".types",
+ "ConversationItemAdded": ".speech_to_speech",
+ "ConversationItemAddedType": ".speech_to_speech",
+ "ConversationItemContentItem": ".types",
+ "ConversationItemContentItemType": ".types",
+ "ConversationItemCreate": ".speech_to_speech",
+ "ConversationItemCreateType": ".speech_to_speech",
+ "ConversationItemDone": ".speech_to_speech",
+ "ConversationItemDoneType": ".speech_to_speech",
+ "ConversationItemRole": ".types",
+ "ConversationItemStatus": ".types",
+ "ConversationItemType": ".types",
+ "CreateVoiceCloneWavesRequestModel": ".types",
+ "CreateVoiceCloneWavesResponse": ".types",
+ "CreateVoiceCloneWavesResponseData": ".types",
+ "CreateVoiceCloneWavesResponseDataSamplesItem": ".types",
+ "CreateVoiceCloneWavesResponseDataStatus": ".types",
+ "DeletePronunciationDictResponse": ".types",
+ "ElectronMessage": ".types",
+ "ElectronToolCall": ".types",
+ "ElectronToolCallFunction": ".types",
+ "ElectronToolCallType": ".types",
+ "Error": ".types",
+ "ErrorError": ".types",
+ "ErrorErrorDetailsItem": ".types",
+ "ErrorEvent": ".speech_to_speech",
+ "ErrorEventError": ".speech_to_speech",
+ "ErrorEventErrorCode": ".speech_to_speech",
+ "ErrorEventType": ".speech_to_speech",
+ "ErrorResponse": ".types",
+ "ErrorResponseError": ".types",
+ "ErrorResponseStatus": ".types",
+ "FinalizeSignal": ".speech_to_text",
+ "FinalizeSignalType": ".speech_to_text",
+ "ForbiddenError": ".errors",
+ "GetVoicesWavesRequestModel": ".types",
+ "GetVoicesWavesResponse": ".types",
+ "GetVoicesWavesResponseVoicesItem": ".types",
+ "GetVoicesWavesResponseVoicesItemTags": ".types",
+ "InputAudioBufferAppend": ".speech_to_speech",
+ "InputAudioBufferAppendType": ".speech_to_speech",
+ "InputAudioBufferSpeechStarted": ".speech_to_speech",
+ "InputAudioBufferSpeechStartedType": ".speech_to_speech",
+ "InputAudioBufferSpeechStopped": ".speech_to_speech",
+ "InputAudioBufferSpeechStoppedType": ".speech_to_speech",
+ "InternalServerError": ".errors",
+ "InternalServerErrorBody": ".types",
+ "InternalServerErrorBodyErrorCode": ".types",
+ "LightningLargeRequest": ".types",
+ "LightningLargeRequestLanguage": ".types",
+ "LightningLargeRequestOutputFormat": ".types",
+ "LightningRequest": ".types",
+ "LightningRequestLanguage": ".types",
+ "LightningRequestOutputFormat": ".types",
+ "Lightningv2Request": ".types",
+ "Lightningv2RequestLanguage": ".types",
+ "Lightningv2RequestOutputFormat": ".types",
+ "ListVoiceClonesWavesResponse": ".types",
+ "ListVoiceClonesWavesResponseDataItem": ".types",
+ "ListVoiceClonesWavesResponseDataItemCloningType": ".types",
+ "ListVoiceClonesWavesResponseDataItemStatus": ".types",
+ "PronunciationDict": ".types",
+ "PronunciationItem": ".types",
+ "ResponseCancel": ".speech_to_speech",
+ "ResponseCancelType": ".speech_to_speech",
+ "ResponseCreate": ".speech_to_speech",
+ "ResponseCreateType": ".speech_to_speech",
+ "ResponseCreated": ".speech_to_speech",
+ "ResponseCreatedResponse": ".speech_to_speech",
+ "ResponseCreatedType": ".speech_to_speech",
+ "ResponseDone": ".speech_to_speech",
+ "ResponseDoneResponse": ".speech_to_speech",
+ "ResponseDoneResponseStatus": ".speech_to_speech",
+ "ResponseDoneResponseStatusDetails": ".speech_to_speech",
+ "ResponseDoneResponseStatusDetailsError": ".speech_to_speech",
+ "ResponseDoneResponseStatusDetailsReason": ".speech_to_speech",
+ "ResponseDoneResponseUsage": ".speech_to_speech",
+ "ResponseDoneType": ".speech_to_speech",
+ "ResponseFunctionCallArgumentsDelta": ".speech_to_speech",
+ "ResponseFunctionCallArgumentsDeltaType": ".speech_to_speech",
+ "ResponseFunctionCallArgumentsDone": ".speech_to_speech",
+ "ResponseFunctionCallArgumentsDoneType": ".speech_to_speech",
+ "ResponseOutputAudioDelta": ".speech_to_speech",
+ "ResponseOutputAudioDeltaType": ".speech_to_speech",
+ "ResponseOutputAudioDone": ".speech_to_speech",
+ "ResponseOutputAudioDoneType": ".speech_to_speech",
+ "ServiceUnavailableError": ".errors",
+ "SessionConfig": ".types",
+ "SessionConfigVoice": ".types",
+ "SessionConfigure": ".speech_to_speech",
+ "SessionConfigureType": ".speech_to_speech",
+ "SessionConfigured": ".speech_to_speech",
+ "SessionConfiguredSession": ".speech_to_speech",
+ "SessionConfiguredType": ".speech_to_speech",
+ "SessionCreated": ".speech_to_speech",
+ "SessionCreatedType": ".speech_to_speech",
+ "SessionUpdate": ".speech_to_speech",
+ "SessionUpdateSession": ".speech_to_speech",
+ "SessionUpdateType": ".speech_to_speech",
+ "SessionUpdated": ".speech_to_speech",
+ "SessionUpdatedType": ".speech_to_speech",
+ "StreamTtsRequestMessage": ".streaming_tts",
+ "StreamTtsRequestMessageLanguage": ".streaming_tts",
+ "StreamTtsResponseMessage": ".streaming_tts",
+ "StreamTtsResponseMessageData": ".streaming_tts",
+ "StreamTtsResponseMessageError": ".streaming_tts",
+ "StreamTtsResponseMessageStatus": ".streaming_tts",
+ "StreamingTtsConfig": ".types",
+ "SttErrorResponse": ".types",
+ "SynthesizeLightningLargeWavesRequestOutputFormat": ".types",
+ "SynthesizeLightningV2WavesRequestOutputFormat": ".types",
+ "SynthesizeLightningWavesRequestOutputFormat": ".types",
+ "SynthesizeSseLightningLargeWavesRequestOutputFormat": ".types",
+ "SynthesizeSseLightningV2WavesRequestOutputFormat": ".types",
+ "TooManyRequestsError": ".errors",
+ "Tool": ".types",
+ "ToolType": ".types",
+ "TranscribeRequestEmotionDetection": ".speech_to_text",
+ "TranscribeRequestGenderDetection": ".speech_to_text",
+ "TranscribeRequestLanguage": ".speech_to_text",
+ "TranscribeRequestModel": ".speech_to_text",
+ "TranscribeRequestRedactPci": ".speech_to_text",
+ "TranscribeRequestRedactPii": ".speech_to_text",
+ "TranscribeRequestWebhookMethod": ".speech_to_text",
+ "TranscribeResponse": ".speech_to_text",
+ "TranscriptionErrorEvent": ".speech_to_text",
+ "TranscriptionErrorEventType": ".speech_to_text",
+ "TranscriptionEvent": ".speech_to_text",
+ "TranscriptionEventType": ".speech_to_text",
+ "TranscriptionEventUtterancesItem": ".speech_to_text",
+ "TranscriptionEventWordsItem": ".speech_to_text",
+ "TranscriptionResponse": ".types",
+ "TranscriptionResponseMetadata": ".types",
+ "TtsError": ".types",
+ "TtsRequest": ".types",
+ "TtsRequestLanguage": ".types",
+ "TtsRequestMessage": ".tts",
+ "TtsRequestMessageLanguage": ".tts",
+ "TtsRequestMessageModel": ".tts",
+ "TtsRequestMessageNumberPronunciationLanguage": ".tts",
+ "TtsRequestModel": ".types",
+ "TtsRequestNumberPronunciationLanguage": ".types",
+ "TtsRequestOutputFormat": ".types",
+ "TtsResponseMessage": ".tts",
+ "TtsResponseMessageData": ".tts",
+ "TtsResponseMessageStatus": ".tts",
+ "UnauthorizedError": ".errors",
+ "UnauthorizedErrorBody": ".types",
+ "UpdatePronunciationDictResponse": ".types",
+ "Usage": ".types",
+ "UsagePromptTokensDetails": ".types",
+ "Utterance": ".types",
+ "Word": ".types",
+ "electron": ".electron",
"speech_to_speech": ".speech_to_speech",
+ "speech_to_text": ".speech_to_text",
"streaming_tts": ".streaming_tts",
"tts": ".tts",
- # Backward-compat shims (4.3.1 surface):
- "WavesStreamingTTS": ".stream_tts",
- "TTSConfig": ".stream_tts",
}
@@ -47,11 +412,189 @@ def __dir__():
__all__ = [
- "PronunciationItem",
"TTSConfig",
"WavesStreamingTTS",
- "pulse_stt_streaming",
+ "AsyncAccepted",
+ "AudioChunk",
+ "AudioChunkData",
+ "AudioChunkStatus",
+ "BadGatewayError",
+ "BadRequestError",
+ "BadRequestErrorBody",
+ "ChatCompletion",
+ "ChatCompletionChoicesItem",
+ "ChatCompletionChoicesItemFinishReason",
+ "ChatCompletionObject",
+ "ChatCompletionRequestResponseFormat",
+ "ChatCompletionRequestResponseFormatType",
+ "ChatCompletionRequestStop",
+ "ChatCompletionRequestStreamOptions",
+ "ChatCompletionRequestToolChoice",
+ "ChatCompletionRequestToolChoiceFunction",
+ "ChatCompletionRequestToolChoiceFunctionFunction",
+ "ChatCompletionRequestToolChoiceFunctionType",
+ "ChatCompletionRequestToolChoiceZero",
+ "CloseStream",
+ "CloseStreamType",
+ "CompletionStatus",
+ "CompletionStatusStatus",
+ "ContentTooLargeError",
+ "ConversationItem",
+ "ConversationItemAdded",
+ "ConversationItemAddedType",
+ "ConversationItemContentItem",
+ "ConversationItemContentItemType",
+ "ConversationItemCreate",
+ "ConversationItemCreateType",
+ "ConversationItemDone",
+ "ConversationItemDoneType",
+ "ConversationItemRole",
+ "ConversationItemStatus",
+ "ConversationItemType",
+ "CreateVoiceCloneWavesRequestModel",
+ "CreateVoiceCloneWavesResponse",
+ "CreateVoiceCloneWavesResponseData",
+ "CreateVoiceCloneWavesResponseDataSamplesItem",
+ "CreateVoiceCloneWavesResponseDataStatus",
+ "DeletePronunciationDictResponse",
+ "ElectronMessage",
+ "ElectronToolCall",
+ "ElectronToolCallFunction",
+ "ElectronToolCallType",
+ "Error",
+ "ErrorError",
+ "ErrorErrorDetailsItem",
+ "ErrorEvent",
+ "ErrorEventError",
+ "ErrorEventErrorCode",
+ "ErrorEventType",
+ "ErrorResponse",
+ "ErrorResponseError",
+ "ErrorResponseStatus",
+ "FinalizeSignal",
+ "FinalizeSignalType",
+ "ForbiddenError",
+ "GetVoicesWavesRequestModel",
+ "GetVoicesWavesResponse",
+ "GetVoicesWavesResponseVoicesItem",
+ "GetVoicesWavesResponseVoicesItemTags",
+ "InputAudioBufferAppend",
+ "InputAudioBufferAppendType",
+ "InputAudioBufferSpeechStarted",
+ "InputAudioBufferSpeechStartedType",
+ "InputAudioBufferSpeechStopped",
+ "InputAudioBufferSpeechStoppedType",
+ "InternalServerError",
+ "InternalServerErrorBody",
+ "InternalServerErrorBodyErrorCode",
+ "LightningLargeRequest",
+ "LightningLargeRequestLanguage",
+ "LightningLargeRequestOutputFormat",
+ "LightningRequest",
+ "LightningRequestLanguage",
+ "LightningRequestOutputFormat",
+ "Lightningv2Request",
+ "Lightningv2RequestLanguage",
+ "Lightningv2RequestOutputFormat",
+ "ListVoiceClonesWavesResponse",
+ "ListVoiceClonesWavesResponseDataItem",
+ "ListVoiceClonesWavesResponseDataItemCloningType",
+ "ListVoiceClonesWavesResponseDataItemStatus",
+ "PronunciationDict",
+ "PronunciationItem",
+ "ResponseCancel",
+ "ResponseCancelType",
+ "ResponseCreate",
+ "ResponseCreateType",
+ "ResponseCreated",
+ "ResponseCreatedResponse",
+ "ResponseCreatedType",
+ "ResponseDone",
+ "ResponseDoneResponse",
+ "ResponseDoneResponseStatus",
+ "ResponseDoneResponseStatusDetails",
+ "ResponseDoneResponseStatusDetailsError",
+ "ResponseDoneResponseStatusDetailsReason",
+ "ResponseDoneResponseUsage",
+ "ResponseDoneType",
+ "ResponseFunctionCallArgumentsDelta",
+ "ResponseFunctionCallArgumentsDeltaType",
+ "ResponseFunctionCallArgumentsDone",
+ "ResponseFunctionCallArgumentsDoneType",
+ "ResponseOutputAudioDelta",
+ "ResponseOutputAudioDeltaType",
+ "ResponseOutputAudioDone",
+ "ResponseOutputAudioDoneType",
+ "ServiceUnavailableError",
+ "SessionConfig",
+ "SessionConfigVoice",
+ "SessionConfigure",
+ "SessionConfigureType",
+ "SessionConfigured",
+ "SessionConfiguredSession",
+ "SessionConfiguredType",
+ "SessionCreated",
+ "SessionCreatedType",
+ "SessionUpdate",
+ "SessionUpdateSession",
+ "SessionUpdateType",
+ "SessionUpdated",
+ "SessionUpdatedType",
+ "StreamTtsRequestMessage",
+ "StreamTtsRequestMessageLanguage",
+ "StreamTtsResponseMessage",
+ "StreamTtsResponseMessageData",
+ "StreamTtsResponseMessageError",
+ "StreamTtsResponseMessageStatus",
+ "StreamingTtsConfig",
+ "SttErrorResponse",
+ "SynthesizeLightningLargeWavesRequestOutputFormat",
+ "SynthesizeLightningV2WavesRequestOutputFormat",
+ "SynthesizeLightningWavesRequestOutputFormat",
+ "SynthesizeSseLightningLargeWavesRequestOutputFormat",
+ "SynthesizeSseLightningV2WavesRequestOutputFormat",
+ "TooManyRequestsError",
+ "Tool",
+ "ToolType",
+ "TranscribeRequestEmotionDetection",
+ "TranscribeRequestGenderDetection",
+ "TranscribeRequestLanguage",
+ "TranscribeRequestModel",
+ "TranscribeRequestRedactPci",
+ "TranscribeRequestRedactPii",
+ "TranscribeRequestWebhookMethod",
+ "TranscribeResponse",
+ "TranscriptionErrorEvent",
+ "TranscriptionErrorEventType",
+ "TranscriptionEvent",
+ "TranscriptionEventType",
+ "TranscriptionEventUtterancesItem",
+ "TranscriptionEventWordsItem",
+ "TranscriptionResponse",
+ "TranscriptionResponseMetadata",
+ "TtsError",
+ "TtsRequest",
+ "TtsRequestLanguage",
+ "TtsRequestMessage",
+ "TtsRequestMessageLanguage",
+ "TtsRequestMessageModel",
+ "TtsRequestMessageNumberPronunciationLanguage",
+ "TtsRequestModel",
+ "TtsRequestNumberPronunciationLanguage",
+ "TtsRequestOutputFormat",
+ "TtsResponseMessage",
+ "TtsResponseMessageData",
+ "TtsResponseMessageStatus",
+ "UnauthorizedError",
+ "UnauthorizedErrorBody",
+ "UpdatePronunciationDictResponse",
+ "Usage",
+ "UsagePromptTokensDetails",
+ "Utterance",
+ "Word",
+ "electron",
"speech_to_speech",
+ "speech_to_text",
"streaming_tts",
"tts",
]
diff --git a/src/smallestai/waves/helpers/__init__.py b/src/smallestai/waves/helpers/__init__.py
new file mode 100644
index 00000000..70e87f70
--- /dev/null
+++ b/src/smallestai/waves/helpers/__init__.py
@@ -0,0 +1,3 @@
+from .streaming_stt import build_stt_stream_query, stream_speech_to_text
+
+__all__ = ["stream_speech_to_text", "build_stt_stream_query"]
diff --git a/src/smallestai/waves/helpers/streaming_stt.py b/src/smallestai/waves/helpers/streaming_stt.py
new file mode 100644
index 00000000..2d01536c
--- /dev/null
+++ b/src/smallestai/waves/helpers/streaming_stt.py
@@ -0,0 +1,214 @@
+"""Typed entry point for streaming speech-to-text (Pulse live WebSocket).
+
+The generated ``client.waves.speech_to_text.stream()`` accepts session config only
+through ``request_options["additional_query_parameters"]``, because the Pulse live
+endpoint reads every knob (``model``, ``language``, ``sample_rate``, keyword boosting,
+redaction, VAD, ...) from the WebSocket handshake query string. That works, but it is
+untyped and undiscoverable.
+
+``stream_speech_to_text`` gives all of those a typed keyword argument and forwards to
+the generated method, so it survives a Fern regen (this module is .fernignore'd). The
+typed params mirror the Speech-to-Text API reference; anything not named here still
+passes through ``additional_query_parameters``.
+
+Example (sync), with keyword boosting::
+
+ from smallestai import SmallestAI
+ from smallestai.waves.helpers import stream_speech_to_text
+
+ client = SmallestAI()
+ with stream_speech_to_text(
+ client, language="en", sample_rate=16000,
+ keywords=["Smallest", "Atoms", "Waves"], # keyword boosting
+ punctuate=True, numerals=True,
+ ) as socket:
+ socket.send_audio_chunk_out(pcm_bytes)
+ socket.send_finalize_signal({"type": "finalize"})
+ for event in socket:
+ print(event)
+
+The same call works with an ``AsyncSmallestAI`` client; it returns whatever the
+underlying ``stream()`` returns (a sync or async context manager).
+
+Booleans are sent to the server as the strings ``"true"`` / ``"false"``, and list
+params (``keywords``, ``redact_pii``, ``redact_pci``) are sent comma-joined, matching
+what the endpoint expects.
+"""
+
+import typing
+
+__all__ = ["stream_speech_to_text", "build_stt_stream_query"]
+
+# Sent to the server as "true" / "false" (the controller compares === "true").
+_BOOLEAN_KNOBS = frozenset(
+ {
+ "word_timestamps",
+ "sentence_timestamps",
+ "diarize",
+ "punctuate",
+ "capitalize",
+ "itn_normalize",
+ "numerals",
+ "finalize_on_words",
+ "full_transcript",
+ "vad",
+ "vad_events",
+ }
+)
+
+# Sent comma-joined when given a list/tuple (a plain string passes through as-is).
+_LIST_KNOBS = frozenset({"keywords", "redact_pii", "redact_pci"})
+
+
+def _coerce(key: str, value: typing.Any) -> typing.Any:
+ if key in _BOOLEAN_KNOBS and isinstance(value, bool):
+ return "true" if value else "false"
+ if key in _LIST_KNOBS and isinstance(value, (list, tuple)):
+ return ",".join(str(v) for v in value)
+ return value
+
+
+def build_stt_stream_query(
+ *,
+ language: str,
+ model: str = "pulse",
+ sample_rate: int = 16000,
+ encoding: str = "linear16",
+ format: typing.Optional[str] = None,
+ # transcript formatting / output
+ word_timestamps: typing.Optional[bool] = None,
+ sentence_timestamps: typing.Optional[bool] = None,
+ diarize: typing.Optional[bool] = None,
+ punctuate: typing.Optional[bool] = None,
+ capitalize: typing.Optional[bool] = None,
+ itn_normalize: typing.Optional[bool] = None,
+ numerals: typing.Optional[bool] = None,
+ full_transcript: typing.Optional[bool] = None,
+ max_words: typing.Optional[int] = None,
+ # turn-taking / endpointing
+ finalize_on_words: typing.Optional[bool] = None,
+ eou_timeout_ms: typing.Optional[int] = None,
+ # voice-activity detection
+ vad: typing.Optional[bool] = None,
+ vad_events: typing.Optional[bool] = None,
+ vad_threshold: typing.Optional[float] = None,
+ vad_min_speech_ms: typing.Optional[int] = None,
+ # redaction
+ redact_pii: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
+ redact_pci: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
+ # keyword boosting
+ keywords: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
+ # escape hatch for anything not named above
+ additional_query_parameters: typing.Optional[typing.Dict[str, typing.Any]] = None,
+) -> typing.Dict[str, typing.Any]:
+ """Build the handshake query dict for the Pulse live STT endpoint.
+
+ Split out so it can be unit-tested and reused without opening a socket. Only
+ ``language`` is required; every other param is omitted from the query when left as
+ ``None``, so the server applies its own default.
+ """
+ params: typing.Dict[str, typing.Any] = {
+ "model": model,
+ "language": language,
+ "sample_rate": sample_rate,
+ "encoding": encoding,
+ }
+ optional = {
+ "format": format,
+ "word_timestamps": word_timestamps,
+ "sentence_timestamps": sentence_timestamps,
+ "diarize": diarize,
+ "punctuate": punctuate,
+ "capitalize": capitalize,
+ "itn_normalize": itn_normalize,
+ "numerals": numerals,
+ "full_transcript": full_transcript,
+ "max_words": max_words,
+ "finalize_on_words": finalize_on_words,
+ "eou_timeout_ms": eou_timeout_ms,
+ "vad": vad,
+ "vad_events": vad_events,
+ "vad_threshold": vad_threshold,
+ "vad_min_speech_ms": vad_min_speech_ms,
+ "redact_pii": redact_pii,
+ "redact_pci": redact_pci,
+ "keywords": keywords,
+ }
+ for key, value in optional.items():
+ if value is None:
+ continue
+ params[key] = _coerce(key, value)
+ if additional_query_parameters:
+ params.update(additional_query_parameters)
+ return params
+
+
+def stream_speech_to_text(
+ client: typing.Any,
+ *,
+ language: str,
+ model: str = "pulse",
+ sample_rate: int = 16000,
+ encoding: str = "linear16",
+ format: typing.Optional[str] = None,
+ word_timestamps: typing.Optional[bool] = None,
+ sentence_timestamps: typing.Optional[bool] = None,
+ diarize: typing.Optional[bool] = None,
+ punctuate: typing.Optional[bool] = None,
+ capitalize: typing.Optional[bool] = None,
+ itn_normalize: typing.Optional[bool] = None,
+ numerals: typing.Optional[bool] = None,
+ full_transcript: typing.Optional[bool] = None,
+ max_words: typing.Optional[int] = None,
+ finalize_on_words: typing.Optional[bool] = None,
+ eou_timeout_ms: typing.Optional[int] = None,
+ vad: typing.Optional[bool] = None,
+ vad_events: typing.Optional[bool] = None,
+ vad_threshold: typing.Optional[float] = None,
+ vad_min_speech_ms: typing.Optional[int] = None,
+ redact_pii: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
+ redact_pci: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
+ keywords: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None,
+ additional_query_parameters: typing.Optional[typing.Dict[str, typing.Any]] = None,
+ request_options: typing.Optional[typing.Dict[str, typing.Any]] = None,
+) -> typing.Any:
+ """Open a streaming STT session with typed handshake params.
+
+ Delegates to ``client.waves.speech_to_text.stream(...)`` and returns its context
+ manager unchanged, so use it with ``with`` (sync client) or ``async with`` (async
+ client). Only ``language`` is required; the rest have Pulse-live defaults or are
+ omitted when ``None``.
+
+ Anything passed via ``additional_query_parameters`` or an existing
+ ``request_options["additional_query_parameters"]`` overrides the typed values.
+ """
+ query = build_stt_stream_query(
+ language=language,
+ model=model,
+ sample_rate=sample_rate,
+ encoding=encoding,
+ format=format,
+ word_timestamps=word_timestamps,
+ sentence_timestamps=sentence_timestamps,
+ diarize=diarize,
+ punctuate=punctuate,
+ capitalize=capitalize,
+ itn_normalize=itn_normalize,
+ numerals=numerals,
+ full_transcript=full_transcript,
+ max_words=max_words,
+ finalize_on_words=finalize_on_words,
+ eou_timeout_ms=eou_timeout_ms,
+ vad=vad,
+ vad_events=vad_events,
+ vad_threshold=vad_threshold,
+ vad_min_speech_ms=vad_min_speech_ms,
+ redact_pii=redact_pii,
+ redact_pci=redact_pci,
+ keywords=keywords,
+ additional_query_parameters=additional_query_parameters,
+ )
+ resolved: typing.Dict[str, typing.Any] = dict(request_options or {})
+ caller_overrides = resolved.get("additional_query_parameters") or {}
+ resolved["additional_query_parameters"] = {**query, **caller_overrides}
+ return client.waves.speech_to_text.stream(request_options=resolved)
diff --git a/tests/custom/test_pulse_rest_format_params.py b/tests/custom/test_pulse_rest_format_params.py
deleted file mode 100644
index f12ecdab..00000000
--- a/tests/custom/test_pulse_rest_format_params.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""Regression test for Pulse REST formatting-flag parameters.
-
-The spec at fern/apis/waves/openapi/pulse-stt-openapi.yaml documents three
-query params on POST /waves/v1/pulse/get_text — `format`, `punctuate`, and
-`capitalize` (string enum "true"/"false"). They were missing from the
-4.3.x SDK; restored in 4.4.0. This test guards against the regression
-by asserting all four client surfaces accept the params.
-"""
-
-import inspect
-
-
-def _assert_pulse_params_present(method, label: str) -> None:
- sig = inspect.signature(method)
- for name in ("format", "punctuate", "capitalize"):
- assert name in sig.parameters, f"{label}.transcribe_pulse missing `{name}` param"
-
-
-def test_sync_wrapper_transcribe_pulse_has_format_params() -> None:
- from smallestai.waves.client import WavesClient
-
- _assert_pulse_params_present(WavesClient.transcribe_pulse, "WavesClient")
-
-
-def test_async_wrapper_transcribe_pulse_has_format_params() -> None:
- from smallestai.waves.client import AsyncWavesClient
-
- _assert_pulse_params_present(AsyncWavesClient.transcribe_pulse, "AsyncWavesClient")
-
-
-def test_sync_raw_transcribe_pulse_has_format_params() -> None:
- from smallestai.waves.raw_client import RawWavesClient
-
- _assert_pulse_params_present(RawWavesClient.transcribe_pulse, "RawWavesClient")
-
-
-def test_async_raw_transcribe_pulse_has_format_params() -> None:
- from smallestai.waves.raw_client import AsyncRawWavesClient
-
- _assert_pulse_params_present(AsyncRawWavesClient.transcribe_pulse, "AsyncRawWavesClient")
diff --git a/tests/custom/test_streaming_stt_helper.py b/tests/custom/test_streaming_stt_helper.py
new file mode 100644
index 00000000..bb85ab7f
--- /dev/null
+++ b/tests/custom/test_streaming_stt_helper.py
@@ -0,0 +1,159 @@
+"""Unit tests for the typed streaming-STT helper (waves.helpers.stream_speech_to_text).
+
+Logic-only: a fake client captures the request_options passed to
+client.waves.speech_to_text.stream(...). No network, no real SDK STT surface needed.
+"""
+
+from smallestai.waves.helpers.streaming_stt import (
+ build_stt_stream_query,
+ stream_speech_to_text,
+)
+
+
+class _FakeStreamNamespace:
+ def __init__(self):
+ self.captured_request_options = None
+
+ def stream(self, *, request_options=None):
+ self.captured_request_options = request_options
+ return "SOCKET_CM" # stand-in for the context manager
+
+
+class _FakeClient:
+ def __init__(self):
+ stt = _FakeStreamNamespace()
+ self.waves = type("_W", (), {"speech_to_text": stt})()
+ self._stt = stt
+
+
+def test_defaults_and_required_language():
+ q = build_stt_stream_query(language="en")
+ assert q == {
+ "model": "pulse",
+ "language": "en",
+ "sample_rate": 16000,
+ "encoding": "linear16",
+ }
+
+
+def test_booleans_serialize_as_strings():
+ q = build_stt_stream_query(
+ language="hi",
+ itn_normalize=True,
+ finalize_on_words=False,
+ punctuate=True,
+ vad_events=False,
+ )
+ assert q["itn_normalize"] == "true"
+ assert q["finalize_on_words"] == "false"
+ assert q["punctuate"] == "true"
+ assert q["vad_events"] == "false"
+
+
+def test_keyword_boosting_list_is_comma_joined():
+ q = build_stt_stream_query(language="en", keywords=["Smallest", "Atoms", "Waves"])
+ assert q["keywords"] == "Smallest,Atoms,Waves"
+
+
+def test_keyword_boosting_string_passes_through():
+ q = build_stt_stream_query(language="en", keywords="Smallest,Atoms")
+ assert q["keywords"] == "Smallest,Atoms"
+
+
+def test_redaction_lists_are_comma_joined():
+ q = build_stt_stream_query(language="en", redact_pii=["ssn", "email"], redact_pci=["card"])
+ assert q["redact_pii"] == "ssn,email"
+ assert q["redact_pci"] == "card"
+
+
+def test_full_boolean_knob_set_serializes():
+ q = build_stt_stream_query(
+ language="en",
+ word_timestamps=True,
+ sentence_timestamps=False,
+ diarize=True,
+ capitalize=False,
+ numerals=True,
+ full_transcript=True,
+ vad=False,
+ )
+ assert q["word_timestamps"] == "true"
+ assert q["sentence_timestamps"] == "false"
+ assert q["diarize"] == "true"
+ assert q["capitalize"] == "false"
+ assert q["numerals"] == "true"
+ assert q["full_transcript"] == "true"
+ assert q["vad"] == "false"
+
+
+def test_value_params_pass_through():
+ q = build_stt_stream_query(language="en", max_words=6, format="json", eou_timeout_ms=800)
+ assert q["max_words"] == 6
+ assert q["format"] == "json"
+ assert q["eou_timeout_ms"] == 800
+
+
+def test_none_optionals_are_omitted():
+ q = build_stt_stream_query(language="en", eou_timeout_ms=None, vad_threshold=None)
+ assert "eou_timeout_ms" not in q
+ assert "vad_threshold" not in q
+
+
+def test_numeric_optionals_pass_through():
+ q = build_stt_stream_query(
+ language="en", eou_timeout_ms=1000, vad_threshold=0.5, vad_min_speech_ms=120
+ )
+ assert q["eou_timeout_ms"] == 1000
+ assert q["vad_threshold"] == 0.5
+ assert q["vad_min_speech_ms"] == 120
+
+
+def test_helper_forwards_query_into_request_options():
+ client = _FakeClient()
+ cm = stream_speech_to_text(client, language="en", sample_rate=8000)
+ assert cm == "SOCKET_CM"
+ ro = client._stt.captured_request_options
+ assert ro["additional_query_parameters"] == {
+ "model": "pulse",
+ "language": "en",
+ "sample_rate": 8000,
+ "encoding": "linear16",
+ }
+
+
+def test_caller_overrides_win_over_typed_values():
+ client = _FakeClient()
+ stream_speech_to_text(
+ client,
+ language="en",
+ additional_query_parameters={"language": "hi", "custom_flag": "x"},
+ )
+ aqp = client._stt.captured_request_options["additional_query_parameters"]
+ assert aqp["language"] == "hi" # caller override wins
+ assert aqp["custom_flag"] == "x" # unknown params still reach the wire
+
+
+def test_existing_request_options_preserved():
+ client = _FakeClient()
+ stream_speech_to_text(
+ client, language="en", request_options={"max_retries": 3}
+ )
+ ro = client._stt.captured_request_options
+ assert ro["max_retries"] == 3
+ assert ro["additional_query_parameters"]["language"] == "en"
+
+
+if __name__ == "__main__":
+ import sys
+
+ fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
+ failed = 0
+ for fn in fns:
+ try:
+ fn()
+ print(f" PASS {fn.__name__}")
+ except AssertionError as e:
+ failed += 1
+ print(f" FAIL {fn.__name__}: {e}")
+ print(f"\n==== {len(fns) - failed} PASS / {failed} FAIL ====")
+ sys.exit(1 if failed else 0)