OpenScriber API v0.2.0

Speech-to-text API — asynchronous batch transcription and live HLS (m3u8) streaming. Multi-tenant, per-key metering and limits.

Base URL: https://proscribe.techon.top/api/v1  ·  WebSocket: wss://proscribe.techon.top/api/v1/speech:streamRecognize

Authentication

Static API keys are issued by the operator — there is no self-service signup yet. Two lines on issuance:

  1. Request a key from the operator; you receive a single key:label credential (the label is your display name in usage reports). Keep it secret.
  2. Send it with every request. Keys cannot be rotated via the API — contact the operator to revoke or replace a key.

Send the key on every HTTP request using either form:

TransportExampleNotes
X-API-Key headerX-API-Key: sk_live_...Preferred; used by all snippets on this page.
Authorization: BearerAuthorization: Bearer sk_live_...Equivalent — the server accepts either form.
WebSocket query paramwss://.../speech:streamRecognize?api_key=sk_live_...For ws/wss connections (see WebSocket); the key may also be sent as an X-API-Key handshake header.

Error bodies are JSON {"detail": "..."}. Keys are compared with a constant-time comparison, and raw key material is never stored or logged (only a truncated SHA-256 id is used for metering). When auth is disabled on a deployment, endpoints behave as anonymous (see usage & limits).

Try it — generate ready-to-run snippets

Enter your key, pick a model and an audio file, and the page generates copy-paste Bash (cURL) and Python (requests) snippets that submit → poll → fetch the transcript with your credentials. Everything runs client-side; nothing is sent from this page.

No file selected — the snippet will use audio.wav.

Must run from the machine that has the audio file; the key is embedded in the snippet exactly as entered above.

The generated snippets submit the job, poll /v1/speech:jobs/{jobId} until COMPLETED/FAILED, then fetch the transcript via /v1/transcripts/{jobId}?format=json. Only fields the API actually accepts are used (see the batchRecognize config table).

Bash — cURL

# click "Generate Bash snippet" …

Python — requests

# click "Generate Python snippet" …

REST endpoints

All endpoints live under https://proscribe.techon.top/api (prefix /api + version /v1). Every route requires an API key (X-API-Key or Authorization: Bearer).

POST /api/v1/speech:batchRecognize

Submit an audio file for asynchronous transcription. Returns a jobId; poll GET /jobs/{jobId} for progress, then fetch the transcript. Server-side, the upload is decoded to 16 kHz mono WAV (mp3/mp4/m4a/ogg/wav/… accepted via ffmpeg) before inference.

Authenticates via Depends(rate_limiter) → 401/403 and 429 (rate limit, per-key pending-job cap, or daily audio quota) all apply.

Request — multipart/form-data

FieldTypeRequiredDescription
filefile (binary)yesThe audio file. Missing → HTTP 400 "No audio file uploaded".
configstring (JSON)noBatchConfig JSON (table below). Omitted → engine:"whisper", model:"whisper-base", all other fields defaulted. Invalid → HTTP 422.

BatchConfig fields

