⚡ ElixirASR Developer Guide

ElixirASR is a medical audio intelligence API. Upload recordings, get back structured transcripts with speaker diarization, and generate clinical notes via LLM templates — all from a single API call.

v1.6.0Current version
Base URLthis deployment's host
AuthNone (secure at gateway)
FormatsMP3 · WAV · M4A · WebM · OGG · FLAC
Async vs Sync — Which to Use?
ElixirASR has three transcription modes. Pick the right one for your use case.
📤
Upload File
→
🆔
Get job_id
→
⏳
Poll Status
→
📥
Download Result
ModeEndpointBest ForLimit
Async Batch POST /process-audio/ Long recordings, diarization + LLM, production pipelines No hard limit
Sync POST /transcribe Short clips (<15 min), quick transcript + optional LLM prompt, no polling needed 15 minutes audio
Live WebSocket WS /ws/transcribe Real-time recording from browser/app, streaming partial results Session-based

Batch Processing
Submit an audio file for full async processing — transcription, speaker diarization, and LLM output.
POST /process-audio/ Submit audio for async processing → returns job_id
When to use this Long recordings, clinical sessions, production pipelines. Returns a job_id immediately — poll for results.
Request Body — multipart/form-data
FieldTypeDescription
audio_filefilerequiredAudio file. Accepted: MP3 · WAV · M4A · WebM · OGG · FLAC · AAC · OPUS
num_speakersintegerrequiredNumber of distinct speakers in the recording (1–20)
output_templatestringrequiredTemplate filename (e.g. soap_note.json). Use GET /list-templates/ to see available options.
client_namestringoptionalClient identifier. Default: mirketa. Organises results under ElixirASR/{client_name}/ in Cloud Storage.
beam_sizeintegeroptionalWhisper beam width. Default: 3. Range 1–10. Lower = faster, higher = more accurate.
vad_filterbooleanoptionalPre-filter silence before transcription. Default: true. Speeds up by 20–40% with no quality loss.
enable_diarizationbooleanoptionalEnable speaker diarization (GPU only). Default: true. Set false for plain transcript.
voice_samplesfile[]optionalReference audio clips, one per named speaker. Used for speaker identity matching.
voice_sample_namesstring[]optionalNames matching each voice sample file (e.g. "Dr. Smith", "Patient").
import requests

BASE = "https://your-elixirasr-host"

with open("consultation.mp3", "rb") as f:
    resp = requests.post(
        f"{BASE}/process-audio/",
        data={
            "num_speakers": 2,
            "output_template": "soap_note.json",
            "client_name": "acme_clinic",
        },
        files={"audio_file": ("consultation.mp3", f, "audio/mpeg")},
    )

job = resp.json()
job_id = job["job_id"]
print(f"Queued: {job_id}")
const submitAudio = async (file) => {
  const form = new FormData();
  form.append("audio_file", file);
  form.append("num_speakers", "2");
  form.append("output_template", "soap_note.json");
  form.append("client_name", "acme_clinic");

  const resp = await fetch("/process-audio/", {
    method: "POST",
    body: form,
  });
  const { job_id } = await resp.json();
  console.log("Queued:", job_id);
  return job_id;
};
curl -X POST https://your-elixirasr-host/process-audio/ \
  -F "audio_file=@consultation.mp3" \
  -F "num_speakers=2" \
  -F "output_template=soap_note.json" \
  -F "client_name=acme_clinic"
Response — 202 Accepted
{ "job_id": "a3f9b2c1-7e4d-4a8f-b1c2-9d0e5f6a7b8c", "status": "queued", "message": "Job accepted. Poll GET /jobs/{job_id} for status." }
GET /jobs/{job_id} Poll job status
Poll this endpoint after submitting to /process-audio/. Status transitions: queued → processing → completed | failed. Recommended poll interval: 3–5 seconds.
import time

def wait_for_job(job_id):
    while True:
        r = requests.get(f"{BASE}/jobs/{job_id}")
        job = r.json()
        if job["status"] == "completed":
            return job
        if job["status"] == "failed":
            raise RuntimeError(job["error"])
        print(f"Status: {job['status']}")
        time.sleep(3)
