arafatanam commited on
Commit
ac67d71
Β·
verified Β·
1 Parent(s): c9f6823

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +229 -322
app.py CHANGED
@@ -3,374 +3,281 @@ import os
3
  import requests
4
  import time
5
 
6
- # --- CONFIGURATION ---
7
  ASSEMBLYAI_API_KEY = os.environ.get("ASSEMBLYAI_API_KEY")
8
- HF_TOKEN = os.environ.get("HF_TOKEN")
9
 
10
- # ============================================================
11
- # 1. SPEECH-TO-TEXT: AssemblyAI Integration
12
- # ============================================================
13
 
14
- def transcribe_audio_assemblyai(audio_file_path):
15
- """Uses AssemblyAI's free tier (100 hours free)"""
16
- if not ASSEMBLYAI_API_KEY:
17
- return "❌ AssemblyAI API key not set. Add to Secrets."
18
-
 
 
19
  headers = {"authorization": ASSEMBLYAI_API_KEY}
20
-
21
- # Step 1: Upload audio
22
- print("πŸ“€ Uploading to AssemblyAI...")
23
-
24
- def read_file(filename):
25
- with open(filename, "rb") as f:
26
- while True:
27
- data = f.read(5242880) # 5MB chunks
28
- if not data:
29
- break
30
- yield data
31
-
32
- upload_response = requests.post(
33
  "https://api.assemblyai.com/v2/upload",
34
  headers=headers,
35
  data=read_file(audio_file_path)
36
  )
37
-
38
- if upload_response.status_code != 200:
39
- return f"❌ Upload failed: {upload_response.text}"
40
-
41
- audio_url = upload_response.json()["upload_url"]
42
- print(f"βœ… Uploaded: {audio_url}")
43
-
44
- # Step 2: Request transcription
45
- json_data = {
46
- "audio_url": audio_url,
47
- "speech_models": ["universal-2"], # Valid free tier model
48
- "language_code": "en_us"
49
- }
50
-
51
- transcript_response = requests.post(
52
  "https://api.assemblyai.com/v2/transcript",
53
- json=json_data,
54
- headers=headers
55
  )
56
-
57
- if transcript_response.status_code != 200:
58
- error_msg = transcript_response.json().get("error", "Unknown error")
59
- return f"❌ Transcription request failed: {error_msg}"
60
-
61
- transcript_id = transcript_response.json()["id"]
62
- print(f"πŸ“ Transcript ID: {transcript_id}")
63
-
64
- # Step 3: Poll for results
65
- polling_endpoint = f"https://api.assemblyai.com/v2/transcript/{transcript_id}"
66
-
67
- for attempt in range(30): # Max 30 seconds
68
- polling_response = requests.get(polling_endpoint, headers=headers)
69
- polling_data = polling_response.json()
70
-
71
- status = polling_data["status"]
72
- print(f"⏳ Status: {status}")
73
-
74
- if status == "completed":
75
- print("βœ… Transcription complete!")
76
- return polling_data["text"]
77
- elif status == "error":
78
- return f"❌ Transcription error: {polling_data.get('error', 'Unknown')}"
79
-
80
- time.sleep(1)
81
-
82
- return "❌ Transcription timed out after 30 seconds"
83
-
84
-
85
- def transcribe_audio_placeholder(audio_file_path):
86
- """Fallback when no API keys are available"""
87
- return """
88
- Doctor: Hello, what brings you in today?
89
- Patient: I've had a cough for about two weeks. It gets worse at night and I feel really tired.
90
- Doctor: Any fever or shortness of breath?
91
- Patient: No fever, but I get winded climbing stairs.
92
- Doctor: I'm going to listen to your lungs. Take a deep breath. I can hear some mild wheezing on the right side.
93
- Patient: Is it serious?
94
- Doctor: It appears to be acute bronchitis. I'll prescribe an inhaler and recommend rest. Follow up in a week.
95
- Patient: Thank you, doctor.
96
- """
97
 
 
 
98
 
99
- # ============================================================
100
- # 2. CLINICAL NOTE GENERATION: Rule-Based NLP
101
- # ============================================================
 
 
 
 
 
102
 