FieldType / defaultConsumed by inference?Description
engine"whisper" | "vosk" | "openrouter", default "whisper"yesExplicit engine. May be omitted — a vosk- model id routes to VOSK, an openai/… or nvidia/… model id routes to OpenRouter, everything else routes to Whisper.
modelstring, default "whisper-base"yesModel id, must be one of the registered models in /v1/models. Invalid → 422.
languagestring | nullyesOptional source language code ("en", "ur", …). null/"auto" → auto-detect.
word_timestampsbool, default trueyesInclude per-word timing in segments[].words.
decoding_modestring, default "balanced"partiallyfast→beam 1/best_of 1, quality→5/3, balanced→3/2 — only for Whisper models outside the CPU-economy set (tiny/base/small always decode greedily).
whisper_configobject, default {}yesOverrides for supported faster-whisper transcribe() options (e.g. beam_size, task, initial_prompt, vad_filter, vad_parameters, temperatures, hotwords). Unknown keys → 422.
vosk_configobject, default {}yesbeam/max_active applied when the VOSK model loads (via conf/model.conf; defaults 10/3000), plus max_alternatives, partial_words.
openrouter_configobject, default {}yesmodel selects the OpenRouter audio-transcription model; falls back to the deployment default.
filtersobject | nullyes{"profanity": "mask"} masks profanity in the final transcript.
match_wordsstring[]yesTerms flagged in the matches/match_counts fields of the transcript.
custom_vocabularystring[], default []no — reservedDeclared + validated, not yet wired into inference.
sample_rateint > 0, default 16000no — reservedDeclared + validated, not yet wired into inference.
punctuatebool, default trueno — reservedDeclared + validated, not yet wired into inference.
diarizebool, default falseno — reservedDeclared + validated, not yet wired into inference.
n_bestint 1–10, default 1no — reservedDeclared + validated, not yet wired into inference.
detect_languagebool, default trueno — reservedDeclared + validated, not yet wired into inference.
webhook_urlstring (uri) | nullno — reservedDeclared + validated, not yet wired into inference.

Example — cURL

curl -s -X POST "https://proscribe.techon.top/api/v1/speech:batchRecognize" \
  -H "X-API-Key: sk_live_..." \
  -F "file=@audio.wav" \
  -F 'config={"engine":"whisper","model":"whisper-base","language":null,"word_timestamps":true}'

Response — 200 OK

{
  "jobId": "3f0c1d2e-...",
  "status": "QUEUED",
  "config": {"engine": "whisper", "model": "whisper-base", "language": null, "word_timestamps": true}
}

Errors: 400 no file · 401 missing key · 403 invalid key · 422 invalid config JSON · 429 rate limit / too many pending jobs / daily audio quota (Retry-After header).

GET /api/v1/speech:jobs/{jobId}

Fetch the status and metadata of one batch job.

Authenticates via Depends(rate_limiter) → 401/403 and 429 apply. Jobs are scoped to the calling key (anonymous sees all when auth is disabled).

ParameterTypeLocationDescription
jobIdstring (UUID)pathThe job id returned by batchRecognize. Unknown id → 404 "Job not found".

Example — cURL

curl -s "https://proscribe.techon.top/api/v1/speech:jobs/3f0c1d2e-..." \
  -H "X-API-Key: sk_live_..."

Response — 200 OK (single JSON object)

{
  "jobId": "3f0c1d2e-...",
  "status": "COMPLETED",
  "progress": 100,
  "result_uri": "/v1/transcripts/3f0c1d2e-...",
  "requested_at": "2026-09-22T17:31:01Z",
  "started_at": "2026-09-22T17:31:02Z",
  "completed_at": "2026-09-22T17:32:10Z",
  "original_filename": "audio.wav",
  "whisper_config_json": "{\"engine\":\"whisper\",\"model\":\"whisper-base\"}",
  "selected_language": "en",
  "resolved_model": "whisper-base"
}

Errors: 401 missing key · 403 invalid key, or the job belongs to another API key · 404 job not found · 429 rate limit.

POST /api/v1/speech:jobs/{jobId}:retry

Requeue a finished/failed job using its original upload and configuration. The original audio file must still be on the server.

Authenticates via Depends(require_api_key) only — no rate limiter → no 429. Jobs are scoped to the calling key.

ParameterTypeLocationDescription
jobIdstring (UUID)pathThe job id to retry. Non-UUID / unknown → 404.

Example — cURL

curl -s -X POST "https://proscribe.techon.top/api/v1/speech:jobs/3f0c1d2e-...:retry" \
  -H "X-API-Key: sk_live_..."

Response — 200 OK

{"jobId": "3f0c1d2e-...", "status": "QUEUED", "retry_count": 1}

Errors: 401 missing key · 403 job belongs to another key · 404 unknown job / original audio no longer available · 409 job already QUEUED/RUNNING · 422 older job has no reusable configuration. No 429 — this route is not rate-limited.

