Repository navigation
feat(dictation): fall back to the buffered endpoint past the live 120 s cap - #202
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 (`POST /v1/transcribe/live`) refuses audio past 120 s with 413 `audio_too_large`, so a live dictation over two minutes failed. With the new `DictationLiveOptions.fallbackToBuffered` (default `true`), `transcribeLive()` and `openLive()` keep an in-memory copy of every audio byte they upload. On that 413 (mid-upload or after the body was sent) they read the source on until it ends, then post the whole copy once to the buffered endpoint (`POST /v1/transcribe`, up to 20 minutes / 256 MB) with the same config, file name and content type, and return that result like a live one. Any other failure is unchanged; a failed buffered request throws its own error; abort() or an abort signal drops everything with no fallback request; a copy past 256 MB is dropped and throws DictationError (413, `audio_too_large`). The live deadline stops applying once the fallback engages and the buffered request gets a fresh `timeout`. With `fallbackToBuffered: false` no copy is kept and the live 413 is thrown as before. Bump to 4.43.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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
client.dictation(transcribeLive(),openLive()) only wraps the live endpoint,POST /v1/transcribe/live, which caps a request at 120 s of audio. Past the cap the server answers413 {"error":"audio exceeds 120000 ms","error_code":"audio_too_large"}, so any live dictation over two minutes fails today.A customer uses live dictation for notes under two minutes but needs it to keep working, without stitching anything together themselves, up to 20 minutes. The buffered endpoint,
POST /v1/transcribe, already takes up to 20 minutes / 256 MB with the same config fields and returns the same response shape. This PR makes the SDK fall back to it automatically. The same behaviour is being built in the Python SDK and in the Blurt macOS app against a shared spec, so the three should match.Behaviour
New option
DictationLiveOptions.fallbackToBuffered?: boolean, defaulttrue. It applies to bothtranscribeLive()andopenLive().errorCode === "audio_too_large". Every other failure (other 413 codes, 401, 429, 5xx, a transport error) is thrown exactly as today.close()/result()on a session) and appends to the copy. This works whether the 413 arrives mid-upload with the producer still recording or after the whole body was sent.${dictationBaseUrl}/v1/transcribeas multipart, with the sameconfigJSON (includingsample_rate/channelsfor PCM) and the same audio filename and Content-Type. It returns that response exactly like a live result, includingfinal_text. If the buffered request fails, its own error is thrown (for example its own 413 past 20 minutes), not the live 413.DictationError(413,audio_too_large). The source is released and no request is sent.session.abort(), an abortsignal, or a producer error drops everything. No fallback request is sent.timeoutno longer cuts off the rest of the recording, because draining is recording time. The buffered POST gets a freshoptions.timeoutdeadline. The abortsignalis honoured throughout.Opt-out
{ fallbackToBuffered: false }keeps no copy and takes the exact code path that exists today, so the caller gets the live 413 as before.Trade-offs (also in the JSDoc, README and CLAUDE.md)
Uint8Arrayand then released.How it works
src/services/dictation/recording.ts(new, internal):AudioRecordingwraps the source. The live body reads it through a view, and every chunk is recorded as it is produced. The source has one reader at a time, so a chunk the live body asked for but abandoned when it was ended early is still recorded, and it is the first one the drain sees. Ending the view does not close the source. On the trigger the source is drained. On any other outcome it is released (iterator.return()), which is what the live upload did before.fetch()resolves with a413Responsewhile the request body is still streaming. The existing code already callsbody.finish()there, which sends the closing boundary on the next pull so the response body (witherror_code) can be read. After that, fetch stops pulling, soAudioRecording.drain()reads the producer directly until it ends.service.ts:transcribe()runs the live request and then the fallback.postBuffered()builds the body with the existingmultipartHead/multipartClosingframing and sends it with aContent-Length(not streamed). Response parsing is shared with the live path (dictationResult()).src/utils/live.tsandsrc/services/sync/are unchanged, so Sync behaves exactly as before.Test evidence
Unit tests (
tests/unit/dictation.test.ts, 22 new; 88/88 dictation tests pass, stable across 6 repeated runs)llm_instruction), filename and Content-Type, andfinal_textis derived. The live body is ended at the 413.fallbackToBuffered: falsethrows the live 413 as before, with one request, and the producer is released.payload_too_large, or a problem-details 413 titled "Payload Too Large"), a 429, a 500, a 401, or a transport failure.Retry-After).signalwhile draining, andsession.abort()after the 413, make no fallback request and release the producer.audio_too_large, and makes no fallback request.timeout: 50with about 200 ms of recording still succeeds with all audio). The buffered request gets a fresh deadline.transcribeLive()andopenLive()are covered (writes after the 413, a 413 at close,abort(), the opt-out, the deadline).The full unit suite gives 570 pass and 3 fail. The 3 failures are in
tests/unit/utils.test.ts(user-agentruntime_env) and fail the same way on unmodifiedmainunder Node 22.lint:eslint(no new warnings),lint:tsc,lint:format,publintandbuildall pass. Integration tests were not run.Real API (production
dictation.assemblyai.com, built package from this branch, 5 requests total)The audio is 130 s of 16 kHz mono S16LE speech. The producer sends the first 121 s as fast as the upload takes it, then 1 s chunks with 250 ms sleeps, so the 413 lands while it is still recording.
transcribeLive()130 s/v1/transcribe/live→ 413, then/v1/transcribe→ 200audio_duration_ms=130000, 300 words, 4.6 s totalopenLive()130 s/v1/transcribe/live→ 413, then/v1/transcribe→ 200audio_duration_ms=130000, 300 words, 4.0 s totaltranscribeLive()30 s/v1/transcribe/live→ 200 onlyaudio_duration_ms=30000, 73 words, 1.5 s totalThe script lives outside the repo and is not part of this PR.
Notes for reviewers
POST /v1/transcribestaying available. It is publicly documented today, but a docs PR (AssemblyAI/assemblyai-docs#490) proposes removing it from the docs. The docs and this fallback need reconciling before either lands.🤖 Generated with Claude Code