103
- def generate_rule_based_note(transcript):
104
- """Extracts clinical info using keyword matching and pattern recognition"""
105
- t = transcript.lower()
106
-
107
- # Extract symptoms
108
- symptoms = []
109
- if "cough" in t:
110
- if "two week" in t or "2 week" in t:
111
- symptoms.append("Cough (2 weeks duration)")
112
- else:
113
- symptoms.append("Cough")
114
- if "fever" in t:
115
- symptoms.append("Fever")
116
- if "tired" in t or "fatigue" in t:
117
- symptoms.append("Fatigue")
118
- if "wheez" in t:
119
- symptoms.append("Wheezing")
120
- if "breath" in t or "winded" in t:
121
- symptoms.append("Dyspnea on exertion")
122
- if "night" in t and "cough" in t:
123
- symptoms.append("Nocturnal cough")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  if "chest" in t and "pain" in t:
125
- symptoms.append("Chest pain")
126
  if "headache" in t:
127
- symptoms.append("Headache")
128
-
129
- # Determine diagnosis
 
 
 
 
 
 
130
  if "bronchitis" in t:
131
- diagnosis = "Acute Bronchitis"
132
- confidence = "High"
133
- elif "pneumonia" in t:
134
- diagnosis = "Community-Acquired Pneumonia"
135
- confidence = "Moderate"
136
- elif "asthma" in t:
137
- diagnosis = "Asthma Exacerbation"
138
- confidence = "Moderate"
139
- elif "covid" in t or "coronavirus" in t:
140
- diagnosis = "COVID-19 Infection"
141
- confidence = "Moderate"
142
- elif "cough" in t and "wheez" in t:
143
- diagnosis = "Acute Bronchitis with Reactive Airway Disease"
144
- confidence = "Moderate"
145
- elif "cough" in t and len(symptoms) >= 2:
146
- diagnosis = "Upper Respiratory Infection"
147
- confidence = "Moderate"
148
- elif "cough" in t:
149
- diagnosis = "Acute Cough, Etiology Pending"
150
- confidence = "Low"
151
- else:
152
- diagnosis = "Pending Further Workup"
153
- confidence = "Low"
154
-
155
- # Extract physical exam findings
156
- exam_findings = []
157
- if "wheez" in t:
158
- exam_findings.append("Mild expiratory wheezing on auscultation")
159
- if "rhonchi" in t:
160
- exam_findings.append("Rhonchi noted")
161
- if "crackle" in t or "rale" in t:
162
- exam_findings.append("Fine crackles at bases")
163
- if "lung" in t and "clear" in t:
164
- exam_findings.append("Lungs clear bilaterally")
165
- if not exam_findings:
166
- exam_findings.append("Unremarkable")
167
-
168
- # Build treatment plan
169
  plan = []
170
- if "inhaler" in t or "wheez" in t:
171
- plan.append("- Albuterol HFA 90mcg, 2 puffs q4-6h PRN for wheezing")
172
  if "bronchitis" in t:
173
- plan.append("- Supportive care (acute bronchitis typically viral, antibiotics not indicated)")
174
  if "antibiotic" in t:
175
- plan.append("- Consider antibiotic therapy if bacterial infection suspected")
176
  if "rest" in t or "tired" in t:
177
- plan.append("- Recommend rest and increased fluid intake")
178
  if "cough" in t:
179
- plan.append("- OTC dextromethorphan or guaifenesin for symptomatic cough relief")
180
-
181
  if not plan:
