Repository navigation
feat(sync): make the live connection the only one the client opens - #245
Conversation
5c7517c to
bd90f46
Compare
bd90f46 to
076f059
Compare
Every entry point — `transcribe()`, `transcribe_async()`,
`transcribe_live()`, `open_live()` — now posts to `/v1/transcribe/live`.
Audio that is already complete is a stream whose bytes happen to all be
ready, so it goes out over the same connection as a single chunk instead of
through a second, buffered request path.
What that buys is one request shape to reason about: one framing, one set of
errors, one timeout, and a complete clip that reaches the service exactly the
way a live capture does — segments transcribed as they land rather than after
the upload finishes.
The public surface is unchanged. `api.transcribe()` keeps its signature and
delegates, so the legacy `assemblyai.sync_api` shim still re-exports it, and
`ENDPOINT_TRANSCRIBE` stays for the same reason with a note that nothing here
requests it. The endpoint constant is renamed to `ENDPOINT_TRANSCRIBE_LIVE`
and points at `/v1/transcribe/live` — the path is also served at
`/v1/transcribe/stream`, which it shipped under.
One observable change: an empty config used to mean no `config` part at all.
The live endpoint requires one ahead of the audio, so `{}` now goes out
instead. Two tests asserting the old absence are updated to assert the
presence and the ordering.
Complete-audio calls now take `sync_live_http_timeout`, whose per-operation
shape fits a streamed body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6AVhXhzQoBBgDoVrPBUap
076f059 to
0811caa
Compare
The buffered transcribe() now streams its request body through the live path, and two tests failed only on the httpx 0.22/0.24 floor envs: - test_sync_async.py read the recorded request with the synchronous request.read(), which asserts the stream is a sync Iterable. A streamed async body is not, and the older pytest_httpx does not consume it. Read the body in an async mock callback instead, which caches it on the recorded request so .read() works on every supported httpx. - test_the_buffered_endpoint_is_never_requested used add_response(is_reusable=True); is_reusable is not in the pytest_httpx release pinned for those floors. Queue one response per submission. Test-only; no product change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The async-callback form of the previous fix does not work on the oldest supported pytest_httpx: it assigns `is_stream_consumed` to whatever the callback returned, so a coroutine raises `AttributeError` before the response is ever used. Reproduced on the httpx 0.22 floor and in CI. Read the body inside an `httpx.MockTransport` handler instead, which consumes the request stream the way a real transport does and works on every supported httpx. This is the approach `test_sync_live.py` already documents for its own body-shape assertions. `test_the_buffered_endpoint_is_never_requested` queues one response per submission for the same reason as before, sized to the four entry points it drives. Test-only; no product change. 531 pass on the modern stack and on the httpx 0.22 floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V6AVhXhzQoBBgDoVrPBUap
|
Heads-up @ccampbell-aai — I pushed 55240d7 on top of your floor fix rather than replacing it, because the async-callback form doesn't survive the oldest pytest_httpx. On pytest-httpx 0.20 (the pin for the Mine reads the body inside an Verified 531 pass on the modern stack and 531 on the httpx 0.22 floor via Happy to take a different shape if you prefer — the constraint is just that it can't rely on pytest_httpx consuming an async body. |
…ell/dictation-sdk Keeps the shared _audio helpers for sync's _resolve_audio and adds master's resolve_format() as a wrapper over _audio.resolve_format so stream_filename() keeps its helper; merges the typing imports in sync/v1/api.py; keeps both sync_live_http_timeout and the dictation settings. Version to 1.7.0 on top of master's 1.6.0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ell/dictation-sdk Keeps the shared _audio helpers for sync's _resolve_audio and adds master's resolve_format() as a wrapper over _audio.resolve_format so stream_filename() keeps its helper; merges the typing imports in sync/v1/api.py; keeps both sync_live_http_timeout and the dictation settings. Version to 1.7.0 on top of master's 1.6.0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Stacked on #244 (
ccampbell/sync-live-session), which is itself stacked on #241. This PR adds only the consolidation on top.What
Every entry point —
transcribe(),transcribe_async(),transcribe_live(),open_live()— now posts to/v1/transcribe/live. There is one connection in this client.Audio that is already complete is a stream whose bytes happen to all be ready, so it goes out over that connection as a single chunk rather than through a separate buffered request. What that buys is one request shape to reason about: one framing, one set of errors, one timeout — and a complete clip reaches the service exactly the way a live capture does, with each speech segment transcribed as it lands instead of after the upload finishes.
The endpoint constant becomes
ENDPOINT_TRANSCRIBE_LIVE = "/v1/transcribe/live", following the rename in DeepLearning #20044 (merged and live in production).The public surface is unchanged
api.transcribe()keeps its exact signature and delegates totranscribe_live(), so the legacyassemblyai.sync_apishim still re-exports it andtest_old_sync_api_module_surface_is_preservedstill passes.ENDPOINT_TRANSCRIBEstays exported for the same reason, with a comment noting nothing here requests it.I went this way deliberately rather than deleting the buffered function: that test pins the old flat module's surface as a compatibility contract, and breaking it would be a major-version change for no gain — the goal is one connection, not a smaller import surface.
Note this is a client-side consolidation only. Sync still serves both
/v1/transcribeand/v1/transcribe/live, and both stay.One observable change
An empty config used to mean no
configpart at all. The live endpoint requires one ahead of the audio, so{}now goes out instead. Anyone inspecting the wire will always see a config part. Two tests asserting the old absence are updated to assert the presence and the ordering.Complete-audio calls also move from
sync_http_timeouttosync_live_http_timeout, whose per-operation shape is the right one for a streamed body.sync_http_timeoutstill backs the warm-up.Compressed formats are not a concern here: this client has always been WAV/PCM only (
_resolve_audiopicks betweenaudio/wavandaudio/pcm), which is exactly what the live endpoint accepts.Validation
transcribe()and its async twin hit the live URL with noContent-Lengthand config-before-audio, and one test drives all four entry points and asserts the set of requested URLs is exactly{/v1/transcribe/live}— so a buffered request reappearing anywhere fails.ruff checkandruff format --checkclean onassemblyai/;mypyreports zero errors insync/v1(the errors elsewhere are pre-existing and identical to the base).No version bump
The stack lands on master as one merge, so the version is set once at the top rather than per PR — this branch leaves
__version__alone. The test matrix and version-bump workflows only trigger for PRs into master, so only lint runs here until the base moves.🤖 Generated with Claude Code