Browser Live Captions
Build a browser-based live caption interface using the LansonAI realtime API.
Architecture
Browser Your backend LansonAI
│ │ │
├── request session token ─────→ POST session-token ───────→ rt_... token
│←── rt_... token ──────────────│ │
│ │ │
├── open WebSocket ─────────────────────────────────────────→ session.created
│ ?access_token=rt_... │
│ │ │
├── capture mic (16kHz mono) ───│ │
├── send PCM16 frames ──────────────────────────────────────→ VAD + STT
│←── conversation.item.input_audio_transcription.completed ───────────────────────────│
│ │ │
└── display captions │ │
Step 1: Get a session token
Your backend exchanges the long-lived API key for a 60-second session token:
// backend route
app.post('/session-token', async (req, res) => {
const resp = await fetch('https://audio.lansonai.com/v1/audio/transcriptions/session-token', {
method: 'POST',
headers: { Authorization: Bearer ${process.env.LANSON_AUDIO_API_KEY} },
});
res.json(await resp.json());
});
::callout{icon="i-lucide-triangle-alert" color="amber"}
Never expose sk-... in frontend code. Always proxy through your backend.
::
Step 2: Open WebSocket
const { token } = await fetch('/session-token', { method: 'POST' }).then(r => r.json());
const ws = new WebSocket(
wss://audio.lansonai.com/v1/audio/transcriptions/stream?access_token=${token}
);
Step 3: Capture and send audio
const audioContext = new AudioContext({ sampleRate: 16000 });
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const source = audioContext.createMediaStreamSource(stream);
// Use AudioWorklet to get PCM16LE frames
await audioContext.audioWorklet.addModule('pcm-processor.js');
const node = new AudioWorkletNode(audioContext, 'pcm-processor');
source.connect(node);
// pcm-processor.js posts PCM16LE buffers to main thread
node.port.onmessage = (e) => {
// Send as binary frame
ws.send(e.data); // ArrayBuffer of PCM16LE data
};
Minimal pcm-processor.js:
class PcmProcessor extends AudioWorkletProcessor {
process(inputs) {
const input = inputs[0][0]; // mono, 16kHz
if (input) {
const pcm16 = new Int16Array(input.length);
for (let i = 0; i < input.length; i++) {
const s = Math.max(-1, Math.min(1, input[i]));
pcm16[i] = s < 0 ? s 0x8000 : s 0x7fff;
}
this.port.postMessage(pcm16.buffer);
}
return true;
}
}
registerProcessor('pcm-processor', PcmProcessor);
Step 4: Display captions
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'conversation.item.input_audio_transcription.completed') {
const div = document.getElementById('captions');
const p = document.createElement('p');
p.textContent = data.text;
div.appendChild(p);
}
};
Browser security
sk-... in frontend codert_... tokens from your backend?access_token keeps the long-lived key off the client