GET /api/v1/speech:jobs/list

List batch jobs, newest first. Scoped to the calling API key (anonymous sees all when auth is disabled).

Authenticates via Depends(rate_limiter) → 401/403 and 429 apply.

ParameterTypeLocationDescription
limitint, default 50queryMax results; clamped to 1–100.

Example — cURL

curl -s "https://proscribe.techon.top/api/v1/speech:jobs/list?limit=50" \
  -H "X-API-Key: sk_live_..."

Response — 200 OK (JSON objects only)

{
  "jobs": [
    {"jobId": "3f0c1d2e-...", "status": "COMPLETED", "progress": 100,
     "result_uri": "/v1/transcripts/3f0c1d2e-...", "requested_at": "2026-09-22T17:31:01Z",
     "started_at": "2026-09-22T17:31:02Z", "completed_at": "2026-09-22T17:32:10Z",
     "original_filename": "audio.wav", "whisper_config_json": "{}",
     "selected_language": "en", "resolved_model": "whisper-base"}
  ],
  "count": 1
}

Errors: 401 missing key · 403 invalid key · 429 rate limit.

GET /api/v1/speech:streams/list

List active streaming sessions. Scoped to the calling API key (anonymous sees all when auth is disabled).

Authenticates via Depends(rate_limiter) → 401/403 and 429 apply.

Example — cURL

curl -s "https://proscribe.techon.top/api/v1/speech:streams/list" \
  -H "X-API-Key: sk_live_..."

Response — 200 OK

{
  "streams": [
    {"streamId": "0a1b2c3d-...", "m3u8_url": "https://example.com/live/playlist.m3u8",
     "started_at": "2026-09-22T18:00:03Z", "model": "whisper-tiny",
     "status": "streaming", "cpu_percent": 12.0, "memory_mb": 640}
  ],
  "count": 1
}

Errors: 401 missing key · 403 invalid key · 429 rate limit.

GET /api/v1/models

Return the model capability catalogue exposed by this deployment — the source of truth for the model table below.

Authenticates via Depends(rate_limiter) → 401/403 and 429 apply.

Example — cURL

curl -s "https://proscribe.techon.top/api/v1/models" \
  -H "X-API-Key: sk_live_..."

Response — 200 OK

{"models": [
  {"id": "whisper-tiny", "engine": "whisper", "device": ["cpu"], "languages": "multilingual", "tier": "economy"},
  {"id": "whisper-base", "engine": "whisper", "device": ["cpu"], "languages": "multilingual", "tier": "balanced"},
  {"id": "vosk-model-ur", "engine": "vosk", "device": ["cpu"], "languages": "ur", "tier": "vosk-fast", "available": false}
]}

Errors: 401 missing key · 403 invalid key · 429 rate limit.

GET /api/v1/transcripts/{jobId}?format=json|srt|vtt

Retrieve the completed transcript of a job, optionally as subtitles.

Authenticates via Depends(require_api_key) only — no rate limiter → no 429. Jobs are scoped to the calling key.

ParameterTypeLocationDescription
jobIdstring (UUID)pathJob id. Unknown → 404.
formatjson | srt | vtt, default jsonqueryOutput format. srt → application/x-subrip, vtt → text/vtt. Invalid value → 400 "format must be json, srt, or vtt".

If the job has not completed, the response is {"status": "...", "progress": N} instead.

Example — cURL

# JSON (default)
curl -s "https://proscribe.techon.top/api/v1/transcripts/3f0c1d2e-..." \
  -H "X-API-Key: sk_live_..."

# Subtitles
curl -s "https://proscribe.techon.top/api/v1/transcripts/3f0c1d2e-...?format=srt" \
  -H "X-API-Key: sk_live_..."
curl -s "https://proscribe.techon.top/api/v1/transcripts/3f0c1d2e-...?format=vtt" \
  -H "X-API-Key: sk_live_..."

