If you have built any kind of AI audio feature lately, you know the 25 MB wall.
A user uploads a normal 1-hour meeting recording (often around 60 MB). You pass it to OpenAI’s Whisper endpoint and the backend returns 413 Payload Too Large.
Now the “quick feature” is a project. You pull ffmpeg into the repo, split the file, fire several API calls, then stitch timestamps and hope you did not cut a word in half.
Already on the official OpenAI SDK, and the file is under about 90 MB? You can point that client at SpeechWeave in two lines and stay on audio.transcriptions.create. This post is for the rest: files above the proxy cap, deferred pricing, and a single upload with no chunking pipeline.
The usual two options
Developers get pushed toward one of two extremes:
- Standard APIs (the 25 MB cap). Synchronous
POSTbodies time out before a GPU finishes a long file, so the vendor caps the upload. OpenAI Whisper is 25 MB. - Enterprise ingest (your bucket). Larger limits exist, but you upload to your own object store, mint presigned URLs, pass those in, and write cleanup so the audio does not sit there forever.
You do not need ffmpeg splits for a 4-hour podcast, and you do not need to stand up a bucket to transcribe one.
Direct upload, then a job
SpeechWeave’s native SDK presigns, PUTs the bytes to storage, and creates the job. The file never rides through the 90 MB OpenAI-compatible proxy body. Typical self-serve account cap is 250 MB (max_input_bytes on GET /v1/limits), which is about 4 to 8 hours of standard MP3.
Your app (SDK) SpeechWeave Your webhook
| | |
| jobs.create(file) | |
| presign + PUT ------------->| |
| | transcribe (deferred) |
| wait_for_job / GET /v1/jobs | |
| | POST job.completed ------------>|
| | delete source audio |
Omitting service_mode on jobs.create uses the standard queue. Pass service_mode: "deferred" for the background queue: 50% cheaper than standard. Webhooks are workspace endpoints configured in the dashboard.
Python:
from speechweave import SpeechWeave, wait_for_job
sw = SpeechWeave() # SPEECHWEAVE_API_KEY
job = sw.jobs.create(
file="./4_hour_podcast.mp3",
model="core",
service_mode="deferred",
)
done = wait_for_job(sw, job["id"])
print(done["transcript"])
Node:
import { SpeechWeave, waitForJob } from "@speechweave/node";
const sw = new SpeechWeave({ api_key: process.env.SPEECHWEAVE_API_KEY! });
const job = await sw.jobs.create({
file: "./4_hour_podcast.mp3",
model: "core",
service_mode: "deferred",
});
const done = await waitForJob(sw, job.id);
console.log(done.transcript);
jobs.create accepts a local path or an open file. Skip wait_for_job if you already have a workspace webhook. Same call shape as the migration guide native-SDK section.
When the job hits completed or failed, the source audio is deleted. A 7-day storage expiry is the backstop if something is left behind. Retention details live in the privacy post.
Which path, which limit
Do not hard-code 90 MB or 250 MB. Ask your key:
curl -sS https://api.speechweave.com/v1/limits \
-H "Authorization: Bearer $SPEECHWEAVE_API_KEY"
You get proxy_max_bytes (OpenAI-compatible multipart), sync_max_bytes, and max_input_bytes (native upload). Base self-serve values can be raised on the account.
| Path | What you call | Typical cap | Mode |
|---|---|---|---|
| OpenAI SDK drop-in | client.audio.transcriptions.create |
~90 MB | Always standard queue (blocking POST) |
| Native SDK, same shape | sw.audio.transcriptions.create |
Account cap (~250 MB) | Standard queue (waits for .text) |
| Native SDK, jobs | sw.jobs.create |
Account cap (~250 MB) | Standard by default. Pass service_mode: "deferred" for the cheaper background queue. |
A 60 MB standup recording fits the drop-in path. A 4-hour podcast usually does not. That is when this upload flow matters, not ffmpeg.
Need files above the self-serve cap (raw WAV archives, uncompressed video)? That is an Enterprise conversation: [email protected].
If the file should never touch your server in the first place (browser upload on Vercel, 4.5 MB function bodies), see async transcription on Next.js.