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/.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 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/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/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/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/__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/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/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/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/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) 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