Parakeet Arabic / English ASR
Real-time automatic speech recognition via NVIDIA NIM
parakeet-1-1b-rnnt-multilingual.
Streaming WSS + REST transcription, automatic language detection,
automatic punctuation, VAD, and speaker diarization.
1. Overview
The service transcribes spoken Arabic and English audio to text. Language is detected automatically from the audio stream — the client should not pin a language, otherwise auto-detection is disabled.
Backend model profile (do not change):
diarizer=sortformer, mode=str, type=default, vad=silero
type=default keeps automatic language detection enabled.
Do not use type=prompt — that mode requires the
client to specify the language up front.
2. Endpoints
https://t8.xhmi.com/v1/health/ready
https://t8.xhmi.com/v1/metadata
https://t8.xhmi.com/v1/audio/transcriptions
wss://t8.xhmi.com/v1/realtime?intent=transcription
| Endpoint | Method | Purpose |
|---|---|---|
/v1/health/ready | GET | Liveness/readiness probe |
/v1/health/live | GET | Liveness probe |
/v1/metadata | GET | Model info, GPU, profile |
/v1/version | GET | NIM version |
/v1/manifest | GET | Model manifest |
/v1/metrics | GET | Prometheus metrics |
/v1/license | GET | License info |
/v1/audio/transcriptions | POST | One-shot transcription (REST) |
/v1/audio/translations | POST | One-shot translation to English |
/v1/realtime?intent=transcription | WSS | Real-time streaming transcription |
/v1/realtime/health | GET | Streaming subsystem health |
3. Accepted languages
The model is multilingual and detects more than Arabic and English. A client-side whitelist must accept only:
| Code | Language | Accept |
|---|---|---|
ar-AR | Arabic | yes |
en-US | English (US) | yes |
en-GB | English (UK) | yes |
| anything else | — | reject |
See §10 Language whitelist for the implementation.
4. Audio format
The client must send audio in this exact format:
| Field | Value |
|---|---|
| Format | PCM16 (signed 16-bit little-endian) |
| Sample rate | 16000 Hz |
| Channels | 1 (mono) |
| Punctuation | automatic (server-side, enabled by profile) |
| Language | automatic / default multilingual — do not pin |
language in the session config to
en-US or ar-AR — that disables automatic language switching.
Convert any audio to the required format (ffmpeg)
ffmpeg -i input.mp3 \
-ar 16000 \
-ac 1 \
-sample_fmt s16 \
output.wav
5. Authentication
The service is fronted by nginx with TLS termination. There is
no API key or bearer token required from clients — access is
controlled at the network layer. If you later need to add a key, insert it
in nginx (e.g. map on $http_authorization).
6. Health check
curl https://t8.xhmi.com/v1/health/ready
Expected response:
{"object":"health.response","message":"ready","status":"ready"}
7. REST transcription (one-shot)
str-multi-silero-sortformer); the server reports
No Offline ASR models found, so
POST /v1/audio/transcriptions returns
bad model / Model not found for language for every
request. Use the streaming WSS endpoint instead. To enable
REST, redeploy with a NIM profile that includes an offline ASR model.
For pre-recorded files. Returns the full transcript after the audio is processed. Auto-detects language; the detected BCP-47 code is in the response so you can apply the whitelist.
curl
curl -X POST https://t8.xhmi.com/v1/audio/transcriptions \
-F "file=@sample.wav" \
-F "model=parakeet-1-1b-rnnt-multilingual" \
-F "response_format=verbose_json"
Example JSON response (verbose)
{
"task": "transcribe",
"language": "ar-AR",
"duration": 5.42,
"text": "مرحبا، هذا اختبار للتعرف على الكلام.",
"segments": [
{
"id": 0,
"start": 0.0,
"end": 5.42,
"text": "مرحبا، هذا اختبار للتعرف على الكلام."
}
]
}
language is in
{ar-AR, en-US, en-GB}.
Request fields
| Field | Type | Required | Notes |
|---|---|---|---|
file | binary | yes | WAV/MP3/etc.; will be decoded server-side |
model | string | yes | parakeet-1-1b-rnnt-multilingual |
response_format | string | no | json (default), text, or verbose_json |
language | string | no | leave unset for auto-detect |
prompt | string | no | Optional context (do NOT use to force language) |
temperature | float | no | Sampling temperature, default 0.0 |
8. Streaming transcription (WSS)
For live audio from a microphone or stream. Open a WebSocket, send PCM16 chunks, and receive incremental transcript events.
wss://t8.xhmi.com/v1/realtime?intent=transcription
Protocol
- Open the WSS connection.
- Server immediately sends
conversation.createdandsession.created. - Optional: client sends
session.update(defaults are fine — the session starts inmultiauto-detect mode). Settinginput_audio_transcription.languageto a single code likeen-USorar-ARpins that language. - Client sends
input_audio_buffer.appendevents with raw PCM16 bytes (base64). - Server streams back
conversation.item.input_audio_transcription.deltapartials as text grows. - When speech ends (VAD silence ≈ 1.2 s), the server sends
conversation.item.input_audio_transcription.completedwith the full utterance and the detectedlanguage.
Client → Server events
| Event | Payload | Notes |
|---|---|---|
session.update |
session config object | Optional. Default is language: "multi" (auto-detect). Pin a single BCP-47 code (e.g. en-US, ar-AR) to disable auto-detection. |
input_audio_buffer.append |
{"audio": "<base64-PCM16>"} |
Send chunks of ~100 ms (3200 samples = 6400 bytes). |
input_audio_buffer.commit / response.create |
{} |
Accepted for compatibility; utterances finalize automatically via server VAD. |
input_audio_buffer.clear |
{} |
Discard buffered audio. |
Server → Client events
| Event | Meaning |
|---|---|
conversation.created | Session opened |
input_audio_buffer.speech_started | VAD detected start of speech |
input_audio_buffer.speech_stopped | VAD detected end of speech |
conversation.item.input_audio_transcription.delta | Partial transcript |
conversation.item.input_audio_transcription.completed | Final transcript (includes language) |
error | Error event |
9. Browser client (JavaScript)
const ALLOWED = new Set(["ar-AR", "en-US", "en-GB"]);
const ws = new WebSocket(
"wss://t8.xhmi.com/v1/realtime?intent=transcription"
);
ws.binaryType = "arraybuffer";
ws.onopen = () => {
console.log("WSS connected");
// Required: set the streaming model and language:"multi" (auto-detect).
// Do NOT rely on defaults and do NOT pin a single language.
ws.send(JSON.stringify({
type: "transcription_session.update",
session: {
input_audio_format: "pcm16",
input_audio_transcription: {
model: "parakeet-rnnt-1.1b-unified-ml-cs-universal-multi-asr-streaming-silero-vad-sortformer",
language: "multi" // multilingual auto-detect (ar / en / …)
},
turn_detection: null // use server default VAD (silero)
}
}));
};
ws.onmessage = (ev) => {
const msg = JSON.parse(ev.data);
switch (msg.type) {
case "conversation.created":
console.log("session:", msg.conversation.id);
break;
case "conversation.item.input_audio_transcription.delta":
// partial text — language may be unknown until completed
process.stdout.write(msg.delta || "");
break;
case "conversation.item.input_audio_transcription.completed":
const lang = msg.language || msg.transcript?.language;
const text = msg.transcript || "";
if (!ALLOWED.has(lang)) {
console.warn(`Rejected: detected language ${lang} not allowed`);
} else {
console.log(`
[${lang}] ${text}`);
}
break;
case "error":
console.error("server error:", msg.error);
break;
}
};
ws.onerror = (e) => console.error("WSS error:", e);
ws.onclose = (e) => console.log("WSS closed:", e.code, e.reason);
// ---- Feed PCM16 (16 kHz mono) chunks from Web Audio/MediaRecorder ----
function sendAudioChunk(int16Samples) {
// int16Samples: Int16Array of PCM16 samples
const bytes = new Uint8Array(int16Samples.buffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
const b64 = btoa(binary);
ws.send(JSON.stringify({
type: "input_audio_buffer.append",
audio: b64
}));
}
Resample microphone audio to 16 kHz PCM16 in the browser
async function micStream() {
const stream = await navigator.mediaDevices.getUserMedia({
audio: { channelCount: 1, sampleRate: 16000, echoCancellation: true }
});
const ctx = new AudioContext({ sampleRate: 16000 });
const src = ctx.createMediaStreamSource(stream);
const bufSize = 4096;
const node = ctx.createScriptProcessor(bufSize, 1, 1);
src.connect(node);
node.connect(ctx.destination);
node.onaudioprocess = (e) => {
const f32 = e.inputBuffer.getChannelData(0);
const i16 = new Int16Array(f32.length);
for (let i = 0; i < f32.length; i++) {
let s = Math.max(-1, Math.min(1, f32[i]));
i16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
}
sendAudioChunk(i16);
};
}
Live output & sentence segmentation
input_audio_buffer.commit, and each delta carries the
full running transcript (cumulative), not just the new words.
To get a live, sentence-by-sentence display, the browser client does two things:
- Periodic commit (~every 0.9 s) while the user is talking, so the server keeps flushing an updated transcript in near real time.
- Client-side silence detection (energy/RMS VAD): when the audio stays below a threshold for ~0.9 s after speech, the current line is finalized as a sentence and a fresh line begins.
Because deltas are cumulative, each new sentence is obtained by stripping the text already finalized (a prefix) from the latest delta — this yields a clean new sentence whether or not the server resets its context on the pause.
const SPEECH_RMS = 0.015; // energy above this = speech
const SILENCE_MS = 900; // silence this long after speech = sentence end
let finalizedPrefix = ""; // full server text at the last finalize
let lastFullText = ""; // most recent cumulative delta
let hasSpeech = false, lastVoice = 0;
// In onaudioprocess, after computing rms of the chunk:
if (rms > SPEECH_RMS) { hasSpeech = true; lastVoice = performance.now(); }
else if (hasSpeech && performance.now() - lastVoice > SILENCE_MS) {
hasSpeech = false;
finalizeSentence();
}
// On each delta: show only the current sentence (strip finalized prefix)
function onDelta(fullText) {
lastFullText = fullText;
let part = fullText;
if (finalizedPrefix && fullText.startsWith(finalizedPrefix))
part = fullText.slice(finalizedPrefix.length);
liveLine.textContent = part.trim();
}
// On a pause: keep the current line and start a fresh one
function finalizeSentence() {
if (liveLine.textContent.trim()) appendSentence(liveLine.textContent.trim());
finalizedPrefix = lastFullText; // everything so far is now done
liveLine.textContent = "";
ws.send(JSON.stringify({ type: "input_audio_buffer.commit" }));
}
10. Python client (streaming)
import asyncio, json, base64, ssl
import websockets
ALLOWED_LANGUAGES = {"ar-AR", "en-US", "en-GB"}
URL = "wss://t8.xhmi.com/v1/realtime?intent=transcription"
async def stream_audio(pcm16_chunks):
"""
pcm16_chunks: iterable of bytes objects, each containing raw PCM16
(16 kHz, mono, signed 16-bit LE).
"""
async with websockets.connect(URL) as ws:
# Initial server message
first = json.loads(await ws.recv())
assert first["type"] == "conversation.created"
print(f"[session] {first['conversation']['id']}")
# Required session update: set the streaming model + language "multi"
await ws.send(json.dumps({
"type": "transcription_session.update",
"session": {
"input_audio_format": "pcm16",
"input_audio_transcription": {
"model": "parakeet-rnnt-1.1b-unified-ml-cs-universal-multi-asr-streaming-silero-vad-sortformer",
"language": "multi"
}
}
}))
async def send_audio():
for chunk in pcm16_chunks:
payload = {
"type": "input_audio_buffer.append",
"audio": base64.b64encode(chunk).decode("ascii")
}
await ws.send(json.dumps(payload))
await asyncio.sleep(0.1) # ~100 ms cadence
await ws.send(json.dumps({"type": "input_audio_buffer.commit"}))
async def recv_results():
while True:
msg = json.loads(await ws.recv())
t = msg.get("type")
if t == "conversation.item.input_audio_transcription.delta":
print(msg.get("delta", ""), end="", flush=True)
elif t == "conversation.item.input_audio_transcription.completed":
text = msg.get("transcript", "")
lang = msg.get("language")
if lang not in ALLOWED_LANGUAGES:
print(f"
[REJECTED] detected {lang}, not in {sorted(ALLOWED_LANGUAGES)}")
else:
print(f"
[{lang}] {text}")
return
elif t == "error":
print(f"
[ERROR] {msg.get('error')}")
return
await asyncio.gather(send_audio(), recv_results())
# Example: stream a 16 kHz mono WAV's PCM body
def wav_pcm16_chunks(path, chunk_ms=100):
import wave
w = wave.open(path, "rb")
assert w.getframerate() == 16000 and w.getnchannels() == 1
frames_per_chunk = int(16000 * chunk_ms / 1000)
while True:
data = w.readframes(frames_per_chunk)
if not data: break
yield data
asyncio.run(stream_audio(wav_pcm16_chunks("sample.wav")))
Python — REST one-shot
import requests
ALLOWED = {"ar-AR", "en-US", "en-GB"}
with open("sample.wav", "rb") as f:
r = requests.post(
"https://t8.xhmi.com/v1/audio/transcriptions",
files={"file": f},
data={
"model": "parakeet-1-1b-rnnt-multilingual",
"response_format": "verbose_json",
},
)
r.raise_for_status()
data = r.json()
lang = data.get("language")
if lang not in ALLOWED:
raise SystemExit(f"unsupported_language: detected {lang}")
print(f"[{lang}] {data['text']}")
11. Language whitelist
The model emits more languages than Arabic/English. Always filter on the detected language returned by the server.
ALLOWED_LANGUAGES = {"ar-AR", "en-US", "en-GB"}
def filter_result(transcript_event):
lang = transcript_event.get("language") # BCP-47 from server
text = transcript_event.get("text") or transcript_event.get("transcript") or ""
if lang not in ALLOWED_LANGUAGES:
return {
"error": "unsupported_language",
"detected_language": lang,
"allowed": sorted(ALLOWED_LANGUAGES),
}
return {"language": lang, "text": text}
language is not in
{ar-AR, en-US, en-GB} — return an
unsupported_language error and do not surface the transcript.
12. Errors
| HTTP / code | Meaning | Action |
|---|---|---|
200 / ready | Healthy | — |
400 | Malformed request (bad format, bad event) | Verify PCM16, 16 kHz, mono |
503 | Service not ready / model still loading | Retry with backoff |
unsupported_language | Detected language not in whitelist | Reject/flag at client |
WSS 1011 | Internal server error | Reconnect |
WSS 1006 | Abnormal close (network/nginx timeout) | Reconnect |
13. Server metadata
curl https://t8.xhmi.com/v1/metadata
Returns model info, GPU, profile, version. Useful for support tickets.
14. Limits & timeouts
| Setting | Value |
|---|---|
| REST body size | 100 MB (nginx client_max_body_size) |
| REST proxy timeout | 600 seconds |
| WSS idle timeout | 86 400 seconds (24 hours) |
| Audio chunk cadence | ~100 ms (3200 samples = 6400 bytes PCM16) |
| Sample rate | 16 000 Hz (strict) |
| Channels | 1 (mono, strict) |
| Format | PCM16 (strict for streaming) |