async function pollJob(jobId) {
  while (true) {
    const resp = await fetch(`/jobs/${jobId}`);
    const job = await resp.json();
    if (job.status === "completed") return job;
    if (job.status === "failed") throw new Error(job.error);
    await new Promise(r => setTimeout(r, 3000));
  }
}
curl https://your-elixirasr-host/jobs/a3f9b2c1-...
Response
{ "job_id": "a3f9b2c1-...", "status": "completed", "created_at": "2025-07-09T10:22:01Z", "started_at": "2025-07-09T10:22:02Z", "completed_at": "2025-07-09T10:22:45Z", "result_blob_path": "ElixirASR/acme_clinic/Results/upload_xxx.json" }
GET /jobs/{job_id}/result Download full result JSON
Returns the full result once the job is completed. Contains the transcript array, speaker labels, and the LLM-generated structured output.
Response
{ "job_id": "a3f9b2c1-...", "transcript": [ { "start": 0.0, "end": 4.2, "text": "Good morning, how are you feeling today?", "speaker": "SPEAKER_00" }, { "start": 4.5, "end": 9.1, "text": "I've had a headache for three days.", "speaker": "SPEAKER_01" } ], "custom_output": { "Subjective": { "Chief Complaint": "Headache for 3 days", "HPI": "..." }, "Objective": { ... }, "Assessment": "...", "Plan": "..." } }
GET /jobs/{job_id}/logs/stream Live log stream via SSE
Server-Sent Events stream. Opens a live log view of the job's pipeline steps. Each message is a JSON-encoded log line. A final done event fires when the job completes or fails.
const es = new EventSource(`/jobs/${jobId}/logs/stream`);

es.onmessage = e => {
  console.log(JSON.parse(e.data));   // log line string
};

es.addEventListener("done", e => {
  console.log("Finished:", JSON.parse(e.data));  // "completed" | "failed"
  es.close();
});
import sseclient, requests

url = f"{BASE}/jobs/{job_id}/logs/stream"
with requests.get(url, stream=True) as r:
    client = sseclient.SSEClient(r)
    for event in client.events():
        if event.event == "done":
            break
        print(event.data)

Synchronous Transcription
Returns the full transcript in a single blocking response — no polling. A job is created automatically for audit purposes and is visible in the Job Monitor.
POST /transcribe Transcribe with optional LLM prompt — one-shot, 15 min limit
When to use this Audio clips under 15 minutes where you want results immediately. Prompt is optional — omit it for a plain transcript with no LLM call.
⚠️ Hard limit: 15 minutes audio. For longer recordings use POST /process-audio/ instead.
Request Body — multipart/form-data
FieldTypeDescription
audio_filefilerequiredAudio file ≤ 15 minutes
promptstringoptionalLLM prompt. Use {transcript} as placeholder. Omit to skip LLM and return transcript only.
timestampedbooleanoptionalDefault: true. When true, transcript is a list of segments with start/end times. When false, returns a single plain string.
client_namestringoptionalClient identifier. Default: mirketa. Used for Cloud Storage result path and Job Monitor grouping.
modelstringoptionalWhisper model. Allowed: tiny.en, base.en, small.en, small. Default: base.en
languagestringoptionalForce language detection. E.g. en, fr. Leave empty for auto-detect.
beam_sizeintegeroptionalBeam width 1–10. Default: 3
vad_filterbooleanoptionalSilence filtering. Default: true
# Plain transcript (no LLM)
with open("clip.mp3", "rb") as f:
    resp = requests.post(
        f"{BASE}/transcribe",
        data={"model": "base.en", "timestamped": "true"},
        files={"audio_file": ("clip.mp3", f, "audio/mpeg")},
    )
result = resp.json()
print(result["job_id"])       # audit trail in Job Monitor
print(result["transcript"])    # list of {start, end, text} segments

# With LLM prompt
PROMPT = """Summarise the following consultation in 3 bullet points:

{transcript}"""

with open("clip.mp3", "rb") as f:
    resp = requests.post(
        f"{BASE}/transcribe",
        data={"prompt": PROMPT, "model": "base.en"},
        files={"audio_file": ("clip.mp3", f, "audio/mpeg")},
    )
result = resp.json()
print(result["prompt_output"])  # structured JSON from LLM
const form = new FormData();
form.append("audio_file", audioBlob, "clip.mp3");
form.append("model", "base.en");
// optionally add prompt:
// form.append("prompt", "Summarise: {transcript}");

const resp = await fetch("/transcribe", { method: "POST", body: form });
const { job_id, transcript, prompt_output } = await resp.json();
console.log("Job:", job_id);
console.log("Transcript:", transcript);
# Plain transcript
curl -X POST https://your-elixirasr-host/transcribe \
  -F "audio_file=@clip.mp3" \
  -F "model=base.en"

# With LLM prompt
curl -X POST https://your-elixirasr-host/transcribe \
  -F "audio_file=@clip.mp3" \
  -F 'prompt=Summarise the consultation: {transcript}' \
  -F "model=base.en"
Response
{ "job_id": "b7e2f1a3-...", "model_used": "base.en", "language_detected": "en", "duration_seconds": 312.4, "diarization_enabled": false, "transcript": [ { "start": 0.0, "end": 3.8, "text": "Good morning, how are you feeling today?" }, { "start": 4.1, "end": 8.5, "text": "I've had a headache for three days." } ], "prompt_output": null }

Live WebSocket Transcription
Stream raw audio bytes from the browser microphone and receive partial transcripts in real time.
WS /ws/transcribe Real-time streaming transcription
When to use this Browser-based recording apps, voice interfaces, real-time note-taking. The server streams partial transcripts back as you speak, then sends the full result when you stop.
💡 Partial transcripts arrive roughly every LIVE_PARTIAL_INTERVAL_SECONDS (default 2s) and each one is a fresh re-transcription of only the last LIVE_PARTIAL_WINDOW_SECONDS (default 8s) of audio — a rolling window, not a running total. Expect recent words to restate or slightly change as more audio arrives; don't concatenate partial messages. The single transcript message sent after the stop sentinel covers the complete session at full quality and is the authoritative result.
Query Parameters
ParamTypeDescription
num_speakersintegeroptionalDefault: 2
output_templatestringoptionalTemplate filename. Default: soap_note.json
job_idstringoptionalUUID for the session. Auto-generated if omitted.
client_namestringoptionalClient identifier. Default: default
// Connect — auto-select wss:// on HTTPS, ws:// on HTTP
const proto = location.protocol === "https:" ? "wss:" : "ws:";
const ws = new WebSocket(
  `${proto}//${location.host}/ws/transcribe?num_speakers=2&output_template=soap_note.json`
);
ws.binaryType = "arraybuffer";

// Stream microphone audio
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(stream, { mimeType: "audio/webm;codecs=opus" });
recorder.ondataavailable = e => {
  if (e.data.size > 0) e.data.arrayBuffer().then(buf => ws.send(buf));
};
recorder.start(250);   // 250ms chunks

// Receive partial / final results
ws.onmessage = e => {
  const data = JSON.parse(e.data);
  if (data.partial)    console.log("Partial:", data.partial);
  if (data.transcript) console.log("Final:", data);   // full result
};

// Stop: send 1-byte sentinel, then wait for final result
const stop = () => {
  recorder.onstop = () => ws.send(new Uint8Array(1));
  recorder.stop();
};

Templates
Templates are JSON files that define the LLM prompt and output structure for a specific use case (SOAP notes, referral letters, summaries, etc.).
GET /list-templates/
Returns a list of template filenames available in Cloud Storage.
templates = requests.get(f"{BASE}/list-templates/").json()
# ["soap_note.json", "referral_letter.json", ...]
curl https://your-elixirasr-host/list-templates/
POST /add-template/
Upload a new template or overwrite an existing one. The filename is derived from the name field.
💡 Use {transcript} in your prompt — it's replaced with the full transcript text before the LLM call.
requests.post(f"{BASE}/add-template/", json={
    "name": "referral_letter",
    "prompt": "Write a specialist referral letter based on:\n\n{transcript}",
})
curl -X POST https://your-elixirasr-host/add-template/ \
  -H "Content-Type: application/json" \
  -d '{"name":"referral_letter","prompt":"Write a referral letter for:\n{transcript}"}'

Results & Client Management
GET /list-results/{client_name}
Lists all result filenames for a client from Cloud Storage. Omit client_name to use GET /list-results/ (defaults to mirketa).
files = requests.get(f"{BASE}/list-results/acme_clinic").json()
# ["upload_abc123.json", "upload_def456.json"]
POST /setup-client-storage/{client_name}
Provisions folder structure for a new client in Cloud Storage. Safe to call multiple times — idempotent. Creates: Uploads/, Results/, templates/.
requests.post(f"{BASE}/setup-client-storage/acme_clinic")
GET /version
Returns the running image version and runtime configuration.
Response
{ "version": "1.6.0", "batch_model": "base", "transcribe_model": "base.en", "live_model": "base.en", "gpu": false, "beam_size": 3, "vad_filter": true, "concurrency": 3 }