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

  • Slam-1 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

Real-Time Streaming Advantages

As medical scribes evolve toward real-time documentation, AssemblyAI’s Universal-Streaming model 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

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
  • Real-time and batch processing 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 async (pre-recorded) versus streaming is critical for clinical workflows.

Use Pre-recorded (Slam-1) when:

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

  • Maximum accuracy required - Slam-1 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-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. Slam-1 post-processing - Run audio through Slam-1 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 Slam-1 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 (Slam-1)

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

Core Features:

  • Speaker diarization (provider-patient separation)
  • Automatic formatting, punctuation, and capitalization
  • Keyterms Prompting for medical specialties and conditions
  • Ability to prompt on related medical terms and improve the accuracy of others (for example, ibuprofen improving naproxen)

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-Streaming)

Languages: For live encounter transcription, Universal-Streaming supports:

  • English model optimized for medical contexts
  • Multilingual model for visits in other languages or with code switching (English, Spanish, German, French, Portuguese, Italian)
  • Post-processing LLM Gateway tight integration for increasing medical accuracy

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
  • End-of-utterance detection for natural clinical conversation flow
  • Post-processing LLM Gateway integration for increasing medical accuracy

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

Coming Soon

  • Medical model which packages up the best ways to contextually influence transcript output
  • Multilingual Slam-1, especially important for multilingual medical conversations to improve accuracy

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

Here’s a complete example implementing async transcription with Slam-1:

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

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, confidence }.

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

Notes: Recognizes brand/generic drug names, medical conditions, surgical procedures.

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, medical_record_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
  • medical_record_number – MRN, account 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: [ { type: "medication", text: "metformin 1000mg" }, { type: "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_model=aai.SpeechModel.slam_1,
194 speaker_labels=True,
195 speakers_expected=2,
196 keyterms_prompt=get_patient_medical_history(patient_id),
197 entity_detection=True,
198 redact_pii=True,
199 redact_pii_policies=[...], # All HIPAA identifiers
200 )
201
202 transcript = await transcribe_async(audio_file, config)
203
204 # Step 2: Load patient context from EHR
205 patient_context = ehr.get_patient(patient_id)
206
207 # Step 3: Generate SOAP note using LLM Gateway
208 soap_generator = MedicalSOAPGenerator(api_key="your_api_key")
209
210 soap_result = soap_generator.generate_soap_note(
211 transcript=transcript.text,
212 patient_context={
213 "age": patient_context.get("age"),
214 "conditions": [c.name for c in patient_context.get("conditions", [])],
215 "medications": [f"{m.name} {m.dosage}" for m in patient_context.get("medications", [])],
216 "allergies": [a.name for a in patient_context.get("allergies", [])]
217 },
218 visit_type="primary_care_followup"
219 )
220
221 # Step 4: Update EHR with structured data
222 ehr.update_patient_record(patient_id, {
223 "soap_note": soap_result["soap_note"],
224 "medications_mentioned": soap_result["structured_data"]["medications"],
225 "diagnoses": soap_result["structured_data"]["diagnoses"],
226 "follow_up_instructions": soap_result["structured_data"]["follow_up"]
227 })
228
229 return {
230 "transcript": transcript,
231 "soap_note": soap_result["soap_note"],
232 "structured_data": soap_result["structured_data"],
233 "clinical_entities": transcript.entities
234 }
235
236# Example output
237"""
238Subjective:
239- Patient presents with chest pain for 3 days
240- Pain is substernal, 7/10 intensity, radiating to left arm
241- Associated with shortness of breath and diaphoresis
242- No relief with rest or nitroglycerin
243- History of hypertension and diabetes mellitus type 2
244
245Objective:
246- Vital signs: BP 160/95, HR 95, RR 22, O2 sat 94% on room air
247- Physical exam: Diaphoretic, mild distress
248- Heart: Regular rate and rhythm, no murmurs
249- Lungs: Clear bilaterally
250- Extremities: No edema
251
252Assessment:
253- Acute coronary syndrome, rule out STEMI
254- Hypertension, poorly controlled
255- Diabetes mellitus type 2, stable
256
257Plan:
258- STAT EKG and troponin levels
259- Aspirin 325mg daily
260- Atorvastatin 80mg at bedtime
261- Cardiology consultation
262- Follow up in 1 week
263- Return to ED if chest pain worsens
264"""

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
19transcriber = aai.RealtimeTranscriber(
20 sample_rate=16000,
21 format_turns=True,
22 key_terms=medical_terms,
23 on_data=lambda transcript: post_process_medical(transcript)
24)
25
26def post_process_medical(transcript):
27 """Apply LLM correction for medical context"""
28 # Send to LLM Gateway for medical terminology correction
29 # This improves contextual accuracy significantly
30 corrected = llm_gateway.correct_medical(transcript.text)
31 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_model=aai.SpeechModel.slam_1,
21 keyterms_prompt=common_medical_terms,
22)

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_model=aai.SpeechModel.slam_1,
6 speaker_labels=True,
7 speakers_expected=2, # Doctor and patient
8
9 # Enable speaker identification with medical roles
10 speech_understanding={
11 "request": {
12 "speaker_identification": {
13 "speaker_type": "role",
14 "known_values": ["Doctor", "Patient"]
15 }
16 }
17 }
18)
19
20# Transcribe medical encounter
21transcript = await transcribe_async(audio_file, config)
22
23# Results show clear role attribution
24for utterance in transcript.utterances:
25 print(f"{utterance.speaker}: {utterance.text}")
26 # Output: "Doctor: What brings you in today?"
27 # "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_model=aai.SpeechModel.slam_1,
3 speaker_labels=True,
4 speakers_expected=2,
5
6 speech_understanding={
7 "request": {
8 "speaker_identification": {
9 "speaker_type": "name",
10 "known_values": ["Dr. Sarah Johnson", "Patient"]
11 }
12 }
13 }
14)

Additional Resources