Response — 200 OK (format: json)

{
  "jobId": "3f0c1d2e-...",
  "status": "COMPLETED",
  "model": "whisper-base",
  "language": "en",
  "language_probability": 0.98,
  "transcript": "The quick brown fox jumps over the lazy dog.",
  "segments": [
    {"start": 0.0, "end": 3.2, "text": "The quick brown fox jumps over the lazy dog.",
     "confidence": 0.96,
     "words": [{"text": "The", "start": 0.0, "end": 0.2, "confidence": 0.98}]}
  ],
  "matches": [],
  "match_counts": {},
  "alternatives": [{"transcript": "The quick brown fox jumps over the lazy dog."}]
}

Errors: 400 invalid format · 401 missing key · 403 job belongs to another key · 404 job not found. No 429 — this route is not rate-limited.

GET /api/v1/usage

Return this API key's live usage counters (self-scoped; you only ever see your own key's usage).

Authenticates via Depends(rate_limiter) → 401/403 and 429 apply.

Example — cURL

curl -s "https://proscribe.techon.top/api/v1/usage" \
  -H "X-API-Key: sk_live_..."

Response — 200 OK (auth enabled)

{
  "keyLabel": "primary",
  "batchRequests": 12,
  "streamSessions": 3,
  "audioSeconds": 45.6,
  "lastUsed": "2026-09-22T18:00:03Z"
}

When auth is disabled the response is {"enabled": false, "note": "auth is disabled — no usage is metered"} — note the enabled key only appears in that anonymous case.

Errors: 401 missing key · 403 invalid key · 429 rate limit.

WebSocket — /api/v1/speech:streamRecognize

WS wss://proscribe.techon.top/api/v1/speech:streamRecognize

Live transcription of an HLS stream (m3u8_url) or a raw PCM binary stream. One WebSocket per active stream.

Authentication

The key must be presented during the handshake, before the server accepts: ?api_key=<key> query param, X-API-Key header, or Authorization: Bearer <key>. Missing/invalid key → close code 1008 (the connection is rejected before accept).

Browsers cannot set WebSocket headers, so the ?api_key= query param is the practical choice in JS.

Protocol — config message first

The client MUST send a single JSON config message as the first frame within 20 seconds; otherwise the server closes with 1008 (idle). Unknown/invalid JSON → text error {"error": "invalid config"} and close.

{
  "type": "config",
  "m3u8_url": "https://example.com/live/playlist.m3u8",   // either this…
  "engine": "whisper",
  "model": "whisper-tiny",        // default "whisper-tiny" for WS
  "language": "en",               // or "auto"/null
  "word_timestamps": false,
  "stream_chunk_seconds": 8,      // optional, 2–30, default 8
  "overlap_seconds": 1,           // optional, 0 ≤ overlap < chunk, default 1
  "whisper_config": {}, "vosk_config": {}, "openrouter_config": {}
  // any other BatchConfig fields are honored too (see batchRecognize table)
}
KeyType / defaultDescription
m3u8_urlhttp/https URIHLS playlist URL. http/https only. Omit to use the raw-PCM binary path. Duplicate URL + model already active → error frame + close 1013.
stream_chunk_secondsfloat, default 8Decode window length. Must be 2–30; invalid → error → close.
overlap_secondsfloat, default 1Overlap between windows. Must satisfy 0 ≤ overlap < chunk.
modelstring, default "whisper-tiny"Any registered model for the chosen engine.

Path A — m3u8 (HLS) streaming

The server runs ffmpeg to decode the stream to 16 kHz mono s16le PCM and emits frames per window:

FrameWhenExample payload
{"type":"info", ...}once, right after connect{"type":"info","message":"connected to m3u8 stream","model":"whisper-tiny"}
{"type":"interim", ...}before each window decode{"type":"interim","transcript":"Processing live audio window...","timestamp":8.0}
{"type":"final", ...}after each window{"type":"final","segments":[...],"matches":[],"language":"en","processing_ms":842}
{"type":"error", ...}on failure{"type":"error","message":"...","state":{...}} then close (1013 on capacity/quota/duplicate).

