diff --git a/.fern/metadata.json b/.fern/metadata.json index baf0338..0d7c7d1 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -9,9 +9,9 @@ }, "pyproject_python_version": ">=3.9" }, - "originGitCommit": "9f50f669dbb3d0db633a6d904db8a5077b94764f", - "originGitCommitIsDirty": true, + "originGitCommit": "10a6db0bf8fe7358d20861af81ae01b0c177f5f0", + "originGitCommitIsDirty": false, "invokedBy": "ci", - "ciProvider": "unknown", - "sdkVersion": "2.0.1" + "ciProvider": "github", + "sdkVersion": "3.0.2" } \ No newline at end of file diff --git a/.fernignore b/.fernignore index a25a56b..4910365 100644 --- a/.fernignore +++ b/.fernignore @@ -8,4 +8,9 @@ .github/workflows/release-please.yml release-please-config.json .release-please-manifest.json -CHANGELOG.md \ No newline at end of file +CHANGELOG.md + +# Release safety net. Regen deleted both of these once; keep them listed. +# manual-publish.yml is the recovery path for a botched release. +.github/workflows/manual-publish.yml +AGENTS.md \ No newline at end of file diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index c914ea4..57a7e11 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -33,13 +33,18 @@ jobs: run: | rm -rf dist poetry build + # inputs.* reach the script through env, never through ${{ }} interpolation: + # a dispatch input is attacker-controllable text and would otherwise be + # pasted into the shell before bash ever parses it. - name: Assert built version matches expected + env: + EXPECTED_VERSION: ${{ inputs.expected_version }} run: | set -euo pipefail - test -f "dist/speechify_api-${{ inputs.expected_version }}.tar.gz" \ - || { echo "::error::expected dist/speechify_api-${{ inputs.expected_version }}.tar.gz not found"; ls -la dist; exit 1; } - test -f "dist/speechify_api-${{ inputs.expected_version }}-py3-none-any.whl" \ - || { echo "::error::expected wheel for ${{ inputs.expected_version }} not found"; ls -la dist; exit 1; } + test -f "dist/speechify_api-${EXPECTED_VERSION}.tar.gz" \ + || { echo "::error::expected dist/speechify_api-${EXPECTED_VERSION}.tar.gz not found"; ls -la dist; exit 1; } + test -f "dist/speechify_api-${EXPECTED_VERSION}-py3-none-any.whl" \ + || { echo "::error::expected wheel for ${EXPECTED_VERSION} not found"; ls -la dist; exit 1; } - name: Publish to PyPI run: | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 5f64cc0..f57d690 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -79,6 +79,195 @@ jobs: curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 - name: Install dependencies run: poetry install + # client_wrapper.py is Fern-generated and cannot be .fernignore'd, so an + # in-repo release-please marker does not survive: every regeneration strips + # the "# x-release-please-version" comments, the generic updater silently + # stops bumping these headers, and the SDK reports a version it is not. + # The tag is the one input a regeneration cannot touch, so stamp from it. + # + # These rewrites apply to the CI checkout ONLY and are never committed + # back. main therefore carries whatever version Fern last generated, and + # those two strings are expected to read stale between releases — that is + # by design, not drift. The published artifact is always correct because + # it is built from this stamped tree. Do not "fix" them on main. + - name: Stamp Fern-generated version strings from the release tag + env: + TAG_NAME: ${{ needs.release-please.outputs.tag_name }} + run: | + set -euo pipefail + python3 - <<'PY' + import os + import re + import sys + + + def fail(message): + sys.exit(f"::error::{message}") + + + raw_tag = os.environ["TAG_NAME"].strip() + if not raw_tag: + fail("release-please produced an empty tag_name; refusing to stamp") + + # include-v-in-tag is false, so the tag is bare semver. Tolerate a + # leading "v" so flipping that setting does not fail a valid release. + tag = raw_tag[1:] if raw_tag.startswith("v") else raw_tag + + # A prerelease or build suffix is legal and must survive verbatim: + # 4.0.0-rc.1 stamps as 4.0.0-rc.1, never as 4.0.0. Anything that is not + # semver means the tag is not what we think it is — stop before writing. + if not re.fullmatch( + r"\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?", tag + ): + fail(f"tag {raw_tag!r} is not bare semver; refusing to stamp") + + CLIENT_WRAPPER_PATH = "src/speechify/core/client_wrapper.py" + + # The "version" group is rewritten; "prefix" and "suffix" are put back + # untouched, so the surrounding literal has to match exactly or not at + # all. Both header values are stamped from the same tag. + TARGETS = ( + ( + "User-Agent", + r'(?P"User-Agent":\s*"speechify-api/)(?P[^"]*)(?P")', + ), + ( + "X-Fern-SDK-Version", + r'(?P"X-Fern-SDK-Version":\s*")(?P[^"]*)(?P")', + ), + ) + + try: + with open(CLIENT_WRAPPER_PATH, encoding="utf-8") as handle: + original = handle.read() + except OSError as error: + fail(f"cannot read {CLIENT_WRAPPER_PATH}: {error}") + + + def stamp_tag_into(match): + return f"{match.group('prefix')}{tag}{match.group('suffix')}" + + + stamped = original + for description, pattern in TARGETS: + replaced = [m.group("version") for m in re.finditer(pattern, stamped)] + if not replaced: + # Never skip. A regeneration that renames or restructures this + # line has to break the release here, loudly — a stamp that + # quietly finds nothing is exactly how 2.0.1 shipped as 3.0.0. + fail( + f"{CLIENT_WRAPPER_PATH}: no {description} version literal " + "matched. Fern regeneration has changed this file; update " + "the stamp step and the publish assertion before releasing." + ) + stamped = re.sub(pattern, stamp_tag_into, stamped) + for previous in replaced: + status = "unchanged" if previous == tag else "stamped" + print(f" [{status}] {description}: {previous} -> {tag}") + + if stamped == original: + print(f"{CLIENT_WRAPPER_PATH} already at {tag}; nothing to rewrite.") + else: + with open(CLIENT_WRAPPER_PATH, "w", encoding="utf-8") as handle: + handle.write(stamped) + print(f"{CLIENT_WRAPPER_PATH} stamped to {tag}.") + PY + # A stale version string is how speechify-api 2.0.1 shipped under a 3.0.0 + # tag. Every version-bearing value must agree with the tag before upload — + # PyPI is immutable, so this is the last point where it is still cheap. + # Runs after the stamp step: it is an independent check of the result, not + # a substitute for it, and it still covers the files nothing stamps. + - name: Assert every version string matches the release tag + env: + TAG_NAME: ${{ needs.release-please.outputs.tag_name }} + run: | + set -euo pipefail + python3 - <<'PY' + import json + import os + import re + import sys + + + def fail(message): + sys.exit(f"::error::{message}") + + + raw_tag = os.environ["TAG_NAME"].strip() + if not raw_tag: + fail("release-please produced an empty tag_name; refusing to publish") + + # include-v-in-tag is false, so the tag is bare semver. Tolerate a + # leading "v" so flipping that setting does not fail a valid release. + tag = raw_tag[1:] if raw_tag.startswith("v") else raw_tag + + + def read(path): + try: + with open(path, encoding="utf-8") as handle: + return handle.read() + except OSError as error: + fail(f"cannot read {path}: {error}") + + + def search(text, pattern, description): + match = re.search(pattern, text, re.MULTILINE | re.DOTALL) + if not match: + fail(f"no version found for {description}") + return match.group("version") + + + pyproject = read("pyproject.toml") + # [project] declares dynamic = ["version"], so poetry-core builds the + # artifact from [tool.poetry].version. That table is the real source. + poetry_table = re.search( + r"^\[tool\.poetry\]\s*$(?P.*?)(?=^\[|\Z)", + pyproject, + re.MULTILINE | re.DOTALL, + ) + if not poetry_table: + fail("pyproject.toml has no [tool.poetry] table") + + client_wrapper = read("src/speechify/core/client_wrapper.py") + metadata = json.loads(read(".fern/metadata.json")) + if "sdkVersion" not in metadata: + fail(".fern/metadata.json has no sdkVersion key") + + found = { + "pyproject.toml [tool.poetry].version": search( + poetry_table.group("body"), + r'^version\s*=\s*"(?P[^"]+)"', + "[tool.poetry].version", + ), + "client_wrapper.py User-Agent": search( + client_wrapper, + r'"User-Agent":\s*"speechify-api/(?P[^"]+)"', + "client_wrapper.py User-Agent", + ), + "client_wrapper.py X-Fern-SDK-Version": search( + client_wrapper, + r'"X-Fern-SDK-Version":\s*"(?P[^"]+)"', + "client_wrapper.py X-Fern-SDK-Version", + ), + ".fern/metadata.json sdkVersion": metadata["sdkVersion"], + } + + print(f"release tag: {raw_tag} (normalised: {tag})") + for source, version in found.items(): + status = "ok" if version == tag else "MISMATCH" + print(f" [{status}] {source}: {version}") + + mismatched = [s for s, v in found.items() if v != tag] + if mismatched: + print( + "::error::version strings disagree with tag " + f"{tag}: {', '.join(mismatched)}. " + "Refusing to publish — PyPI uploads are irreversible." + ) + sys.exit(1) + + print(f"All version strings agree with {tag}.") + PY - name: Publish to PyPI run: | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index 8e1c91b..9b594e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,41 +30,169 @@ The #1 failure mode is **a generated version string not getting bumped**, so the package publishes under the wrong number (or reports a false version to the API). A single stale string is a published contract break, not a typo. -When any version changes, **grep the whole tree and confirm ALL of these agree** -before release. Never check one and assume the rest: - -- `.release-please-manifest.json` → `"."` -- `pyproject.toml` → `[project].version` **and** `[tool.poetry].version` (both!) +There are exactly **four** version literals in the tree, plus the tag. All five +must agree **in the published artifact** — check every one, never check one and +assume the rest. Two are committed by the release PR, two are stamped in CI: + +| Literal | Who sets it | Correct on `main`? | +|---|---|---| +| `pyproject.toml` → `[tool.poetry].version` | release-please `extra-files` (toml) | yes — committed | +| `.fern/metadata.json` → `sdkVersion` | release-please `extra-files` (json) | yes — committed | +| `client_wrapper.py` → `User-Agent` | **stamped in CI from the tag** | no — stale between releases, by design | +| `client_wrapper.py` → `X-Fern-SDK-Version` | **stamped in CI from the tag** | no — stale between releases, by design | + +- `pyproject.toml` → `[tool.poetry].version` — the artifact version. `[project]` + declares `dynamic = ["version"]` and has **no** `version` key; poetry-core reads + `[tool.poetry]`. Do not add `[project].version`. **Load-bearing**: `poetry build` + reads it, so it must be committed by the release PR. - `src/speechify/core/client_wrapper.py` → `User-Agent` **and** `X-Fern-SDK-Version` -- the git tag created for the release -- the version in the built artifact filename (`speechify_api-X.Y.Z-*.whl`) +- `.fern/metadata.json` → `sdkVersion` +- the git tag created for the release (bare semver — `include-v-in-tag: false`) + +**The tag is the source of truth.** Everything else is either bumped to match it +by release-please or rewritten to match it at publish time. + +Not version literals, so leave them alone: + +- `.release-please-manifest.json` — release-please's own state, it owns this. +- `src/speechify/version.py` — resolves `__version__` from installed package + metadata at import time. Nothing to bump. Fast audit: ```bash grep -rnE '[0-9]+\.[0-9]+\.[0-9]+' \ - .release-please-manifest.json pyproject.toml \ - src/speechify/core/client_wrapper.py | grep -v '<3.0.0' + pyproject.toml src/speechify/core/client_wrapper.py .fern/metadata.json ``` +On a checked-out `main` this grep will show `client_wrapper.py` behind the last +release. **That is expected** — see the stamping section below. What must never +disagree is the *published* artifact, which the publish job builds from a stamped +and asserted tree. + `X-Fern-SDK-Version` / `User-Agent` are sent on every request. If they lie, your telemetry, version-gating, and support debugging are all wrong for that release. ## Known plumbing traps -- **`pyproject.toml` version wiring.** release-please's Python updater **skips - `pyproject.toml` when `[project].dynamic = ["version"]`**. If the version is - dynamic and `extra-files` does not target it, `poetry build` builds a STALE - version while the tag/manifest say something else. Keep the version **static** - in `[project].version` and ensure `release-please-config.json` `extra-files` - bumps it. (This is exactly how we shipped `2.0.1` under a `3.0.0` tag.) -- **`type: "generic"` extra-files no-op silently** unless the target line carries - an `x-release-please-version` marker comment. Fern regenerates - `client_wrapper.py` each run, so prefer the `toml` updaters on `pyproject.toml` - as the real source of truth; do not rely on the generic updater alone. -- The publish job trusts whatever `poetry build` produces. There is a - `manual-publish.yml` workflow that asserts the built version before uploading — - prefer it for any one-off republish, and keep the assert in the automatic path. +- **The default `python` release-type does NOT bump this `pyproject.toml`.** Its + updater resolves `parsed.project || parsed.tool.poetry` — our `[project]` table + exists, so it wins the `||`, has no `version`, declares `dynamic = ["version"]`, + and the updater logs `dynamic version found ... Skipping update` and returns the + file unchanged. It never falls through to `[tool.poetry]`. The `extra-files` + entry `$.tool.poetry.version` is therefore **load-bearing, not redundant** — + delete it and the published artifact silently keeps the old version. +- **Do not add a `$.project.version` jsonpath.** There is no such key. The + `GenericToml` updater logs `No entries modified` and no-ops, so it buys nothing + while implying coverage that does not exist. Adding a static `[project].version` + to "fix" it would contradict `dynamic = ["version"]` and break the PEP 621 build. +- **Never point a `type: "generic"` extra-file at a Fern-generated file.** The + `generic` updater only rewrites a line carrying an `x-release-please-version` + marker, and it no-ops **silently** when the marker is absent. `client_wrapper.py` + is generated and **cannot** be `.fernignore`d, so every regeneration strips the + marker and re-arms that trap. This is the exact failure that shipped `2.0.1` + under the `3.0.0` tag. There is deliberately no `generic` entry in + `release-please-config.json` — do not add one back. +- **`.fern/metadata.json` was uncovered until now.** It drifted to `2.0.1` while + the package was on `3.0.1`. It is covered by a `json` updater on `$.sdkVersion`. + It is *not* a marker file — the `json` updater is jsonpath-based and survives + regeneration, which is why it is safe to rely on. + +## Version stamping — how `client_wrapper.py` gets the right version + +**A fix that lives inside a generated file is not a fix.** Markers are gone. +Instead, the `publish` job rewrites the two Fern-owned literals from +`needs.release-please.outputs.tag_name` immediately before building. + +The `publish` job runs three steps in this order, and the order is the design: + +1. **Stamp** — rewrite `User-Agent` and `X-Fern-SDK-Version` in + `client_wrapper.py` from the tag. +2. **Assert** — independently re-read all four literals and compare each to the + tag. Fails on a mismatch *or* on a literal that has gone missing. +3. **Publish** — `poetry publish --build`. + +Never reorder these, and never let the publish step run without both in front of +it. The assertion is a check on the stamp's result, not a substitute for it. + +The stamp step **fails the build if a target literal is not found**. A Fern +regeneration that renames or restructures those lines must break the release +loudly — a stamp that quietly matches nothing is the same silent-no-op class that +caused the 3.0.0 incident. If it fails, fix the pattern in the stamp step *and* +the matching pattern in the assertion; do not delete the check. + +`include-v-in-tag` is false so the tag is bare semver. The stamp tolerates a +leading `v`, rejects anything that is not semver, and preserves a prerelease or +build suffix verbatim (`4.0.0-rc.1` stamps as `4.0.0-rc.1`, never `4.0.0`). + +### Stale strings on `main` are expected — do not "fix" them + +The stamp edits the **CI checkout only**. Nothing is committed back. So between +releases, `main`'s `client_wrapper.py` carries whatever version Fern last +generated, and it will usually look out of date. + +**This is correct behaviour, not a bug.** The published artifact is always right +because it is built from the stamped tree. If you "helpfully" hand-edit those +strings on `main`, you gain nothing — the next regeneration overwrites you and +the next release stamps over you anyway. Leave them alone. + +## After every Fern regeneration + +`.fernignore` protects these from being overwritten — they are safe: + +`.github/workflows/ci.yml`, `.github/workflows/release-please.yml`, +`.github/workflows/manual-publish.yml`, `release-please-config.json`, +`.release-please-manifest.json`, `CHANGELOG.md`, `AGENTS.md`. + +These are **regenerated** and must be re-checked by hand every single time: + +- [ ] `src/speechify/core/client_wrapper.py` — the two header lines still have + the shape the stamp step matches. Do **not** check the version values + themselves; they are stamped at publish time and are expected to be stale: + + ```python + "User-Agent": "speechify-api/", + "X-Fern-SDK-Version": "", + ``` + + Same key names, same quoting, same `speechify-api/` prefix. If a regen + changed any of that, update the patterns in **both** the stamp step and the + publish assertion in `release-please.yml`. +- [ ] `pyproject.toml` — `[tool.poetry].version` still present; `[project]` still + `dynamic` and still has no `version` key. +- [ ] `.fern/metadata.json` — `sdkVersion` key still named that. +- [ ] `release-please-config.json` — still has **no** `type: "generic"` entry. + +Verify the shapes without caring about the values: + +```bash +grep -nE '"(User-Agent|X-Fern-SDK-Version)":' src/speechify/core/client_wrapper.py +``` + +If any of the above moved or was renamed, fix it *before* merging — the stamp +step and the publish assertion will both fail the release otherwise (by design). + +## Publish assertion + +`release-please.yml`'s `publish` job runs a version/tag assertion **after the +stamp step and before** `poetry publish --build`. It fails the job if any of the +four literals disagrees with the tag, **or if a literal has gone missing +entirely** — a missing literal is the silent-no-op class and is treated as a hard +failure, not a skip. Every value and the tag are printed on failure. Keep the +order **stamp → assert → publish**. + +`manual-publish.yml` is the recovery path for a one-off republish; it asserts the +built artifact filename against an explicit `expected_version` input. + +## Merging a release PR + +- The repo allows **squash merge only** (merge commits and rebase are disabled). + Squash uses `PR_TITLE` + `PR_BODY`, so the **PR body becomes the commit message + on `main`** — that is the only thing release-please parses. +- A `Release-As: X.Y.Z` footer must therefore live in the **PR body**, as the + final line, standalone, unindented and without backticks. In a commit message + trailer position it is ignored if indented or fenced. +- `BREAKING CHANGE:` footers belong in the PR body too, for the same reason. ## If a release has already gone wrong @@ -80,11 +208,33 @@ telemetry, version-gating, and support debugging are all wrong for that release. A routine regeneration was pushed straight to publish. Failures, in order: 1. Breaking-change PRs were admin-merged past the review gate. -2. release-please tagged `3.0.0`, but `poetry build` used a stale `pyproject.toml` - version → **`speechify-api 2.0.1` was published to PyPI** (immutable), carrying - the 3.0.0 breaking code under a patch number. -3. Each stale version string was found reactively, one at a time, instead of via +2. Nothing bumped the version. `git show 2f19983 --stat` is the evidence: the + release commit touched **only** `.release-please-manifest.json` and + `CHANGELOG.md`. Both updaters had failed silently, for two different reasons: + - `pyproject.toml` was already `dynamic = ["version"]`, so the default `python` + updater logged `Skipping update` and left `[tool.poetry].version` at `2.0.1`. + The config had no `extra-files` TOML entry to catch it. + - the `generic` entry for `client_wrapper.py` was configured but the file had + no `x-release-please-version` marker, so it no-opped without erroring. +3. release-please tagged `3.0.0`; `poetry build` read `[tool.poetry].version` and + produced `2.0.1` → **`speechify-api 2.0.1` was published to PyPI** (immutable), + carrying the 3.0.0 breaking code under a patch number. +4. Each stale version string was found reactively, one at a time, instead of via one exhaustive version-surface audit. +The follow-up fix at the time was to make `[project].version` static. **That fix +does not survive a Fern regeneration** — the next regen restored +`dynamic = ["version"]` and re-armed the trap. Adding an `x-release-please-version` +marker to `client_wrapper.py` was the same mistake wearing a different hat: also +inside a generated file, also stripped by the next regen, also silent. Both are +gone now. + +Durable fixes live in exactly three places: `.fernignore`-protected files +(`release-please-config.json`), values derived from the tag at publish time (the +stamp step), or an assertion that fails the build (the publish assertion). If a +proposed fix is a literal written into a generated file, it is not durable — +it is a countdown to the next regeneration. + Lesson: **audit the whole release surface first, dry-run, get sign-off, then -publish.** Treat one stale version string as a signal to check every other one. +publish.** Treat one stale version string as a signal to check every other one, +and never rely on a fix that lives in a generated file. diff --git a/README.md b/README.md index f15bb1a..e15d1df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Speechify Python Library -[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=https%3A%2F%2Fgithub.com%2Fspeechifyinc%2Fspeechify-api-sdk-python) +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=https%3A%2F%2Fgithub.com%2Fspeechify-ai%2Fsdk-python) [![pypi](https://img.shields.io/pypi/v/speechify-api)](https://pypi.python.org/pypi/speechify-api) The Speechify Python library provides convenient access to the Speechify APIs from Python. @@ -14,6 +14,7 @@ The Speechify Python library provides convenient access to the Speechify APIs fr - [Environments](#environments) - [Async Client](#async-client) - [Exception Handling](#exception-handling) +- [Streaming](#streaming) - [Pagination](#pagination) - [Advanced](#advanced) - [Access Raw Response Data](#access-raw-response-data) @@ -34,7 +35,7 @@ pip install speechify-api ## Reference -A full reference for this library is available [here](https://github.com/speechifyinc/speechify-api-sdk-python/blob/HEAD/./reference.md). +A full reference for this library is available [here](https://github.com/speechify-ai/sdk-python/blob/HEAD/./reference.md). ## Usage @@ -50,8 +51,8 @@ client = Speechify( client.audio.speech( audio_format="mp3", input="Hello! This is the Speechify text-to-speech API.", - model="simba-english", - voice_id="george", + model="simba-3.2", + voice_id="geffen_32", ) ``` @@ -86,8 +87,8 @@ async def main() -> None: await client.audio.speech( audio_format="mp3", input="Hello! This is the Speechify text-to-speech API.", - model="simba-english", - voice_id="george", + model="simba-3.2", + voice_id="geffen_32", ) @@ -109,6 +110,24 @@ except ApiError as e: print(e.body) ``` +## Streaming + +The SDK supports streaming responses, as well, the response will be a generator that you can loop over. + +```python +from speechify import Speechify + +client = Speechify( + token="", +) + +client.audio.stream_with_timestamps( + input="Streaming long-form audio with the Speechify API.", + model="simba-3.2", + voice_id="geffen_32", +) +``` + ## Pagination Paginated requests will return a `SyncPager` or `AsyncPager`, which can be used as generators for the underlying object. @@ -120,7 +139,10 @@ client = Speechify( token="", ) -client.voices.list() +client.voices.list( + locale="en", + model="simba-3.2", +) ``` ```python diff --git a/poetry.lock b/poetry.lock index a7caaa7..8e768cf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -15,132 +15,132 @@ files = [ [[package]] name = "aiohttp" -version = "3.14.1" +version = "3.14.3" description = "Async http client/server framework (asyncio)" optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"aiohttp\"" files = [ - {file = "aiohttp-3.14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e"}, - {file = "aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491"}, - {file = "aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce"}, - {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3"}, - {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505"}, - {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521"}, - {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd"}, - {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb"}, - {file = "aiohttp-3.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42"}, - {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b"}, - {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192"}, - {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05"}, - {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe"}, - {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d"}, - {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966"}, - {file = "aiohttp-3.14.1-cp310-cp310-win32.whl", hash = "sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6"}, - {file = "aiohttp-3.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df"}, - {file = "aiohttp-3.14.1-cp310-cp310-win_arm64.whl", hash = "sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c"}, - {file = "aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2"}, - {file = "aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f"}, - {file = "aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8"}, - {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04"}, - {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8"}, - {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6"}, - {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af"}, - {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730"}, - {file = "aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621"}, - {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee"}, - {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573"}, - {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7"}, - {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf"}, - {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85"}, - {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3"}, - {file = "aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126"}, - {file = "aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5"}, - {file = "aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b"}, - {file = "aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480"}, - {file = "aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d"}, - {file = "aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2"}, - {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2"}, - {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a"}, - {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264"}, - {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842"}, - {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c"}, - {file = "aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95"}, - {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199"}, - {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817"}, - {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a"}, - {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4"}, - {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087"}, - {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3"}, - {file = "aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4"}, - {file = "aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271"}, - {file = "aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847"}, - {file = "aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178"}, - {file = "aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf"}, - {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd"}, - {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe"}, - {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4"}, - {file = "aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876"}, - {file = "aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da"}, - {file = "aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6"}, - {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96"}, - {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f"}, - {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296"}, - {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa"}, - {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451"}, - {file = "aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c"}, - {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca"}, - {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09"}, - {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397"}, - {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080"}, - {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345"}, - {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588"}, - {file = "aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780"}, - {file = "aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a"}, - {file = "aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8"}, - {file = "aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15"}, - {file = "aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8"}, - {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3"}, - {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba"}, - {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397"}, - {file = "aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448"}, - {file = "aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004"}, - {file = "aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983"}, - {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe"}, - {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333"}, - {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0"}, - {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a"}, - {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602"}, - {file = "aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca"}, - {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35"}, - {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844"}, - {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496"}, - {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5"}, - {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95"}, - {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444"}, - {file = "aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0"}, - {file = "aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719"}, - {file = "aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec"}, - {file = "aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2"}, - {file = "aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340"}, - {file = "aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d"}, - {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94"}, - {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc"}, - {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251"}, - {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1"}, - {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3"}, - {file = "aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c"}, - {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203"}, - {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1"}, - {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665"}, - {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1"}, - {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d"}, - {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c"}, - {file = "aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365"}, - {file = "aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9"}, - {file = "aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6"}, - {file = "aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32"}, + {file = "aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7"}, + {file = "aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19"}, + {file = "aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71"}, + {file = "aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf"}, + {file = "aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7"}, + {file = "aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc"}, ] [package.dependencies] @@ -176,26 +176,26 @@ typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" description = "Reusable constraint types to use with typing.Annotated" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, ] [[package]] name = "anyio" -version = "4.14.1" +version = "4.14.2" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72"}, - {file = "anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e"}, + {file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"}, + {file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"}, ] [package.dependencies] @@ -247,14 +247,14 @@ files = [ [[package]] name = "certifi" -version = "2026.6.17" +version = "2026.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, - {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, ] [[package]] @@ -776,14 +776,14 @@ files = [ [[package]] name = "packaging" -version = "26.2" +version = "26.3" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, - {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] [[package]] @@ -1285,14 +1285,14 @@ files = [ [[package]] name = "types-python-dateutil" -version = "2.9.0.20260518" +version = "2.9.0.20260807" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "types_python_dateutil-2.9.0.20260518-py3-none-any.whl", hash = "sha256:d6a9c5bd0de61460c8fdef8ab2b400f956a1a1075cce08d4e2b4434e478c50b8"}, - {file = "types_python_dateutil-2.9.0.20260518.tar.gz", hash = "sha256:51f02dc03b61c7f6a07df45797d4dfe8a1aa47f0b7db9ad89f6fd3a1a70e1b51"}, + {file = "types_python_dateutil-2.9.0.20260807-py3-none-any.whl", hash = "sha256:54aa3707350ed7a9cc0776fd2f6739679d6967d11b40150985e81edcb86df4db"}, + {file = "types_python_dateutil-2.9.0.20260807.tar.gz", hash = "sha256:e0b8a90d464c8684c66b7b8e4556d9074afdddcc56ca45323f0987134f9e7034"}, ] [[package]] @@ -1309,18 +1309,18 @@ files = [ [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" description = "Runtime typing introspection tools" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, + {file = "typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147"}, + {file = "typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47"}, ] [package.dependencies] -typing-extensions = ">=4.12.0" +typing-extensions = ">=4.15.0" [[package]] name = "urllib3" @@ -1342,117 +1342,117 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "yarl" -version = "1.24.2" +version = "1.24.5" description = "Yet another URL library" optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"aiohttp\"" files = [ - {file = "yarl-1.24.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5249a113065c2b7a958bc699759e359cd61cfc81e3069662208f48f191b7ed12"}, - {file = "yarl-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4425fa244fbf530b006d0c5f79ce920114cfff5b4f5f6056e669f8e160fdc0"}, - {file = "yarl-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15c0b5e49d3c44e2a0b93e6a49476c5edad0a7686b92c395765a7ea775572a75"}, - {file = "yarl-1.24.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:246d32a53a947c8f0189f5d699cbd4c7036de45d9359e13ba238d1239678c727"}, - {file = "yarl-1.24.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:64480fb3e4d4ed9ed71c48a91a477384fc342a50ca30071d2f8a88d51d9c9413"}, - {file = "yarl-1.24.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:349de4701dc3760b6e876628423a8f147ef4f5599d10aba1e10702075d424ed9"}, - {file = "yarl-1.24.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d162677af8d5d3d6ebab8394b021f4d041ac107a4b705873148a77a49dc9e1b2"}, - {file = "yarl-1.24.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5f5c6ec23a9043f2d139cc072f53dd23168d202a334b9b2fda8de4c3e890d90"}, - {file = "yarl-1.24.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:60de6742447fbbf697f16f070b8a443f1b5fe6ca3826fbef9fe70ecd5328e643"}, - {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acf93187c3710e422368eb768aee98db551ec7c85adc250207a95c16548ab7ac"}, - {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f4b0352fd41fd34b6651934606268816afd6914d09626f9bcbbf018edb0afb3f"}, - {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b208bb939099b4b297438da4e9b25357f0b1c791888669b963e45b203ea9f36"}, - {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4b85b8825e631295ff4bc8943f7471d54c533a9360bbe15ebb38e018b555bb8a"}, - {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e26acf20c26cb4fefc631fdb75aca2a6b8fa8b7b5d7f204fb6a8f1e63c706f53"}, - {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:819ca24f8eafcfb683c1bd5f44f2f488cea1274eb8944731ffd2e1f10f619342"}, - {file = "yarl-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:5cb0f995a901c36be096ccbf4c673591c2faabbe96279598ffaec8c030f85bf4"}, - {file = "yarl-1.24.2-cp310-cp310-win_arm64.whl", hash = "sha256:f408eace7e22a68b467a0562e0d27d322f91fe3eaaa6f466b962c6cfaea9fa39"}, - {file = "yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1"}, - {file = "yarl-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a97e42c8a2233f2f279ecadd9e4a037bcb5d813b78435e8eedd4db5a9e9708c"}, - {file = "yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d"}, - {file = "yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae"}, - {file = "yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a"}, - {file = "yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e"}, - {file = "yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50"}, - {file = "yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003"}, - {file = "yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f"}, - {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f"}, - {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294"}, - {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2"}, - {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c"}, - {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b"}, - {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5"}, - {file = "yarl-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:f8fdbcff8b2c7c9284e60c196f693588598ddcee31e11c18e14949ce44519d45"}, - {file = "yarl-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:b32c37a7a337e90822c45797bf3d79d60875cfcccd3ecc80e9f453d87026c122"}, - {file = "yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8"}, - {file = "yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2"}, - {file = "yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d"}, - {file = "yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035"}, - {file = "yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576"}, - {file = "yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8"}, - {file = "yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7"}, - {file = "yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c"}, - {file = "yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d"}, - {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db"}, - {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712"}, - {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996"}, - {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b"}, - {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c"}, - {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1"}, - {file = "yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad"}, - {file = "yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30"}, - {file = "yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536"}, - {file = "yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607"}, - {file = "yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1"}, - {file = "yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986"}, - {file = "yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488"}, - {file = "yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b"}, - {file = "yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592"}, - {file = "yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617"}, - {file = "yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92"}, - {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a"}, - {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44"}, - {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a"}, - {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf"}, - {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056"}, - {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992"}, - {file = "yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656"}, - {file = "yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461"}, - {file = "yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2"}, - {file = "yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630"}, - {file = "yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8"}, - {file = "yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14"}, - {file = "yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535"}, - {file = "yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14"}, - {file = "yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3"}, - {file = "yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208"}, - {file = "yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50"}, - {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd"}, - {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67"}, - {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1"}, - {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1"}, - {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b"}, - {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8"}, - {file = "yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0"}, - {file = "yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57"}, - {file = "yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b"}, - {file = "yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761"}, - {file = "yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8"}, - {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed"}, - {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543"}, - {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0"}, - {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024"}, - {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf"}, - {file = "yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc"}, - {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb"}, - {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420"}, - {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f"}, - {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa"}, - {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe"}, - {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd"}, - {file = "yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215"}, - {file = "yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d"}, - {file = "yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9"}, - {file = "yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d"}, + {file = "yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224"}, + {file = "yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd"}, + {file = "yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25"}, + {file = "yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba"}, + {file = "yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b"}, + {file = "yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4"}, + {file = "yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740"}, + {file = "yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4"}, + {file = "yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad"}, + {file = "yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047"}, + {file = "yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104"}, + {file = "yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688"}, + {file = "yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7"}, + {file = "yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f"}, ] [package.dependencies] diff --git a/pyproject.toml b/pyproject.toml index 169349c..7a17e96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [project] name = "speechify-api" -version = "3.0.1" +dynamic = ["version"] [tool.poetry] name = "speechify-api" -version = "3.0.1" +version = "3.0.2" description = "Official Speechify API SDK" readme = "README.md" authors = [ @@ -44,7 +44,7 @@ packages = [ [tool.poetry.urls] Documentation = 'https://docs.speechify.ai/api-reference' Homepage = 'https://docs.speechify.ai' -Repository = 'https://github.com/speechifyinc/speechify-api-sdk-python' +Repository = 'https://github.com/speechify-ai/sdk-python' [tool.poetry.dependencies] python = "^3.10" diff --git a/reference.md b/reference.md index 95b946a..892c6ff 100644 --- a/reference.md +++ b/reference.md @@ -42,8 +42,8 @@ client = Speechify( client.audio.speech( audio_format="mp3", input="Hello! This is the Speechify text-to-speech API.", - model="simba-english", - voice_id="george", + model="simba-3.2", + voice_id="geffen_32", ) ``` @@ -99,7 +99,7 @@ Please refer to the list of the supported languages and recommendations regardin
-**model:** `typing.Optional[GetSpeechRequestModel]` — Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. +**model:** `typing.Optional[GetSpeechRequestModel]` — Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility.
@@ -195,19 +195,7 @@ client.audio.stream(
-**input:** `str` - -Plain text or SSML to be synthesized to speech. -Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. -Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody - -
-
- -
-
- -**voice_id:** `str` — Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices +**request:** `GetStreamRequest`
@@ -231,18 +219,98 @@ body instead; it takes precedence over this header.
-**language:** `typing.Optional[str]` - -Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. -Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+ + + + + + + + +
client.audio.stream_with_timestamps(...) -> typing.Iterator[bytes] +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Synthesize speech and stream it back together with word-level speech +marks, for text highlighting, captions and audio-text synchronization +while the audio is still arriving. + +The response is a Server-Sent Events stream. Each `speech.chunk` event +carries a Base64-encoded run of audio, the speech marks that became +final with it, or both - a chunk may carry only one of the two, and the +last chunk of a stream is often marks-only. A terminal `speech.done` +event ends the stream; there is no `[DONE]` sentinel. Ignore any event +type you do not recognize, so that new event types do not break your +integration. + +Speech-mark times are absolute milliseconds from the start of the +synthesis, so concatenate the audio chunks into one stream and apply the +marks against that single timeline. Which chunk a mark arrives on is a +delivery detail and carries no meaning. Times stay correct for every +`output_format`: changing the codec or sample rate does not change the +duration. + +Speech marks are produced by the streaming-native models. The default +`simba-3.0` and `simba-3.2` both serve this route; the legacy +`simba-english` and `simba-multilingual` models return 400 +`speech_marks_unsupported` here. +For Base64-encoded audio and speech marks in one non-streamed JSON +response, on any model, use POST /v1/audio/speech. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from speechify import Speechify +from speechify.environment import SpeechifyEnvironment + +client = Speechify( + token="", + environment=SpeechifyEnvironment.DEFAULT, +) + +client.audio.stream_with_timestamps( + input="Streaming long-form audio with the Speechify API.", + model="simba-3.2", + voice_id="geffen_32", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
-**model:** `typing.Optional[GetStreamRequestModel]` — Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. +**request:** `GetStreamRequest`
@@ -250,7 +318,12 @@ Please refer to the list of the supported languages and recommendations regardin
-**options:** `typing.Optional[GetStreamOptionsRequest]` +**accept:** `typing.Optional[StreamWithTimestampsAudioRequestAccept]` + +Selects the audio container/codec carried inside the events when +`output_format` is not set in the request body. The selected media +type is echoed on the `Speechify-Audio-Content-Type` response +header, since the response's own Content-Type is `text/event-stream`.
@@ -258,10 +331,74 @@ Please refer to the list of the supported languages and recommendations regardin
-**output_format:** `typing.Optional[AudioStreamOutputFormat]` — The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over the `Accept` header when set, so you can request formats the `Accept` enum does not cover (e.g. `pcm_16000`, `ulaw_8000`). `wav_*` formats are not supported on streaming - use `POST /v1/audio/speech` for wav. +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+ + +
+
+
+ +## models +
client.models.list() -> ModelsResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List the text-to-speech models available for synthesis. Drive a model +picker from this response, then pass a model `id` as the `model` +parameter to POST /v1/audio/speech or /v1/audio/stream. The response +marks the default model (used when a request omits `model`), the +routes each model may be passed to, and which voices it accepts. +Multi-speaker models arrive in a separate `dialogue_models` array +because they are valid only on POST /v1/audio/dialogue. Returns +the full set in a single response: the model catalog is static +platform reference data, so it is intentionally not paginated. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from speechify import Speechify +from speechify.environment import SpeechifyEnvironment + +client = Speechify( + token="", + environment=SpeechifyEnvironment.DEFAULT, +) + +client.models.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
@@ -292,11 +429,13 @@ Please refer to the list of the supported languages and recommendations regardin
Lists the voices available to the caller - the shared voice -catalog plus the workspace's personal cloned voices. By default +catalog plus the workspace's cloned voices, whichever member or +service-account key created them. By default the full catalogue is returned in one response. Pagination is opt-in: pass `limit` (and then `cursor` from the previous response) to page through the list while `has_more` is true. Max -page size is 200. +page size is 200. Narrow the list with the `type` and `locale` +filters (applied before pagination, so pages stay full).
@@ -319,7 +458,10 @@ client = Speechify( environment=SpeechifyEnvironment.DEFAULT, ) -client.voices.list() +client.voices.list( + locale="en", + model="simba-3.2", +) ```
@@ -351,6 +493,48 @@ client.voices.list()
+**type:** `typing.Optional[ListVoicesRequestType]` + +Filter by voice type: `personal` (the workspace's cloned voices) +or `shared` (the public catalogue). Omit to return both. + +
+
+ +
+
+ +**locale:** `typing.Optional[str]` + +Filter to voices whose locale matches this BCP-47 language range, +prefix-matched: `en` matches `en-US` and `en-GB`; `en-US` matches +only `en-US`. Case-insensitive. Omit to return all locales. + +
+
+ +
+
+ +**gender:** `typing.Optional[ListVoicesRequestGender]` — Filter by voice gender. Omit to return all genders. + +
+
+ +
+
+ +**model:** `typing.Optional[str]` + +Filter to voices that support this model (as listed in each voice's +`models[]`), e.g. `simba-3.2`. Omit to return voices for all models. + +
+
+ +
+
+ **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -375,7 +559,13 @@ client.voices.list()
-Create a personal (cloned) voice for the user +Create a cloned voice for the workspace from a 10-30 second audio sample, with verified consent from the speaker. + +Cloning requires proof that the speaker agreed to it. Create a consent challenge with `POST /v1/voices/consent-challenges`, show the returned `phrase` to the speaker, record them reading it aloud, and send that recording here as `consent_recording` together with the challenge's `consent_challenge_id`. Speechify transcribes the recording, checks it against the phrase it issued, and keeps it as the consent record for the voice. A challenge is single use and short-lived, so record and submit in one sitting. + +The clone belongs to the workspace rather than the member who created it, and access follows the caller's workspace role and API-key scopes exactly as for any other voice: voices scopes to list it, audio scopes to synthesize with it, and the content-management permission plus a write scope on the key to delete it. Cloned voices are usable self-serve on `simba-3.0`, `simba-english` and `simba-multilingual`. `simba-3.2` also serves cloned voices, currently as a limited release enabled per workspace; contact Speechify to have it enabled for yours. + +Callers pinned before `Speechify-Version: 2026-09-13` use the previous flow instead: no challenge, and a `consent` form field carrying the speaker's name and email as a JSON string. That flow is deprecated and will be removed after a sunset window announced in the changelog.
@@ -402,9 +592,10 @@ client.voices.create( idempotency_key="a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", sample="example_sample", avatar="example_avatar", + consent_recording="example_consent_recording", name="name", gender="male", - consent="consent", + consent_challenge_id="consent_challenge_id", ) ``` @@ -442,7 +633,7 @@ not_specified GenderNotSpecified
-**sample:** `core.File` — Audio sample file +**sample:** `core.File` — Audio sample of the voice to clone, 10-30 seconds of clean speech.
@@ -450,11 +641,26 @@ not_specified GenderNotSpecified
-**consent:** `str` +**consent_challenge_id:** `str` -A **string** representing the user consent information in JSON format -This should include the fullName and email of the consenting individual. -For example, `{"fullName": "John Doe", "email": "john@example.com"}` +The `id` of the consent challenge this create consumes, from +`POST /v1/voices/consent-challenges`. Single use: once a +create has consumed it, whether or not that create +succeeded, it cannot be used again. + +
+
+ +
+
+ +**consent_recording:** `core.File` + +Recording of the speaker reading the challenge's `phrase` +aloud. This is the consent record for the voice, not a +second voice sample: it must be the same person as in +`sample`, and it is retained as evidence. 5-30 seconds, at +most 25 MB, in any common audio container.
@@ -518,9 +724,9 @@ response carries the `Idempotent-Replayed: true` header.
Fetch a single voice by id - a shared catalogue voice or one of -the caller's own personal (cloned) voices. A personal voice that -belongs to another workspace returns 404, identical to an -unknown id, so voice inventory is never enumerable across tenants. +the workspace's cloned voices. A cloned voice that belongs to +another workspace returns 404, identical to an unknown id, so +voice inventory is never enumerable across tenants.
@@ -593,7 +799,9 @@ client.voices.get(
-Delete a personal (cloned) voice +Delete one of the workspace's cloned voices. Requires the +`content.manage` permission (owner, admin, or member); a +service-account key is authorized by its scopes instead.
@@ -727,3 +935,111 @@ client.voices.download_sample(
+## Voices ConsentChallenges +
client.voices.consent_challenges.create(...) -> ConsentChallenge +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Start the consent check for a voice clone. + +Returns a `phrase` for the speaker to read aloud and an `id` that identifies this challenge. Show the phrase to the speaker exactly as returned, record them reading it, then send the recording and the `id` to `POST /v1/voices`, which verifies the recording against the phrase and keeps it as the consent record. + +A challenge is single use, is bound to the workspace that created it, and expires at `expires_at` - it is proof that a speaker was in front of a microphone just now, so create it when you are ready to record, not at the start of your flow. If it expires, create another one and record again. + +Challenge creation is rate limited per workspace at a few dozen per hour, far more tightly than the rest of the voice surface, because each one precedes a person recording themselves - mint it when your speaker is ready, not speculatively. Read the live ceiling off `RateLimit-*` rather than hard-coding it. **On a `429`, always honour `Retry-After` rather than a fixed backoff of your own**: the wait is measured in minutes and can run to most of an hour. `RateLimit-*` are omitted rather than reporting a bucket that is not the one refusing. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from speechify import Speechify +from speechify.environment import SpeechifyEnvironment + +client = Speechify( + token="", + environment=SpeechifyEnvironment.DEFAULT, +) + +client.voices.consent_challenges.create( + idempotency_key="a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", + full_name="Jane Doe", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**full_name:** `str` + +Full name of the person consenting to have their voice cloned. +Speechify binds it to the challenge and stores it with the consent +record, so the create that consumes the challenge does not carry it +and cannot change it. + +At most 120 bytes once UTF-8 encoded, which is 120 characters of +Latin script but around 40 of Chinese, Japanese or Korean. Stated in +bytes rather than as a `maxLength` because the two only agree on +single-byte scripts, and a character count that never over-accepts +would have to refuse Latin names at 30. A name over the limit comes +back as `validation_failed` reporting its measured length. + +
+
+ +
+
+ +**idempotency_key:** `typing.Optional[str]` + +A client-generated key (an opaque string, max 255 chars) that makes a +side-effect POST safe to retry: the server runs the operation exactly +once and replays the first response (its status and body) for 24 hours. +Reusing a key with a different request body, or while the first request +is still in flight, returns `409 idempotency_conflict`. A replayed +response carries the `Idempotent-Replayed: true` header. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ diff --git a/release-please-config.json b/release-please-config.json index 89ee893..05c2be3 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -7,19 +7,15 @@ ".": { "package-name": "speechify-api", "extra-files": [ - { - "type": "toml", - "path": "pyproject.toml", - "jsonpath": "$.project.version" - }, { "type": "toml", "path": "pyproject.toml", "jsonpath": "$.tool.poetry.version" }, { - "type": "generic", - "path": "src/speechify/core/client_wrapper.py" + "type": "json", + "path": ".fern/metadata.json", + "jsonpath": "$.sdkVersion" } ] } diff --git a/src/speechify/__init__.py b/src/speechify/__init__.py index fd29896..dc621df 100644 --- a/src/speechify/__init__.py +++ b/src/speechify/__init__.py @@ -9,6 +9,7 @@ from .types import ( AudioOutputFormat, AudioStreamOutputFormat, + ConsentChallenge, Error, ErrorCode, ErrorDetail, @@ -16,6 +17,8 @@ GetSpeechResponse, GetSpeechResponseAudioFormat, GetStreamOptionsRequest, + GetStreamRequest, + GetStreamRequestModel, GetVoice, GetVoiceGender, GetVoiceLanguage, @@ -23,6 +26,8 @@ GetVoicesModel, GetVoicesModelName, ListVoicesResponse, + Model, + ModelsResponse, NestedChunk, PaginationMeta, SpeechMarks, @@ -31,6 +36,7 @@ BadGatewayError, BadRequestError, ConflictError, + ContentTooLargeError, ForbiddenError, InternalServerError, NotFoundError, @@ -40,18 +46,18 @@ UnauthorizedError, UnprocessableEntityError, ) - from . import audio, voices + from . import audio, models, voices from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient from .audio import ( GetSpeechRequestAudioFormat, GetSpeechRequestModel, - GetStreamRequestModel, StreamAudioRequestAccept, + StreamWithTimestampsAudioRequestAccept, ) from .client import AsyncSpeechify, Speechify from .environment import SpeechifyEnvironment from .version import __version__ - from .voices import CreateVoicesRequestGender + from .voices import CreateVoicesRequestGender, ListVoicesRequestGender, ListVoicesRequestType _dynamic_imports: typing.Dict[str, str] = { "AsyncSpeechify": ".client", "AudioOutputFormat": ".types", @@ -59,6 +65,8 @@ "BadGatewayError": ".errors", "BadRequestError": ".errors", "ConflictError": ".errors", + "ConsentChallenge": ".types", + "ContentTooLargeError": ".errors", "CreateVoicesRequestGender": ".voices", "DefaultAioHttpClient": "._default_clients", "DefaultAsyncHttpxClient": "._default_clients", @@ -72,7 +80,8 @@ "GetSpeechResponse": ".types", "GetSpeechResponseAudioFormat": ".types", "GetStreamOptionsRequest": ".types", - "GetStreamRequestModel": ".audio", + "GetStreamRequest": ".types", + "GetStreamRequestModel": ".types", "GetVoice": ".types", "GetVoiceGender": ".types", "GetVoiceLanguage": ".types", @@ -80,7 +89,11 @@ "GetVoicesModel": ".types", "GetVoicesModelName": ".types", "InternalServerError": ".errors", + "ListVoicesRequestGender": ".voices", + "ListVoicesRequestType": ".voices", "ListVoicesResponse": ".types", + "Model": ".types", + "ModelsResponse": ".types", "NestedChunk": ".types", "NotFoundError": ".errors", "PaginationMeta": ".types", @@ -90,11 +103,13 @@ "Speechify": ".client", "SpeechifyEnvironment": ".environment", "StreamAudioRequestAccept": ".audio", + "StreamWithTimestampsAudioRequestAccept": ".audio", "TooManyRequestsError": ".errors", "UnauthorizedError": ".errors", "UnprocessableEntityError": ".errors", "__version__": ".version", "audio": ".audio", + "models": ".models", "voices": ".voices", } @@ -127,6 +142,8 @@ def __dir__(): "BadGatewayError", "BadRequestError", "ConflictError", + "ConsentChallenge", + "ContentTooLargeError", "CreateVoicesRequestGender", "DefaultAioHttpClient", "DefaultAsyncHttpxClient", @@ -140,6 +157,7 @@ def __dir__(): "GetSpeechResponse", "GetSpeechResponseAudioFormat", "GetStreamOptionsRequest", + "GetStreamRequest", "GetStreamRequestModel", "GetVoice", "GetVoiceGender", @@ -148,7 +166,11 @@ def __dir__(): "GetVoicesModel", "GetVoicesModelName", "InternalServerError", + "ListVoicesRequestGender", + "ListVoicesRequestType", "ListVoicesResponse", + "Model", + "ModelsResponse", "NestedChunk", "NotFoundError", "PaginationMeta", @@ -158,10 +180,12 @@ def __dir__(): "Speechify", "SpeechifyEnvironment", "StreamAudioRequestAccept", + "StreamWithTimestampsAudioRequestAccept", "TooManyRequestsError", "UnauthorizedError", "UnprocessableEntityError", "__version__", "audio", + "models", "voices", ] diff --git a/src/speechify/audio/__init__.py b/src/speechify/audio/__init__.py index 3167496..ebbb090 100644 --- a/src/speechify/audio/__init__.py +++ b/src/speechify/audio/__init__.py @@ -9,14 +9,14 @@ from .types import ( GetSpeechRequestAudioFormat, GetSpeechRequestModel, - GetStreamRequestModel, StreamAudioRequestAccept, + StreamWithTimestampsAudioRequestAccept, ) _dynamic_imports: typing.Dict[str, str] = { "GetSpeechRequestAudioFormat": ".types", "GetSpeechRequestModel": ".types", - "GetStreamRequestModel": ".types", "StreamAudioRequestAccept": ".types", + "StreamWithTimestampsAudioRequestAccept": ".types", } @@ -41,4 +41,9 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = ["GetSpeechRequestAudioFormat", "GetSpeechRequestModel", "GetStreamRequestModel", "StreamAudioRequestAccept"] +__all__ = [ + "GetSpeechRequestAudioFormat", + "GetSpeechRequestModel", + "StreamAudioRequestAccept", + "StreamWithTimestampsAudioRequestAccept", +] diff --git a/src/speechify/audio/client.py b/src/speechify/audio/client.py index aecb9e1..314d94c 100644 --- a/src/speechify/audio/client.py +++ b/src/speechify/audio/client.py @@ -9,11 +9,12 @@ from ..types.get_speech_options_request import GetSpeechOptionsRequest from ..types.get_speech_response import GetSpeechResponse from ..types.get_stream_options_request import GetStreamOptionsRequest +from ..types.get_stream_request_model import GetStreamRequestModel from .raw_client import AsyncRawAudioClient, RawAudioClient from .types.get_speech_request_audio_format import GetSpeechRequestAudioFormat from .types.get_speech_request_model import GetSpeechRequestModel -from .types.get_stream_request_model import GetStreamRequestModel from .types.stream_audio_request_accept import StreamAudioRequestAccept +from .types.stream_with_timestamps_audio_request_accept import StreamWithTimestampsAudioRequestAccept # this is used as the default value for optional parameters OMIT = typing.cast(typing.Any, ...) @@ -71,7 +72,7 @@ def speech( Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetSpeechRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. options : typing.Optional[GetSpeechOptionsRequest] @@ -91,14 +92,14 @@ def speech( from speechify import Speechify client = Speechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) client.audio.speech( audio_format="mp3", input="Hello! This is the Speechify text-to-speech API.", - model="simba-english", - voice_id="george", + model="simba-3.2", + voice_id="geffen_32", ) """ _response = self._raw_client.speech( @@ -158,7 +159,7 @@ def stream( Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetStreamRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. options : typing.Optional[GetStreamOptionsRequest] @@ -182,7 +183,7 @@ def stream( from speechify import Speechify client = Speechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) client.audio.stream( @@ -202,6 +203,112 @@ def stream( ) as r: yield from r.data + def stream_with_timestamps( + self, + *, + input: str, + voice_id: str, + accept: typing.Optional[StreamWithTimestampsAudioRequestAccept] = None, + language: typing.Optional[str] = OMIT, + model: typing.Optional[GetStreamRequestModel] = OMIT, + options: typing.Optional[GetStreamOptionsRequest] = OMIT, + output_format: typing.Optional[AudioStreamOutputFormat] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Iterator[str]: + """ + Synthesize speech and stream it back together with word-level speech + marks, for text highlighting, captions and audio-text synchronization + while the audio is still arriving. + + The response is a Server-Sent Events stream. Each `speech.chunk` event + carries a Base64-encoded run of audio, the speech marks that became + final with it, or both - a chunk may carry only one of the two, and the + last chunk of a stream is often marks-only. A terminal `speech.done` + event ends the stream; there is no `[DONE]` sentinel. Ignore any event + type you do not recognize, so that new event types do not break your + integration. + + Speech-mark times are absolute milliseconds from the start of the + synthesis, so concatenate the audio chunks into one stream and apply the + marks against that single timeline. Which chunk a mark arrives on is a + delivery detail and carries no meaning. Times stay correct for every + `output_format`: changing the codec or sample rate does not change the + duration. + + Speech marks are produced by the streaming-native models. The default + `simba-3.0` and `simba-3.2` both serve this route; the legacy + `simba-english` and `simba-multilingual` models return 400 + `speech_marks_unsupported` here. + For Base64-encoded audio and speech marks in one non-streamed JSON + response, on any model, use POST /v1/audio/speech. + + Parameters + ---------- + input : str + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + + voice_id : str + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + + accept : typing.Optional[StreamWithTimestampsAudioRequestAccept] + Selects the audio container/codec carried inside the events when + `output_format` is not set in the request body. The selected media + type is echoed on the `Speechify-Audio-Content-Type` response + header, since the response's own Content-Type is `text/event-stream`. + + language : typing.Optional[str] + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + + model : typing.Optional[GetStreamRequestModel] + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. + + options : typing.Optional[GetStreamOptionsRequest] + + output_format : typing.Optional[AudioStreamOutputFormat] + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over the `Accept` header when set, so you can request formats the `Accept` enum does not cover (e.g. `pcm_16000`, `ulaw_8000`). `wav_*` formats are not supported on streaming - use `POST /v1/audio/speech` for wav. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.Iterator[str] + A Server-Sent Events stream of `speech.chunk` events followed by one + terminal `speech.done` event. A failure after the stream has started + is delivered as a `speech.error` event carrying the standard error + envelope, because the status code is already committed. + + Examples + -------- + from speechify import Speechify + + client = Speechify( + "2026-09-13", + token="YOUR_TOKEN", + ) + response = client.audio.stream_with_timestamps( + input="Streaming long-form audio with the Speechify API.", + model="simba-3.2", + voice_id="geffen_32", + ) + for chunk in response: + yield chunk + """ + with self._raw_client.stream_with_timestamps( + input=input, + voice_id=voice_id, + accept=accept, + language=language, + model=model, + options=options, + output_format=output_format, + request_options=request_options, + ) as r: + yield from r.data + class AsyncAudioClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): @@ -255,7 +362,7 @@ async def speech( Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetSpeechRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. options : typing.Optional[GetSpeechOptionsRequest] @@ -277,7 +384,7 @@ async def speech( from speechify import AsyncSpeechify client = AsyncSpeechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) @@ -286,8 +393,8 @@ async def main() -> None: await client.audio.speech( audio_format="mp3", input="Hello! This is the Speechify text-to-speech API.", - model="simba-english", - voice_id="george", + model="simba-3.2", + voice_id="geffen_32", ) @@ -350,7 +457,7 @@ async def stream( Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetStreamRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. options : typing.Optional[GetStreamOptionsRequest] @@ -376,7 +483,7 @@ async def stream( from speechify import AsyncSpeechify client = AsyncSpeechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) @@ -402,3 +509,118 @@ async def main() -> None: ) as r: async for _chunk in r.data: yield _chunk + + async def stream_with_timestamps( + self, + *, + input: str, + voice_id: str, + accept: typing.Optional[StreamWithTimestampsAudioRequestAccept] = None, + language: typing.Optional[str] = OMIT, + model: typing.Optional[GetStreamRequestModel] = OMIT, + options: typing.Optional[GetStreamOptionsRequest] = OMIT, + output_format: typing.Optional[AudioStreamOutputFormat] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.AsyncIterator[str]: + """ + Synthesize speech and stream it back together with word-level speech + marks, for text highlighting, captions and audio-text synchronization + while the audio is still arriving. + + The response is a Server-Sent Events stream. Each `speech.chunk` event + carries a Base64-encoded run of audio, the speech marks that became + final with it, or both - a chunk may carry only one of the two, and the + last chunk of a stream is often marks-only. A terminal `speech.done` + event ends the stream; there is no `[DONE]` sentinel. Ignore any event + type you do not recognize, so that new event types do not break your + integration. + + Speech-mark times are absolute milliseconds from the start of the + synthesis, so concatenate the audio chunks into one stream and apply the + marks against that single timeline. Which chunk a mark arrives on is a + delivery detail and carries no meaning. Times stay correct for every + `output_format`: changing the codec or sample rate does not change the + duration. + + Speech marks are produced by the streaming-native models. The default + `simba-3.0` and `simba-3.2` both serve this route; the legacy + `simba-english` and `simba-multilingual` models return 400 + `speech_marks_unsupported` here. + For Base64-encoded audio and speech marks in one non-streamed JSON + response, on any model, use POST /v1/audio/speech. + + Parameters + ---------- + input : str + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + + voice_id : str + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + + accept : typing.Optional[StreamWithTimestampsAudioRequestAccept] + Selects the audio container/codec carried inside the events when + `output_format` is not set in the request body. The selected media + type is echoed on the `Speechify-Audio-Content-Type` response + header, since the response's own Content-Type is `text/event-stream`. + + language : typing.Optional[str] + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + + model : typing.Optional[GetStreamRequestModel] + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. + + options : typing.Optional[GetStreamOptionsRequest] + + output_format : typing.Optional[AudioStreamOutputFormat] + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over the `Accept` header when set, so you can request formats the `Accept` enum does not cover (e.g. `pcm_16000`, `ulaw_8000`). `wav_*` formats are not supported on streaming - use `POST /v1/audio/speech` for wav. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.AsyncIterator[str] + A Server-Sent Events stream of `speech.chunk` events followed by one + terminal `speech.done` event. A failure after the stream has started + is delivered as a `speech.error` event carrying the standard error + envelope, because the status code is already committed. + + Examples + -------- + import asyncio + + from speechify import AsyncSpeechify + + client = AsyncSpeechify( + "2026-09-13", + token="YOUR_TOKEN", + ) + + + async def main() -> None: + response = await client.audio.stream_with_timestamps( + input="Streaming long-form audio with the Speechify API.", + model="simba-3.2", + voice_id="geffen_32", + ) + async for chunk in response: + yield chunk + + + asyncio.run(main()) + """ + async with self._raw_client.stream_with_timestamps( + input=input, + voice_id=voice_id, + accept=accept, + language=language, + model=model, + options=options, + output_format=output_format, + request_options=request_options, + ) as r: + async for _chunk in r.data: + yield _chunk diff --git a/src/speechify/audio/raw_client.py b/src/speechify/audio/raw_client.py index d02ac39..96103a7 100644 --- a/src/speechify/audio/raw_client.py +++ b/src/speechify/audio/raw_client.py @@ -3,12 +3,14 @@ import contextlib import typing from json.decoder import JSONDecodeError +from logging import error, warning from ..core.api_error import ApiError from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.http_sse._api import EventSource from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as +from ..core.pydantic_utilities import parse_obj_as, parse_sse_obj from ..core.request_options import RequestOptions from ..core.serialization import convert_and_respect_annotation_metadata from ..errors.bad_gateway_error import BadGatewayError @@ -26,10 +28,11 @@ from ..types.get_speech_options_request import GetSpeechOptionsRequest from ..types.get_speech_response import GetSpeechResponse from ..types.get_stream_options_request import GetStreamOptionsRequest +from ..types.get_stream_request_model import GetStreamRequestModel from .types.get_speech_request_audio_format import GetSpeechRequestAudioFormat from .types.get_speech_request_model import GetSpeechRequestModel -from .types.get_stream_request_model import GetStreamRequestModel from .types.stream_audio_request_accept import StreamAudioRequestAccept +from .types.stream_with_timestamps_audio_request_accept import StreamWithTimestampsAudioRequestAccept from pydantic import ValidationError # this is used as the default value for optional parameters @@ -77,7 +80,7 @@ def speech( Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetSpeechRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. options : typing.Optional[GetSpeechOptionsRequest] @@ -276,7 +279,7 @@ def stream( Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetStreamRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. options : typing.Optional[GetStreamOptionsRequest] @@ -439,6 +442,252 @@ def _stream() -> HttpResponse[typing.Iterator[bytes]]: yield _stream() + @contextlib.contextmanager + def stream_with_timestamps( + self, + *, + input: str, + voice_id: str, + accept: typing.Optional[StreamWithTimestampsAudioRequestAccept] = None, + language: typing.Optional[str] = OMIT, + model: typing.Optional[GetStreamRequestModel] = OMIT, + options: typing.Optional[GetStreamOptionsRequest] = OMIT, + output_format: typing.Optional[AudioStreamOutputFormat] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Iterator[HttpResponse[typing.Iterator[str]]]: + """ + Synthesize speech and stream it back together with word-level speech + marks, for text highlighting, captions and audio-text synchronization + while the audio is still arriving. + + The response is a Server-Sent Events stream. Each `speech.chunk` event + carries a Base64-encoded run of audio, the speech marks that became + final with it, or both - a chunk may carry only one of the two, and the + last chunk of a stream is often marks-only. A terminal `speech.done` + event ends the stream; there is no `[DONE]` sentinel. Ignore any event + type you do not recognize, so that new event types do not break your + integration. + + Speech-mark times are absolute milliseconds from the start of the + synthesis, so concatenate the audio chunks into one stream and apply the + marks against that single timeline. Which chunk a mark arrives on is a + delivery detail and carries no meaning. Times stay correct for every + `output_format`: changing the codec or sample rate does not change the + duration. + + Speech marks are produced by the streaming-native models. The default + `simba-3.0` and `simba-3.2` both serve this route; the legacy + `simba-english` and `simba-multilingual` models return 400 + `speech_marks_unsupported` here. + For Base64-encoded audio and speech marks in one non-streamed JSON + response, on any model, use POST /v1/audio/speech. + + Parameters + ---------- + input : str + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + + voice_id : str + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + + accept : typing.Optional[StreamWithTimestampsAudioRequestAccept] + Selects the audio container/codec carried inside the events when + `output_format` is not set in the request body. The selected media + type is echoed on the `Speechify-Audio-Content-Type` response + header, since the response's own Content-Type is `text/event-stream`. + + language : typing.Optional[str] + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + + model : typing.Optional[GetStreamRequestModel] + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. + + options : typing.Optional[GetStreamOptionsRequest] + + output_format : typing.Optional[AudioStreamOutputFormat] + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over the `Accept` header when set, so you can request formats the `Accept` enum does not cover (e.g. `pcm_16000`, `ulaw_8000`). `wav_*` formats are not supported on streaming - use `POST /v1/audio/speech` for wav. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.Iterator[HttpResponse[typing.Iterator[str]]] + A Server-Sent Events stream of `speech.chunk` events followed by one + terminal `speech.done` event. A failure after the stream has started + is delivered as a `speech.error` event carrying the standard error + envelope, because the status code is already committed. + """ + with self._client_wrapper.httpx_client.stream( + "v1/audio/stream/with-timestamps", + method="POST", + json={ + "input": input, + "language": language, + "model": model, + "options": convert_and_respect_annotation_metadata( + object_=options, annotation=GetStreamOptionsRequest, direction="write" + ), + "output_format": output_format, + "voice_id": voice_id, + }, + headers={ + "content-type": "application/json", + "Accept": str(accept) if accept is not None else None, + }, + request_options=request_options, + omit=OMIT, + ) as _response: + + def _stream() -> HttpResponse[typing.Iterator[str]]: + try: + if 200 <= _response.status_code < 300: + + def _iter(): + _event_source = EventSource(_response) + for _sse in _event_source.iter_sse(): + if _sse.data == None: + return + try: + yield typing.cast( + str, + parse_sse_obj( + sse=_sse, + type_=str, # type: ignore + ), + ) + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + return + + return HttpResponse(response=_response, data=_iter()) + _response.read() + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield _stream() + class AsyncRawAudioClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): @@ -481,7 +730,7 @@ async def speech( Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetSpeechRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. options : typing.Optional[GetSpeechOptionsRequest] @@ -680,7 +929,7 @@ async def stream( Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. model : typing.Optional[GetStreamRequestModel] - Model used for audio synthesis. `simba-english` is optimized for English, `simba-multilingual` for non-English or mixed input. `simba-3.2` is the streaming-native model with lower TTFB and richer expressivity, and the recommended Simba 3 model. `simba-3.0` is the earlier Simba 3.0 model, still available. `simba-3.0` and `simba-3.2` are currently English only; multilingual coming soon, and non-English voices return 400 until it ships. + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. options : typing.Optional[GetStreamOptionsRequest] @@ -843,3 +1092,249 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[bytes]]: raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) yield await _stream() + + @contextlib.asynccontextmanager + async def stream_with_timestamps( + self, + *, + input: str, + voice_id: str, + accept: typing.Optional[StreamWithTimestampsAudioRequestAccept] = None, + language: typing.Optional[str] = OMIT, + model: typing.Optional[GetStreamRequestModel] = OMIT, + options: typing.Optional[GetStreamOptionsRequest] = OMIT, + output_format: typing.Optional[AudioStreamOutputFormat] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[str]]]: + """ + Synthesize speech and stream it back together with word-level speech + marks, for text highlighting, captions and audio-text synchronization + while the audio is still arriving. + + The response is a Server-Sent Events stream. Each `speech.chunk` event + carries a Base64-encoded run of audio, the speech marks that became + final with it, or both - a chunk may carry only one of the two, and the + last chunk of a stream is often marks-only. A terminal `speech.done` + event ends the stream; there is no `[DONE]` sentinel. Ignore any event + type you do not recognize, so that new event types do not break your + integration. + + Speech-mark times are absolute milliseconds from the start of the + synthesis, so concatenate the audio chunks into one stream and apply the + marks against that single timeline. Which chunk a mark arrives on is a + delivery detail and carries no meaning. Times stay correct for every + `output_format`: changing the codec or sample rate does not change the + duration. + + Speech marks are produced by the streaming-native models. The default + `simba-3.0` and `simba-3.2` both serve this route; the legacy + `simba-english` and `simba-multilingual` models return 400 + `speech_marks_unsupported` here. + For Base64-encoded audio and speech marks in one non-streamed JSON + response, on any model, use POST /v1/audio/speech. + + Parameters + ---------- + input : str + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + + voice_id : str + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + + accept : typing.Optional[StreamWithTimestampsAudioRequestAccept] + Selects the audio container/codec carried inside the events when + `output_format` is not set in the request body. The selected media + type is echoed on the `Speechify-Audio-Content-Type` response + header, since the response's own Content-Type is `text/event-stream`. + + language : typing.Optional[str] + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + + model : typing.Optional[GetStreamRequestModel] + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. + + options : typing.Optional[GetStreamOptionsRequest] + + output_format : typing.Optional[AudioStreamOutputFormat] + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over the `Accept` header when set, so you can request formats the `Accept` enum does not cover (e.g. `pcm_16000`, `ulaw_8000`). `wav_*` formats are not supported on streaming - use `POST /v1/audio/speech` for wav. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[str]]] + A Server-Sent Events stream of `speech.chunk` events followed by one + terminal `speech.done` event. A failure after the stream has started + is delivered as a `speech.error` event carrying the standard error + envelope, because the status code is already committed. + """ + async with self._client_wrapper.httpx_client.stream( + "v1/audio/stream/with-timestamps", + method="POST", + json={ + "input": input, + "language": language, + "model": model, + "options": convert_and_respect_annotation_metadata( + object_=options, annotation=GetStreamOptionsRequest, direction="write" + ), + "output_format": output_format, + "voice_id": voice_id, + }, + headers={ + "content-type": "application/json", + "Accept": str(accept) if accept is not None else None, + }, + request_options=request_options, + omit=OMIT, + ) as _response: + + async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[str]]: + try: + if 200 <= _response.status_code < 300: + + async def _iter(): + _event_source = EventSource(_response) + async for _sse in _event_source.aiter_sse(): + if _sse.data == None: + return + try: + yield typing.cast( + str, + parse_sse_obj( + sse=_sse, + type_=str, # type: ignore + ), + ) + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + return + + return AsyncHttpResponse(response=_response, data=_iter()) + await _response.aread() + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.text + ) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.json(), + cause=e, + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + yield await _stream() diff --git a/src/speechify/audio/types/__init__.py b/src/speechify/audio/types/__init__.py index 0d1a756..73ce14e 100644 --- a/src/speechify/audio/types/__init__.py +++ b/src/speechify/audio/types/__init__.py @@ -8,13 +8,13 @@ if typing.TYPE_CHECKING: from .get_speech_request_audio_format import GetSpeechRequestAudioFormat from .get_speech_request_model import GetSpeechRequestModel - from .get_stream_request_model import GetStreamRequestModel from .stream_audio_request_accept import StreamAudioRequestAccept + from .stream_with_timestamps_audio_request_accept import StreamWithTimestampsAudioRequestAccept _dynamic_imports: typing.Dict[str, str] = { "GetSpeechRequestAudioFormat": ".get_speech_request_audio_format", "GetSpeechRequestModel": ".get_speech_request_model", - "GetStreamRequestModel": ".get_stream_request_model", "StreamAudioRequestAccept": ".stream_audio_request_accept", + "StreamWithTimestampsAudioRequestAccept": ".stream_with_timestamps_audio_request_accept", } @@ -39,4 +39,9 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = ["GetSpeechRequestAudioFormat", "GetSpeechRequestModel", "GetStreamRequestModel", "StreamAudioRequestAccept"] +__all__ = [ + "GetSpeechRequestAudioFormat", + "GetSpeechRequestModel", + "StreamAudioRequestAccept", + "StreamWithTimestampsAudioRequestAccept", +] diff --git a/src/speechify/audio/types/stream_with_timestamps_audio_request_accept.py b/src/speechify/audio/types/stream_with_timestamps_audio_request_accept.py new file mode 100644 index 0000000..2cc504a --- /dev/null +++ b/src/speechify/audio/types/stream_with_timestamps_audio_request_accept.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +StreamWithTimestampsAudioRequestAccept = typing.Union[ + typing.Literal["audio/mpeg", "audio/ogg", "audio/aac", "audio/pcm"], typing.Any +] diff --git a/src/speechify/client.py b/src/speechify/client.py index 2cf7eef..4c47cb4 100644 --- a/src/speechify/client.py +++ b/src/speechify/client.py @@ -13,6 +13,7 @@ if typing.TYPE_CHECKING: from .audio.client import AsyncAudioClient, AudioClient + from .models.client import AsyncModelsClient, ModelsClient from .voices.client import AsyncVoicesClient, VoicesClient @@ -59,7 +60,7 @@ class Speechify: from speechify import Speechify client = Speechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) """ @@ -69,7 +70,7 @@ def __init__( *, base_url: typing.Optional[str] = None, environment: SpeechifyEnvironment = SpeechifyEnvironment.DEFAULT, - version: typing.Optional[str] = "2026-07-07", + version: typing.Optional[str] = "2026-09-13", token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SPEECHIFY_API_KEY"), headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, @@ -101,6 +102,7 @@ def __init__( logging=logging, ) self._audio: typing.Optional[AudioClient] = None + self._models: typing.Optional[ModelsClient] = None self._voices: typing.Optional[VoicesClient] = None @property @@ -111,6 +113,14 @@ def audio(self): self._audio = AudioClient(client_wrapper=self._client_wrapper) return self._audio + @property + def models(self): + if self._models is None: + from .models.client import ModelsClient # noqa: E402 + + self._models = ModelsClient(client_wrapper=self._client_wrapper) + return self._models + @property def voices(self): if self._voices is None: @@ -184,7 +194,7 @@ class AsyncSpeechify: from speechify import AsyncSpeechify client = AsyncSpeechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) """ @@ -194,7 +204,7 @@ def __init__( *, base_url: typing.Optional[str] = None, environment: SpeechifyEnvironment = SpeechifyEnvironment.DEFAULT, - version: typing.Optional[str] = "2026-07-07", + version: typing.Optional[str] = "2026-09-13", token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SPEECHIFY_API_KEY"), headers: typing.Optional[typing.Dict[str, str]] = None, async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, @@ -226,6 +236,7 @@ def __init__( logging=logging, ) self._audio: typing.Optional[AsyncAudioClient] = None + self._models: typing.Optional[AsyncModelsClient] = None self._voices: typing.Optional[AsyncVoicesClient] = None @property @@ -236,6 +247,14 @@ def audio(self): self._audio = AsyncAudioClient(client_wrapper=self._client_wrapper) return self._audio + @property + def models(self): + if self._models is None: + from .models.client import AsyncModelsClient # noqa: E402 + + self._models = AsyncModelsClient(client_wrapper=self._client_wrapper) + return self._models + @property def voices(self): if self._voices is None: diff --git a/src/speechify/core/client_wrapper.py b/src/speechify/core/client_wrapper.py index 11e6103..63452dc 100644 --- a/src/speechify/core/client_wrapper.py +++ b/src/speechify/core/client_wrapper.py @@ -31,12 +31,12 @@ def get_headers(self) -> typing.Dict[str, str]: import platform headers: typing.Dict[str, str] = { - "User-Agent": "speechify-api/3.0.1", + "User-Agent": "speechify-api/3.0.2", "X-Fern-Language": "Python", "X-Fern-Runtime": f"python/{platform.python_version()}", "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", "X-Fern-SDK-Name": "speechify-api", - "X-Fern-SDK-Version": "3.0.1", + "X-Fern-SDK-Version": "3.0.2", **(self.get_custom_headers() or {}), } if self._version is not None: diff --git a/src/speechify/errors/__init__.py b/src/speechify/errors/__init__.py index d620695..de3a90e 100644 --- a/src/speechify/errors/__init__.py +++ b/src/speechify/errors/__init__.py @@ -9,6 +9,7 @@ from .bad_gateway_error import BadGatewayError from .bad_request_error import BadRequestError from .conflict_error import ConflictError + from .content_too_large_error import ContentTooLargeError from .forbidden_error import ForbiddenError from .internal_server_error import InternalServerError from .not_found_error import NotFoundError @@ -21,6 +22,7 @@ "BadGatewayError": ".bad_gateway_error", "BadRequestError": ".bad_request_error", "ConflictError": ".conflict_error", + "ContentTooLargeError": ".content_too_large_error", "ForbiddenError": ".forbidden_error", "InternalServerError": ".internal_server_error", "NotFoundError": ".not_found_error", @@ -57,6 +59,7 @@ def __dir__(): "BadGatewayError", "BadRequestError", "ConflictError", + "ContentTooLargeError", "ForbiddenError", "InternalServerError", "NotFoundError", diff --git a/src/speechify/errors/content_too_large_error.py b/src/speechify/errors/content_too_large_error.py new file mode 100644 index 0000000..ee8652e --- /dev/null +++ b/src/speechify/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.error import Error + + +class ContentTooLargeError(ApiError): + def __init__(self, body: Error, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=413, headers=headers, body=body) diff --git a/src/speechify/models/__init__.py b/src/speechify/models/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/speechify/models/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/speechify/models/client.py b/src/speechify/models/client.py new file mode 100644 index 0000000..b4c204f --- /dev/null +++ b/src/speechify/models/client.py @@ -0,0 +1,118 @@ +# 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.models_response import ModelsResponse +from .raw_client import AsyncRawModelsClient, RawModelsClient + + +class ModelsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawModelsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawModelsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawModelsClient + """ + return self._raw_client + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ModelsResponse: + """ + List the text-to-speech models available for synthesis. Drive a model + picker from this response, then pass a model `id` as the `model` + parameter to POST /v1/audio/speech or /v1/audio/stream. The response + marks the default model (used when a request omits `model`), the + routes each model may be passed to, and which voices it accepts. + Multi-speaker models arrive in a separate `dialogue_models` array + because they are valid only on POST /v1/audio/dialogue. Returns + the full set in a single response: the model catalog is static + platform reference data, so it is intentionally not paginated. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ModelsResponse + The available text-to-speech models. + + Examples + -------- + from speechify import Speechify + + client = Speechify( + "2026-09-13", + token="YOUR_TOKEN", + ) + client.models.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + +class AsyncModelsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawModelsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawModelsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawModelsClient + """ + return self._raw_client + + async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> ModelsResponse: + """ + List the text-to-speech models available for synthesis. Drive a model + picker from this response, then pass a model `id` as the `model` + parameter to POST /v1/audio/speech or /v1/audio/stream. The response + marks the default model (used when a request omits `model`), the + routes each model may be passed to, and which voices it accepts. + Multi-speaker models arrive in a separate `dialogue_models` array + because they are valid only on POST /v1/audio/dialogue. Returns + the full set in a single response: the model catalog is static + platform reference data, so it is intentionally not paginated. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ModelsResponse + The available text-to-speech models. + + Examples + -------- + import asyncio + + from speechify import AsyncSpeechify + + client = AsyncSpeechify( + "2026-09-13", + token="YOUR_TOKEN", + ) + + + async def main() -> None: + await client.models.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data diff --git a/src/speechify/models/raw_client.py b/src/speechify/models/raw_client.py new file mode 100644 index 0000000..a389101 --- /dev/null +++ b/src/speechify/models/raw_client.py @@ -0,0 +1,233 @@ +# 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.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..errors.bad_request_error import BadRequestError +from ..errors.forbidden_error import ForbiddenError +from ..errors.internal_server_error import InternalServerError +from ..errors.too_many_requests_error import TooManyRequestsError +from ..errors.unauthorized_error import UnauthorizedError +from ..types.error import Error +from ..types.models_response import ModelsResponse +from pydantic import ValidationError + + +class RawModelsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[ModelsResponse]: + """ + List the text-to-speech models available for synthesis. Drive a model + picker from this response, then pass a model `id` as the `model` + parameter to POST /v1/audio/speech or /v1/audio/stream. The response + marks the default model (used when a request omits `model`), the + routes each model may be passed to, and which voices it accepts. + Multi-speaker models arrive in a separate `dialogue_models` array + because they are valid only on POST /v1/audio/dialogue. Returns + the full set in a single response: the model catalog is static + platform reference data, so it is intentionally not paginated. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ModelsResponse] + The available text-to-speech models. + """ + _response = self._client_wrapper.httpx_client.request( + "v1/audio/models", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ModelsResponse, + parse_obj_as( + type_=ModelsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _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 AsyncRawModelsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ModelsResponse]: + """ + List the text-to-speech models available for synthesis. Drive a model + picker from this response, then pass a model `id` as the `model` + parameter to POST /v1/audio/speech or /v1/audio/stream. The response + marks the default model (used when a request omits `model`), the + routes each model may be passed to, and which voices it accepts. + Multi-speaker models arrive in a separate `dialogue_models` array + because they are valid only on POST /v1/audio/dialogue. Returns + the full set in a single response: the model catalog is static + platform reference data, so it is intentionally not paginated. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ModelsResponse] + The available text-to-speech models. + """ + _response = await self._client_wrapper.httpx_client.request( + "v1/audio/models", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ModelsResponse, + parse_obj_as( + type_=ModelsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _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/speechify/types/__init__.py b/src/speechify/types/__init__.py index 9d924d4..5e5e6c7 100644 --- a/src/speechify/types/__init__.py +++ b/src/speechify/types/__init__.py @@ -8,6 +8,7 @@ if typing.TYPE_CHECKING: from .audio_output_format import AudioOutputFormat from .audio_stream_output_format import AudioStreamOutputFormat + from .consent_challenge import ConsentChallenge from .error import Error from .error_code import ErrorCode from .error_detail import ErrorDetail @@ -15,6 +16,8 @@ from .get_speech_response import GetSpeechResponse from .get_speech_response_audio_format import GetSpeechResponseAudioFormat from .get_stream_options_request import GetStreamOptionsRequest + from .get_stream_request import GetStreamRequest + from .get_stream_request_model import GetStreamRequestModel from .get_voice import GetVoice from .get_voice_gender import GetVoiceGender from .get_voice_language import GetVoiceLanguage @@ -22,12 +25,15 @@ from .get_voices_model import GetVoicesModel from .get_voices_model_name import GetVoicesModelName from .list_voices_response import ListVoicesResponse + from .model import Model + from .models_response import ModelsResponse from .nested_chunk import NestedChunk from .pagination_meta import PaginationMeta from .speech_marks import SpeechMarks _dynamic_imports: typing.Dict[str, str] = { "AudioOutputFormat": ".audio_output_format", "AudioStreamOutputFormat": ".audio_stream_output_format", + "ConsentChallenge": ".consent_challenge", "Error": ".error", "ErrorCode": ".error_code", "ErrorDetail": ".error_detail", @@ -35,6 +41,8 @@ "GetSpeechResponse": ".get_speech_response", "GetSpeechResponseAudioFormat": ".get_speech_response_audio_format", "GetStreamOptionsRequest": ".get_stream_options_request", + "GetStreamRequest": ".get_stream_request", + "GetStreamRequestModel": ".get_stream_request_model", "GetVoice": ".get_voice", "GetVoiceGender": ".get_voice_gender", "GetVoiceLanguage": ".get_voice_language", @@ -42,6 +50,8 @@ "GetVoicesModel": ".get_voices_model", "GetVoicesModelName": ".get_voices_model_name", "ListVoicesResponse": ".list_voices_response", + "Model": ".model", + "ModelsResponse": ".models_response", "NestedChunk": ".nested_chunk", "PaginationMeta": ".pagination_meta", "SpeechMarks": ".speech_marks", @@ -72,6 +82,7 @@ def __dir__(): __all__ = [ "AudioOutputFormat", "AudioStreamOutputFormat", + "ConsentChallenge", "Error", "ErrorCode", "ErrorDetail", @@ -79,6 +90,8 @@ def __dir__(): "GetSpeechResponse", "GetSpeechResponseAudioFormat", "GetStreamOptionsRequest", + "GetStreamRequest", + "GetStreamRequestModel", "GetVoice", "GetVoiceGender", "GetVoiceLanguage", @@ -86,6 +99,8 @@ def __dir__(): "GetVoicesModel", "GetVoicesModelName", "ListVoicesResponse", + "Model", + "ModelsResponse", "NestedChunk", "PaginationMeta", "SpeechMarks", diff --git a/src/speechify/types/consent_challenge.py b/src/speechify/types/consent_challenge.py new file mode 100644 index 0000000..075a7f4 --- /dev/null +++ b/src/speechify/types/consent_challenge.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class ConsentChallenge(UniversalBaseModel): + id: str = pydantic.Field() + """ + Identifier for this challenge, sent back as `consent_challenge_id` + on the create. Treat it as an opaque string - the format is not part + of the contract and will not stay stable. + """ + + phrase: str = pydantic.Field() + """ + The sentence the speaker must read aloud. Show it exactly as + returned - the recording is transcribed and matched against this + text, so re-wording, re-casing or re-punctuating it will fail the + check. + """ + + expires_at: dt.datetime = pydantic.Field() + """ + When the challenge stops being usable. This is the only authority on + the window - do not hard-code a duration. Past it, create a new + challenge and record the new phrase. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/speechify/types/error.py b/src/speechify/types/error.py index 851bdf8..23b380b 100644 --- a/src/speechify/types/error.py +++ b/src/speechify/types/error.py @@ -14,7 +14,7 @@ class Error(UniversalBaseModel): Anthropic / Stripe style: a machine-readable `error.code` for SDK consumers to switch on, a human `error.message` for UI, and an optional `error.fields` map for per-field validation - errors. `request_id` matches the `X-Request-ID` response + errors. `request_id` matches the `Speechify-Request-Id` response header and is what customers quote when filing support tickets. """ @@ -23,7 +23,7 @@ class Error(UniversalBaseModel): request_id: typing.Optional[str] = pydantic.Field(default=None) """ Server-side request identifier. Echoes the - `X-Request-ID` response header. Stable across the + `Speechify-Request-Id` response header. Stable across the request's lifetime, written to structured logs, and useful when reporting issues. """ diff --git a/src/speechify/types/error_code.py b/src/speechify/types/error_code.py index ce5e090..1a66e34 100644 --- a/src/speechify/types/error_code.py +++ b/src/speechify/types/error_code.py @@ -44,15 +44,33 @@ "phone_number_quota_reached", "batch_calls_not_included", "voice_cloning_not_included", + "consent_challenge_not_found", + "consent_challenge_expired", + "consent_challenge_already_used", + "consent_phrase_mismatch", + "consent_speaker_mismatch", + "consent_recording_unusable", + "consent_verification_unavailable", "workspace_last_owner", "workspace_last_workspace", + "account_deletion_blocked", + "workspace_free_limit", + "workspace_single_owner", "invite_email_mismatch", "invite_already_pending", "service_account_limit_reached", "service_accounts_not_in_plan", + "speech_marks_unsupported", + "too_many_voices", + "content_policy_violation", + "topup_not_in_plan", + "credit_purchase_unpaid", "tool_config_shared", "spend_cap_exceeded", "spend_budget_exceeded", + "share_link_not_found", + "share_link_exhausted", + "share_link_limit_reached", "destination_not_allowed", "international_dialing_not_enabled", ], diff --git a/src/speechify/types/error_detail.py b/src/speechify/types/error_detail.py index c697538..aaa7065 100644 --- a/src/speechify/types/error_detail.py +++ b/src/speechify/types/error_detail.py @@ -37,6 +37,14 @@ class ErrorDetail(UniversalBaseModel): it - the `code` + `message` contract is unchanged. """ + docs_url: typing.Optional[str] = pydantic.Field(default=None) + """ + Link to the documentation that resolves this class of + error, when a stable page exists. Rate and concurrency + 429s link the API limits reference, which lists each + plan's limits and how to raise them. + """ + if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 else: diff --git a/src/speechify/types/get_stream_request.py b/src/speechify/types/get_stream_request.py new file mode 100644 index 0000000..83b0b16 --- /dev/null +++ b/src/speechify/types/get_stream_request.py @@ -0,0 +1,53 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .audio_stream_output_format import AudioStreamOutputFormat +from .get_stream_options_request import GetStreamOptionsRequest +from .get_stream_request_model import GetStreamRequestModel + + +class GetStreamRequest(UniversalBaseModel): + """ + GetStreamRequest is the wrapper for request parameters to the client + """ + + input: str = pydantic.Field() + """ + Plain text or SSML to be synthesized to speech. + Refer to https://docs.speechify.ai/docs/api-limits for the input size limits. + Emotion, Pitch and Speed Rate are configured in the ssml input, please refer to the ssml documentation for more information: https://docs.speechify.ai/docs/ssml#prosody + """ + + language: typing.Optional[str] = pydantic.Field(default=None) + """ + Language of the input. Follow the format of an ISO 639-1 language code and an ISO 3166-1 region code, separated by a hyphen, e.g. en-US. + Please refer to the list of the supported languages and recommendations regarding this parameter: https://docs.speechify.ai/docs/language-support. + """ + + model: typing.Optional[GetStreamRequestModel] = pydantic.Field(default=None) + """ + Model used for audio synthesis. Defaults to `simba-3.0`, which is streaming-native and multilingual: it officially supports English plus `de-DE`, `es-ES`, `es-MX`, `fr-FR`, `it-IT` and `pt-BR`, and routes each request to its English or its multilingual training based on `language` (falling back to the voice's locale when `language` is omitted). `simba-3.2` is the streaming-native model with the lowest TTFB and richest expressivity, and the recommended Simba 3 model; it is English only, so a non-English voice returns 400. `simba-english` and `simba-multilingual` are the legacy Simba 1.6 models, kept for compatibility. + """ + + options: typing.Optional[GetStreamOptionsRequest] = None + output_format: typing.Optional[AudioStreamOutputFormat] = pydantic.Field(default=None) + """ + The output audio format as a `codec_sampleRate_bitrate` string. Takes precedence over the `Accept` header when set, so you can request formats the `Accept` enum does not cover (e.g. `pcm_16000`, `ulaw_8000`). `wav_*` formats are not supported on streaming - use `POST /v1/audio/speech` for wav. + """ + + voice_id: str = pydantic.Field() + """ + Id of the voice to be used for synthesizing speech. Refer to /v1/voices endpoint for available voices + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/speechify/audio/types/get_stream_request_model.py b/src/speechify/types/get_stream_request_model.py similarity index 100% rename from src/speechify/audio/types/get_stream_request_model.py rename to src/speechify/types/get_stream_request_model.py diff --git a/src/speechify/types/model.py b/src/speechify/types/model.py new file mode 100644 index 0000000..b8aad5f --- /dev/null +++ b/src/speechify/types/model.py @@ -0,0 +1,94 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class Model(UniversalBaseModel): + """ + One selectable text-to-speech model. + """ + + id: str = pydantic.Field() + """ + Model identifier. Pass this as the `model` parameter to + POST /v1/audio/speech or /v1/audio/stream. + """ + + name: str = pydantic.Field() + """ + Human-readable model name, for a model picker. + """ + + default: bool = pydantic.Field() + """ + Whether this is the model used when a synthesis request omits + `model`. Exactly one model in the list is the default. Distinct + from `recommended`: the default accepts every voice, while the + recommended model may serve a curated or English-only set. + """ + + recommended: bool = pydantic.Field() + """ + Whether this is the model we recommend for new integrations. + Exactly one model in the list is recommended, and it may differ + from the `default`. + """ + + deprecated: bool = pydantic.Field() + """ + Whether this is a legacy model. Advisory only: a deprecated model + stays selectable and behaves exactly as before, and nothing is + scheduled for removal. De-emphasise it in a picker and steer new + integrations to a current model. + """ + + description: str = pydantic.Field() + """ + One-line summary of the model, for a model picker. + """ + + languages: typing.List[str] = pydantic.Field() + """ + Languages the model can synthesize, as BCP-47 locale strings + matching the `language` request parameter (e.g. `en`, `fr-FR`). + English-only models return `["en"]`. This set reflects current + capability and can grow over time. + """ + + endpoints: typing.List[str] = pydantic.Field() + """ + The synthesis routes this model may be passed to. Only the + streaming-native models serve `/v1/audio/stream/with-timestamps`; + passing a model this list omits is a 400 rather than a degraded + response, so branch on it instead of discovering it at call time. + """ + + curated_voices: bool = pydantic.Field() + """ + Whether the model's stock voices are restricted to the set curated + for it. When true, pick a stock voice whose `models` array in + GET /v1/voices names this model; any other stock voice is rejected. + When false, every stock catalogue voice works. Cloned voices are + governed separately - always read each voice's own `models` array in + GET /v1/voices, which reflects what your workspace may actually + synthesize. + """ + + english_voices_only: bool = pydantic.Field() + """ + Whether the model rejects a non-English voice. Independent of + `languages`: a model can publish English only and still accept any + 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/speechify/types/models_response.py b/src/speechify/types/models_response.py new file mode 100644 index 0000000..def867b --- /dev/null +++ b/src/speechify/types/models_response.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, UniversalBaseModel +from .model import Model + + +class ModelsResponse(UniversalBaseModel): + """ + The catalog of text-to-speech models available for synthesis. + """ + + models: typing.List[Model] = pydantic.Field() + """ + The models selectable on the single-utterance synthesis endpoints. + """ + + dialogue_models: typing.List[Model] = pydantic.Field() + """ + The multi-speaker models selectable on POST /v1/audio/dialogue. + Disjoint from `models`: a dialogue model consumes a + speaker-attributed script rather than one utterance, so it is + rejected on the single-utterance endpoints and vice versa. Its + `default` marks the model that endpoint resolves to when a request + omits `model`, independently of the `models` default. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/speechify/voices/__init__.py b/src/speechify/voices/__init__.py index ac3d80d..4f05ee9 100644 --- a/src/speechify/voices/__init__.py +++ b/src/speechify/voices/__init__.py @@ -6,8 +6,14 @@ from importlib import import_module if typing.TYPE_CHECKING: - from .types import CreateVoicesRequestGender -_dynamic_imports: typing.Dict[str, str] = {"CreateVoicesRequestGender": ".types"} + from .types import CreateVoicesRequestGender, ListVoicesRequestGender, ListVoicesRequestType + from . import consent_challenges +_dynamic_imports: typing.Dict[str, str] = { + "CreateVoicesRequestGender": ".types", + "ListVoicesRequestGender": ".types", + "ListVoicesRequestType": ".types", + "consent_challenges": ".consent_challenges", +} def __getattr__(attr_name: str) -> typing.Any: @@ -31,4 +37,4 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = ["CreateVoicesRequestGender"] +__all__ = ["CreateVoicesRequestGender", "ListVoicesRequestGender", "ListVoicesRequestType", "consent_challenges"] diff --git a/src/speechify/voices/client.py b/src/speechify/voices/client.py index 036c918..930cf22 100644 --- a/src/speechify/voices/client.py +++ b/src/speechify/voices/client.py @@ -1,5 +1,7 @@ # This file was auto-generated by Fern from our API Definition. +from __future__ import annotations + import typing from .. import core @@ -10,7 +12,11 @@ from ..types.list_voices_response import ListVoicesResponse from .raw_client import AsyncRawVoicesClient, RawVoicesClient from .types.create_voices_request_gender import CreateVoicesRequestGender +from .types.list_voices_request_gender import ListVoicesRequestGender +from .types.list_voices_request_type import ListVoicesRequestType +if typing.TYPE_CHECKING: + from .consent_challenges.client import AsyncConsentChallengesClient, ConsentChallengesClient # this is used as the default value for optional parameters OMIT = typing.cast(typing.Any, ...) @@ -18,6 +24,8 @@ class VoicesClient: def __init__(self, *, client_wrapper: SyncClientWrapper): self._raw_client = RawVoicesClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._consent_challenges: typing.Optional[ConsentChallengesClient] = None @property def with_raw_response(self) -> RawVoicesClient: @@ -35,15 +43,21 @@ def list( *, cursor: typing.Optional[str] = None, limit: typing.Optional[int] = None, + type: typing.Optional[ListVoicesRequestType] = None, + locale: typing.Optional[str] = None, + gender: typing.Optional[ListVoicesRequestGender] = None, + model: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> SyncPager[GetVoice, ListVoicesResponse]: """ Lists the voices available to the caller - the shared voice - catalog plus the workspace's personal cloned voices. By default + catalog plus the workspace's cloned voices, whichever member or + service-account key created them. By default the full catalogue is returned in one response. Pagination is opt-in: pass `limit` (and then `cursor` from the previous response) to page through the list while `has_more` is true. Max - page size is 200. + page size is 200. Narrow the list with the `type` and `locale` + filters (applied before pagination, so pages stay full). Parameters ---------- @@ -53,6 +67,22 @@ def list( limit : typing.Optional[int] Max items per page (default 50, max 200). + type : typing.Optional[ListVoicesRequestType] + Filter by voice type: `personal` (the workspace's cloned voices) + or `shared` (the public catalogue). Omit to return both. + + locale : typing.Optional[str] + Filter to voices whose locale matches this BCP-47 language range, + prefix-matched: `en` matches `en-US` and `en-GB`; `en-US` matches + only `en-US`. Case-insensitive. Omit to return all locales. + + gender : typing.Optional[ListVoicesRequestGender] + Filter by voice gender. Omit to return all genders. + + model : typing.Optional[str] + Filter to voices that support this model (as listed in each voice's + `models[]`), e.g. `simba-3.2`. Omit to return voices for all models. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -66,17 +96,28 @@ def list( from speechify import Speechify client = Speechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) - response = client.voices.list() + response = client.voices.list( + locale="en", + model="simba-3.2", + ) for item in response: yield item # alternatively, you can paginate page-by-page for page in response.iter_pages(): yield page """ - return self._raw_client.list(cursor=cursor, limit=limit, request_options=request_options) + return self._raw_client.list( + cursor=cursor, + limit=limit, + type=type, + locale=locale, + gender=gender, + model=model, + request_options=request_options, + ) def create( self, @@ -84,14 +125,21 @@ def create( name: str, gender: CreateVoicesRequestGender, sample: core.File, - consent: str, + consent_challenge_id: str, + consent_recording: core.File, idempotency_key: typing.Optional[str] = None, locale: typing.Optional[str] = OMIT, avatar: typing.Optional[core.File] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> GetVoice: """ - Create a personal (cloned) voice for the user + Create a cloned voice for the workspace from a 10-30 second audio sample, with verified consent from the speaker. + + Cloning requires proof that the speaker agreed to it. Create a consent challenge with `POST /v1/voices/consent-challenges`, show the returned `phrase` to the speaker, record them reading it aloud, and send that recording here as `consent_recording` together with the challenge's `consent_challenge_id`. Speechify transcribes the recording, checks it against the phrase it issued, and keeps it as the consent record for the voice. A challenge is single use and short-lived, so record and submit in one sitting. + + The clone belongs to the workspace rather than the member who created it, and access follows the caller's workspace role and API-key scopes exactly as for any other voice: voices scopes to list it, audio scopes to synthesize with it, and the content-management permission plus a write scope on the key to delete it. Cloned voices are usable self-serve on `simba-3.0`, `simba-english` and `simba-multilingual`. `simba-3.2` also serves cloned voices, currently as a limited release enabled per workspace; contact Speechify to have it enabled for yours. + + Callers pinned before `Speechify-Version: 2026-09-13` use the previous flow instead: no challenge, and a `consent` form field carrying the speaker's name and email as a JSON string. That flow is deprecated and will be removed after a sunset window announced in the changelog. Parameters ---------- @@ -107,10 +155,14 @@ def create( sample : core.File See core.File for more documentation - consent : str - A **string** representing the user consent information in JSON format - This should include the fullName and email of the consenting individual. - For example, `{"fullName": "John Doe", "email": "john@example.com"}` + consent_challenge_id : str + The `id` of the consent challenge this create consumes, from + `POST /v1/voices/consent-challenges`. Single use: once a + create has consumed it, whether or not that create + succeeded, it cannot be used again. + + consent_recording : core.File + See core.File for more documentation idempotency_key : typing.Optional[str] A client-generated key (an opaque string, max 255 chars) that makes a @@ -139,21 +191,22 @@ def create( from speechify import Speechify client = Speechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) client.voices.create( idempotency_key="a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", name="name", gender="male", - consent="consent", + consent_challenge_id="consent_challenge_id", ) """ _response = self._raw_client.create( name=name, gender=gender, sample=sample, - consent=consent, + consent_challenge_id=consent_challenge_id, + consent_recording=consent_recording, idempotency_key=idempotency_key, locale=locale, avatar=avatar, @@ -164,9 +217,9 @@ def create( def get(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> GetVoice: """ Fetch a single voice by id - a shared catalogue voice or one of - the caller's own personal (cloned) voices. A personal voice that - belongs to another workspace returns 404, identical to an - unknown id, so voice inventory is never enumerable across tenants. + the workspace's cloned voices. A cloned voice that belongs to + another workspace returns 404, identical to an unknown id, so + voice inventory is never enumerable across tenants. Parameters ---------- @@ -186,7 +239,7 @@ def get(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] from speechify import Speechify client = Speechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) client.voices.get( @@ -198,7 +251,9 @@ def get(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] def delete(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: """ - Delete a personal (cloned) voice + Delete one of the workspace's cloned voices. Requires the + `content.manage` permission (owner, admin, or member); a + service-account key is authorized by its scopes instead. Parameters ---------- @@ -217,7 +272,7 @@ def delete(self, voice_id: str, *, request_options: typing.Optional[RequestOptio from speechify import Speechify client = Speechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) client.voices.delete( @@ -251,7 +306,7 @@ def download_sample( from speechify import Speechify client = Speechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) client.voices.download_sample( @@ -261,10 +316,20 @@ def download_sample( with self._raw_client.download_sample(voice_id, request_options=request_options) as r: yield from r.data + @property + def consent_challenges(self): + if self._consent_challenges is None: + from .consent_challenges.client import ConsentChallengesClient # noqa: E402 + + self._consent_challenges = ConsentChallengesClient(client_wrapper=self._client_wrapper) + return self._consent_challenges + class AsyncVoicesClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): self._raw_client = AsyncRawVoicesClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._consent_challenges: typing.Optional[AsyncConsentChallengesClient] = None @property def with_raw_response(self) -> AsyncRawVoicesClient: @@ -282,15 +347,21 @@ async def list( *, cursor: typing.Optional[str] = None, limit: typing.Optional[int] = None, + type: typing.Optional[ListVoicesRequestType] = None, + locale: typing.Optional[str] = None, + gender: typing.Optional[ListVoicesRequestGender] = None, + model: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> AsyncPager[GetVoice, ListVoicesResponse]: """ Lists the voices available to the caller - the shared voice - catalog plus the workspace's personal cloned voices. By default + catalog plus the workspace's cloned voices, whichever member or + service-account key created them. By default the full catalogue is returned in one response. Pagination is opt-in: pass `limit` (and then `cursor` from the previous response) to page through the list while `has_more` is true. Max - page size is 200. + page size is 200. Narrow the list with the `type` and `locale` + filters (applied before pagination, so pages stay full). Parameters ---------- @@ -300,6 +371,22 @@ async def list( limit : typing.Optional[int] Max items per page (default 50, max 200). + type : typing.Optional[ListVoicesRequestType] + Filter by voice type: `personal` (the workspace's cloned voices) + or `shared` (the public catalogue). Omit to return both. + + locale : typing.Optional[str] + Filter to voices whose locale matches this BCP-47 language range, + prefix-matched: `en` matches `en-US` and `en-GB`; `en-US` matches + only `en-US`. Case-insensitive. Omit to return all locales. + + gender : typing.Optional[ListVoicesRequestGender] + Filter by voice gender. Omit to return all genders. + + model : typing.Optional[str] + Filter to voices that support this model (as listed in each voice's + `models[]`), e.g. `simba-3.2`. Omit to return voices for all models. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -315,13 +402,16 @@ async def list( from speechify import AsyncSpeechify client = AsyncSpeechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) async def main() -> None: - response = await client.voices.list() + response = await client.voices.list( + locale="en", + model="simba-3.2", + ) async for item in response: yield item @@ -332,7 +422,15 @@ async def main() -> None: asyncio.run(main()) """ - return await self._raw_client.list(cursor=cursor, limit=limit, request_options=request_options) + return await self._raw_client.list( + cursor=cursor, + limit=limit, + type=type, + locale=locale, + gender=gender, + model=model, + request_options=request_options, + ) async def create( self, @@ -340,14 +438,21 @@ async def create( name: str, gender: CreateVoicesRequestGender, sample: core.File, - consent: str, + consent_challenge_id: str, + consent_recording: core.File, idempotency_key: typing.Optional[str] = None, locale: typing.Optional[str] = OMIT, avatar: typing.Optional[core.File] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> GetVoice: """ - Create a personal (cloned) voice for the user + Create a cloned voice for the workspace from a 10-30 second audio sample, with verified consent from the speaker. + + Cloning requires proof that the speaker agreed to it. Create a consent challenge with `POST /v1/voices/consent-challenges`, show the returned `phrase` to the speaker, record them reading it aloud, and send that recording here as `consent_recording` together with the challenge's `consent_challenge_id`. Speechify transcribes the recording, checks it against the phrase it issued, and keeps it as the consent record for the voice. A challenge is single use and short-lived, so record and submit in one sitting. + + The clone belongs to the workspace rather than the member who created it, and access follows the caller's workspace role and API-key scopes exactly as for any other voice: voices scopes to list it, audio scopes to synthesize with it, and the content-management permission plus a write scope on the key to delete it. Cloned voices are usable self-serve on `simba-3.0`, `simba-english` and `simba-multilingual`. `simba-3.2` also serves cloned voices, currently as a limited release enabled per workspace; contact Speechify to have it enabled for yours. + + Callers pinned before `Speechify-Version: 2026-09-13` use the previous flow instead: no challenge, and a `consent` form field carrying the speaker's name and email as a JSON string. That flow is deprecated and will be removed after a sunset window announced in the changelog. Parameters ---------- @@ -363,10 +468,14 @@ async def create( sample : core.File See core.File for more documentation - consent : str - A **string** representing the user consent information in JSON format - This should include the fullName and email of the consenting individual. - For example, `{"fullName": "John Doe", "email": "john@example.com"}` + consent_challenge_id : str + The `id` of the consent challenge this create consumes, from + `POST /v1/voices/consent-challenges`. Single use: once a + create has consumed it, whether or not that create + succeeded, it cannot be used again. + + consent_recording : core.File + See core.File for more documentation idempotency_key : typing.Optional[str] A client-generated key (an opaque string, max 255 chars) that makes a @@ -397,7 +506,7 @@ async def create( from speechify import AsyncSpeechify client = AsyncSpeechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) @@ -407,7 +516,7 @@ async def main() -> None: idempotency_key="a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", name="name", gender="male", - consent="consent", + consent_challenge_id="consent_challenge_id", ) @@ -417,7 +526,8 @@ async def main() -> None: name=name, gender=gender, sample=sample, - consent=consent, + consent_challenge_id=consent_challenge_id, + consent_recording=consent_recording, idempotency_key=idempotency_key, locale=locale, avatar=avatar, @@ -428,9 +538,9 @@ async def main() -> None: async def get(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> GetVoice: """ Fetch a single voice by id - a shared catalogue voice or one of - the caller's own personal (cloned) voices. A personal voice that - belongs to another workspace returns 404, identical to an - unknown id, so voice inventory is never enumerable across tenants. + the workspace's cloned voices. A cloned voice that belongs to + another workspace returns 404, identical to an unknown id, so + voice inventory is never enumerable across tenants. Parameters ---------- @@ -452,7 +562,7 @@ async def get(self, voice_id: str, *, request_options: typing.Optional[RequestOp from speechify import AsyncSpeechify client = AsyncSpeechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) @@ -470,7 +580,9 @@ async def main() -> None: async def delete(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: """ - Delete a personal (cloned) voice + Delete one of the workspace's cloned voices. Requires the + `content.manage` permission (owner, admin, or member); a + service-account key is authorized by its scopes instead. Parameters ---------- @@ -491,7 +603,7 @@ async def delete(self, voice_id: str, *, request_options: typing.Optional[Reques from speechify import AsyncSpeechify client = AsyncSpeechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) @@ -533,7 +645,7 @@ async def download_sample( from speechify import AsyncSpeechify client = AsyncSpeechify( - "2026-07-07", + "2026-09-13", token="YOUR_TOKEN", ) @@ -549,3 +661,11 @@ async def main() -> None: async with self._raw_client.download_sample(voice_id, request_options=request_options) as r: async for _chunk in r.data: yield _chunk + + @property + def consent_challenges(self): + if self._consent_challenges is None: + from .consent_challenges.client import AsyncConsentChallengesClient # noqa: E402 + + self._consent_challenges = AsyncConsentChallengesClient(client_wrapper=self._client_wrapper) + return self._consent_challenges diff --git a/src/speechify/voices/consent_challenges/__init__.py b/src/speechify/voices/consent_challenges/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/speechify/voices/consent_challenges/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/speechify/voices/consent_challenges/client.py b/src/speechify/voices/consent_challenges/client.py new file mode 100644 index 0000000..3b38eb5 --- /dev/null +++ b/src/speechify/voices/consent_challenges/client.py @@ -0,0 +1,181 @@ +# 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.consent_challenge import ConsentChallenge +from .raw_client import AsyncRawConsentChallengesClient, RawConsentChallengesClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class ConsentChallengesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawConsentChallengesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawConsentChallengesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawConsentChallengesClient + """ + return self._raw_client + + def create( + self, + *, + full_name: str, + idempotency_key: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ConsentChallenge: + """ + Start the consent check for a voice clone. + + Returns a `phrase` for the speaker to read aloud and an `id` that identifies this challenge. Show the phrase to the speaker exactly as returned, record them reading it, then send the recording and the `id` to `POST /v1/voices`, which verifies the recording against the phrase and keeps it as the consent record. + + A challenge is single use, is bound to the workspace that created it, and expires at `expires_at` - it is proof that a speaker was in front of a microphone just now, so create it when you are ready to record, not at the start of your flow. If it expires, create another one and record again. + + Challenge creation is rate limited per workspace at a few dozen per hour, far more tightly than the rest of the voice surface, because each one precedes a person recording themselves - mint it when your speaker is ready, not speculatively. Read the live ceiling off `RateLimit-*` rather than hard-coding it. **On a `429`, always honour `Retry-After` rather than a fixed backoff of your own**: the wait is measured in minutes and can run to most of an hour. `RateLimit-*` are omitted rather than reporting a bucket that is not the one refusing. + + Parameters + ---------- + full_name : str + Full name of the person consenting to have their voice cloned. + Speechify binds it to the challenge and stores it with the consent + record, so the create that consumes the challenge does not carry it + and cannot change it. + + At most 120 bytes once UTF-8 encoded, which is 120 characters of + Latin script but around 40 of Chinese, Japanese or Korean. Stated in + bytes rather than as a `maxLength` because the two only agree on + single-byte scripts, and a character count that never over-accepts + would have to refuse Latin names at 30. A name over the limit comes + back as `validation_failed` reporting its measured length. + + idempotency_key : typing.Optional[str] + A client-generated key (an opaque string, max 255 chars) that makes a + side-effect POST safe to retry: the server runs the operation exactly + once and replays the first response (its status and body) for 24 hours. + Reusing a key with a different request body, or while the first request + is still in flight, returns `409 idempotency_conflict`. A replayed + response carries the `Idempotent-Replayed: true` header. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ConsentChallenge + The created consent challenge. + + Examples + -------- + from speechify import Speechify + + client = Speechify( + "2026-09-13", + token="YOUR_TOKEN", + ) + client.voices.consent_challenges.create( + idempotency_key="a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", + full_name="Jane Doe", + ) + """ + _response = self._raw_client.create( + full_name=full_name, idempotency_key=idempotency_key, request_options=request_options + ) + return _response.data + + +class AsyncConsentChallengesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawConsentChallengesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawConsentChallengesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawConsentChallengesClient + """ + return self._raw_client + + async def create( + self, + *, + full_name: str, + idempotency_key: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ConsentChallenge: + """ + Start the consent check for a voice clone. + + Returns a `phrase` for the speaker to read aloud and an `id` that identifies this challenge. Show the phrase to the speaker exactly as returned, record them reading it, then send the recording and the `id` to `POST /v1/voices`, which verifies the recording against the phrase and keeps it as the consent record. + + A challenge is single use, is bound to the workspace that created it, and expires at `expires_at` - it is proof that a speaker was in front of a microphone just now, so create it when you are ready to record, not at the start of your flow. If it expires, create another one and record again. + + Challenge creation is rate limited per workspace at a few dozen per hour, far more tightly than the rest of the voice surface, because each one precedes a person recording themselves - mint it when your speaker is ready, not speculatively. Read the live ceiling off `RateLimit-*` rather than hard-coding it. **On a `429`, always honour `Retry-After` rather than a fixed backoff of your own**: the wait is measured in minutes and can run to most of an hour. `RateLimit-*` are omitted rather than reporting a bucket that is not the one refusing. + + Parameters + ---------- + full_name : str + Full name of the person consenting to have their voice cloned. + Speechify binds it to the challenge and stores it with the consent + record, so the create that consumes the challenge does not carry it + and cannot change it. + + At most 120 bytes once UTF-8 encoded, which is 120 characters of + Latin script but around 40 of Chinese, Japanese or Korean. Stated in + bytes rather than as a `maxLength` because the two only agree on + single-byte scripts, and a character count that never over-accepts + would have to refuse Latin names at 30. A name over the limit comes + back as `validation_failed` reporting its measured length. + + idempotency_key : typing.Optional[str] + A client-generated key (an opaque string, max 255 chars) that makes a + side-effect POST safe to retry: the server runs the operation exactly + once and replays the first response (its status and body) for 24 hours. + Reusing a key with a different request body, or while the first request + is still in flight, returns `409 idempotency_conflict`. A replayed + response carries the `Idempotent-Replayed: true` header. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ConsentChallenge + The created consent challenge. + + Examples + -------- + import asyncio + + from speechify import AsyncSpeechify + + client = AsyncSpeechify( + "2026-09-13", + token="YOUR_TOKEN", + ) + + + async def main() -> None: + await client.voices.consent_challenges.create( + idempotency_key="a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", + full_name="Jane Doe", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create( + full_name=full_name, idempotency_key=idempotency_key, request_options=request_options + ) + return _response.data diff --git a/src/speechify/voices/consent_challenges/raw_client.py b/src/speechify/voices/consent_challenges/raw_client.py new file mode 100644 index 0000000..ea49e68 --- /dev/null +++ b/src/speechify/voices/consent_challenges/raw_client.py @@ -0,0 +1,392 @@ +# 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.pydantic_utilities import parse_obj_as +from ...core.request_options import RequestOptions +from ...errors.bad_gateway_error import BadGatewayError +from ...errors.bad_request_error import BadRequestError +from ...errors.conflict_error import ConflictError +from ...errors.forbidden_error import ForbiddenError +from ...errors.internal_server_error import InternalServerError +from ...errors.payment_required_error import PaymentRequiredError +from ...errors.service_unavailable_error import ServiceUnavailableError +from ...errors.too_many_requests_error import TooManyRequestsError +from ...errors.unauthorized_error import UnauthorizedError +from ...types.consent_challenge import ConsentChallenge +from ...types.error import Error +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawConsentChallengesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def create( + self, + *, + full_name: str, + idempotency_key: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ConsentChallenge]: + """ + Start the consent check for a voice clone. + + Returns a `phrase` for the speaker to read aloud and an `id` that identifies this challenge. Show the phrase to the speaker exactly as returned, record them reading it, then send the recording and the `id` to `POST /v1/voices`, which verifies the recording against the phrase and keeps it as the consent record. + + A challenge is single use, is bound to the workspace that created it, and expires at `expires_at` - it is proof that a speaker was in front of a microphone just now, so create it when you are ready to record, not at the start of your flow. If it expires, create another one and record again. + + Challenge creation is rate limited per workspace at a few dozen per hour, far more tightly than the rest of the voice surface, because each one precedes a person recording themselves - mint it when your speaker is ready, not speculatively. Read the live ceiling off `RateLimit-*` rather than hard-coding it. **On a `429`, always honour `Retry-After` rather than a fixed backoff of your own**: the wait is measured in minutes and can run to most of an hour. `RateLimit-*` are omitted rather than reporting a bucket that is not the one refusing. + + Parameters + ---------- + full_name : str + Full name of the person consenting to have their voice cloned. + Speechify binds it to the challenge and stores it with the consent + record, so the create that consumes the challenge does not carry it + and cannot change it. + + At most 120 bytes once UTF-8 encoded, which is 120 characters of + Latin script but around 40 of Chinese, Japanese or Korean. Stated in + bytes rather than as a `maxLength` because the two only agree on + single-byte scripts, and a character count that never over-accepts + would have to refuse Latin names at 30. A name over the limit comes + back as `validation_failed` reporting its measured length. + + idempotency_key : typing.Optional[str] + A client-generated key (an opaque string, max 255 chars) that makes a + side-effect POST safe to retry: the server runs the operation exactly + once and replays the first response (its status and body) for 24 hours. + Reusing a key with a different request body, or while the first request + is still in flight, returns `409 idempotency_conflict`. A replayed + response carries the `Idempotent-Replayed: true` header. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ConsentChallenge] + The created consent challenge. + """ + _response = self._client_wrapper.httpx_client.request( + "v1/voices/consent-challenges", + method="POST", + json={ + "full_name": full_name, + }, + headers={ + "content-type": "application/json", + "Idempotency-Key": str(idempotency_key) if idempotency_key is not None else None, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ConsentChallenge, + parse_obj_as( + type_=ConsentChallenge, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawConsentChallengesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def create( + self, + *, + full_name: str, + idempotency_key: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ConsentChallenge]: + """ + Start the consent check for a voice clone. + + Returns a `phrase` for the speaker to read aloud and an `id` that identifies this challenge. Show the phrase to the speaker exactly as returned, record them reading it, then send the recording and the `id` to `POST /v1/voices`, which verifies the recording against the phrase and keeps it as the consent record. + + A challenge is single use, is bound to the workspace that created it, and expires at `expires_at` - it is proof that a speaker was in front of a microphone just now, so create it when you are ready to record, not at the start of your flow. If it expires, create another one and record again. + + Challenge creation is rate limited per workspace at a few dozen per hour, far more tightly than the rest of the voice surface, because each one precedes a person recording themselves - mint it when your speaker is ready, not speculatively. Read the live ceiling off `RateLimit-*` rather than hard-coding it. **On a `429`, always honour `Retry-After` rather than a fixed backoff of your own**: the wait is measured in minutes and can run to most of an hour. `RateLimit-*` are omitted rather than reporting a bucket that is not the one refusing. + + Parameters + ---------- + full_name : str + Full name of the person consenting to have their voice cloned. + Speechify binds it to the challenge and stores it with the consent + record, so the create that consumes the challenge does not carry it + and cannot change it. + + At most 120 bytes once UTF-8 encoded, which is 120 characters of + Latin script but around 40 of Chinese, Japanese or Korean. Stated in + bytes rather than as a `maxLength` because the two only agree on + single-byte scripts, and a character count that never over-accepts + would have to refuse Latin names at 30. A name over the limit comes + back as `validation_failed` reporting its measured length. + + idempotency_key : typing.Optional[str] + A client-generated key (an opaque string, max 255 chars) that makes a + side-effect POST safe to retry: the server runs the operation exactly + once and replays the first response (its status and body) for 24 hours. + Reusing a key with a different request body, or while the first request + is still in flight, returns `409 idempotency_conflict`. A replayed + response carries the `Idempotent-Replayed: true` header. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ConsentChallenge] + The created consent challenge. + """ + _response = await self._client_wrapper.httpx_client.request( + "v1/voices/consent-challenges", + method="POST", + json={ + "full_name": full_name, + }, + headers={ + "content-type": "application/json", + "Idempotency-Key": str(idempotency_key) if idempotency_key is not None else None, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ConsentChallenge, + parse_obj_as( + type_=ConsentChallenge, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 429: + raise TooManyRequestsError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/speechify/voices/raw_client.py b/src/speechify/voices/raw_client.py index 8645cf9..ca8551a 100644 --- a/src/speechify/voices/raw_client.py +++ b/src/speechify/voices/raw_client.py @@ -16,6 +16,7 @@ from ..errors.bad_gateway_error import BadGatewayError from ..errors.bad_request_error import BadRequestError from ..errors.conflict_error import ConflictError +from ..errors.content_too_large_error import ContentTooLargeError from ..errors.forbidden_error import ForbiddenError from ..errors.internal_server_error import InternalServerError from ..errors.not_found_error import NotFoundError @@ -28,6 +29,8 @@ from ..types.get_voice import GetVoice from ..types.list_voices_response import ListVoicesResponse from .types.create_voices_request_gender import CreateVoicesRequestGender +from .types.list_voices_request_gender import ListVoicesRequestGender +from .types.list_voices_request_type import ListVoicesRequestType from pydantic import ValidationError # this is used as the default value for optional parameters @@ -43,15 +46,21 @@ def list( *, cursor: typing.Optional[str] = None, limit: typing.Optional[int] = None, + type: typing.Optional[ListVoicesRequestType] = None, + locale: typing.Optional[str] = None, + gender: typing.Optional[ListVoicesRequestGender] = None, + model: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> SyncPager[GetVoice, ListVoicesResponse]: """ Lists the voices available to the caller - the shared voice - catalog plus the workspace's personal cloned voices. By default + catalog plus the workspace's cloned voices, whichever member or + service-account key created them. By default the full catalogue is returned in one response. Pagination is opt-in: pass `limit` (and then `cursor` from the previous response) to page through the list while `has_more` is true. Max - page size is 200. + page size is 200. Narrow the list with the `type` and `locale` + filters (applied before pagination, so pages stay full). Parameters ---------- @@ -61,6 +70,22 @@ def list( limit : typing.Optional[int] Max items per page (default 50, max 200). + type : typing.Optional[ListVoicesRequestType] + Filter by voice type: `personal` (the workspace's cloned voices) + or `shared` (the public catalogue). Omit to return both. + + locale : typing.Optional[str] + Filter to voices whose locale matches this BCP-47 language range, + prefix-matched: `en` matches `en-US` and `en-GB`; `en-US` matches + only `en-US`. Case-insensitive. Omit to return all locales. + + gender : typing.Optional[ListVoicesRequestGender] + Filter by voice gender. Omit to return all genders. + + model : typing.Optional[str] + Filter to voices that support this model (as listed in each voice's + `models[]`), e.g. `simba-3.2`. Omit to return voices for all models. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -75,6 +100,10 @@ def list( params={ "cursor": cursor, "limit": limit, + "type": type, + "locale": locale, + "gender": gender, + "model": model, }, request_options=request_options, ) @@ -93,6 +122,10 @@ def list( _get_next = lambda: self.list( cursor=_parsed_next, limit=limit, + type=type, + locale=locale, + gender=gender, + model=model, request_options=request_options, ) return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) @@ -166,14 +199,21 @@ def create( name: str, gender: CreateVoicesRequestGender, sample: core.File, - consent: str, + consent_challenge_id: str, + consent_recording: core.File, idempotency_key: typing.Optional[str] = None, locale: typing.Optional[str] = OMIT, avatar: typing.Optional[core.File] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[GetVoice]: """ - Create a personal (cloned) voice for the user + Create a cloned voice for the workspace from a 10-30 second audio sample, with verified consent from the speaker. + + Cloning requires proof that the speaker agreed to it. Create a consent challenge with `POST /v1/voices/consent-challenges`, show the returned `phrase` to the speaker, record them reading it aloud, and send that recording here as `consent_recording` together with the challenge's `consent_challenge_id`. Speechify transcribes the recording, checks it against the phrase it issued, and keeps it as the consent record for the voice. A challenge is single use and short-lived, so record and submit in one sitting. + + The clone belongs to the workspace rather than the member who created it, and access follows the caller's workspace role and API-key scopes exactly as for any other voice: voices scopes to list it, audio scopes to synthesize with it, and the content-management permission plus a write scope on the key to delete it. Cloned voices are usable self-serve on `simba-3.0`, `simba-english` and `simba-multilingual`. `simba-3.2` also serves cloned voices, currently as a limited release enabled per workspace; contact Speechify to have it enabled for yours. + + Callers pinned before `Speechify-Version: 2026-09-13` use the previous flow instead: no challenge, and a `consent` form field carrying the speaker's name and email as a JSON string. That flow is deprecated and will be removed after a sunset window announced in the changelog. Parameters ---------- @@ -189,10 +229,14 @@ def create( sample : core.File See core.File for more documentation - consent : str - A **string** representing the user consent information in JSON format - This should include the fullName and email of the consenting individual. - For example, `{"fullName": "John Doe", "email": "john@example.com"}` + consent_challenge_id : str + The `id` of the consent challenge this create consumes, from + `POST /v1/voices/consent-challenges`. Single use: once a + create has consumed it, whether or not that create + succeeded, it cannot be used again. + + consent_recording : core.File + See core.File for more documentation idempotency_key : typing.Optional[str] A client-generated key (an opaque string, max 255 chars) that makes a @@ -223,11 +267,12 @@ def create( "name": name, "locale": locale, "gender": gender, - "consent": consent, + "consent_challenge_id": consent_challenge_id, }, files={ "sample": sample, **({"avatar": avatar} if avatar is not None else {}), + "consent_recording": consent_recording, }, headers={ "Idempotency-Key": str(idempotency_key) if idempotency_key is not None else None, @@ -301,6 +346,17 @@ def create( ), ), ) + if _response.status_code == 413: + raise ContentTooLargeError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) if _response.status_code == 422: raise UnprocessableEntityError( headers=dict(_response.headers), @@ -368,9 +424,9 @@ def create( def get(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[GetVoice]: """ Fetch a single voice by id - a shared catalogue voice or one of - the caller's own personal (cloned) voices. A personal voice that - belongs to another workspace returns 404, identical to an - unknown id, so voice inventory is never enumerable across tenants. + the workspace's cloned voices. A cloned voice that belongs to + another workspace returns 404, identical to an unknown id, so + voice inventory is never enumerable across tenants. Parameters ---------- @@ -488,7 +544,9 @@ def get(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] def delete(self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[None]: """ - Delete a personal (cloned) voice + Delete one of the workspace's cloned voices. Requires the + `content.manage` permission (owner, admin, or member); a + service-account key is authorized by its scopes instead. Parameters ---------- @@ -755,15 +813,21 @@ async def list( *, cursor: typing.Optional[str] = None, limit: typing.Optional[int] = None, + type: typing.Optional[ListVoicesRequestType] = None, + locale: typing.Optional[str] = None, + gender: typing.Optional[ListVoicesRequestGender] = None, + model: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> AsyncPager[GetVoice, ListVoicesResponse]: """ Lists the voices available to the caller - the shared voice - catalog plus the workspace's personal cloned voices. By default + catalog plus the workspace's cloned voices, whichever member or + service-account key created them. By default the full catalogue is returned in one response. Pagination is opt-in: pass `limit` (and then `cursor` from the previous response) to page through the list while `has_more` is true. Max - page size is 200. + page size is 200. Narrow the list with the `type` and `locale` + filters (applied before pagination, so pages stay full). Parameters ---------- @@ -773,6 +837,22 @@ async def list( limit : typing.Optional[int] Max items per page (default 50, max 200). + type : typing.Optional[ListVoicesRequestType] + Filter by voice type: `personal` (the workspace's cloned voices) + or `shared` (the public catalogue). Omit to return both. + + locale : typing.Optional[str] + Filter to voices whose locale matches this BCP-47 language range, + prefix-matched: `en` matches `en-US` and `en-GB`; `en-US` matches + only `en-US`. Case-insensitive. Omit to return all locales. + + gender : typing.Optional[ListVoicesRequestGender] + Filter by voice gender. Omit to return all genders. + + model : typing.Optional[str] + Filter to voices that support this model (as listed in each voice's + `models[]`), e.g. `simba-3.2`. Omit to return voices for all models. + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -787,6 +867,10 @@ async def list( params={ "cursor": cursor, "limit": limit, + "type": type, + "locale": locale, + "gender": gender, + "model": model, }, request_options=request_options, ) @@ -807,6 +891,10 @@ async def _get_next(): return await self.list( cursor=_parsed_next, limit=limit, + type=type, + locale=locale, + gender=gender, + model=model, request_options=request_options, ) @@ -881,14 +969,21 @@ async def create( name: str, gender: CreateVoicesRequestGender, sample: core.File, - consent: str, + consent_challenge_id: str, + consent_recording: core.File, idempotency_key: typing.Optional[str] = None, locale: typing.Optional[str] = OMIT, avatar: typing.Optional[core.File] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[GetVoice]: """ - Create a personal (cloned) voice for the user + Create a cloned voice for the workspace from a 10-30 second audio sample, with verified consent from the speaker. + + Cloning requires proof that the speaker agreed to it. Create a consent challenge with `POST /v1/voices/consent-challenges`, show the returned `phrase` to the speaker, record them reading it aloud, and send that recording here as `consent_recording` together with the challenge's `consent_challenge_id`. Speechify transcribes the recording, checks it against the phrase it issued, and keeps it as the consent record for the voice. A challenge is single use and short-lived, so record and submit in one sitting. + + The clone belongs to the workspace rather than the member who created it, and access follows the caller's workspace role and API-key scopes exactly as for any other voice: voices scopes to list it, audio scopes to synthesize with it, and the content-management permission plus a write scope on the key to delete it. Cloned voices are usable self-serve on `simba-3.0`, `simba-english` and `simba-multilingual`. `simba-3.2` also serves cloned voices, currently as a limited release enabled per workspace; contact Speechify to have it enabled for yours. + + Callers pinned before `Speechify-Version: 2026-09-13` use the previous flow instead: no challenge, and a `consent` form field carrying the speaker's name and email as a JSON string. That flow is deprecated and will be removed after a sunset window announced in the changelog. Parameters ---------- @@ -904,10 +999,14 @@ async def create( sample : core.File See core.File for more documentation - consent : str - A **string** representing the user consent information in JSON format - This should include the fullName and email of the consenting individual. - For example, `{"fullName": "John Doe", "email": "john@example.com"}` + consent_challenge_id : str + The `id` of the consent challenge this create consumes, from + `POST /v1/voices/consent-challenges`. Single use: once a + create has consumed it, whether or not that create + succeeded, it cannot be used again. + + consent_recording : core.File + See core.File for more documentation idempotency_key : typing.Optional[str] A client-generated key (an opaque string, max 255 chars) that makes a @@ -938,11 +1037,12 @@ async def create( "name": name, "locale": locale, "gender": gender, - "consent": consent, + "consent_challenge_id": consent_challenge_id, }, files={ "sample": sample, **({"avatar": avatar} if avatar is not None else {}), + "consent_recording": consent_recording, }, headers={ "Idempotency-Key": str(idempotency_key) if idempotency_key is not None else None, @@ -1016,6 +1116,17 @@ async def create( ), ), ) + if _response.status_code == 413: + raise ContentTooLargeError( + headers=dict(_response.headers), + body=typing.cast( + Error, + parse_obj_as( + type_=Error, # type: ignore + object_=_response.json(), + ), + ), + ) if _response.status_code == 422: raise UnprocessableEntityError( headers=dict(_response.headers), @@ -1085,9 +1196,9 @@ async def get( ) -> AsyncHttpResponse[GetVoice]: """ Fetch a single voice by id - a shared catalogue voice or one of - the caller's own personal (cloned) voices. A personal voice that - belongs to another workspace returns 404, identical to an - unknown id, so voice inventory is never enumerable across tenants. + the workspace's cloned voices. A cloned voice that belongs to + another workspace returns 404, identical to an unknown id, so + voice inventory is never enumerable across tenants. Parameters ---------- @@ -1207,7 +1318,9 @@ async def delete( self, voice_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[None]: """ - Delete a personal (cloned) voice + Delete one of the workspace's cloned voices. Requires the + `content.manage` permission (owner, admin, or member); a + service-account key is authorized by its scopes instead. Parameters ---------- diff --git a/src/speechify/voices/types/__init__.py b/src/speechify/voices/types/__init__.py index caa91ac..d013e55 100644 --- a/src/speechify/voices/types/__init__.py +++ b/src/speechify/voices/types/__init__.py @@ -7,7 +7,13 @@ if typing.TYPE_CHECKING: from .create_voices_request_gender import CreateVoicesRequestGender -_dynamic_imports: typing.Dict[str, str] = {"CreateVoicesRequestGender": ".create_voices_request_gender"} + from .list_voices_request_gender import ListVoicesRequestGender + from .list_voices_request_type import ListVoicesRequestType +_dynamic_imports: typing.Dict[str, str] = { + "CreateVoicesRequestGender": ".create_voices_request_gender", + "ListVoicesRequestGender": ".list_voices_request_gender", + "ListVoicesRequestType": ".list_voices_request_type", +} def __getattr__(attr_name: str) -> typing.Any: @@ -31,4 +37,4 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = ["CreateVoicesRequestGender"] +__all__ = ["CreateVoicesRequestGender", "ListVoicesRequestGender", "ListVoicesRequestType"] diff --git a/src/speechify/voices/types/list_voices_request_gender.py b/src/speechify/voices/types/list_voices_request_gender.py new file mode 100644 index 0000000..7cea93a --- /dev/null +++ b/src/speechify/voices/types/list_voices_request_gender.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ListVoicesRequestGender = typing.Union[typing.Literal["male", "female", "not_specified"], typing.Any] diff --git a/src/speechify/voices/types/list_voices_request_type.py b/src/speechify/voices/types/list_voices_request_type.py new file mode 100644 index 0000000..a4aa04a --- /dev/null +++ b/src/speechify/voices/types/list_voices_request_type.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ListVoicesRequestType = typing.Union[typing.Literal["personal", "shared"], typing.Any]