TL;DR — "Realtime vs batch" is the wrong framing. The real decision is who does VAD (server or you) and what happens to silence. LansonAI exposes three planes — Realtime WS, Segment HTTP, Batch jobs — and the right one is determined by your audio's shape (stream / clip / file) and your latency need. Pick by the table, not by instinct.
Problem
You have audio and you need text. It sounds like one problem, but it's three different problems:
- A person is talking right now and you want text as they speak.
- Your app already cut the speech into short clips (a voice UI) and you want each clip transcribed.
- You have a finished file (a meeting recording, a podcast) at a URL and you want it processed.
Most integrations pick a plane by instinct — "I'll just stream everything" or "I'll just upload a file" — and then hit a wall: unexpected latency, billing for silence, or a WebSocket where they needed a simple HTTP call. The wall is almost always the same root cause: nobody decided who does VAD.
Architecture
LansonAI exposes three transcription planes. The differentiator that matters is VAD ownership and when you need the text:
| Dimension | Realtime WS | Segment HTTP | Batch jobs |
|---|---|---|---|
| When you need text | While they're speaking | Per clip, immediately | Minutes later (async) |
| Who does VAD | Server (on the stream) | You (client) | Server (time-slice) |
| Transport | WebSocket | HTTP POST | HTTP POST + GET poll |
| Input | PCM16LE stream | Multipart file |
JSON audio_url |
| Output | Event stream | Transcript JSON | Full job JSON |
| Latency | Milliseconds | ~≤1.6 s (sync) | Minutes |
| Success signal | WS events | 200 | 202 + poll |
The three planes, in one line each:
- Realtime WS (
WS /v1/audio/transcriptions/stream) — you send a continuous PCM stream; the server runs VAD and emits utterances as they stabilize. For live captions and dialogue. - Segment HTTP (
POST /v1/audio/transcriptions) — you send one pre-cut clip (multipartfile); you get a synchronous200with the transcript. For voice UIs that already detect boundaries locally. - Batch jobs (
POST /v1/audio/transcriptions/jobs) — you send a publicaudio_url; you get202and poll. The server slices by time, transcribes chunks concurrently, and aggregates. For meeting/podcast files.
The trap to know up front — Segment has no server-side silence filtering. Whatever you POST is transcribed and billed. If your client VAD is loose and you send 3 seconds of silence, you pay for 3 seconds of silence. Realtime and Batch handle silence for you; Segment does not, because you own the boundary.
Code
Minimal, real example for each plane. (Full audio pipeline for the WS plane is in How to stream microphone audio over WebSocket.)
Realtime WS — text while they speak
// Server-side: Bearer header. Browser: ?access_token=rt_... (from your backend)
const ws = new WebSocket("wss://audio.lansonai.com/v1/audio/transcriptions/stream");
// ws = new WebSocket(url, { headers: { Authorization: `Bearer ${SK}` } }) // Node
ws.onopen = () => {
ws.send(JSON.stringify({ type: "session.update", language: "zh" }));
// ... send PCM16LE/16kHz/mono frames as input_audio_buffer.append ...
// ws.send(JSON.stringify({ type: "input_audio_buffer.append", audio: "<base64>" }));
// ws.send(JSON.stringify({ type: "input_audio_buffer.flush" })); // end of segment
};
ws.onmessage = (e) => {
const ev = JSON.parse(e.data);
if (ev.type === "conversation.item.input_audio_transcription.completed") {
console.log(`[${ev.utterance_index}] ${ev.text}`); // stable text
}
};Segment HTTP — you already cut the clip
curl -X POST https://audio.lansonai.com/v1/audio/transcriptions \
-H "Authorization: Bearer sk-..." \
-F "file=@utterance.wav" \
-F "language=zh" \
-F "response_format=verbose_json"
# → 200 { "text": "...", "language": "zh", "duration": 1.2, ... }Batch jobs — you have a file at a URL
# 1. Submit (202)
curl -X POST https://audio.lansonai.com/v1/audio/transcriptions/jobs \
-H "Authorization: Bearer sk-..." -H "Content-Type: application/json" \
-d '{ "audio_url": "https://cdn.example.com/meeting.wav", "language": "zh" }'
# → { "workflow_id": "cf_5519...", "status": "queued", "poll_endpoint": "GET /v1/audio/transcriptions/jobs/cf_5519..." }
# 2. Poll until status == "complete"
curl https://audio.lansonai.com/v1/audio/transcriptions/jobs/cf_5519... \
-H "Authorization: Bearer sk-..."Result
You pick a plane you can defend in one sentence:
- "Live captions → Realtime, because the server should run VAD on a stream and I need text in milliseconds."
- "Voice input → Segment, because my client already detects utterance boundaries and I want a synchronous result per clip."
- "Podcast processing → Batch, because I have a file at a URL and can wait minutes for a complete, aggregated transcript."
And the whole decision compresses into three questions you can run top-down:
Is someone talking right now, and must text appear while they speak?
├─ Yes → Realtime WS (server VAD on a continuous stream)
└─ No → Have you already cut the speech into short clips (your own VAD)?
├─ Yes → Segment HTTP (sync 200 per clip; you own silence)
└─ No → Is it a complete recording at a URL?
├─ Yes → Batch jobs (202 + poll; server time-slices)
└─ No → Capture or upload it first, then re-ask.The latency figures in the table come from the API contract itself (Realtime: millisecond-level event stream; Segment: ~≤1.6s wall-clock budget on the warm path; Batch: minutes, async). Cost varies by plan and usage — check the rate-limits page before committing.
Why it matters
VAD ownership is the real decision, not "realtime vs batch." The question "do I need it live?" gets you 80% there, but the deeper question is "who cuts the speech into utterances, and who is responsible for silence?" Answer that and the plane chooses itself.
Silence is a billing and quality decision. On Segment you own the boundary, so you own the silence — send it and you're billed for it. On Realtime and Batch the service handles it. Knowing which plane pushes silence-handling onto you prevents a class of "why am I paying for nothing" surprises.
The transferable lesson. Match the transport to the shape of your data: a stream wants a full-duplex socket, a clip wants a synchronous request, a file wants an async job you poll. Reaching for the wrong shape is how you end up writing reconnect logic you didn't need, or blocking a request for minutes.
Further reading (Docs)
- Choose an API — 决策表 + 三平面概览
- Segment Transcription API — client-VAD 同步接口
- Batch Transcription Jobs — 异步任务接口
- Realtime Quickstart — 最小可跑示例
Related cookbook recipes
- How to stream microphone audio over WebSocket — Realtime 平面的音频管线
- How to build realtime captions that don't jitter — Realtime 平面的渲染侧