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
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.
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/.
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.
{"type": "end"} (text): the audio is over. The last guesses and the end of the turn come, then the socket closes.A conversation can hold several turns.
All are JSON text messages. Each carries its turn's number as turn.
{"type": "ready"}: Bell is awake; send the audio.{"type": "heard", "turn", "text"}: the words heard so far.{"type": "guess", "turn", "id", "text", "heard"}: a guess of Bell's at the turn's words (text), and the words heard when it was made (heard). A guess already sent in the turn is not sent again.{"type": "end_of_turn", "turn", "text", "guess_id"}: the turn's final words, and the id of the earliest guess equal to them (null when none was).{"type": "error", "error", "message", "retry_after_s"}: see below. The socket closes after it.GET /v1/sample.wav is a sample turn, 5.12 seconds, 16 kHz mono.
GET /v1/account with the header Authorization: Bearer <key> answers {"email", "free_seconds_left", "credit_cents",
"used_seconds_month", "limits"}.GET /v1/usage with the same header answers your use by day: {"unit": "seconds of audio", "days": [{"date", "seconds",
"conversations"}]}.
POST /v1/codes/redeem with the same header and {"code": "..."} adds the code's free seconds to your account and answers
{"free_seconds_left"}. Each code works once for an account. A wrong, used-up or expired code answers 400 bad_code.
POST /v1/billing/checkout with the same header and {"amount_cents": 500} answers {"url"}, Stripe's payment page for that
much credit. The credit is used after the free seconds.
Use is counted in seconds of audio streamed to the API, by the second.
ready is sent.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.