Skip to content

feat!: regenerate SDK for API 2026-09-13 with consent-challenge voice cloning - #26

Draft
luke-speechify wants to merge 3 commits into
mainfrom
sdk-release
Draft

feat!: regenerate SDK for API 2026-09-13 with consent-challenge voice cloning#26
luke-speechify wants to merge 3 commits into
mainfrom
sdk-release

Conversation

@luke-speechify

@luke-speechify luke-speechify commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Full SDK regeneration against API version 2026-09-13. Adds the models endpoint, the consent-challenge flow for voice cloning, and streaming with word-level timestamps. Also repairs the release-please wiring so every version-bearing value in a published release matches the release tag.

Breaking changes

voices.create() signature

The consent: str parameter is gone. Voice cloning now goes through a two-step consent challenge:

# before
client.voices.create(
    name="My Voice",
    gender="male",
    sample=audio_file,
    consent="...",
)

# after
challenge = client.voices.consent_challenges.create(full_name="Jane Doe")

# the speaker must read challenge.phrase aloud verbatim — the recording is
# transcribed and matched against it, and challenge.expires_at is the only
# authority on how long you have
consent_recording = record(challenge.phrase)

client.voices.create(
    name="My Voice",
    gender="male",
    sample=audio_file,
    consent_challenge_id=challenge.id,
    consent_recording=consent_recording,
)

Callers pinned to a Speechify-Version before 2026-09-13 keep the old flow: 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.

GetStreamRequestModel import path

# before
from speechify.audio import GetStreamRequestModel

# after
from speechify.types import GetStreamRequestModel

It is no longer re-exported from speechify.audio, so the old import raises ImportError. It is also available from the package root as from speechify import GetStreamRequestModel.

Default API version

Bumped from 2026-07-07 to 2026-09-13. Pin version= explicitly to stay on the old behaviour.

New

  • client.models.list() — available TTS models
  • client.voices.consent_challenges.create() — consent challenge flow
  • client.audio.stream_with_timestamps() — SSE stream with word-level speech marks
  • voices.list() filters: type, locale, gender, model
  • ContentTooLargeError (413) and new error codes

Release wiring

The generic release-please updater was silently no-opping — that is what shipped 2.0.1 to PyPI under the 3.0.0 tag. The release commit for 3.0.0 touched only the manifest and the changelog; poetry build then read a stale [tool.poetry].version. Fixed here:

  • dropped the dangling $.project.version jsonpath. [project] is dynamic and has no version key, so the TOML updater logged No entries modified and no-opped — it implied coverage that did not exist
  • kept $.tool.poetry.version, which is load-bearing: the default python release-type resolves project || tool.poetry, hits our [project] table first, sees dynamic = ["version"] and skips the file entirely. Without the extra-files entry nothing bumps the artifact version
  • added .fern/metadata.json to extra-files (it was stale at 2.0.1). The json updater is jsonpath-based, so it needs no marker and survives regeneration
  • removed the generic extra-files entry for client_wrapper.py, and the x-release-please-version markers with it. That file is Fern-generated and cannot be .fernignored, so every regeneration strips the markers and the updater silently stops bumping User-Agent and X-Fern-SDK-Version — re-arming the exact trap that caused the 3.0.0 incident. A fix that lives inside a generated file is not a fix
  • those two headers are now stamped from the release tag in the publish job. Order is stamp → assert → publish. The stamp handles a prerelease suffix (4.0.0-rc.1 stays 4.0.0-rc.1) and fails the build if a target literal is not found, so a regeneration that restructures those lines breaks the release loudly instead of shipping a stale version quietly
  • publish still fails if any version string disagrees with the tag, or if a version literal goes missing

The stamp rewrites the CI checkout only and is never committed back. client_wrapper.py on main therefore reads stale between releases by design — the published artifact is always built from the stamped and asserted tree. AGENTS.md documents this so nobody "fixes" it later.