182
- plan.append("- Symptomatic management")
183
-
184
- plan.extend([
185
- "- Avoid respiratory irritants and smoking",
186
- "- Follow up in 7 days if symptoms persist or worsen",
187
- "- Return to clinic sooner if fever develops or shortness of breath increases"
188
- ])
189
-
190
- return f"""
191
- SUBJECTIVE:
192
- Chief Complaint: {symptoms[0] if symptoms else 'Not specified'}
193
- Associated Symptoms: {', '.join(symptoms[1:]) if len(symptoms) > 1 else 'None reported'}
194
- Duration: {'2 weeks' if 'two week' in t or '2 week' in t else 'Not specified'}
195
- Onset: {'Gradual' if 'week' in t else 'Not specified'}
196
- Severity: Moderate
197
- Aggravating Factors: {'Nighttime, exertion' if 'night' in t or 'breath' in t else 'None reported'}
198
-
199
- OBJECTIVE:
200
- Physical Exam: {', '.join(exam_findings)}
201
- Vital Signs: Temperature 98.6Β°F, HR 72, BP 118/76, RR 16, SpO2 97% on room air
202
- General: Alert, in no acute distress, well-appearing
203
-
204
- ASSESSMENT:
205
- Primary Diagnosis: {diagnosis}
206
- Clinical Confidence: {confidence}
207
- Differential Diagnoses:
208
- - Viral Upper Respiratory Infection
209
- - Allergic Rhinitis with Post-nasal Drip
210
- - Asthma Exacerbation
211
- - GERD
212
-
213
- PLAN:
214
- {chr(10).join(plan)}
215
-
216
- ---
217
- **πŸ“‹ Note**: This clinical note was generated using rule-based NLP extraction (keyword matching + pattern recognition) demonstrating the underlying logic used in production LLM fine-tuning. At Viscrow Health, the production pipeline used a fine-tuned Llama 3 8B model achieving 94% accuracy in clinical note generation.
218
- """
219
 
 
 
 
 
220
 
221
- def generate_clinical_note(transcript):
222
- """Main clinical note generation function"""
223
- if not transcript or len(transcript) < 20:
224
- return "❌ Transcription too short. Please provide a longer audio file."
225
-
226
- if transcript.startswith("❌"):
227
- return transcript
228
-
229
- # Use rule-based extraction (always works, no API needed)
230
- return generate_rule_based_note(transcript)
231
 
 
 
 
 
 
 
 
 
232
 
233
- # ============================================================
234
- # 3. MAIN PIPELINE
235
- # ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
  def process_encounter(audio):
238
- """Main workflow: Audio β†’ Transcription β†’ SOAP Note"""
239
  if audio is None:
240
- return "⚠️ Please upload an audio file.", ""
241
-
242
- print(f"\n{'='*60}")
243
- print(f"🎀 Processing: {os.path.basename(audio)}")
244
- print(f"πŸ“ File size: {os.path.getsize(audio)} bytes")
245
-
246
- # Step 1: Transcribe audio
247
  if ASSEMBLYAI_API_KEY:
248
- print("πŸ”‘ Using AssemblyAI for transcription...")
249
- transcript = transcribe_audio_assemblyai(audio)
250
  else:
251
- print("⚠️ No AssemblyAI key - using sample transcript")
252
- transcript = "⚠️ DEMO MODE - Add AssemblyAI API key to Secrets for live transcription\n\n"
253
- transcript += transcribe_audio_placeholder(audio)
254
-
255
- print(f"πŸ“ Transcript preview: {transcript[:150]}...")
256
-
257
- # Step 2: Generate clinical note
258
- print("πŸ“‹ Generating clinical note...")
259
- note = generate_clinical_note(transcript)
260
-
261
- print(f"βœ… Complete! Note length: {len(note)} chars")
262
- print(f"{'='*60}\n")
263
-
264
  return transcript, note
265
 
266
 
267
- # ============================================================
268
- # 4. GRADIO USER INTERFACE
269
- # ============================================================
270
-
271
- demo = gr.Blocks(title="OpenScribe - Clinical AI Demo")
272
-
273
- with demo:
274
- gr.Markdown("""
275
- # πŸ₯ OpenScribe: AI Clinical Documentation
276
-
277
- ### **Educational Demonstration of the Viscrow Health Pipeline**
278
- *Built by Arafat Anam Chowdhury*
279
-
280
- ---
281
-
282
- This tool replicates the **exact architecture** used in production for automated clinical documentation:
283
- 1. **Speech-to-Text**: AssemblyAI transcription (100 hours free tier)
284
- 2. **NLP Processing**: Rule-based clinical entity extraction
285
- 3. **Output**: Structured SOAP note ready for EHR integration
286
-
287
- *In production at Viscrow Health, this pipeline used fine-tuned Llama 3 8B for summarization, achieving 94% accuracy and reducing documentation time by 60%.*
 
 
288
  """)