Only the stable portion of each window is finalized (the trailing overlap is finalized by the following window with more context).

Path B — raw PCM binary streaming

Omit m3u8_url: send raw 16 kHz mono signed 16-bit (s16le) PCM binary frames. There is no samplerate query param — the audio is always interpreted as 16 kHz mono s16le (32 000 bytes/second).

  1. Server replies {"type":"info","message":"send binary audio frames or close to finish","model":"..."}.
  2. Send binary s16le PCM frames continuously (e.g. from a microphone or ffmpeg -f s16le pipe). The server buffers them into the same windows as the m3u8 path (stream_chunk_seconds / overlap_seconds from the config) and runs real speech-to-text on each window.
  3. Per window the server emits the same {"type":"interim","transcript":"Processing live audio window..."} and {"type":"final","segments":[...],"matches":[...],"language":"...","processing_ms":...} frames as the m3u8 path.
  4. Send the text message "close" (or close the socket) to finish; the server closes and meters the decoded audio seconds.

A connected-but-silent socket is closed after ~30 s of no frames (STREAM_WS_IDLE_TIMEOUT). Raw-PCM sessions ignore the duplicate-URL rule (each mic/source is independent) but still count against per-key and global stream caps. This is what powers the "Live microphone" mode in the test console.

Example — JavaScript (browser)

const key = "sk_live_...";
const ws = new WebSocket(
  "wss://proscribe.techon.top/api/v1/speech:streamRecognize?api_key=" + encodeURIComponent(key)
);
ws.onopen = () => ws.send(JSON.stringify({
  type: "config",
  m3u8_url: "https://example.com/live/playlist.m3u8",
  model: "whisper-tiny",
  language: "en",
  word_timestamps: false,
  stream_chunk_seconds: 8,
  overlap_seconds: 1
}));
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === "final") console.log(JSON.stringify(msg, null, 2));
  if (msg.type === "error") console.error(msg);
};

Example — Bash (websocat + ffmpeg)

# m3u8 path
printf '%s' '{"type":"config","m3u8_url":"https://example.com/live/playlist.m3u8","model":"whisper-tiny","language":"en","stream_chunk_seconds":8,"overlap_seconds":1}' \
  | websocat -H "X-API-Key: sk_live_..." "wss://proscribe.techon.top/api/v1/speech:streamRecognize"

# raw PCM path (16 kHz mono s16le, no samplerate param)
ffmpeg -re -i audio.wav -ar 16000 -ac 1 -f s16le - 2>/dev/null \
  | websocat -H "X-API-Key: sk_live_..." "wss://proscribe.techon.top/api/v1/speech:streamRecognize" \
    --text '{"type":"config"}'

Close codes you may see: 1008 missing/invalid key, or no config within 20 s · 1013 per-key/global stream cap, duplicate stream, or daily stream-session quota · 1000/normal on clean finish.

Usage & limits

Per-key limits and quotas are configured by environment variables on the deployment. Values below are the code defaults and current preview deployment — 0 always means unlimited. Everything is per API key; anonymous callers (auth disabled) are exempt.

KnobDefaultScopeBehaviour when exceeded
MAX_STREAMS_PER_KEY2per keyConcurrent active WS streams. Error frame + close 1013.
MAX_JOBS_PER_KEY10per keyConcurrent QUEUED/RUNNING batch jobs. HTTP 429 "too many pending jobs for this key".
MAX_ACTIVE_STREAMS4 (deployment: 5)globalTotal active WS streams across all keys. Error frame + close 1013.
QUOTA_AUDIO_SECONDS_PER_DAY0 (unlimited)per keyDaily audio-seconds for batch jobs. HTTP 429 + Retry-After seconds until UTC midnight.
QUOTA_STREAM_SESSIONS_PER_DAY0 (unlimited)per keyDaily WS stream sessions. Error frame + close 1013.
RATE_LIMIT_REQUESTS_PER_MIN0 (off)per keyPer-minute HTTP request rate over all REST /v1 endpoints. HTTP 429 + Retry-After: 60.

