Repository navigation
feat(dictation): fall back to the buffered endpoint past the live 120 s cap - #289
Draft
dan-ince-aai wants to merge 1 commit into
Draft
dan-ince-aai wants to merge 1 commit into
dan-ince-aai wants to merge 1 commit into
Conversation
… s cap The live endpoint (/v1/transcribe/live) answers 413 audio_too_large once a dictation passes 120 s of audio. With fallback_to_buffered=True (the default on DictationTranscriber and AsyncDictationTranscriber), the transcriber keeps an in-memory copy of the audio it uploads; on that one error it reads the source until it ends and posts the whole recording once to /v1/transcribe (up to 20 min / 256 MB) with the same config, filename and Content-Type. Every other error, abort() and producer exceptions behave as before. fallback_to_buffered=False keeps no copy and raises the live 413. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
A customer uses
DictationTranscriberlive dictation for audio under 2 minutes, and wants it to keep working up to 20 minutes without stitching anything together themselves. Today every entry point posts toPOST /v1/transcribe/live, which caps a request at 120 s. Past it, production answers413 {"error":"audio exceeds 120000 ms","error_code":"audio_too_large"}, so any live dictation over 2 minutes fails.The Dictation API also serves
POST /v1/transcribe, the buffered endpoint: the whole recording in one body, up to 20 minutes / 256 MB, with the same config fields and the same response shape. This PR makes the SDK fall back to it on its own when a live dictation runs past the cap.The same behaviour is being built in the Node SDK and in the Blurt macOS app from one shared spec, so the three should match.
Behaviour
Applies to
transcribe_live()andopen_live()on bothDictationTranscriberandAsyncDictationTranscriber:error_code == "audio_too_large". Every other failure (another 413 code, 401/404, 429, 5xx, transport errors) raises exactly as before.close()/result()is called on a live session. It appends to the copy as it reads. It wraps the source in its own iterator (_RecordedChunks/_AsyncRecordedChunks), so the producer can still be read after httpx stops pulling the request body. That covers a 413 that arrives mid-upload as well as one that arrives after the whole body was sent.{dictation_base_url}/v1/transcribeas multipart, with the sameconfigJSON (includingsample_rate/channelsfor PCM) and the same audio filename and Content-Type. The body is sent with aContent-Lengthin 1 MB pieces, so the recording is never copied whole a second time. Its response is parsed and returned like a live result. If it fails, the transcriber raises its error (for example its own 413 past 20 minutes), not the live 413.api.BUFFERED_MAX_BYTES, 256 MiB), the copy is dropped, the producer is not read further, and aDictationError(status_code=413, error_code="audio_too_large")is raised. See the reviewer note below on when this check runs.abort()or an exception from the producer still drops everything, with no fallback request. That includes anabort()afterclose()while the live 413 is still in flight: the session passes an "aborted?" check that runs just before the buffered POST.settings.dictation_http_timeout.Opt-out
With
False, behaviour is today's: no copy is kept, the request body is built exactly as before, and a dictation past 120 s raises the live 413. Both transcribers also expose a read-onlyfallback_to_bufferedproperty.Trade-offs
The constructor,
transcribe_live()andopen_live()docstrings, the README Dictation section (new "Dictate for longer than 2 minutes" block) and CLAUDE.md all state both trade-offs plainly. Version bumped to 1.10.0.How the live 413 actually surfaces (worth knowing)
Two probes against production with plain httpx/httpcore over HTTP/1.1 showed that the server keeps reading the live body after it has decided on the 413. httpx sent the whole body first and only then read the response:
So in production the 413 reaches the client at key-up, and the drain step is usually a no-op. The drain path still matters: if a server answers early and closes the connection, httpcore suppresses the write error and returns the response mid-upload. The unit tests cover that case with a transport that stops reading the body part-way.
The same finding is why the 256 MB guard runs on every append, not only while draining. Most of a long recording arrives during the live upload, so a drain-only check would leave memory unbounded until key-up. Any copy over 256 MB is already far past the live cap (120 s of 16-bit audio is at most about 23 MB), so the visible result is the same 413
audio_too_large, just raised sooner. Please reconcile this with the Node SDK and Blurt, since the shared spec says "while draining".Test evidence
Unit tests (
tests/unit/test_dictation.py,tests/unit/test_dictation_async.py) use a scriptedhttpxtransport. It reads the live body for N pieces (or all of it) and then answers, which mimics httpcore stopping the body pull once a response is in. They cover:audio_too_largemid-upload: the producer keeps being read after the 413, and the buffered request carries all the audio, including chunks produced after the 413. It also has the same config, filename and Content-Type, aContent-Length, and no chunked encodingfallback_to_buffered=Falseraises the live 413 and doesn't read the producer furtherpayload_too_large, problem-details title, no code), 401, 429, 500 (even witherror_code=audio_too_large) and 503 don't fall backopen_live(): audio written after the 413 reaches the buffered request;abort()while draining, or afterclose()with the 413 still pending, makes no fallback requestAsyncDictationTranscriber.transcribe_live()andAsyncDictationLiveSessionTwo existing tests encoded "the buffered endpoint is never requested". They now say "not requested while the live request succeeds", and the envelope-parsing test for a 413
audio_too_largeturns the fallback off.Results:
pytest tests/unit: 998 passed (958 onmaster)httpx0.22andhttpx0.24tox factors: 148 passed on each (httpx 0.22.0 / httpcore 0.14.7; httpx 0.24.1 / httpcore 0.17.3). That includes the explicitContent-Lengthon a streamed body.ruff format --checkandruff checkon the changed files: cleanmypy==1.5.1 --follow-imports=silent --ignore-missing-imports --python-version 3.9on the changedassemblyai/files, as the lint job runs it: cleanReal API (production,
dictation.assemblyai.com, 16 kHz mono S16LE,config={"sample_rate":16000,"channels":1}). The producer sent the first 121 s in 0.25 s chunks, then 1 s chunks with 0.3 s sleeps, so it was still recording past the cap. An httpx event hook logged each endpoint:audio_duration_mstranscribe_live()/v1/transcribe/live(200) onlytranscribe_live()/v1/transcribe/live(413) →/v1/transcribe(200)open_live()/v1/transcribe/live(413) →/v1/transcribe(200)AsyncDictationTranscriber.transcribe_live()/v1/transcribe/live(413) →/v1/transcribe(200)In each 130 s run the live 413 arrived at key-up (within 10 ms), and the fallback posted the full 4,160,000 bytes. The buffered
request_time_mswas about 1.5 to 1.65 s.Notes for reviewers
POST /v1/transcribestaying available. It is publicly documented today, but AssemblyAI/assemblyai-docs#490 proposes removing it from the docs. The two need reconciling before this ships: either the endpoint stays supported, or this fallback needs another target.BUFFERED_MAX_BYTESis 256 MiB (268,435,456 bytes). If the server's 256 MB limit is decimal, it should be lowered to match, here and in the sibling clients.🤖 Generated with Claude Code