Use cases & integrationsUse case guides

Best Practices for Building Medical Scribes

Introduction

Building a robust medical scribe requires careful consideration of accuracy, latency, speaker identification, and real-time capabilities while maintaining HIPAA compliance and clinical documentation standards. This guide addresses common questions and provides practical solutions for both post-visit and live encounter transcription scenarios.

Why AssemblyAI for Medical Scribes?

AssemblyAI stands out as the premier choice for medical scribes with several key advantages:

Industry-Leading Accuracy with Pre-recorded Audio

  • Universal-3-Pro model delivers exceptional accuracy for medical terminology and clinical documentation
  • 2.9% speaker diarization error rate for precise attribution between provider and patient
  • Comprehensive LLM Gateway integration for intelligent post-processing into structured clinical notes

Streaming with Universal-3 Pro

As medical scribes evolve toward real-time documentation, AssemblyAI’s Universal-3 Pro Streaming model (u3-rt-pro) offers significant benefits:

  • Ultra-low latency (~300ms) enables live transcription during patient encounters
  • Format turns feature provides structured, speaker-aware output in real-time
  • Keyterms prompt allows providing medical context and patient history to improve accuracy
  • Medical mode (domain: "medical-v1") for improved medical terminology accuracy

End-to-End Voice AI Platform

Unlike fragmented solutions, AssemblyAI provides a unified API for:

  • Transcription with speaker diarization (provider vs. patient)
  • Medical terminology recognition and contextual understanding
  • HIPAA-compliant PII redaction on both text and audio
  • Post-processing workflows with LLM Gateway - from SOAP notes to completely custom clinical documentation
  • Streaming and pre-recorded transcription in a single platform
  • Compliance and Security built for medical workloads (BAA, HIPAA, DPA, etc.)

When Should I Use Pre-recorded vs Streaming for Medical Scribes?

Understanding when to use pre-recorded versus streaming is critical for clinical workflows.

Use Pre-recorded (Universal-3-Pro) when:

Post-visit documentation - Encounter already happened, need highest accuracy

  • Maximum accuracy required - Universal-3-Pro has highest medical terminology accuracy
  • Complex medical terminology - Rare medications, genetic conditions, specialized procedures
  • HIPAA compliance critical - Full PII redaction with audio de-identification
  • Structured note generation - SOAP notes, H&P, discharge summaries via LLM Gateway
  • Quality assurance - Review and editing workflow needed
  • Specialty documentation - Oncology, cardiology, neurology with complex terminology
  • Speaker diarization needed - Automatic provider vs. patient separation

Best for: Post-visit SOAP notes, specialist consultations, hospital discharge summaries, quality review

Use Streaming (Universal-3 Pro Streaming) When:

Live encounter documentation - Real-time transcription during patient visit

  • Immediate documentation - No delay between encounter and note
  • Telemedicine visits - Document while seeing patient virtually
  • Emergency department - Fast-paced, immediate documentation needed
  • Primary care visits - Standard encounters with common terminology
  • Real-time review - Provider can review and correct during visit
  • Ambient documentation - Microphone running throughout encounter

Best for: Telemedicine, primary care visits, ED encounters, real-time clinical decision support

Many medical scribes use both:

  1. Streaming during visit - Real-time documentation, immediate review by provider
  2. Universal-3-Pro post-processing - Run audio through Universal-3-Pro after visit for:
    • Highest accuracy verification
    • Complex terminology correction
    • Complete HIPAA compliance workflow
    • Final structured note generation
    • Speaker diarization (provider vs. patient)

Example workflow:

  • Provider sees patient → Streaming captures real-time notes
  • Visit ends → Audio sent to Universal-3-Pro for final high-accuracy transcription
  • LLM Gateway generates structured SOAP note from high-accuracy transcript
  • Provider reviews and signs final note

This gives real-time utility during visits while ensuring maximum accuracy for official documentation.

What Languages and Features for a Medical Scribe?

Pre-Recorded doctor patient visits (Universal-3-Pro)

Languages: For post-visit documentation, Universal-3-Pro supports English for the highest accuracy transcription. If you want to use other languages, Universal-2 is a suitable alternative.

Core Features:

  • Speaker diarization (provider-patient separation)
  • Multichannel audio support — when provider and patient are on separate audio channels, enables perfect speaker separation without diarization
  • Automatic formatting, punctuation, and capitalization
  • Keyterms Prompting for medical specialties and conditions (up to 1000 terms for Universal-3-Pro)
  • Ability to prompt on related medical terms and improve the accuracy of others (for example, ibuprofen improving naproxen)
  • Natural language prompting (Universal-3-Pro) — up to 1,500 words to guide transcription behavior

Speech Understanding Models:

  • Entity detection for medications, conditions, and procedures
  • Sentiment analysis for patient experience insights
  • Speaker identification for separating doctor and patient in a visit

Guardrails:

  • PII redaction on text and audio for HIPAA compliance

Real-Time Streaming (Universal-3 Pro Streaming)

Languages: For live encounter transcription, Universal-3 Pro Streaming (u3-rt-pro) supports English, optimized for medical contexts with the highest streaming accuracy. Use with domain: "medical-v1" for improved medical terminology recognition.

Streaming-Specific Features:

  • Partial and final transcripts for responsive documentation
  • Format turns for structured provider-patient dialogue
  • Keyterms Prompt for patient history and current medications (up to 1000 terms)
  • Natural language prompting (up to 1,500 words) for guiding transcription behavior
  • Turn detection with configurable silence thresholds for natural clinical conversation flow
  • Mid-session configuration updates via UpdateConfiguration messages — dynamically update keyterms and prompt mid-session. Use keyterms for known context like prescription medications and disease names, and use the prompt for unknown context like “this is a doctor-patient visit in a cardiology clinic”
  • Post-processing LLM Gateway integration for increasing medical accuracy

Recommended approach: Use streaming for real-time documentation, then run through Universal-3-Pro post-visit for accurate speaker-labeled final notes.

For full details, see the Universal-3 Pro Streaming documentation.

How Can I Get Started Building a Post-Visit Medical Scribe?

Here’s a complete example implementing pre-recorded transcription with Universal-3-Pro:

1import assemblyai as aai
2import asyncio
3from typing import Dict, List
4from assemblyai.types import (
5 PIIRedactionPolicy,
6 PIISubstitutionPolicy,
7)
8
9# Configure API key
10aai.settings.api_key = "your_api_key_here"
11
12async def transcribe_encounter_async(audio_source: str) -> Dict:
13 """
14 Asynchronously transcribe a medical encounter with Universal-3-Pro
15
16 Args:
17 audio_source: Either a local file path or publicly accessible URL
18 """
19 # Configure comprehensive medical transcription
20 config = aai.TranscriptionConfig(
21 speech_models=["universal-3-pro", "universal-2"],
22 domain="medical-v1",
23 language_detection=True,
24
25 # Diarize provider and patient
26 speaker_labels=True,
27 speakers_expected=2, # Typically provider and patient
28
29 # Punctuation and Formatting
30 punctuate=True,
31 format_text=True,
32
33 # Boost accuracy of medical terminology
34 keyterms_prompt=[
35 # Patient-specific context
36 "hypertension", "diabetes mellitus type 2", "metformin",
37
38 # Specialty-specific terms
39 "auscultation", "palpation", "differential diagnosis",
40 "chief complaint", "review of systems", "physical examination",
41
42 # Common medications
43 "lisinopril", "atorvastatin", "levothyroxine",
44
45 # Procedure terms
46 "electrocardiogram", "complete blood count", "hemoglobin A1c"
47 ],
48
49 # Speech understanding for medical documentation
50 entity_detection=True, # Extract medications, conditions, procedures
51 redact_pii=True, # HIPAA compliance
52 redact_pii_policies=[
53 PIIRedactionPolicy.person_name,
54 PIIRedactionPolicy.date_of_birth,
55 PIIRedactionPolicy.phone_number,
56 PIIRedactionPolicy.email_address,
57 ],
58 redact_pii_sub=PIISubstitutionPolicy.hash,
59 redact_pii_audio=True # Create HIPAA-compliant audio
60 )
61
62 # Create async transcriber
63 transcriber = aai.Transcriber()
64
65 try:
66 # Submit transcription job - works with both file paths and URLs
67 transcript = await asyncio.to_thread(
68 transcriber.transcribe,
69 audio_source,
70 config=config
71 )
72
73 # Check status
74 if transcript.status == aai.TranscriptStatus.error:
75 raise Exception(f"Transcription failed: {transcript.error}")
76
77 # Process speaker-labeled utterances
78 print("\n=== PROVIDER-PATIENT DIALOGUE ===\n")
79
80 for utterance in transcript.utterances:
81 # Format timestamp
82 start_time = utterance.start / 1000 # Convert to seconds
83 end_time = utterance.end / 1000
84
85 # Identify speaker role
86 speaker_label = "Provider" if utterance.speaker == "A" else "Patient"
87
88 # Print formatted utterance
89 print(f"[{start_time:.1f}s - {end_time:.1f}s] {speaker_label}:")
90 print(f" {utterance.text}")
91 print(f" Confidence: {utterance.confidence:.2%}\n")
92
93 # Extract clinical entities
94 if transcript.entities:
95 print("\n=== CLINICAL ENTITIES DETECTED ===\n")
96 medications = [e for e in transcript.entities if e.entity_type == "drug"]
97 conditions = [e for e in transcript.entities if e.entity_type == "medical_condition"]
98 procedures = [e for e in transcript.entities if e.entity_type == "medical_process"]
99
100 if medications:
101 print("Medications:", ", ".join([m.text for m in medications]))
102 if conditions:
103 print("Conditions:", ", ".join([c.text for c in conditions]))
104 if procedures:
105 print("Procedures:", ", ".join([p.text for p in procedures]))
106
107 return {
108 "transcript": transcript,
109 "utterances": transcript.utterances,
110 "entities": transcript.entities,
111 "redacted_audio_url": transcript.redacted_audio_url
112 }
113
114 except Exception as e:
115 print(f"Error during transcription: {e}")
116 raise
117
118async def main():
119 """
120 Example usage for medical encounter
121 """
122 # Can use either local file or URL
123 audio_source = "path/to/patient_encounter.mp3" # Or use URL
124 # audio_source = "https://your-secure-storage.com/encounter.mp3"
125
126 try:
127 result = await transcribe_encounter_async(audio_source)
128
129 # Additional processing
130 print(f"\nEncounter duration: {result['transcript'].audio_duration} seconds")
131
132 # Could send to LLM Gateway for SOAP note generation here
133
134 except Exception as e:
135 print(f"Failed to process encounter: {e}")
136
137if __name__ == "__main__":
138 asyncio.run(main())