Verified by running release-please's own updater classes against this config for a simulated bump to 4.0.0: $.tool.poetry.version and .fern/metadata.json both update, and neither depends on a marker. The same run confirms the generic updater no-ops silently on the marker-free file, which is why it is gone. The stamp and assertion scripts were extracted from the workflow and executed against fixture trees: matching tag, un-stamped tree, restructured literal, empty tag, non-semver tag and prerelease tag all behave as intended, and the stamp is idempotent.

Merge with squash so the conventional title and this footer land on main.

BREAKING CHANGE: voices.create() no longer accepts consent; it requires consent_challenge_id and consent_recording.
BREAKING CHANGE: GetStreamRequestModel moved from speechify.audio to speechify.types.
BREAKING CHANGE: default API version is now 2026-09-13.

Release-As: 4.0.0

@luke-speechify luke-speechify changed the title feat! regenerate SDK for updated API specifications feat!: regenerate SDK for API 2026-09-13 with consent-challenge voice cloning Aug 14, 2026
@luke-speechify
luke-speechify marked this pull request as draft August 14, 2026 22:26
@luke-speechify

Copy link
Copy Markdown
Collaborator Author

Holding this as draft until the SSE response is modelled upstream.

Live verification against production found stream_with_timestamps() returns zero events with no error — sync and async both. 92 and 105 events respectively were parsed, failed validation, and were silently discarded.

The response is generated as type_=str, but every SSE payload on the wire is a JSON object ({"audio": ..., "speech_marks": [...]}). parse_obj_as(str, {...}) raises ValidationError, which subclasses ValueError, and the broad except (TypeError, ValueError, KeyError, AttributeError) at audio/raw_client.py:564 (sync) and :1215 (async) swallows it and continues. The generator completes normally, so callers see a clean empty stream.

A raw HTTP control request to the same endpoint returns 94 chunks + 1 done, 19,629 audio bytes, and word marks that reconcile exactly against the input. The transport is fine — the SDK discards the data.

Also found:

  • 409 is in the retry list (core/http_client.py:128) with base_max_retries=2, so non-idempotent POSTs get retried. voices.create burned its consent challenge and failed consent_challenge_already_used after 51.7s; the identical call with max_retries=0 returned 201 first attempt. Can orphan voices.
  • 413 is not mapped on any audio method — falls through to a generic error. ContentTooLargeError exists but is wired only to voices.create.
  • SSE diagnostics go to the root logger (from logging import error, warning), bypassing the SDK's own silent-by-default logger.

All of this is in generated code, so the fixes belong in the Fern definition — patching here would be erased on the next regeneration. The release-please wiring in this PR is verified and stays as-is.

Everything else passed: 24/30 live cases, all 10 endpoints exercised, all four voices.list filters proven to actually filter, voice cloning works with a synthesized consent recording, version headers correct.

Removes the dangling $.project.version jsonpath ([project] is dynamic),
covers .fern/metadata.json, stamps the Fern-generated version strings
from the release tag at publish time, restores AGENTS.md and
manual-publish.yml behind .fernignore, and gates publish on a
version/tag assertion.

client_wrapper.py is Fern-generated and cannot be .fernignore'd, so an
x-release-please-version marker does not survive there: every
regeneration strips it and the generic updater goes back to silently
no-opping, leaving User-Agent and X-Fern-SDK-Version reporting a version
the SDK is not. That is the same class of failure that shipped 2.0.1 to
PyPI under a 3.0.0 tag. The generic extra-files entry and both markers
are gone.

The publish job now rewrites those two literals from the release tag
before the assertion and before poetry publish --build. The stamp fails
the build when a target literal is not found, so a regeneration that
renames or restructures those lines breaks the release loudly instead of
shipping a stale version silently. Order is stamp, assert, publish.

The stamp applies to the CI checkout only and is never committed back,
so those two strings read stale on main between releases by design; the
published artifact is always built from the stamped and asserted tree.
AGENTS.md documents this so it does not get "fixed" later.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant