Rabel API (beta)

Stream audio in. Bell sends its guesses at each turn's words before the speaker finishes, then the end of the turn.

The Rabel API is in beta. It starts free: every account has free seconds of audio, and the limits are below. A first call can take up to a minute while Bell wakes.

Base address: https://api.rabellabs.com

Get an API key

Sign up with your email:

curl -X POST https://api.rabellabs.com/v1/signup   -H "Content-Type: application/json"   -d '{"email": "you@example.com"}'

It answers status 202, and one email comes with a link. Open the link: it answers {"email": "...", "key": "rabel_..."}. The key is shown once; keep it. The link works once and expires in 24 hours. Signing up again with the same email gives a new key and turns the old one off; your use stays with your email.

Set RABEL_API_KEY as an environment variable, or put your key in place of YOUR_API_KEY in the snippets below.

The snippets

Each one downloads a sample turn (5 seconds of speech), streams it to Bell in 80 ms chunks, and prints Bell's guesses and the end of the turn.

TypeScript (Node 22), node snippet.ts:

const API = "https://api.rabellabs.com";
const KEY = process.env.RABEL_API_KEY ?? "YOUR_API_KEY";

const wav = Buffer.from(await (await fetch(`${API}/v1/sample.wav`)).arrayBuffer());
const audio = wav.subarray(wav.indexOf("data") + 8); // 16 kHz mono PCM16

const ws = new WebSocket(`${API.replace("https", "wss")}/v1/conversation`);
ws.onopen = () => {
  ws.send(JSON.stringify({ key: KEY }));
  console.log("Bell is waking up (a first call can take up to a minute)...");
};
ws.onmessage = async (e) => {
  const m = JSON.parse(e.data);
  if (m.type === "guess") console.log("guess:", m.text);
  if (m.type === "end_of_turn") console.log("end of turn:", m.text);
  if (m.type === "error") console.log("error:", m.message);
  if (m.type === "ready") {
    for (let i = 0; i < audio.length; i += 2560) { // 80 ms chunks
      ws.send(audio.subarray(i, i + 2560));
      await new Promise((r) => setTimeout(r, 80));
    }
    ws.send(JSON.stringify({ type: "end" }));
  }
};

Python, pip install websockets, then python snippet.py:

import asyncio, io, json, os, urllib.request, wave
import websockets

API = "https://api.rabellabs.com"
KEY = os.environ.get("RABEL_API_KEY", "YOUR_API_KEY")

async def main():
    wav = wave.open(io.BytesIO(urllib.request.urlopen(API + "/v1/sample.wav").read()))
    audio = wav.readframes(wav.getnframes())  # 16 kHz mono PCM16
    async with websockets.connect(API.replace("https", "wss") + "/v1/conversation") as ws:
        await ws.send(json.dumps({"key": KEY}))
        print("Bell is waking up (a first call can take up to a minute)...", flush=True)

        async def send():
            for i in range(0, len(audio), 2560):  # 80 ms chunks
                await ws.send(audio[i:i + 2560])
                await asyncio.sleep(0.08)
            await ws.send(json.dumps({"type": "end"}))

        async for msg in ws:
            m = json.loads(msg)
            if m["type"] == "ready":
                sender = asyncio.create_task(send())
            elif m["type"] == "guess":
                print("guess:", m["text"])
            elif m["type"] == "end_of_turn":
                print("end of turn:", m["text"])
            elif m["type"] == "error":
                print("error:", m["message"])

asyncio.run(main())

The same two files are in snippets/.

A conversation

Open a WebSocket to /v1/conversation. Send the key first, either as the header Authorization: Bearer <key> or as the first text message {"key": "<key>"} (within 10 seconds).

Bell wakes, then the API sends {"type": "ready"}. A first call can take up to a minute while Bell wakes; a call right after another is ready in about a tenth of a second. Send audio only after ready.

Messages in

A conversation can hold several turns.

Messages out

All are JSON text messages. Each carries its turn's number as turn.

Sample audio

GET /v1/sample.wav is a sample turn, 5.12 seconds, 16 kHz mono.

Your account and your use

Use is counted in seconds of audio streamed to the API, by the second.

Limits and errors

On the WebSocket an error is the message {"type": "error", "status", "error", "message", "retry_after_s"}, then the socket closes. Over HTTP it is the status with {"error", "message"}.

status error when retry_after_s
401 bad_key The key is wrong. 0
402 no_seconds The free seconds and the credit of this key are used up. 0
429 key_day_limit This key has had its 20 conversations today. seconds to 00:00 UTC
429 key_at_once This key already has 2 conversations open. 1
529 out_of_capacity Out of capacity, contact us for more usage. 60
503 waking Bell is still waking up after 90 seconds. Try again in a minute. 60
413 audio_limit A conversation is at most 120 seconds of audio. Start a new conversation to go on. 0
408 no_audio No audio came for 15 seconds. 0
400 bad_message An audio frame is over 65,536 bytes. 0

POST /v1/signup answers 400 bad_email when the body has no email, and 429 too_many_signups after too many signups from one address in an hour. GET /v1/confirm answers 400 bad_link for a link that is wrong, used or expired.