How Can I Get Started Building a Real-Time Medical Scribe?

Here’s a complete example for real-time streaming transcription with LLM post-processing:

1import os
2import json
3import time
4import threading
5from datetime import datetime
6from urllib.parse import urlencode
7
8import pyaudio
9import websocket
10import requests
11from dotenv import load_dotenv
12from simple_term_menu import TerminalMenu
13
14# Load environment variables from .env if present
15try:
16 load_dotenv()
17except Exception:
18 pass
19
20"""
21Medical Scribe – Streaming STT + LLM Gateway Enhancement (SOAP-ready)
22
23What this does
24--------------
251) Streams mic audio to AssemblyAI Streaming STT (with formatted turns + keyterms)
262) On every utterance or formatted final turn, calls AssemblyAI LLM Gateway to
27 apply *medical* edits (terminology, punctuation, proper nouns, etc.)
283) Logs encounter turns and generates a SOAP note at session end via the Gateway
29
30Quick start
31-----------
32export ASSEMBLYAI_API_KEY=your_key
33# Optional: pick a Gateway model (defaults to Claude 3.5 Haiku)
34export LLM_GATEWAY_MODEL=claude-3-5-haiku-20241022
35
36python medical_scribe_llm_gateway.py
37"""
38
39# === Config ===
40ASSEMBLYAI_API_KEY = os.environ.get("ASSEMBLYAI_API_KEY", "your_api_key_here")
41
42# Medical context and terminology (seed – you can swap at runtime)
43MEDICAL_KEYTERMS = [
44 "hypertension",
45 "diabetes mellitus",
46 "coronary artery disease",
47 "metformin 1000mg",
48 "lisinopril 10mg",
49 "atorvastatin 20mg",
50 "chief complaint",
51 "history of present illness",
52 "review of systems",
53 "physical examination",
54 "assessment and plan",
55 "auscultation",
56 "palpation",
57 "reflexes",
58 "range of motion",
59]
60
61# WebSocket / STT parameters - CONSERVATIVE SETTINGS FOR MEDICAL
62CONNECTION_PARAMS = {
63 "sample_rate": 16000,
64 "speech_model": "u3-rt-pro",
65 "domain": "medical-v1",
66 "format_turns": True, # Always true for readable clinical notes
67
68 # MEDICAL SCRIBE CONFIGURATION - Conservative for clinical accuracy
69 # Medical conversations have LONG pauses (provider thinking, examining patient, reviewing charts)
70 # u3-rt-pro defaults: min_turn_silence=100ms, max_turn_silence=1000ms
71 "min_turn_silence": 800, # Wait much longer (vs ~100ms for voice agents, 560ms for meetings)
72 "max_turn_silence": 2000, # Longer for clinical thinking pauses
73
74 "keyterms_prompt": MEDICAL_KEYTERMS,
75}
76
77API_ENDPOINT_BASE_URL = "wss://streaming.assemblyai.com/v3/ws"
78API_ENDPOINT = f"{API_ENDPOINT_BASE_URL}?{urlencode(CONNECTION_PARAMS, doseq=True)}"
79
80# Audio config
81FRAMES_PER_BUFFER = 800 # 50ms @ 16kHz
82SAMPLE_RATE = CONNECTION_PARAMS["sample_rate"]
83CHANNELS = 1
84FORMAT = pyaudio.paInt16
85
86# Globals
87audio = None
88stream = None
89ws_app = None
90audio_thread = None
91stop_event = threading.Event()
92encounter_buffer = [] # list of dicts with turn data
93last_processed_turn = None
94
95# === Model selection ===
96AVAILABLE_MODELS = [
97 {"id": "claude-3-haiku-20240307", "name": "Claude 3 Haiku", "description": "Fastest Claude, good for simple tasks"},
98 {"id": "claude-3-5-haiku-20241022", "name": "Claude 3.5 Haiku", "description": "Fast with better reasoning"},
99 {"id": "claude-sonnet-4-20250514", "name": "Claude Sonnet 4", "description": "Balanced speed & intelligence"},
100 {"id": "claude-sonnet-4-5-20250929", "name": "Claude Sonnet 4.5", "description": "Best for coding & agents"},
101 {"id": "claude-opus-4-20250514", "name": "Claude Opus 4", "description": "Most powerful, deep reasoning"},
102]
103
104def select_model():
105 menu_entries = [f"{m['name']} - {m['description']}" for m in AVAILABLE_MODELS]
106 terminal_menu = TerminalMenu(
107 menu_entries,
108 title="Select a model (Use ↑↓ arrows, Enter to select):",
109 menu_cursor="❯ ",
110 menu_cursor_style=("fg_cyan", "bold"),
111 menu_highlight_style=("bg_cyan", "fg_black"),
112 cycle_cursor=True,
113 clear_screen=False,
114 show_search_hint=True,
115 )
116 idx = terminal_menu.show()
117 if idx is None:
118 print("Model selection cancelled. Exiting...")
119 raise SystemExit(0)
120 return AVAILABLE_MODELS[idx]["id"]
121
122selected_model = None
123
124# === Gateway helpers ===
125
126def _gateway_chat(messages, max_tokens=800, temperature=0.2, retries=3, backoff=0.75):
127 """Call AssemblyAI LLM Gateway with debug logging and retry."""
128 url = "https://llm-gateway.assemblyai.com/v1/chat/completions"
129 headers = {
130 "Authorization": ASSEMBLYAI_API_KEY,
131 "Content-Type": "application/json",
132 }
133 payload = {
134 "model": selected_model,
135 "messages": messages,
136 "max_tokens": max_tokens,
137 "temperature": temperature,
138 }
139
140 last = None
141 for attempt in range(retries):
142 try:
143 print(f"[LLM] POST {url} (model={selected_model}, attempt {attempt+1}/{retries})")
144 resp = requests.post(url, headers=headers, json=payload, timeout=60)
145 print(f"[LLM] ← status {resp.status_code}, bytes {len(resp.content)}")
146 last = resp
147 except Exception as e:
148 if attempt == retries - 1:
149 raise RuntimeError(f"Gateway request error: {e}")
150 time.sleep(backoff * (attempt + 1))
151 continue
152
153 if resp.status_code == 200:
154 data = resp.json()
155 if not data.get("choices") or not data["choices"][0].get("message"):
156 raise RuntimeError(f"Gateway OK but empty body: {str(data)[:200]}")
157 return data
158 if resp.status_code in (429, 500, 502, 503, 504):
159 print(f"[LLM RETRY] {resp.status_code}: {resp.text[:180]}")
160 time.sleep(backoff * (attempt + 1))
161 continue
162 raise RuntimeError(f"Gateway error {resp.status_code}: {resp.text[:300]}")
163
164 raise RuntimeError(
165 f"Gateway failed after retries. Last={getattr(last,'status_code','n/a')} {getattr(last,'text','')[:180]}"
166 )
167
168
169def post_process_with_llm(text: str) -> str:
170 """Medical editing & normalization using LLM Gateway."""
171 system = {
172 "role": "system",
173 "content": (
174 "You are a clinical transcription editor. Keep the speaker's words, "
175 "fix medical terminology (drug names, dosages, anatomy), proper nouns, "
176 "and punctuation for readability. Preserve meaning and avoid inventing "
177 "details. Prefer U.S. clinical style. If a medication or condition is "
178 "phonetically close, correct to the most likely clinical term."
179 ),
180 }
181
182 user = {
183 "role": "user",
184 "content": (
185 "Context keyterms (JSON array):\n" + json.dumps(MEDICAL_KEYTERMS) + "\n\n"
186 "Edit this short transcript for medical accuracy and readability.\n\n"
187 f"Transcript:\n{text}"
188 ),
189 }
190
191 try:
192 res = _gateway_chat([system, user], max_tokens=600)
193 return res["choices"][0]["message"]["content"].strip()
194 except Exception as e:
195 print(f"[LLM EDIT ERROR] {e}. Falling back to original.")
196 return text
197
198
199def generate_clinical_note():
200 """Create a SOAP note from the encounter buffer via Gateway."""
201 if not encounter_buffer:
202 print("No encounter data to summarize.")
203 return
204
205 print("\n=== GENERATING CLINICAL DOCUMENTATION (SOAP) ===")
206 # Build a compact transcript string for the LLM
207 lines = []
208 for e in encounter_buffer:
209 if e.get("type") == "utterance":
210 lines.append(f"[{e['timestamp']}] {e.get('speaker', 'Speaker')}: {e['text']}")
211 elif e.get("type") == "final":
212 lines.append(f"[{e['timestamp']}] FINAL: {e['text']}")
213 combined = "\n".join(lines)
214
215 system = {
216 "role": "system",
217 "content": (
218 "You are a clinician generating concise, structured notes. "
219 "Produce a SOAP note (Subjective, Objective, Assessment, Plan). "
220 "Use bullet points, keep it factual, infer reasonable clinical semantics "
221 "from the transcript but do NOT invent data. Include medications with dosage "
222 "and frequency if mentioned."
223 ),
224 }
225 user = {
226 "role": "user",
227 "content": (
228 "Create a SOAP note from this clinical encounter transcript.\n\n"
229 f"Transcript:\n{combined}\n\n"
230 "Format strictly as:\n"
231 "Subjective:\n- ...\n\nObjective:\n- ...\n\nAssessment:\n- ...\n\nPlan:\n- ...\n"
232 ),
233 }
234
235 try:
236 res = _gateway_chat([system, user], max_tokens=1200)
237 soap = res["choices"][0]["message"]["content"].strip()
238 fname = f"clinical_note_soap_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
239 with open(fname, "w", encoding="utf-8") as f:
240 f.write(soap)
241 print(f"SOAP note saved: {fname}")
242 except Exception as e:
243 print(f"[SOAP ERROR] {e}")
244
245
246# === WebSocket callbacks ===
247
248def on_open(ws):
249 print("=" * 80)
250 print(f"[{datetime.now().strftime('%H:%M:%S')}] Medical transcription started")
251 print(f"Connected to: {API_ENDPOINT_BASE_URL}")
252 print(f"Gateway model: {selected_model}")
253 print("=" * 80)
254 print("\nSpeak to begin. Press Ctrl+C to stop.\n")
255
256 def stream_audio():
257 global stream
258 while not stop_event.is_set():
259 try:
260 audio_data = stream.read(FRAMES_PER_BUFFER, exception_on_overflow=False)
261 ws.send(audio_data, websocket.ABNF.OPCODE_BINARY)
262 except Exception as e:
263 if not stop_event.is_set():
264 print(f"Error streaming audio: {e}")
265 break
266
267 global audio_thread
268 audio_thread = threading.Thread(target=stream_audio, daemon=True)
269 audio_thread.start()
270
271
272def on_message(ws, message):
273 global last_processed_turn
274 try:
275 data = json.loads(message)
276 msg_type = data.get("type")
277
278 if msg_type == "Begin":
279 print(f"[SESSION] Started - ID: {data.get('id','N/A')}\n")
280
281 elif msg_type == "Turn":
282 end_of_turn = data.get("end_of_turn", False)
283 turn_is_formatted = data.get("turn_is_formatted", False)
284 transcript = data.get("transcript", "")
285 utterance = data.get("utterance", "")
286 turn_order = data.get("turn_order", 0)
287 eot_conf = data.get("end_of_turn_confidence", 0.0)
288
289 # live partials
290 if not end_of_turn and transcript:
291 print(f"\r[PARTIAL] {transcript[:120]}...", end="", flush=True)
292
293 # If AssemblyAI has finalized a turn AND it's formatted, LLM-edit the transcript
294 if end_of_turn and transcript:
295 if not turn_is_formatted:
296 # Explicitly skip unformatted finals
297 print("[DEBUG] EOT received (unformatted) – skipping LLM edit, waiting for formatted final…")
298 elif turn_is_formatted:
299 if last_processed_turn == turn_order:
300 return # avoid duplicate processing
301 last_processed_turn = turn_order
302
303 ts = datetime.now().strftime('%H:%M:%S')
304 print("\n[DEBUG] EOT received (formatted). Calling LLM…")
305 edited = post_process_with_llm(transcript)
306
307 changed = "(edited)" if edited.strip() != transcript.strip() else "(no change)"
308 print(f"\n[{ts}] [FINAL {changed}]")
309 print(f" ├─ Original STT : {transcript}")
310 print(f" └─ Edited by LLM: {edited}")
311 print(f"Turn: {turn_order} | Confidence: {eot_conf:.2%}")
312
313 encounter_buffer.append({
314 "timestamp": ts,
315 "text": edited,
316 "original_text": transcript,
317 "turn_order": turn_order,
318 "confidence": eot_conf,
319 "type": "final",
320 })
321
322 # If we also get per-utterance chunks, just log them raw (no LLM) for timeline
323 if utterance:
324 ts = datetime.now().strftime('%H:%M:%S')
325
326 low = utterance.lower()
327 if any(t in low for t in ["medication", "prescribe", "dosage", "mg", "daily"]):
328 print(" 💊 MEDICATION MENTIONED")
329 if any(t in low for t in ["pain", "symptom", "complaint", "problem"]):
330 print(" 🏥 SYMPTOM REPORTED")
331 if any(t in low for t in ["diagnose", "assessment", "impression"]):
332 print(" 📋 DIAGNOSIS DISCUSSED")
333
334 encounter_buffer.append({
335 "timestamp": ts,
336 "text": utterance,
337 "original_text": utterance,
338 "turn_order": turn_order,
339 "confidence": eot_conf,
340 "type": "utterance",
341 })
342 print()
343
344 elif msg_type == "Termination":
345 dur = data.get("audio_duration_seconds", 0)
346 print(f"\n[SESSION] Terminated – Duration: {dur}s")
347 save_encounter_transcript()
348 generate_clinical_note()
349
350 elif msg_type == "Error":
351 print(f"\n[ERROR] {data.get('error', 'Unknown error')}")
352
353 except json.JSONDecodeError as e:
354 print(f"Error decoding message: {e}")
355 except Exception as e:
356 print(f"Error handling message: {e}")
357
358
359def on_error(ws, error):
360 print(f"\n[WEBSOCKET ERROR] {error}")
361 stop_event.set()
362
363
364def on_close(ws, close_status_code, close_msg):
365 print(f"\n[WEBSOCKET] Disconnected – Status: {close_status_code}")
366 global stream, audio
367 stop_event.set()
368
369 if stream:
370 if stream.is_active():
371 stream.stop_stream()
372 stream.close()
373 stream = None
374 if audio:
375 audio.terminate()
376 audio = None
377 if audio_thread and audio_thread.is_alive():
378 audio_thread.join(timeout=1.0)
379
380
381# === Persist artifacts ===
382
383def save_encounter_transcript():
384 if not encounter_buffer:
385 print("No encounter data to save.")
386 return
387
388 fname = f"encounter_transcript_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
389 with open(fname, "w", encoding="utf-8") as f:
390 f.write("Clinical Encounter Transcript\n")
391 f.write(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
392 f.write("=" * 80 + "\n\n")
393 for e in encounter_buffer:
394 if e.get("speaker"):
395 f.write(f"[{e['timestamp']}] {e['speaker']}: {e['text']}\n")
396 else:
397 f.write(f"[{e['timestamp']}] {e['text']}\n")
398 f.write(f"Confidence: {e['confidence']:.2%}\n\n")
399 print(f"Encounter transcript saved: {fname}")
400
401
402# === Main ===
403
404def run():
405 global audio, stream, ws_app, selected_model
406
407 print("=" * 60)
408 print(" 🎙️ Medical Scribe - STT + LLM Gateway")
409 print("=" * 60)
410 selected_model = select_model()
411 print(f"✓ Using model: {selected_model}")
412
413 # Init mic
414 audio = pyaudio.PyAudio()
415 try:
416 stream = audio.open(
417 input=True,
418 frames_per_buffer=FRAMES_PER_BUFFER,
419 channels=CHANNELS,
420 format=FORMAT,
421 rate=SAMPLE_RATE,
422 )
423 print("Audio stream opened successfully.")
424 except Exception as e:
425 print(f"Error opening audio stream: {e}")
426 if audio:
427 audio.terminate()
428 return
429
430 # Connect WS
431 ws_headers = [f"Authorization: {ASSEMBLYAI_API_KEY}"]
432 ws_app = websocket.WebSocketApp(
433 API_ENDPOINT,
434 header=ws_headers,
435 on_open=on_open,
436 on_message=on_message,
437 on_error=on_error,
438 on_close=on_close,
439 )
440
441 ws_thread = threading.Thread(target=ws_app.run_forever, daemon=True)
442 ws_thread.start()
443
444 try:
445 while ws_thread.is_alive():
446 time.sleep(0.1)
447 except KeyboardInterrupt:
448 print("\n\nCtrl+C received. Stopping...")
449 stop_event.set()
450 # best-effort terminate
451 if ws_app and ws_app.sock and ws_app.sock.connected:
452 try:
453 ws_app.send(json.dumps({"type": "Terminate"}))
454 time.sleep(0.5)
455 except Exception as e:
456 print(f"Error sending termination: {e}")
457 if ws_app:
458 ws_app.close()
459 ws_thread.join(timeout=2.0)
460 finally:
461 if stream and stream.is_active():
462 stream.stop_stream()
463 if stream:
464 stream.close()
465 if audio:
466 audio.terminate()
467 print("Cleanup complete. Exiting.")
468
469
470if __name__ == "__main__":
471 run()

How Do I Handle HIPAA Compliance?

HIPAA compliance is mandatory for all medical transcription workflows. Here’s how to ensure your medical scribe meets requirements:

Required HIPAA Guardrails

1. Business Associate Agreement (BAA)

  • AssemblyAI provides a BAA for healthcare customers
  • Required before processing any PHI
  • Contact us to execute BAA

2. PII Redaction (Required)

1config = aai.TranscriptionConfig(
2 # HIPAA-mandated PII redaction
3 redact_pii=True,
4 redact_pii_policies=[
5 # HIPAA identifiers mapped to available PII policies
6 PIIRedactionPolicy.person_name, # Patient & provider names
7 PIIRedactionPolicy.date_of_birth, # DOB
8 PIIRedactionPolicy.date, # All dates (except year)
9 PIIRedactionPolicy.phone_number, # Phone numbers
10 PIIRedactionPolicy.email_address, # Email addresses
11 PIIRedactionPolicy.healthcare_number, # MRN, health plan numbers
12 PIIRedactionPolicy.us_social_security_number, # SSN
13 PIIRedactionPolicy.account_number, # Financial accounts
14 PIIRedactionPolicy.drivers_license, # License numbers
15 PIIRedactionPolicy.vehicle_id, # License plates, VINs
16 PIIRedactionPolicy.number_sequence, # Serial numbers, device identifiers
17 PIIRedactionPolicy.url, # URLs
18 PIIRedactionPolicy.ip_address, # IP addresses
19 PIIRedactionPolicy.location, # Geographic identifiers
20 PIIRedactionPolicy.username, # Usernames and online identifiers
21 ],
22 redact_pii_sub=PIISubstitutionPolicy.hash, # Use stable hash tokens
23 redact_pii_audio=True, # Create de-identified audio file
24)

3. Secure Audio Storage

1# HIPAA-compliant storage requirements
2# - Encryption at rest (AES-256)
3# - Encryption in transit (TLS 1.2+)
4# - Access controls and audit logging
5# - Automatic deletion after retention period
6
7# Example with AWS S3
8audio_url = "https://your-hipaa-compliant-s3.amazonaws.com/encounters/patient123.mp3"
9# Ensure bucket has:
10# - Server-side encryption enabled
11# - Access logging enabled
12# - Bucket policy restricting access
13# - Lifecycle policy for automatic deletion

4. Access Controls

1# Implement role-based access control
2def can_access_encounter(user_role: str, patient_id: str) -> bool:
3 """Verify user has permission to access patient encounter"""
4 # Check EHR permissions
5 # Verify provider-patient relationship
6 # Audit access attempt
7 return has_clinical_relationship(user_role, patient_id)

5. Audit Logging

1# Log all PHI access
2def log_phi_access(user_id: str, patient_id: str, action: str):
3 """HIPAA requires audit trail of all PHI access"""
4 audit_log.write({
5 "timestamp": datetime.now(),
6 "user_id": user_id,
7 "patient_id": patient_id,
8 "action": action, # "transcribe", "view", "edit", "delete"
9 "ip_address": request.remote_addr,
10 })

For complete HIPAA guidance, see our Healthcare Compliance Guide.

What Workflows Can I Build for My AI Medical Scribe?

Use these flags to transform raw medical conversations into structured clinical documentation. Below is plain-English behavior, output shape, and clinical use cases for each option.

Entity Detection (Medical)

entity_detection: true

What it does: Extracts medical entities (medications, conditions, procedures, anatomy).

Output: Array of { entity_type, text, start, end }.

Great for: Medication reconciliation, problem list updates, procedure coding.

Notes: Recognizes brand/generic drug names, medical conditions, surgical procedures. Entity types include drug, medical_condition, and medical_process.

Redact PII Text (HIPAA Compliance)

redact_pii: true

What it does: Scans transcript for Protected Health Information and replaces per HIPAA requirements.

Output: text with PHI replaced; original timing preserved.

Great for: De-identification, research datasets, training data.

Notes: Covers all 18 HIPAA identifiers when properly configured.

redact_pii_policies: [person_name, date_of_birth, healthcare_number, phone_number, email_address]

Restricts redaction scope to key HIPAA identifiers:

  • person_name – patient and provider names
  • date_of_birth – full or partial DOB
  • healthcare_number – MRN, health plan numbers
  • phone_number – contact numbers
  • email_address – electronic addresses

Why this set: Ensures HIPAA compliance while preserving clinical content for documentation.

redact_pii_sub: hash

What it does: Replaces each PHI span with a stable hash token.

Example: "Patient John Doe, DOB 1/15/1980, MRN 12345""Patient #2af4…, DOB #7b91…, MRN #e13c…"

Benefits:

  • Maintains referential integrity across document
  • Preserves sentence structure for NLP/LLM processing
  • Prevents reconstruction of original PHI

Redact PII Audio (HIPAA Compliance)

redact_pii_audio: true

What it does: Produces HIPAA-compliant audio with PHI portions silenced.

Output: redacted_audio_url in the transcript payload.

Great for: Quality assurance, training, research.

Notes: Original audio preserved separately; ensure proper access controls.

Sentiment Analysis (Patient Experience)

sentiment_analysis: true

What it does: Analyzes emotional tone of patient responses.

Output: Array of { text, sentiment, confidence, start, end }.

Great for: Patient satisfaction, pain assessment, mental health screening.

Notes: Helpful for identifying distressed or dissatisfied patients.

End-to-End Clinical Documentation Effect

ModelYou getTypical consumer
entity_detectionMedical entities extractedEHR integration, coding
sentiment_analysisPatient emotion trackingQuality metrics, alerts
redact_pii (+ policies)HIPAA-compliant textResearch, QA, training
redact_pii_sub=hashStable PHI placeholdersAnalytics & LLM processing
redact_pii_audioDe-identified audioCompliance archives

Clinical Documentation Example

Original Encounter:

“Hi, I’m Dr. Smith. John Doe, born 1/15/1980, is here for follow-up. He’s taking metformin 1000mg twice daily for his diabetes.”

With medical scribe settings:

  • Text: “Hi, I’m #2af4…. #7b91…, born #e13c…, is here for follow-up. He’s taking metformin 1000mg twice daily for his diabetes.”
  • Entities: [ { entity_type: "drug", text: "metformin 1000mg" }, { entity_type: "medical_condition", text: "diabetes" } ]
  • Clinical note: Structured SOAP format via LLM Gateway
  • Redacted audio: PHI portions silenced for compliance

LLM Gateway for Clinical Notes

Our LLM Gateway enables transformation of raw transcripts into structured clinical documentation using the same API.

Here’s a complete example of generating structured SOAP notes from medical encounter transcripts:

1import requests
2import json
3from typing import Dict, List, Optional
4
5class MedicalSOAPGenerator:
6 """Generate structured SOAP notes from medical transcripts using LLM Gateway"""
7
8 def __init__(self, api_key: str):
9 self.api_key = api_key
10 self.base_url = "https://llm-gateway.assemblyai.com/v1/chat/completions"
11 self.headers = {"authorization": api_key}
12
13 def generate_soap_note(self,
14 transcript: str,
15 patient_context: Optional[Dict] = None,
16 visit_type: str = "general") -> Dict:
17 """Generate SOAP note from medical transcript"""
18
19 # Build context for the LLM
20 context_prompt = self._build_context_prompt(transcript, patient_context, visit_type)
21
22 messages = [
23 {
24 "role": "system",
25 "content": """You are a clinical documentation specialist. Generate a structured SOAP note from the medical encounter transcript.
26
27SOAP Format:
28- Subjective: Patient's chief complaint, history of present illness, and symptoms
29- Objective: Provider's observations, physical exam findings, vital signs, and test results
30- Assessment: Provider's clinical impressions, diagnoses, and differential diagnoses
31- Plan: Treatment recommendations, medications, follow-up instructions, and referrals
32
33Guidelines:
34- Use medical terminology appropriately
35- Include specific details mentioned in the encounter
36- Maintain clinical accuracy
37- Use bullet points for clarity
38- Include medications with dosages and frequencies
39- Note any follow-up appointments or referrals"""
40 },
41 {
42 "role": "user",
43 "content": context_prompt
44 }
45 ]
46
47 response = requests.post(
48 self.base_url,
49 headers=self.headers,
50 json={
51 "model": "claude-sonnet-4-5-20250929", # Best for medical reasoning
52 "messages": messages,
53 "max_tokens": 2000,
54 "temperature": 0.1 # Low temperature for consistent medical documentation
55 }
56 )
57
58 if response.status_code == 200:
59 result = response.json()
60 soap_content = result["choices"][0]["message"]["content"]
61
62 return {
63 "soap_note": soap_content,
64 "structured_data": self._extract_structured_data(soap_content),
65 "visit_type": visit_type,
66 "generation_timestamp": self._get_timestamp()
67 }
68 else:
69 raise Exception(f"LLM Gateway error: {response.status_code} - {response.text}")
70
71 def _build_context_prompt(self, transcript: str, patient_context: Optional[Dict], visit_type: str) -> str:
72 """Build comprehensive context prompt for SOAP generation"""
73
74 prompt_parts = [
75 f"Generate a SOAP note for a {visit_type} medical encounter.",
76 "",
77 "MEDICAL ENCOUNTER TRANSCRIPT:",
78 transcript,
79 ""
80 ]
81
82 if patient_context:
83 prompt_parts.extend([
84 "PATIENT CONTEXT:",
85 f"- Age: {patient_context.get('age', 'Not specified')}",
86 f"- Known conditions: {', '.join(patient_context.get('conditions', []))}",
87 f"- Current medications: {', '.join(patient_context.get('medications', []))}",
88 f"- Allergies: {', '.join(patient_context.get('allergies', []))}",
89 ""
90 ])
91
92 prompt_parts.extend([
93 "Please generate a comprehensive SOAP note following the format:",
94 "Subjective:",
95 "Objective:",
96 "Assessment:",
97 "Plan:",
98 "",
99 "Include specific details, medications with dosages, and any follow-up instructions mentioned."
100 ])
101
102 return "\n".join(prompt_parts)
103
104 def _extract_structured_data(self, soap_content: str) -> Dict:
105 """Extract structured data from SOAP note"""
106
107 sections = {
108 "subjective": self._extract_section(soap_content, "Subjective"),
109 "objective": self._extract_section(soap_content, "Objective"),
110 "assessment": self._extract_section(soap_content, "Assessment"),
111 "plan": self._extract_section(soap_content, "Plan")
112 }
113
114 return {
115 "sections": sections,
116 "medications": self._extract_medications(soap_content),
117 "diagnoses": self._extract_diagnoses(soap_content),
118 "follow_up": self._extract_follow_up(soap_content)
119 }
120
121 def _extract_section(self, content: str, section_name: str) -> str:
122 """Extract specific SOAP section"""
123 import re
124
125 pattern = rf"{section_name}:\s*(.*?)(?=\n\w+:|$)"
126 match = re.search(pattern, content, re.DOTALL | re.IGNORECASE)
127 return match.group(1).strip() if match else ""
128
129 def _extract_medications(self, content: str) -> List[str]:
130 """Extract medication mentions from SOAP note"""
131 import re
132
133 # Look for medication patterns
134 medication_patterns = [
135 r"([A-Z][a-z]+(?:mycin|pril|sartan|pine|zole|statin|pam|zepam))\s+\d+\s*mg",
136 r"([A-Z][a-z]+)\s+\d+\s*mg\s+(?:daily|twice daily|BID|TID|QID)",
137 r"([A-Z][a-z]+)\s+\d+\s*mg\s+(?:once|twice|three times)\s+(?:daily|a day)"
138 ]
139
140 medications = []
141 for pattern in medication_patterns:
142 matches = re.findall(pattern, content, re.IGNORECASE)
143 medications.extend(matches)
144
145 return list(set(medications)) # Remove duplicates
146
147 def _extract_diagnoses(self, content: str) -> List[str]:
148 """Extract diagnosis mentions from Assessment section"""
149 assessment = self._extract_section(content, "Assessment")
150
151 # Common diagnosis patterns
152 diagnosis_patterns = [
153 r"([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+(?:syndrome|disease|disorder|condition)",
154 r"(?:diagnosis|impression):\s*([^,\n]+)",
155 r"([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+likely"
156 ]
157
158 diagnoses = []
159 for pattern in diagnosis_patterns:
160 matches = re.findall(pattern, assessment, re.IGNORECASE)
161 diagnoses.extend(matches)
162
163 return list(set(diagnoses))
164
165 def _extract_follow_up(self, content: str) -> List[str]:
166 """Extract follow-up instructions from Plan section"""
167 plan = self._extract_section(content, "Plan")
168
169 follow_up_patterns = [
170 r"follow[-\s]up\s+in\s+([^,\n]+)",
171 r"return\s+in\s+([^,\n]+)",
172 r"recheck\s+in\s+([^,\n]+)",
173 r"schedule\s+([^,\n]+)"
174 ]
175
176 follow_ups = []
177 for pattern in follow_up_patterns:
178 matches = re.findall(pattern, plan, re.IGNORECASE)
179 follow_ups.extend(matches)
180
181 return list(set(follow_ups))
182
183 def _get_timestamp(self) -> str:
184 """Get current timestamp"""
185 from datetime import datetime
186 return datetime.now().isoformat()
187
188# Example usage
189async def generate_clinical_documentation(audio_file: str, patient_id: str):
190 """Complete workflow: transcribe + generate SOAP note"""
191
192 config = aai.TranscriptionConfig(
193 speech_models=["universal-3-pro", "universal-2"],
194 language_detection=True,
195 speaker_labels=True,
196 speakers_expected=2,
197 keyterms_prompt=get_patient_medical_history(patient_id),
198 entity_detection=True,
199 redact_pii=True,
200 redact_pii_policies=[...], # All HIPAA identifiers
201 )
202
203 transcript = await transcribe_async(audio_file, config)
204
205 # Step 2: Load patient context from EHR
206 patient_context = ehr.get_patient(patient_id)
207
208 # Step 3: Generate SOAP note using LLM Gateway
209 soap_generator = MedicalSOAPGenerator(api_key="your_api_key")
210
211 soap_result = soap_generator.generate_soap_note(
212 transcript=transcript.text,
213 patient_context={
214 "age": patient_context.get("age"),
215 "conditions": [c.name for c in patient_context.get("conditions", [])],
216 "medications": [f"{m.name} {m.dosage}" for m in patient_context.get("medications", [])],
217 "allergies": [a.name for a in patient_context.get("allergies", [])]
218 },
219 visit_type="primary_care_followup"
220 )
221
222 # Step 4: Update EHR with structured data
223 ehr.update_patient_record(patient_id, {
224 "soap_note": soap_result["soap_note"],
225 "medications_mentioned": soap_result["structured_data"]["medications"],
226 "diagnoses": soap_result["structured_data"]["diagnoses"],
227 "follow_up_instructions": soap_result["structured_data"]["follow_up"]
228 })
229
230 return {
231 "transcript": transcript,
232 "soap_note": soap_result["soap_note"],
233 "structured_data": soap_result["structured_data"],
234 "clinical_entities": transcript.entities
235 }
236
237# Example output
238"""
239Subjective:
240- Patient presents with chest pain for 3 days
241- Pain is substernal, 7/10 intensity, radiating to left arm
242- Associated with shortness of breath and diaphoresis
243- No relief with rest or nitroglycerin
244- History of hypertension and diabetes mellitus type 2
245
246Objective:
247- Vital signs: BP 160/95, HR 95, RR 22, O2 sat 94% on room air
248- Physical exam: Diaphoretic, mild distress
249- Heart: Regular rate and rhythm, no murmurs
250- Lungs: Clear bilaterally
251- Extremities: No edema
252
253Assessment:
254- Acute coronary syndrome, rule out STEMI
255- Hypertension, poorly controlled
256- Diabetes mellitus type 2, stable
257
258Plan:
259- STAT EKG and troponin levels
260- Aspirin 325mg daily
261- Atorvastatin 80mg at bedtime
262- Cardiology consultation
263- Follow up in 1 week
264- Return to ED if chest pain worsens
265"""

Advanced SOAP Note Features

1class AdvancedSOAPGenerator(MedicalSOAPGenerator):
2 """Enhanced SOAP generator with specialty-specific templates"""
3
4 def generate_specialty_soap(self, transcript: str, specialty: str, patient_context: Dict) -> Dict:
5 """Generate specialty-specific SOAP notes"""
6
7 specialty_templates = {
8 "cardiology": self._cardiology_template(),
9 "endocrinology": self._endocrinology_template(),
10 "oncology": self._oncology_template(),
11 "psychiatry": self._psychiatry_template()
12 }
13
14 template = specialty_templates.get(specialty, self._general_template())
15
16 messages = [
17 {
18 "role": "system",
19 "content": f"""You are a {specialty} specialist. Generate a detailed SOAP note using this template:
20
21 {template}
22
23 Focus on {specialty}-specific terminology, assessments, and treatment plans."""
24 },
25 {
26 "role": "user",
27 "content": f"Generate a {specialty} SOAP note for this encounter:\n\n{transcript}"
28 }
29 ]
30
31 response = requests.post(
32 self.base_url,
33 headers=self.headers,
34 json={
35 "model": "claude-sonnet-4-5-20250929",
36 "messages": messages,
37 "max_tokens": 2500,
38 "temperature": 0.1
39 }
40 )
41
42 return response.json()
43
44 def _cardiology_template(self) -> str:
45 return """
46 Subjective:
47 - Chief complaint and cardiac history
48 - Chest pain characteristics (quality, location, radiation, timing)
49 - Dyspnea, orthopnea, paroxysmal nocturnal dyspnea
50 - Palpitations, syncope, presyncope
51 - Risk factors (smoking, diabetes, hypertension, family history)
52
53 Objective:
54 - Vital signs including orthostatic vitals
55 - Cardiovascular exam (heart sounds, murmurs, gallops)
56 - Peripheral vascular exam (pulses, edema, JVD)
57 - Relevant lab values (troponin, BNP, lipids)
58 - Imaging results (EKG, echo, stress test, cath)
59
60 Assessment:
61 - Primary cardiac diagnosis
62 - Secondary diagnoses
63 - Risk stratification (Framingham, ASCVD)
64
65 Plan:
66 - Medications with cardiac indications
67 - Procedures (cath, echo, stress test)
68 - Lifestyle modifications
69 - Follow-up and monitoring
70 """
71
72 def _endocrinology_template(self) -> str:
73 return """
74 Subjective:
75 - Diabetes symptoms (polyuria, polydipsia, weight changes)
76 - Thyroid symptoms (fatigue, weight changes, heat/cold intolerance)
77 - Medication adherence and side effects
78 - Blood glucose monitoring results
79
80 Objective:
81 - Vital signs including BMI
82 - Thyroid exam
83 - Diabetic foot exam
84 - Lab values (HbA1c, glucose, thyroid function)
85
86 Assessment:
87 - Diabetes control and complications
88 - Thyroid function status
89 - Endocrine disorders
90
91 Plan:
92 - Medication adjustments
93 - Lab monitoring schedule
94 - Patient education
95 - Specialist referrals
96 """
97
98# Usage example for specialty SOAP notes
99async def generate_cardiology_soap(transcript: str, patient_id: str):
100 """Generate cardiology-specific SOAP note"""
101
102 generator = AdvancedSOAPGenerator(api_key="your_api_key")
103
104 patient_context = ehr.get_patient(patient_id)
105
106 soap_result = generator.generate_specialty_soap(
107 transcript=transcript,
108 specialty="cardiology",
109 patient_context=patient_context
110 )
111
112 return soap_result

How Do I Improve the Accuracy of My Medical Scribe?

Medical Keyterms Strategy

The most effective approach for medical keyterms:

1. Patient-Specific Context

1# Load from EHR before transcription
2patient_keyterms = [
3 # Current medications with dosages
4 "metformin 1000mg twice daily",
5 "lisinopril 20mg once daily",
6 "atorvastatin 40mg at bedtime",
7
8 # Known conditions
9 "type 2 diabetes mellitus",
10 "hypertension stage 2",
11 "hyperlipidemia",
12 "chronic kidney disease stage 3",
13
14 # Recent procedures
15 "colonoscopy March 2024",
16 "echocardiogram",
17
18 # Allergies
19 "penicillin anaphylaxis",
20 "sulfa drugs rash",
21]

2. Specialty-Specific Terms

1# Cardiology
2cardiology_terms = [
3 "ejection fraction",
4 "coronary artery disease",
5 "atrial fibrillation",
6 "ST elevation",
7 "myocardial infarction",
8 "cardiac catheterization",
9]
10
11# Endocrinology
12endocrinology_terms = [
13 "hemoglobin A1c",
14 "thyroid stimulating hormone",
15 "diabetic ketoacidosis",
16 "insulin resistance",
17]

3. Visit-Specific Context

1# Load based on appointment type
2if appointment_type == "diabetes_followup":
3 visit_keyterms = [
4 "blood glucose monitoring",
5 "hemoglobin A1c",
6 "retinopathy screening",
7 "foot examination",
8 "diabetes self-management",
9 ]

Using Keyterms Prompt for Streaming with LLM Gateway Enhancement

1# Streaming with medical context and post-processing
2medical_terms = [
3 # Patient-specific history
4 "coronary artery disease",
5 "CABG in 2019",
6 "ejection fraction 45%",
7
8 # Current visit context
9 "chest pain",
10 "shortness of breath",
11 "orthopnea",
12
13 # Likely medications
14 "carvedilol",
15 "furosemide",
16 "spironolactone"
17]
18
19# Connect via WebSocket to the v3 streaming API with medical keyterms
20CONNECTION_PARAMS = {
21 "sample_rate": 16000,
22 "speech_model": "u3-rt-pro",
23 "domain": "medical-v1",
24 "format_turns": True,
25 "keyterms_prompt": medical_terms,
26}
27
28url = f"wss://streaming.assemblyai.com/v3/ws?{urlencode(CONNECTION_PARAMS, doseq=True)}"
29
30# Post-process each final turn with LLM Gateway for medical terminology correction
31def post_process_medical(transcript_text):
32 """Apply LLM correction for medical context"""
33 corrected = llm_gateway.correct_medical(transcript_text)
34 return corrected

Common Medical Terminology - Top 1000 Terms

Even if you don’t know the context of a specific medical conversation, you can boost the accuracy of transcription by providing the top 1000 medical words in your field.

1# General medical keyterms for improved accuracy
2common_medical_terms = [
3 "hypertension","diabetes","sepsis","asthma","pneumonia","influenza","bronchitis","copd","stroke","migraine","epilepsy","cancer","angina","obesity","gerd","ulcer","colitis","crohn","hepatitis","cirrhosis","pancreatitis","appendicitis","cholecystitis","diverticulitis","anemia","leukemia","lymphoma","myeloma","arthritis","osteoarthritis","gout","lupus","psoriasis","eczema","dermatitis","cellulitis","abscess","depression","anxiety","bipolar","schizophrenia","adhd","autism","dementia","parkinson","hypothyroidism","hyperthyroidism","hyperlipidemia","dehydration","preeclampsia","endometriosis","pcos","bph","uti","pyelonephritis","prostatitis","meningitis","encephalitis","sinusitis","otitis","pharyngitis","tonsillitis","gastritis","enteritis","nephrolithiasis","urolithiasis","thrombosis","embolism","dvt","pe","concussion","sciatica","scoliosis","stenosis","herniation","tendonitis","bursitis","fracture","dislocation","laceration","contusion","hematoma","neuropathy","myopathy","cardiomyopathy","nephropathy","retinopathy","keratitis","uveitis","iritis","scleritis","blepharitis","conjunctivitis","keratoconus","cataract","glaucoma","maculopathy","retinoblastoma","sarcoidosis","amyloidosis","hemochromatosis","thalassemia","hemophilia","hyperglycemia","hypoglycemia","hypernatremia","hyponatremia","hyperkalemia","hypokalemia","hypercalcemia","hypocalcemia","hypermagnesemia","hypomagnesemia","acidosis","alkalosis","ketoacidosis","endocarditis","myocarditis","pericarditis","peritonitis","osteomyelitis","spondylitis","vasculitis","arteritis","phlebitis","panniculitis","tenosynovitis","myositis","rhabdomyolysis","xerostomia","dysgeusia","anosmia","ageusia","aphasia","ataxia","dystonia","chorea","tremor","bradykinesia","tachycardia","bradycardia","hypotension","syncope","hemoptysis","hematuria","melena","hematochezia","leukocytosis","leukopenia","neutropenia","thrombocytopenia","thrombocytosis","hyperbilirubinemia","jaundice","pruritus","urticaria","angioedema","anaphylaxis","dyspnea","orthopnea","paronychia","onychomycosis","candidiasis","histoplasmosis","blastomycosis","coccidioidomycosis","aspergillosis","toxoplasmosis","giardiasis","amebiasis","malaria","trichomoniasis","syphilis","gonorrhea","chlamydia","herpes","varicella","measles","rubella","mumps","pertussis","tetanus","diphtheria","botulism","anthrax","smallpox","tularemia","brucellosis","leptospirosis","yersiniosis","campylobacteriosis","listeriosis","norovirus","rotavirus","hepatitisb","hepatitisc","hepatitisa","hiv","aids","covid19","sars","mers","ebola","zika","dengue","chikungunya","yellowfever","rabies","lyme","babesiosis","ehrlichiosis","anaplasmosis","rockymountainspottedfever",
4 "appendectomy","cholecystectomy","mastectomy","hysterectomy","oophorectomy","salpingectomy","prostatectomy","nephrectomy","splenectomy","thyroidectomy","parathyroidectomy","adenoidectomy","tonsillectomy","colectomy","hemicolectomy","gastrectomy","pancreatectomy","hepatectomy","laminectomy","discectomy","craniotomy","craniectomy","thoracotomy","lobectomy","pneumonectomy","tracheostomy","bronchoscopy","laryngoscopy","endoscopy","colonoscopy","sigmoidoscopy","cystoscopy","ureteroscopy","arthroscopy","laparoscopy","angiography","ventriculostomy","thoracentesis","paracentesis","pericardiocentesis","arthrocentesis","amniocentesis","cardioversion","defibrillation","intubation","extubation","tracheotomy","catheterization","cannulation","dialysis","plasmapheresis","phototherapy","radiotherapy","chemotherapy","brachytherapy","cryotherapy","cauterization","electrocardiography","echocardiography","electroencephalography","spirometry","plethysmography","manometry","oximetry","capnography","ventriculography","mammography","tomography","fluoroscopy","ultrasonography","scintigraphy","venography","myelography","cholangiography","hysterosalpingography","urography","pyelography","amniotomy","episiotomy","debridement","fasciotomy","arthroplasty","angioplasty","valvuloplasty","sclerotherapy","phlebectomy","embolectomy","thrombectomy","endarterectomy","stenting","bypass","ablation","curettage","aspiration","biopsy",
5 "hemoglobin","hematocrit","leukocyte","erythrocyte","platelet","creatinine","bun","bilirubin","alkalinephosphatase","aspartatetransaminase","alaninetransaminase","lactatedehydrogenase","troponin","bnp","procalcitonin","ferritin","transferrin","uricacid","cholesterol","triglyceride","hdl","ldl","hba1c","glucose","insulin","cpeptide","tsh","t3","t4","cortisol","prolactin","testosterone","estradiol","progesterone","psa","crp","esr","dimer","lactate","amylase","lipase","natriuretic","urinalysis","ketones","proteinuria","microalbumin","bacteriuria","pyuria","nitrites","leukocyteesterase","culture","cytology","histology","serology","pcr","antigen","antibody","titer","crossmatch","coagulation","protime","inr","aptt","fibrinogen","ddu","osmolality","sodium","potassium","chloride","bicarbonate","calcium","magnesium","phosphate","albumin","globulin","totalprotein","aniongap","osmolarity","hematology","biochemistry","microbiology","virology","parasitology","toxicology",
6 "mandible","maxilla","zygomatic","sphenoid","ethmoid","occipital","temporal","parietal","frontal","atlas","axis","clavicle","scapula","sternum","humerus","radius","ulna","carpals","scaphoid","lunate","triquetrum","pisiform","trapezium","trapezoid","capitate","hamate","metacarpal","phalanx","pelvis","ilium","ischium","pubis","acetabulum","femur","patella","tibia","fibula","tarsals","talus","calcaneus","navicular","cuboid","cuneiform","sacrum","coccyx","diaphragm","pleura","peritoneum","mesentery","omentum","myocardium","endocardium","pericardium","epidermis","dermis","hypodermis","alveolus","bronchiole","larynx","pharynx","esophagus","duodenum","jejunum","ileum","cecum","colon","sigmoid","rectum","anus","hepatocyte","nephron","glomerulus","tubule","ureter","bladder","urethra","cortex","medulla","cerebrum","cerebellum","thalamus","hypothalamus","hippocampus","amygdala","pituitary","pineal","thyroid","parathyroid","adrenal","pancreas","spleen","thymus","tonsil","lymphocyte","macrophage","neutrophil","eosinophil","basophil","monocyte","osteocyte","chondrocyte","myocyte","neuron","astrocyte","oligodendrocyte","microglia","retina","cornea","sclera","choroid","iris","lens","cochlea","vestibule","stapes","tarsus","metatarsal","falx","tentorium","dura","arachnoid","pia",
7 "acetaminophen","ibuprofen","naproxen","ketorolac","diclofenac","celecoxib","meloxicam","morphine","hydromorphone","oxycodone","hydrocodone","fentanyl","buprenorphine","naloxone","tramadol","gabapentin","pregabalin","lidocaine","bupivacaine","ropivacaine","amoxicillin","ampicillin","penicillin","piperacillin","tazobactam","cefazolin","cephalexin","cefuroxime","cefdinir","ceftriaxone","cefepime","ceftaroline","meropenem","imipenem","ertapenem","aztreonam","vancomycin","daptomycin","linezolid","tedizolid","clindamycin","metronidazole","azithromycin","clarithromycin","erythromycin","doxycycline","minocycline","tigecycline","ciprofloxacin","levofloxacin","moxifloxacin","trimethoprim","sulfamethoxazole","nitrofurantoin","fosfomycin","rifampin","isoniazid","pyrazinamide","ethambutol","amphotericin","fluconazole","itraconazole","voriconazole","posaconazole","micafungin","caspofungin","anidulafungin","acyclovir","valacyclovir","famciclovir","oseltamivir","zanamivir","remdesivir","ivermectin","albendazole","mebendazole","praziquantel","hydroxychloroquine","chloroquine","atovaquone","proguanil","pyrimethamine","clotrimazole","nystatin","terbinafine",
8 "lisinopril","enalapril","captopril","benazepril","ramipril","perindopril","losartan","valsartan","candesartan","irbesartan","olmesartan","telmisartan","azilsartan","amlodipine","nifedipine","felodipine","isradipine","nicardipine","diltiazem","verapamil","metoprolol","atenolol","propranolol","carvedilol","bisoprolol","nebivolol","labetalol","hydrochlorothiazide","chlorthalidone","indapamide","furosemide","torsemide","bumetanide","spironolactone","eplerenone","triamterene","amiloride","hydralazine","minoxidil","nitroglycerin","isosorbidedinitrate","isosorbidemononitrate","ranolazine","digoxin","amiodarone","sotalol","dofetilide","dronedarone","flecainide","propafenone","adenosine","warfarin","heparin","enoxaparin","dalteparin","fondaparinux","apixaban","rivaroxaban","edoxaban","dabigatran","clopidogrel","prasugrel","ticagrelor","abciximab","eptifibatide","tirofiban","atorvastatin","simvastatin","rosuvastatin","pravastatin","lovastatin","pitavastatin","fluvastatin","ezetimibe","fenofibrate","gemfibrozil","niacin","alirocumab","evolocumab","inclisiran",
9 "metformin","glipizide","glyburide","glimepiride","pioglitazone","rosiglitazone","sitagliptin","saxagliptin","linagliptin","alogliptin","exenatide","liraglutide","dulaglutide","semaglutide","tirzepatide","pramlintide","acarbose","miglitol","canagliflozin","dapagliflozin","empagliflozin","ertugliflozin","insulin","glargine","detemir","degludec","lispro","aspart","glulisine","levothyroxine","liothyronine","methimazole","propylthiouracil","hydrocortisone","prednisone","prednisolone","methylprednisolone","dexamethasone","fludrocortisone","desmopressin","somatropin","octreotide","lanreotide","cabergoline","bromocriptine","calcitriol","alendronate","risedronate","ibandronate","zoledronic","denosumab","teriparatide","abaloparatide",
10 "albuterol","levalbuterol","ipratropium","tiotropium","umeclidinium","aclidinium","budesonide","fluticasone","beclomethasone","mometasone","ciclesonide","salmeterol","formoterol","vilanterol","montelukast","zafirlukast","zileuton","roflumilast","omalizumab","mepolizumab","reslizumab","benralizumab","dupilumab","tezepelumab",
11 "sertraline","fluoxetine","paroxetine","citalopram","escitalopram","vilazodone","vortioxetine","venlafaxine","desvenlafaxine","duloxetine","bupropion","mirtazapine","trazodone","amitriptyline","nortriptyline","imipramine","clomipramine","lithium","lamotrigine","valproate","carbamazepine","oxcarbazepine","topiramate","levetiracetam","phenytoin","phenobarbital","clonazepam","lorazepam","diazepam","alprazolam","temazepam","buspirone","zolpidem","eszopiclone","zaleplon","quetiapine","olanzapine","risperidone","ziprasidone","aripiprazole","clozapine","haloperidol","lurasidone","paliperidone","brexpiprazole","cariprazine","iloperidone",
12 "omeprazole","esomeprazole","lansoprazole","pantoprazole","rabeprazole","sucralfate","famotidine","cimetidine","ranitidine","metoclopramide","ondansetron","prochlorperazine","promethazine","dicyclomine","hyoscyamine","loperamide","diphenoxylate","mesalamine","sulfasalazine","infliximab","adalimumab","vedolizumab","ustekinumab","tofacitinib","linaclotide","plecanatide","lubiprostone","polyethyleneglycol","senna","bisacodyl","lactulose","rifaximin","pancrelipase","ursodiol","cholestyramine","colesevelam","colestipol",
13 "methotrexate","leflunomide","hydroxychloroquine","sulfasalazine","azathioprine","mycophenolate","cyclosporine","tacrolimus","sirolimus","everolimus","belatacept","rituximab","abatacept","tocilizumab","sarilumab","anakinra","canakinumab","secukinumab","ixekizumab","brodalumab","guselkumab","risankizumab","tildrakizumab","dupilumab","omalizumab","certolizumab","golimumab","etanercept","apremilast","thalidomide","lenalidomide","pomalidomide","acitretin","isotretinoin","tretinoin","clobetasol","betamethasone","triamcinolone","pimecrolimus","calcipotriene",
14 "imatinib","dasatinib","nilotinib","bosutinib","ponatinib","erlotinib","gefitinib","afatinib","osimertinib","lapatinib","sorafenib","sunitinib","pazopanib","axitinib","cabozantinib","lenvatinib","vandetanib","regorafenib","nintedanib","bevacizumab","ramucirumab","cetuximab","panitumumab","trastuzumab","pertuzumab","ado","trastuzumabemtansine","obinutuzumab","ofatumumab","brentuximab","inotuzumab","blinatumomab","pembrolizumab","nivolumab","cemiplimab","atezolizumab","durvalumab","avelumab","ipilimumab","talimogene","olaparib","rucaparib","niraparib","talazoparib","palbociclib","ribociclib","abemaciclib","bortezomib","carfilzomib","ixazomib","venetoclax","idelalisib","copanlisib","duvelisib","acalabrutinib","ibrutinib","zanubrutinib","midostaurin","gilteritinib","enasidenib","ivosidenib","selpercatinib","pralsetinib","larotrectinib","entrectinib","dabrafenib","trametinib","binimetinib","cobimetinib","vemurafenib","encorafenib","tepotinib","capmatinib","savolitinib","crizotinib","ceritinib","alectinib","brigatinib","lorlatinib",
15 "gravida","para","abortus","primigravida","multigravida","nulliparous","primiparous","multiparous","parturition","lactation","menarche","menopause","metrorrhagia","menorrhagia","oligomenorrhea","amenorrhea","dysmenorrhea","dyspareunia","placenta","chorion","amnion","decidua","endometrium","myometrium","cervix","oocyte","follicle","corpusluteum",
16 "staphylococcus","streptococcus","enterococcus","pneumococcus","meningococcus","listeria","clostridium","pseudomonas"
17]
18
19config = aai.TranscriptionConfig(
20 speech_models=["universal-3-pro", "universal-2"],
21 language_detection=True,
22 keyterms_prompt=common_medical_terms,
23)

How Should I Handle Pre-recorded Transcription in Production?

For high-volume clinical workflows, use webhooks instead of polling:

1config = aai.TranscriptionConfig(
2 speech_models=["universal-3-pro", "universal-2"],
3 domain="medical-v1",
4 webhook_url="https://your-app.com/webhooks/assemblyai",
5 webhook_auth_header_name="X-Webhook-Secret",
6 webhook_auth_header_value="your_secret_here",
7 speaker_labels=True,
8 speakers_expected=2,
9 entity_detection=True,
10 redact_pii=True,
11 redact_pii_policies=[
12 PIIRedactionPolicy.person_name,
13 PIIRedactionPolicy.date_of_birth,
14 PIIRedactionPolicy.healthcare_number,
15 PIIRedactionPolicy.us_social_security_number,
16 ],
17 redact_pii_sub=PIISubstitutionPolicy.hash,
18 redact_pii_audio=True,
19)
20
21# Submit job and return immediately (non-blocking)
22transcript = transcriber.submit(audio_url, config=config)
23print(f"Job submitted: {transcript.id}")
24# Your app continues processing other encounters

Webhook handler example:

1from flask import Flask, request, jsonify
2
3app = Flask(__name__)
4
5@app.route("/webhooks/assemblyai", methods=["POST"])
6def assemblyai_webhook():
7 if request.headers.get("X-Webhook-Secret") != "your_secret_here":
8 return jsonify({"error": "Unauthorized"}), 401
9
10 import requests as http_requests
11
12 data = request.json
13 transcript_id = data["transcript_id"]
14 status = data["status"]
15
16 if status == "completed":
17 # Fetch the full transcript (webhook only sends transcript_id and status)
18 transcript = http_requests.get(
19 f"https://api.assemblyai.com/v2/transcript/{transcript_id}",
20 headers={"authorization": "your_api_key"}
21 ).json()
22 # Generate SOAP note from transcript
23 generate_soap_note(transcript)
24 # Update EHR
25 update_patient_record(transcript)
26 elif status == "error":
27 log_transcription_error(transcript_id)
28
29 return jsonify({"received": True}), 200

Scaling Considerations

  • Rate limits: 20,000 POST requests per 5-minute window
  • Concurrent transcriptions: 200+ for paid accounts (queued beyond that)
  • Ramp up gradually - Start at 10-50 concurrent, double incrementally
  • Use exponential backoff with jitter for 429 errors
  • Contact Sales before large-scale rollouts

How Can I Improve the Latency of My Medical Scribe?

Async Chunking for Long Encounters

For lengthy patient visits, implement chunking to get progressive documentation. This is especially useful for:

  • Hospital rounds (in-person microphone running ambient)
  • Comprehensive physicals
  • Specialty consultations

When to Use Streaming Instead

For optimal clinical workflow integration, streaming is ideal when:

  1. Real-time documentation needed:

    • Emergency department encounters
    • Telemedicine visits
    • Procedure documentation
  2. Immediate clinical decision support:

    • Medication interaction checking
    • Diagnosis suggestion
    • Protocol reminders
  3. Live quality assurance:

    • Compliance monitoring
    • Training supervision
    • Documentation coaching

Streaming provides:

  • ~300ms latency for immediate documentation
  • Real-time partial results for provider review
  • No delay between encounter end and note availability
  • Live clinical decision support integration

How Can I Use Speaker Identification for Doctor and Patient Recognition?

Speaker Identification can automatically distinguish between doctors and patients in medical encounters, replacing generic “Speaker A” and “Speaker B” labels with meaningful role-based identifiers.

Why Use Speaker Identification in Medical Scribes?

Clinical Benefits:

  • Clear attribution - Know exactly who said what in clinical documentation
  • SOAP note structure - Automatically separate subjective (patient) from objective (provider) statements
  • Compliance documentation - Proper attribution for regulatory requirements
  • Quality assurance - Review provider-patient communication patterns
  • Training analysis - Analyze communication styles for medical education

Medical Speaker Identification Setup

1import assemblyai as aai
2
3# Configure for medical encounter
4config = aai.TranscriptionConfig(
5 speech_models=["universal-3-pro", "universal-2"],
6 language_detection=True,
7 speaker_labels=True,
8 speakers_expected=2, # Doctor and patient
9
10 # Enable speaker identification with medical roles
11 speech_understanding={
12 "request": {
13 "speaker_identification": {
14 "speaker_type": "role",
15 "known_values": ["Doctor", "Patient"]
16 }
17 }
18 }
19)
20
21# Transcribe medical encounter
22transcript = await transcribe_async(audio_file, config)
23
24# Results show clear role attribution
25for utterance in transcript.utterances:
26 print(f"{utterance.speaker}: {utterance.text}")
27 # Output: "Doctor: What brings you in today?"
28 # "Patient: I've been having chest pain for the past week."

Method 2: Name-Based Identification

For scenarios where you know the specific doctor’s name:

1config = aai.TranscriptionConfig(
2 speech_models=["universal-3-pro", "universal-2"],
3 language_detection=True,
4 speaker_labels=True,
5 speakers_expected=2,
6
7 speech_understanding={
8 "request": {
9 "speaker_identification": {
10 "speaker_type": "name",
11 "known_values": ["Dr. Sarah Johnson", "Patient"]
12 }
13 }
14 }
15)

Additional Resources