289
-
290
- with gr.Row():
291
- with gr.Column(scale=1):
 
 
292
  audio_input = gr.Audio(
293
  type="filepath",
294
- label="πŸ“ Upload Medical Conversation",
295
  sources=["upload", "microphone"]
296
  )
297
-
298
- run_btn = gr.Button(
299
- "πŸ“‹ Generate Clinical Note",
300
- variant="primary",
301
- size="lg"
302
- )
303
-
304
- # Status indicators
305
- with gr.Group():
306
- gr.Markdown("### πŸ”§ System Status")
307
- if ASSEMBLYAI_API_KEY:
308
- gr.Markdown("βœ… **AssemblyAI:** Connected")
309
- else:
310
- gr.Markdown("⚠️ **AssemblyAI:** Not configured (demo mode)")
311
-
312
- gr.Markdown("βœ… **NLP Engine:** Rule-Based Extraction (Active)")
313
-
314
- gr.Markdown("""
315
- ---
316
- ### πŸ“ Sample Files
317
-
318
- **Test Audio Files:**
319
- - [Medical WAV Sample](https://www.voiptroubleshooter.com/open_speech/american/OSR_us_000_0010_8k.wav)
320
-
321
- **Or record your own conversation:**
322
- *"Hi, what brings you in? - I've had this cough for two weeks. - Any fever? - No. - Let me listen... I hear wheezing. - It's bronchitis."*
323
- """)
324
-
325
  with gr.Column(scale=2):
326
- transcript_output = gr.Textbox(
327
- label="πŸ“ Step 1: Transcription",
328
  lines=6,
329
- placeholder="Transcribed conversation will appear here..."
 
330
  )
331
-
332
- note_output = gr.Textbox(
333
- label="πŸ“‹ Step 2: Generated SOAP Note",
334
- lines=20,
335
- placeholder="Clinical documentation will appear here..."
336
  )
337
-
338
  run_btn.click(
339
  fn=process_encounter,
340
  inputs=audio_input,
341
- outputs=[transcript_output, note_output]
342
  )
343
-
344
- gr.Markdown("""
345
- ---
346
-
347
- ### πŸ”¬ Technical Implementation
348
-
349
- | Component | This Demo | Production (Viscrow Health) |
350
- |-----------|-----------|------------------------------|
351
- | **Speech-to-Text** | AssemblyAI Universal-2 | Azure Speech Services / Whisper |
352
- | **Entity Extraction** | Rule-Based NLP (Keyword + Pattern) | Fine-tuned Llama 3 8B |
353
- | **Output Format** | SOAP Note | SOAP Note + Billing Codes |
354
- | **Error Handling** | Multi-tier Fallback | Validation Pipeline |
355
-
356
- ### πŸ“Š Key Achievements (Viscrow Health)
357
-
358
- - βœ… Integrated speech-to-text and LLM summarization pipelines
359
- - βœ… Designed tools linking clinician notes to billing details
360
- - βœ… Evaluated AI outputs and reduced common errors
361
- - βœ… Built React frontend components for clinical dashboards
362
-
363
- ---
364
-
365
- **⚠️ Educational Disclaimer**: This is a portfolio demonstration. Not for real clinical use.
366
-
367
- [GitHub](https://github.com/arafatanam) | [LinkedIn](https://www.linkedin.com/in/arafat-anam-chowdhury) | [Hugging Face](https://huggingface.co/arafatanam)
368
- """)
369
-
370
-
371
- # ============================================================
372
- # 5. LAUNCH
373
- # ============================================================
374
 
375
  if __name__ == "__main__":
376
- demo.launch(theme=gr.themes.Soft())
 
3
  import requests
4
  import time
5
 
6
+ # ── Environment variables (set these in HF Space Secrets) ──────────────────────
7
  ASSEMBLYAI_API_KEY = os.environ.get("ASSEMBLYAI_API_KEY")
 
8
 
 
 
 
9
 
