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:
- Request a key from the operator; you receive a single
key:labelcredential (the label is your display name in usage reports). Keep it secret. - 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:
| Transport | Example | Notes |
|---|---|---|
X-API-Key header | X-API-Key: sk_live_... | Preferred; used by all snippets on this page. |
Authorization: Bearer | Authorization: Bearer sk_live_... | Equivalent — the server accepts either form. |
| WebSocket query param | wss://.../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.
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
| Field | Type | Required | Description |
|---|---|---|---|
| file | file (binary) | yes | The audio file. Missing → HTTP 400 "No audio file uploaded". |
| config | string (JSON) | no | BatchConfig JSON (table below). Omitted → engine:"whisper", model:"whisper-base", all other fields defaulted. Invalid → HTTP 422. |
BatchConfig fields
| Field | Type / default | Consumed by inference? | Description |
|---|---|---|---|
| engine | "whisper" | "vosk" | "openrouter", default "whisper" | yes | Explicit 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. |
| model | string, default "whisper-base" | yes | Model id, must be one of the registered models in /v1/models. Invalid → 422. |
| language | string | null | yes | Optional source language code ("en", "ur", …). null/"auto" → auto-detect. |
| word_timestamps | bool, default true | yes | Include per-word timing in segments[].words. |
| decoding_mode | string, default "balanced" | partially | fast→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_config | object, default {} | yes | Overrides for supported faster-whisper transcribe() options (e.g. beam_size, task, initial_prompt, vad_filter, vad_parameters, temperatures, hotwords). Unknown keys → 422. |
| vosk_config | object, default {} | yes | beam/max_active applied when the VOSK model loads (via conf/model.conf; defaults 10/3000), plus max_alternatives, partial_words. |
| openrouter_config | object, default {} | yes | model selects the OpenRouter audio-transcription model; falls back to the deployment default. |
| filters | object | null | yes | {"profanity": "mask"} masks profanity in the final transcript. |
| match_words | string[] | yes | Terms flagged in the matches/match_counts fields of the transcript. |
| custom_vocabulary | string[], default [] | no — reserved | Declared + validated, not yet wired into inference. |
| sample_rate | int > 0, default 16000 | no — reserved | Declared + validated, not yet wired into inference. |
| punctuate | bool, default true | no — reserved | Declared + validated, not yet wired into inference. |
| diarize | bool, default false | no — reserved | Declared + validated, not yet wired into inference. |
| n_best | int 1–10, default 1 | no — reserved | Declared + validated, not yet wired into inference. |
| detect_language | bool, default true | no — reserved | Declared + validated, not yet wired into inference. |
| webhook_url | string (uri) | null | no — reserved | Declared + 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).
| Parameter | Type | Location | Description |
|---|---|---|---|
| jobId | string (UUID) | path | The 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.
| Parameter | Type | Location | Description |
|---|---|---|---|
| jobId | string (UUID) | path | The 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.
| Parameter | Type | Location | Description |
|---|---|---|---|
| limit | int, default 50 | query | Max 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.
| Parameter | Type | Location | Description |
|---|---|---|---|
| jobId | string (UUID) | path | Job id. Unknown → 404. |
| format | json | srt | vtt, default json | query | Output 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)
}
| Key | Type / default | Description |
|---|---|---|
| m3u8_url | http/https URI | HLS playlist URL. http/https only. Omit to use the raw-PCM binary path. Duplicate URL + model already active → error frame + close 1013. |
| stream_chunk_seconds | float, default 8 | Decode window length. Must be 2–30; invalid → error → close. |
| overlap_seconds | float, default 1 | Overlap between windows. Must satisfy 0 ≤ overlap < chunk. |
| model | string, 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:
| Frame | When | Example 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).
- Server replies
{"type":"info","message":"send binary audio frames or close to finish","model":"..."}. - Send binary s16le PCM frames continuously (e.g. from a microphone or
ffmpeg -f s16lepipe). The server buffers them into the same windows as the m3u8 path (stream_chunk_seconds/overlap_secondsfrom the config) and runs real speech-to-text on each window. - 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. - 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.
| Knob | Default | Scope | Behaviour when exceeded |
|---|---|---|---|
| MAX_STREAMS_PER_KEY | 2 | per key | Concurrent active WS streams. Error frame + close 1013. |
| MAX_JOBS_PER_KEY | 10 | per key | Concurrent QUEUED/RUNNING batch jobs. HTTP 429 "too many pending jobs for this key". |
| MAX_ACTIVE_STREAMS | 4 (deployment: 5) | global | Total active WS streams across all keys. Error frame + close 1013. |
| QUOTA_AUDIO_SECONDS_PER_DAY | 0 (unlimited) | per key | Daily audio-seconds for batch jobs. HTTP 429 + Retry-After seconds until UTC midnight. |
| QUOTA_STREAM_SESSIONS_PER_DAY | 0 (unlimited) | per key | Daily WS stream sessions. Error frame + close 1013. |
| RATE_LIMIT_REQUESTS_PER_MIN | 0 (off) | per key | Per-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 id | Engine | Device | Languages | Tier | Notes |
|---|---|---|---|---|---|
| whisper-tiny | whisper | cpu | multilingual | economy | Preloaded. Greedy decode (beam 1) for CPU speed. |
| whisper-base | whisper | cpu | multilingual | balanced | Preloaded. Greedy decode. |
| whisper-small | whisper | cpu | multilingual | quality | Preloaded. Greedy decode. |
| whisper-small-en | whisper | cpu | (english-only upstream) | — | Registered in schemas; not exposed by this deployment's /v1/models. |
| whisper-base-urdu | whisper | cpu | ur | routed-fast | Urdu-specialized, CPU-friendly int8 build. |
| whisper-small-urdu | whisper | cpu | ur | routed-quality | Urdu-specialized, CPU-friendly int8 build. |
| vosk-model-en-us | vosk | cpu | en | vosk-fast | Loads on first use; beam/max_active via model.conf (defaults 10/3000). |
| vosk-model-ur | vosk | cpu | ur | vosk-fast | Not deployed (available: false). |
| vosk-model-ar | vosk | cpu | ar | vosk-fast | Loads on first use. |
| vosk-model-hi | vosk | cpu | hi | vosk-fast | Loads on first use. |
| qwen/qwen3-asr-0.6b | openrouter | remote | multi | economy | Qwen3 ASR, ~$0.000003/second. Requires server-side provider key. |
| openai/whisper-large-v3-turbo | openrouter | remote | 99+ | economy | Whisper Large V3 Turbo, ~$0.000003/second (budget tier). Requires server-side provider key. |
| nvidia/nemotron-3.5-asr-streaming-multilingual-0.6 | openrouter | remote | multi | fast | Nemotron 3.5 ASR streaming, multilingual, ~$0.000003/second. |
| openai/whisper-large-v3 | openrouter | remote | 99+ | economy | Remote transcription ~$0.0048/min. |
| nvidia/parakeet-tdt-0.6b-v3 | openrouter | remote | 60 | fast | Remote, 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
| Code | Meaning | Detail example |
|---|---|---|
| 400 | Bad request | "No audio file uploaded" · "format must be json, srt, or vtt" |
| 401 | Missing API key | "Missing API key. Send it as 'X-API-Key' header or 'Authorization: Bearer <key>'." |
| 403 | Invalid key / other tenant | "Invalid API key." · "Job belongs to another API key." |
| 404 | Not found | "Job not found" · "Original uploaded audio is no longer available" |
| 409 | Conflict | "Job is already queued" / "Job is already running" (retry) |
| 422 | Unprocessable content | "Invalid batch configuration: ..." · "This older job has no reusable configuration" |
| 429 | Limit exceeded | "rate limit exceeded" · "too many pending jobs for this key" · "daily audio quota exceeded" — with Retry-After header |
WebSocket close codes
| Code | Meaning |
|---|---|
| 1008 | Missing/invalid API key at handshake, or no config message within 20 seconds (idle). |
| 1013 | Capacity/quota: per-key or global stream cap, duplicate URL+model, or daily stream-session quota. An error frame is sent first. |
| 1000 / normal | Clean finish (e.g. after "close" on the binary path); invalid config also closes without a code. |
Changelog
- 2026-09-22 — DOC-2: Developer-facing API documentation portal shipped. Adds
frontend/docs.html(single-file portal: auth, try-it snippet generator, all REST endpoints, WebSocket protocol incl. m3u8 + raw-PCM paths, per-key limits, model catalogue, error/close-code tables);frontend/nginx.conf+frontend/Dockerfileserve it with no-store caching;openapi.yamlcompleted (streams/list + retry paths, per-route 429 audit, BatchConfig engine/vosk_config/openrouter_config, UsageReport verification, servers →https://proscribe.techon.top/api, WS block aligned to code);frontend/index.htmlgains an "API Docs →" link. See the full task board for verification notes. - 2026-09-22 — DEP-4 / ORCH / WRK-1: Reaper crash-test PASSED; quotas/rate-limit knobs restored to safe 0/0 in production.
- 2026-09-15 — OPT-3/DEP-2: CPU economy decoding for tiny/base/small; 14 models live.