Which REST routes are rate-limited (matches code): batchRecognize ✓ · jobs/{jobId} GET ✓ · jobs/list ✓ · streams/list ✓ · models ✓ · usage ✓ — but transcripts/{jobId} ✗ and jobs/{jobId}:retry ✗ (plain key-auth only).

Model catalogue

Everything below is served verbatim by GET /v1/models (12 models). vosk-model-ur is listed with "available": false — it is not deployed. Resource notes come from code/documented deployment comments only.

Model idEngineDeviceLanguagesTierNotes
whisper-tinywhispercpumultilingualeconomyPreloaded. Greedy decode (beam 1) for CPU speed.
whisper-basewhispercpumultilingualbalancedPreloaded. Greedy decode.
whisper-smallwhispercpumultilingualqualityPreloaded. Greedy decode.
whisper-small-enwhispercpu(english-only upstream)—Registered in schemas; not exposed by this deployment's /v1/models.
whisper-base-urduwhispercpuurrouted-fastUrdu-specialized, CPU-friendly int8 build.
whisper-small-urduwhispercpuurrouted-qualityUrdu-specialized, CPU-friendly int8 build.
vosk-model-en-usvoskcpuenvosk-fastLoads on first use; beam/max_active via model.conf (defaults 10/3000).
vosk-model-urvoskcpuurvosk-fastNot deployed (available: false).
vosk-model-arvoskcpuarvosk-fastLoads on first use.
vosk-model-hivoskcpuhivosk-fastLoads on first use.
qwen/qwen3-asr-0.6bopenrouterremotemultieconomyQwen3 ASR, ~$0.000003/second. Requires server-side provider key.
openai/whisper-large-v3-turboopenrouterremote99+economyWhisper Large V3 Turbo, ~$0.000003/second (budget tier). Requires server-side provider key.
nvidia/nemotron-3.5-asr-streaming-multilingual-0.6openrouterremotemultifastNemotron 3.5 ASR streaming, multilingual, ~$0.000003/second.
openai/whisper-large-v3openrouterremote99+economyRemote transcription ~$0.0048/min.
nvidia/parakeet-tdt-0.6b-v3openrouterremote60fastRemote, fast lightweight option.

CPU inference runs under CTranslate2 int8. The hot tier (tiny/base/small) is preloaded so first-job latency is near zero. Local medium and large-v3 are disabled — on the 8 GB host each would consume ~2–3 GB RAM and risk OOM; route quality-critical work to the remote openai/whisper-large-v3* models instead. Transcription on CPU int8 is only usable at beam_size 1 — beam 5 roughly triples wall time for no meaningful accuracy gain.

Errors & close codes

HTTP

CodeMeaningDetail example
400Bad request"No audio file uploaded" · "format must be json, srt, or vtt"
401Missing API key"Missing API key. Send it as 'X-API-Key' header or 'Authorization: Bearer <key>'."
403Invalid key / other tenant"Invalid API key." · "Job belongs to another API key."
404Not found"Job not found" · "Original uploaded audio is no longer available"
409Conflict"Job is already queued" / "Job is already running" (retry)
422Unprocessable content"Invalid batch configuration: ..." · "This older job has no reusable configuration"
429Limit exceeded"rate limit exceeded" · "too many pending jobs for this key" · "daily audio quota exceeded" — with Retry-After header

WebSocket close codes

CodeMeaning
1008Missing/invalid API key at handshake, or no config message within 20 seconds (idle).
1013Capacity/quota: per-key or global stream cap, duplicate URL+model, or daily stream-session quota. An error frame is sent first.
1000 / normalClean finish (e.g. after "close" on the binary path); invalid config also closes without a code.

Changelog