10
+ # ══════════════════════════════════════════════════════════════════════════════
11
+ # MODULE 1 β€” SPEECH-TO-TEXT (AssemblyAI)
12
+ # Uploads audio to AssemblyAI, requests transcription, polls until complete.
13
+ # Free tier: 100 hours/month. No local GPU needed.
14
+ # ══════════════════════════════════════════════════════════════════════════════
15
+
16
+ def transcribe_audio(audio_file_path):
17
  headers = {"authorization": ASSEMBLYAI_API_KEY}
18
+
19
+ # Upload the audio file in 5 MB chunks
20
+ def read_file(path):
21
+ with open(path, "rb") as f:
22
+ while chunk := f.read(5_242_880):
23
+ yield chunk
24
+
25
+ upload_res = requests.post(
 
 
 
 
 
26
  "https://api.assemblyai.com/v2/upload",
27
  headers=headers,
28
  data=read_file(audio_file_path)
29
  )
30
+ if upload_res.status_code != 200:
31
+ return f"Upload failed: {upload_res.text}"
32
+
33
+ audio_url = upload_res.json()["upload_url"]
34
+
35
+ # Request transcription job
36
+ transcript_res = requests.post(
 
 
 
 
 
 
 
 
37
  "https://api.assemblyai.com/v2/transcript",
38
+ headers=headers,
39
+ json={"audio_url": audio_url, "language_code": "en_us"}
40
  )
41
+ if transcript_res.status_code != 200:
42
+ return f"Transcription request failed: {transcript_res.json().get('error', 'Unknown error')}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ transcript_id = transcript_res.json()["id"]
45
+ polling_url = f"https://api.assemblyai.com/v2/transcript/{transcript_id}"
46
 
47
+ # Poll every 2 seconds until completed or failed (max 2 minutes)
48
+ for _ in range(60):
49
+ poll = requests.get(polling_url, headers=headers).json()
50
+ if poll["status"] == "completed":
51
+ return poll["text"]
52
+ if poll["status"] == "error":
53
+ return f"Transcription error: {poll.get('error', 'Unknown')}"
54
+ time.sleep(2)
55
 
56
+ return "Timed out waiting for transcription. Please try again."
57
+
58
+
59
+ def demo_transcript():
60
+ """Returns a sample transcript when no API key is configured."""
61
+ return (
62
+ "Doctor: Hello, what brings you in today?\n"
63
+ "Patient: I've had a cough for about two weeks. It gets worse at night and I feel really tired.\n"
64
+ "Doctor: Any fever or shortness of breath?\n"
65
+ "Patient: No fever, but I get winded climbing stairs.\n"
66
+ "Doctor: Let me listen to your lungs. I can hear some mild wheezing on the right side. "
67
+ "It appears to be acute bronchitis. I'll prescribe an inhaler and recommend rest. Follow up in a week.\n"
68
+ "Patient: Thank you, doctor."
69
+ )
70
+
71
+
72
+ # ══════════════════════════════════════════════════════════════════════════════
73
+ # MODULE 2 β€” CLINICAL NOTE GENERATION (Rule-Based NLP)
74
+ # Extracts clinical entities from the transcript using keyword matching and
75
+ # builds a structured SOAP note. This is the core NLP logic.
76
+ # ══════════════════════════════════════════════════════════════════════════════
77
+
78
+ def extract_symptoms(text):
79
+ """Identify symptoms mentioned in the transcript."""
80
+ t = text.lower()
81
+ found = []
82
+ if "cough" in t:
83
+ found.append("Cough β€” 2 weeks duration" if ("two week" in t or "2 week" in t) else "Cough")
84
+ if "fever" in t:
85
+ found.append("Fever")
86
+ if "tired" in t or "fatigue" in t:
87
+ found.append("Fatigue")
88
+ if "wheez" in t:
89
+ found.append("Wheezing")
90
+ if "winded" in t or "shortness of breath" in t or "dyspnea" in t:
91
+ found.append("Dyspnea on exertion")
92
+ if "night" in t and "cough" in t:
93
+ found.append("Nocturnal cough")
94
  if "chest" in t and "pain" in t:
95
+ found.append("Chest pain")
96
  if "headache" in t:
97
+ found.append("Headache")
98
+ if "nausea" in t:
99
+ found.append("Nausea")
100
+ return found
101
+
102
+
103
+ def determine_diagnosis(text):
104
+ """Map transcript keywords to a primary diagnosis."""
105
+ t = text.lower()
106
  if "bronchitis" in t:
107
+ return "Acute Bronchitis"
108
+ if "pneumonia" in t:
109
+ return "Community-Acquired Pneumonia"
110
+ if "asthma" in t:
111
+ return "Asthma Exacerbation"
112
+ if "covid" in t or "coronavirus" in t:
113
+ return "COVID-19 Infection"
114
+ if "cough" in t and "wheez" in t:
115
+ return "Acute Bronchitis with Reactive Airway Disease"
116
+ if "cough" in t:
117
+ return "Upper Respiratory Infection"
118
+ return "Pending Further Workup"
119
+
120
+
121
+ def build_plan(text):
122
+ """Construct a treatment plan based on clinical keywords."""
123
+ t = text.lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  plan = []
125
+ if "inhaler" in t or "wheez" in t:
126
+ plan.append("Albuterol HFA 90 mcg β€” 2 puffs q4-6h PRN for wheezing")
127
  if "bronchitis" in t:
128
+ plan.append("Supportive care (acute bronchitis is typically viral; antibiotics not indicated)")
129
  if "antibiotic" in t:
130
+ plan.append("Antibiotic therapy β€” consider if bacterial infection suspected")
131
  if "rest" in t or "tired" in t:
132
+ plan.append("Rest and increased fluid intake")
133
  if "cough" in t:
134
+ plan.append("OTC dextromethorphan or guaifenesin for symptomatic relief")
 
135
  if not plan:
136
+ plan.append("Symptomatic management")
137
+ # Standard advice added to every plan
138
+ plan.append("Avoid respiratory irritants")
139
+ plan.append("Follow up in 7 days, or sooner if symptoms worsen or fever develops")
140
+ return plan
141
+
142
+
143
+ def generate_soap_note(transcript):
144
+ """
145
+ Orchestrates entity extraction and assembles the final SOAP note.
146
+ SOAP = Subjective / Objective / Assessment / Plan
147
+ """
148
+ t = transcript.lower()
149
+ symptoms = extract_symptoms(transcript)
150
+ diagnosis = determine_diagnosis(transcript)
151
+ plan = build_plan(transcript)
152
+
153
+ # Physical exam findings
154
+ findings = []
155
+ if "wheez" in t:
156
+ findings.append("Mild expiratory wheezing on auscultation")
157
+ if "rhonchi" in t:
158
+ findings.append("Rhonchi noted")
159
+ if "crackle" in t or "rale" in t:
160
+ findings.append("Fine crackles at lung bases")
161
+ if "lung" in t and "clear" in t:
162
+ findings.append("Lungs clear bilaterally")
163
+ if not findings:
164
+ findings.append("Unremarkable")
 
 
 
 
 
 
 
 
165
 
166
+ chief = symptoms[0] if symptoms else "Not specified"
167
+ associated = ", ".join(symptoms[1:]) if len(symptoms) > 1 else "None reported"
168
+ duration = "2 weeks" if ("two week" in t or "2 week" in t) else "Not specified"
169
+ agg = "Nighttime, physical exertion" if ("night" in t or "winded" in t) else "None reported"
170
 
171
+ plan_text = "\n".join(f" - {item}" for item in plan)
 
 
 
 
 
 
 
 
 
172
 
173
+ return (
174
+ f"SUBJECTIVE\n"
175
+ f"{'─' * 42}\n"
176
+ f"Chief Complaint : {chief}\n"
177
+ f"Associated Symptoms : {associated}\n"
178
+ f"Duration : {duration}\n"
179
+ f"Aggravating Factors : {agg}\n"
180
+ f"Severity : Moderate\n\n"
181
 
182
+ f"OBJECTIVE\n"
183
+ f"{'─' * 42}\n"
184
+ f"Vital Signs : Temp 98.6F HR 72 BP 118/76 RR 16 SpO2 97% RA\n"
185
+ f"General : Alert, in no acute distress\n"
186
+ f"Physical Exam : {', '.join(findings)}\n\n"
187
+
188
+ f"ASSESSMENT\n"
189
+ f"{'─' * 42}\n"
190
+ f"Primary Diagnosis : {diagnosis}\n"
191
+ f"Differential Diagnoses : Viral URI, Allergic rhinitis, Asthma exacerbation, GERD\n\n"
192
+
193
+ f"PLAN\n"
194
+ f"{'─' * 42}\n"
195
+ f"{plan_text}"
196
+ )
197
+
198
+
199
+ # ══════════════════════════════════════════════════════════════════════════════
200
+ # MAIN PIPELINE
201
+ # Connects transcription -> note generation and handles missing API key gracefully.
202
+ # ══════════════════════════════════════════════════════════════════════════════
203
 
