Streaming transcription

Rem streams interim and final transcripts in real time. Create a session with the samelw_ API key you use for your workspace. The response contains a short-lived WebSocket URL. The key is never placed in the WebSocket URL.

1. Create a session

POST https://app.lucidweights.ai/api/v1/stt/sessions
x-api-key: lw_YOUR_SECRET
Content-Type: application/json

{"language":"ar"}

Supported language hints: ar, en, es, fr, hi, ja. Omit language for English. The response has sessionId,sampleRate: 16000, andwsUrl. Use the URL once within two minutes.

2. Send audio

Connect to the returned wsUrl. Send JSON text frames, each containing base64-encoded mono PCM16 little-endian audio at 16 kHz. Each frame may contain up to 32,000 samples (two seconds). Send frames at roughly real-time pace and close the socket when finished.

{"audio_data":"<base64 PCM16 bytes>"}

3. Receive transcripts

FieldTypeDefaultDescription
transcriptionstring-Recognized text so far, or a completed utterance
finalboolean-true when the utterance is complete
errorstring-Present when the stream cannot continue
{"transcription":"مرحبا، كيف الحال؟","final":false}
{"transcription":"مرحبا، كيف الحال؟","final":true}

Credits and limits

Rem and Reverie share workspace credits. Each started second of audio accepted by the stream costs one credit; a new session with no audio costs none. The relay stops accepting audio when credits run out. Sessions last at most five minutes. Concurrent Rem stream limits are Free 3, Starter 15, Growth 40, and Scale 100.

WARNING
A revoked key cannot create a new session. An issued WebSocket URL cannot be reused. If the session endpoint returns 402, the workspace is out of credits; 429 means its Rem stream limit is reached.

Python example

Example
import asyncio, base64, json, os, wave
import requests, websockets

# Use a 16 kHz, mono, signed 16-bit PCM WAV for this example.
response = requests.post(
    "https://app.lucidweights.ai/api/v1/stt/sessions",
    headers={"x-api-key": os.environ["LUCIDWEIGHTS_API_KEY"]},
    json={"language": "ar"},
    timeout=15,
)
response.raise_for_status()
ws_url = response.json()["wsUrl"]  # Signed, short-lived, single use.

async def main():
    async with websockets.connect(ws_url) as ws:
        async def receive():
            async for message in ws:
                event = json.loads(message)
                if "error" in event:
                    raise RuntimeError(event["error"])
                if event.get("transcription"):
                    print("FINAL" if event.get("final") else "INTERIM", event["transcription"])

        reader = asyncio.create_task(receive())
        with wave.open("sample-16k-mono.wav", "rb") as audio:
            assert audio.getnchannels() == 1 and audio.getframerate() == 16000
            assert audio.getsampwidth() == 2
            while chunk := audio.readframes(1600):
                await ws.send(json.dumps({"audio_data": base64.b64encode(chunk).decode()}))
                await asyncio.sleep(len(chunk) / 2 / 16000)
        await asyncio.sleep(2)  # Allow final transcript to arrive.
        await ws.close()
        await reader

asyncio.run(main())
NOTE
The older api.lucidweights.ai/v1/stt/stream is a separate STT route. Use the session flow above when testing the metered Rem stream.