204
  def process_encounter(audio):
 
205
  if audio is None:
206
+ return "No audio provided. Please upload a file or use the microphone.", ""
207
+
208
+ # Step 1: Transcribe
 
 
 
 
209
  if ASSEMBLYAI_API_KEY:
210
+ transcript = transcribe_audio(audio)
 
211
  else:
212
+ # No API key β€” use the built-in demo transcript so the app is still usable
213
+ transcript = "[Demo mode β€” add ASSEMBLYAI_API_KEY to Secrets for live transcription]\n\n" + demo_transcript()
214
+
215
+ if not transcript or len(transcript.strip()) < 20:
216
+ return transcript, "Transcript too short to generate a note."
217
+
218
+ # Step 2: Generate SOAP note
219
+ note = generate_soap_note(transcript)
220
+
 
 
 
 
221
  return transcript, note
222
 
223
 
224
+ # ══════════════════════════════════════════════════════════════════════════════
225
+ # GRADIO INTERFACE
226
+ # ══════════════════════════════════════════════════════════════════════════════
227
+
228
+ css = """
229
+ .gradio-container { max-width: 920px !important; margin: auto; }
230
+ footer { display: none !important; }
231
+ #app-title h1 { font-size: 1.35rem; font-weight: 600; margin: 0 0 0.2rem; }
232
+ #app-title p { font-size: 0.82rem; color: #6b7280; margin: 0 0 1.25rem; padding-bottom: 1rem; border-bottom: 1px solid #e5e7eb; }
233
+ #hint { font-size: 0.8rem; color: #9ca3af; margin-top: 0.4rem; line-height: 1.5; }
234
+ """
235
+
236
+ with gr.Blocks(
237
+ title="OpenScribe β€” AI Clinical Scribe",
238
+ theme=gr.themes.Soft(primary_hue="slate", neutral_hue="slate"),
239
+ css=css
240
+ ) as demo:
241
+
242
+ gr.HTML("""
243
+ <div id="app-title">
244
+ <h1>OpenScribe</h1>
245
+ <p>AI clinical scribe &mdash; upload a doctor&ndash;patient recording to generate a structured SOAP note.</p>
246
+ </div>
247
  """)
248
+
249
+ with gr.Row(equal_height=False):
250
+
251
+ # Left column β€” input
252
+ with gr.Column(scale=1, min_width=240):
253
  audio_input = gr.Audio(
254
  type="filepath",
255
+ label="Recording",
256
  sources=["upload", "microphone"]
257
  )
258
+ run_btn = gr.Button("Generate note", variant="primary")
259
+ gr.HTML('<p id="hint">Supports MP3, WAV, M4A.<br>No file? Record yourself reading a short mock encounter.</p>')
260
+
261
+ # Right column β€” outputs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  with gr.Column(scale=2):
263
+ transcript_out = gr.Textbox(
264
+ label="Transcript",
265
  lines=6,
266
+ placeholder="Transcribed conversation will appear here...",
267
+ show_copy_button=True
268
  )
269
+ note_out = gr.Textbox(
270
+ label="SOAP Note",
271
+ lines=18,
272
+ placeholder="Generated clinical note will appear here...",
273
+ show_copy_button=True
274
  )
275
+
276
  run_btn.click(
277
  fn=process_encounter,
278
  inputs=audio_input,
279
+ outputs=[transcript_out, note_out]
280
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
  if __name__ == "__main__":
283
+ demo.launch()