quantum-drive commited on
Commit
c9c065e
·
verified ·
1 Parent(s): 3ffceef

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +594 -0
app.py ADDED
@@ -0,0 +1,594 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException
2
+ from fastapi.responses import JSONResponse, FileResponse, HTMLResponse
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from pydantic import BaseModel
5
+ from dotenv import load_dotenv
6
+ import google.generativeai as genai
7
+ from gtts import gTTS
8
+ import speech_recognition as sr
9
+ import os
10
+ import json
11
+ import tempfile
12
+ import subprocess
13
+ from datetime import datetime
14
+ import firebase_admin
15
+ from firebase_admin import credentials, firestore
16
+
17
+ # Load environment variables
18
+ load_dotenv()
19
+
20
+ # ==================== ENVIRONMENT VALIDATION ====================
21
+ def validate_environment():
22
+ """Validate required environment variables"""
23
+ required_vars = ["GEMINI_API_KEY", "FIREBASE_CREDENTIALS"]
24
+ missing = [var for var in required_vars if not os.getenv(var)]
25
+ if missing:
26
+ raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
27
+
28
+ validate_environment()
29
+
30
+ # ==================== INITIALIZE SERVICES ====================
31
+ # Initialize Gemini client (NO HARDCODED KEY)
32
+ genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
33
+
34
+ # Initialize Firebase
35
+ firebase_creds = os.getenv("FIREBASE_CREDENTIALS")
36
+ cred_dict = json.loads(firebase_creds)
37
+
38
+ if not firebase_admin._apps:
39
+ cred = credentials.Certificate(cred_dict)
40
+ firebase_admin.initialize_app(cred)
41
+
42
+ db = firestore.client()
43
+
44
+ # Initialize FastAPI
45
+ app = FastAPI(title="Dr. HealBot - Medical Consultation API")
46
+
47
+ # CORS
48
+ app.add_middleware(
49
+ CORSMiddleware,
50
+ allow_origins=["*"],
51
+ allow_credentials=True,
52
+ allow_methods=["*"],
53
+ allow_headers=["*"],
54
+ )
55
+
56
+ # ==================== MODELS ====================
57
+ class ChatRequest(BaseModel):
58
+ message: str
59
+ user_id: str
60
+ language: str = "auto"
61
+
62
+ class PatientData(BaseModel):
63
+ name: str
64
+ patient_profile: dict
65
+ lab_test_results: dict
66
+
67
+ class TTSRequest(BaseModel):
68
+ text: str
69
+ language_code: str = "en"
70
+
71
+ # ==================== SYSTEM PROMPT ====================
72
+ DOCTOR_SYSTEM_PROMPT = """
73
+ You are Dr. HealBot, a calm, knowledgeable, and empathetic virtual doctor.
74
+
75
+ GOAL:
76
+ Hold a natural, focused conversation with the patient to understand their health issue through a series of questions (ONE AT A TIME) before providing comprehensive guidance.
77
+
78
+ PATIENT HISTORY (IMPORTANT):
79
+ The following medical profile belongs to the current patient:
80
+ {patient_summary}
81
+
82
+ RULES FOR PATIENT HISTORY:
83
+ - ALWAYS use patient history in your reasoning (chronic diseases, medications, allergies, surgeries, recent labs, lifestyle).
84
+ - NEVER ignore relevant risks or medication interactions.
85
+ - TAILOR all advice (possible causes, medication safety, red flags) based on the patient's medical profile.
86
+ - Keep references to history natural and brief—only if medically relevant.
87
+
88
+ ⚠️ CRITICAL: ASK ONLY ONE QUESTION AT A TIME - This makes the conversation natural and not overwhelming.
89
+
90
+ RESTRICTIONS:
91
+ - ONLY provide information related to medical, health, or wellness topics.
92
+ - If asked anything non-medical, politely decline:
93
+ "I'm a medical consultation assistant and can only help with health or medical-related concerns."
94
+
95
+ CONVERSATION FLOW:
96
+
97
+ **PHASE 1: INFORMATION GATHERING (Conversational)**
98
+ When a patient first mentions a symptom or health concern:
99
+ - Acknowledge their concern warmly (1-2 sentences)
100
+ - Ask ONLY ONE relevant follow-up question
101
+ - Keep it conversational - like a real doctor's consultation
102
+ - DO NOT give the final detailed response yet
103
+ - DO NOT ask multiple questions at once
104
+
105
+ Examples of good single questions:
106
+ - "How long have you been experiencing this fever?"
107
+ - "Have you taken your temperature? If yes, what was the reading?"
108
+ - "Are you experiencing any other symptoms along with the fever?"
109
+ - "Have you taken any medication for this yet?"
110
+
111
+ **PHASE 2: CONTINUED CONVERSATION**
112
+ - Continue asking clarifying questions ONE AT A TIME until you have enough information
113
+ - Typical consultations need 2-3 exchanges before final assessment
114
+ - Each response should have: brief acknowledgment + ONE question
115
+ - Consider asking about: onset, duration, severity, location, triggers, relieving factors
116
+ - Factor in patient's medical history when asking questions
117
+ - Never overwhelm the patient with multiple questions at once
118
+
119
+ **PHASE 3: FINAL COMPREHENSIVE RESPONSE**
120
+ Only provide the detailed response format AFTER you have gathered sufficient information through conversation.
121
+
122
+ 📋 Based on what you've told me...
123
+ [Brief summary of patient's symptoms, plus any relevant history factors]
124
+
125
+ 🔍 Possible Causes (Preliminary)
126
+ - 1–2 possible explanations using soft language ("It could be…", "This might be…")
127
+ - Include disclaimer that this is not a confirmed diagnosis
128
+ - NOTE: Adjust based on patient's history (conditions, meds, allergies)
129
+
130
+ 💊 Medication Advice (Safe & OTC)
131
+ - Suggest only widely available OTC medicines
132
+ - ENSURE medication is safe given the patient's:
133
+ - allergies
134
+ - chronic illnesses
135
+ - current medications
136
+ - Use disclaimers:
137
+ "Use only if you have no allergies to this medication."
138
+ "Follow packaging instructions or consult a doctor for exact dosing."
139
+
140
+ 💡 Lifestyle & Home Care Tips
141
+ - 2–3 simple, practical suggestions
142
+
143
+ ⚠️ When to See a Real Doctor
144
+ - Warning signs adjusted to the patient's underlying medical risks
145
+
146
+ 📅 Follow-Up Advice
147
+ - One short recommendation about monitoring symptoms or follow-up timing
148
+
149
+ **HOW TO DECIDE WHEN TO GIVE FINAL RESPONSE:**
150
+ Give the detailed final response when you have:
151
+ ✅ Duration of symptoms
152
+ ✅ Severity level
153
+ ✅ Main accompanying symptoms
154
+ ✅ Any relevant patient history considerations
155
+ ✅ Patient has answered at least 2-3 of your questions
156
+
157
+ If patient explicitly asks for immediate advice or says "just tell me what to do", you can provide the final response earlier.
158
+
159
+ CONVERSATION MODES:
160
+
161
+ 1. **Doctor Mode** (for symptoms/health issues):
162
+ - Start with conversational questions
163
+ - Gather information progressively
164
+ - Only provide structured final response after sufficient information
165
+
166
+ 2. **Instructor Mode** (for general medical questions):
167
+ - If patient asks "What is diabetes?" or "How does aspirin work?" - provide direct educational answer
168
+ - Give clear, educational explanations
169
+ - Use short paragraphs or bullet points
170
+ - No need for lengthy information gathering
171
+
172
+ TONE & STYLE:
173
+ - Warm, calm, professional—like a caring doctor in a consultation
174
+ - Conversational and natural in early exchanges
175
+ - Clear, empathetic, no jargon
176
+ - Show you're listening by referencing what they've told you
177
+ - Never give definitive diagnoses; always use soft language
178
+
179
+ IMPORTANT:
180
+ - This is preliminary guidance, not a substitute for professional care.
181
+ - Never provide non-medical information.
182
+ - Be conversational first, comprehensive later.
183
+ - response has No Emoji or No emojis No smileys No flags No pictographs
184
+ """
185
+
186
+ # ==================== HELPER FUNCTIONS ====================
187
+ def generate_patient_summary(patient_data: dict) -> str:
188
+ """Generate a comprehensive summary of patient's medical profile and lab results"""
189
+ if not patient_data:
190
+ return ""
191
+
192
+ summary = "\n🏥 **PATIENT MEDICAL PROFILE**\n"
193
+
194
+ # Patient Profile Section
195
+ if "patient_profile" in patient_data:
196
+ profile = patient_data["patient_profile"]
197
+
198
+ # Critical Medical Info
199
+ if "critical_medical_info" in profile:
200
+ cmi = profile["critical_medical_info"]
201
+ summary += "\n📌 **Critical Medical Information:**\n"
202
+ summary += f"- Major Conditions: {cmi.get('major_conditions', 'None')}\n"
203
+ summary += f"- Current Medications: {cmi.get('current_medications', 'None')}\n"
204
+ summary += f"- Allergies: {cmi.get('allergies', 'None')}\n"
205
+ if cmi.get('past_surgeries_or_treatments') and cmi['past_surgeries_or_treatments'] != 'None':
206
+ summary += f"- Past Surgeries: {cmi.get('past_surgeries_or_treatments')}\n"
207
+
208
+ # Vital Risk Factors
209
+ if "vital_risk_factors" in profile:
210
+ vrf = profile["vital_risk_factors"]
211
+ summary += "\n⚠️ **Risk Factors:**\n"
212
+ if vrf.get('smoking_status') and 'smok' in vrf['smoking_status'].lower():
213
+ summary += f"- Smoking: {vrf.get('smoking_status')}\n"
214
+ if vrf.get('blood_pressure_issue') and vrf['blood_pressure_issue'] != 'No':
215
+ summary += f"- Blood Pressure: {vrf.get('blood_pressure_issue')}\n"
216
+ if vrf.get('cholesterol_issue') and vrf['cholesterol_issue'] != 'No':
217
+ summary += f"- Cholesterol: {vrf.get('cholesterol_issue')}\n"
218
+ if vrf.get('diabetes_status') and 'diabetes' in vrf['diabetes_status'].lower():
219
+ summary += f"- Diabetes: {vrf.get('diabetes_status')}\n"
220
+ if vrf.get('family_history_major_disease'):
221
+ summary += f"- Family History: {vrf.get('family_history_major_disease')}\n"
222
+
223
+ # Organ Health Summary
224
+ if "organ_health_summary" in profile:
225
+ ohs = profile["organ_health_summary"]
226
+ issues = []
227
+ if ohs.get('heart_health') and 'normal' not in ohs['heart_health'].lower():
228
+ issues.append(f"Heart: {ohs['heart_health']}")
229
+ if ohs.get('kidney_health') and 'no' not in ohs['kidney_health'].lower():
230
+ issues.append(f"Kidney: {ohs['kidney_health']}")
231
+ if ohs.get('liver_health') and 'normal' not in ohs['liver_health'].lower() and 'no' not in ohs['liver_health'].lower():
232
+ issues.append(f"Liver: {ohs['liver_health']}")
233
+ if ohs.get('gut_health') and 'normal' not in ohs['gut_health'].lower():
234
+ issues.append(f"Gut: {ohs['gut_health']}")
235
+
236
+ if issues:
237
+ summary += "\n🫀 **Organ Health Concerns:**\n"
238
+ for issue in issues:
239
+ summary += f"- {issue}\n"
240
+
241
+ # Mental & Sleep Health
242
+ if "mental_sleep_health" in profile:
243
+ msh = profile["mental_sleep_health"]
244
+ summary += "\n🧠 **Mental & Sleep Health:**\n"
245
+ summary += f"- Mental Status: {msh.get('mental_health_status', 'Not specified')}\n"
246
+ if msh.get('mental_conditions'):
247
+ summary += f"- Mental Conditions: {msh.get('mental_conditions')}\n"
248
+ summary += f"- Sleep: {msh.get('sleep_hours', 'Not specified')} per night"
249
+ if msh.get('sleep_problems'):
250
+ summary += f" ({msh.get('sleep_problems')})\n"
251
+ else:
252
+ summary += "\n"
253
+
254
+ # Lifestyle
255
+ if "lifestyle" in profile:
256
+ ls = profile["lifestyle"]
257
+ summary += "\n🏃 **Lifestyle:**\n"
258
+ summary += f"- Activity: {ls.get('physical_activity_level', 'Not specified')}\n"
259
+ summary += f"- Diet: {ls.get('diet_type', 'Not specified')}\n"
260
+
261
+ # Lab Test Results Section
262
+ if "lab_test_results" in patient_data:
263
+ lab_results = patient_data["lab_test_results"]
264
+ abnormal_results = []
265
+
266
+ # Check each test category for abnormal results
267
+ for test_category, tests in lab_results.items():
268
+ if isinstance(tests, dict):
269
+ for test_name, result in tests.items():
270
+ if result and isinstance(result, str):
271
+ result_lower = result.lower()
272
+ if any(word in result_lower for word in ['high', 'low', 'elevated', 'borderline']):
273
+ abnormal_results.append(f"{test_name.replace('_', ' ').title()}: {result}")
274
+
275
+ if abnormal_results:
276
+ summary += "\n🔬 **Key Lab Results (Abnormal):**\n"
277
+ for result in abnormal_results[:10]:
278
+ summary += f"- {result}\n"
279
+
280
+ # Health Goals
281
+ if "patient_profile" in patient_data and "primary_health_goals" in patient_data["patient_profile"]:
282
+ goals = patient_data["patient_profile"]["primary_health_goals"]
283
+ summary += f"\n🎯 **Health Goals:** {goals}\n"
284
+
285
+ return summary
286
+
287
+ def save_patient_data(user_id: str, data: dict):
288
+ """Save patient data to Firebase Firestore"""
289
+ data["last_updated"] = datetime.now().isoformat()
290
+ db.collection("patients").document(user_id).set(data)
291
+
292
+ def load_patient_data(user_id: str) -> dict:
293
+ """Load patient data from Firebase Firestore"""
294
+ doc = db.collection("patients").document(user_id).get()
295
+ if doc.exists:
296
+ return doc.to_dict()
297
+ return None
298
+
299
+ def save_chat_history(user_id: str, messages: list):
300
+ db.collection("chat_history").document(user_id).set({
301
+ "messages": messages,
302
+ "last_updated": datetime.now().isoformat()
303
+ })
304
+
305
+ def load_chat_history(user_id: str) -> list:
306
+ doc = db.collection("chat_history").document(user_id).get()
307
+ if doc.exists:
308
+ return doc.to_dict().get("messages", [])
309
+ return []
310
+
311
+ def delete_chat_history(user_id: str):
312
+ db.collection("chat_history").document(user_id).delete()
313
+
314
+ import re
315
+
316
+ def remove_emojis(text: str) -> str:
317
+ """
318
+ Remove all emojis from a string.
319
+ """
320
+ emoji_pattern = re.compile(
321
+ "["
322
+ "\U0001F600-\U0001F64F" # emoticons
323
+ "\U0001F300-\U0001F5FF" # symbols & pictographs
324
+ "\U0001F680-\U0001F6FF" # transport & map symbols
325
+ "\U0001F1E0-\U0001F1FF" # flags
326
+ "\U00002700-\U000027BF" # Dingbats
327
+ "\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs
328
+ "\U00002600-\U000026FF" # Misc symbols
329
+ "\U00002B00-\U00002BFF" # Misc symbols & arrows
330
+ "]+", flags=re.UNICODE
331
+ )
332
+ return emoji_pattern.sub(r'', text)
333
+ import markdown
334
+
335
+ def generate_patient_summary_html(patient_data: dict) -> str:
336
+ """
337
+ Generate patient summary as HTML instead of Markdown.
338
+ """
339
+ md_summary = generate_patient_summary(patient_data)
340
+ html_summary = markdown.markdown(md_summary)
341
+ return html_summary
342
+
343
+
344
+ # ==================== ROOT ENDPOINT ====================
345
+ @app.get("/", response_class=HTMLResponse)
346
+ async def root():
347
+ """Root endpoint - tries to serve index.html, falls back to JSON"""
348
+ try:
349
+ with open("index.html", "r", encoding="utf-8") as f:
350
+ return f.read()
351
+ except FileNotFoundError:
352
+ return JSONResponse({
353
+ "status": "healthy",
354
+ "service": "Dr. HealBot API",
355
+ "version": "1.0.0",
356
+ "endpoints": {
357
+ "chat": "/chat",
358
+ "tts": "/tts",
359
+ "stt": "/stt",
360
+ "patient_data": "/patient-data/{user_id}",
361
+ "chat_history": "/chat-history/{user_id}",
362
+ "patient_summary": "/patient-summary/{user_id}"
363
+ }
364
+ })
365
+
366
+ @app.get("/ping")
367
+ async def ping():
368
+ return {"message": "pong"}
369
+
370
+ # ==================== CHAT ENDPOINT ====================
371
+ @app.post("/chat")
372
+ async def chat(request: ChatRequest):
373
+ """
374
+ Chat endpoint that:
375
+ - Loads patient data and chat history
376
+ - Updates patient data if new symptoms are reported
377
+ - Sends patient summary + chat history + current message to Gemini
378
+ - Returns structured, history-aware medical response
379
+ """
380
+ try:
381
+ user_id = request.user_id
382
+ user_message = request.message.strip()
383
+
384
+ # Load patient data & chat history
385
+ patient_data = load_patient_data(user_id) or {}
386
+ chat_history = load_chat_history(user_id)
387
+
388
+ # Update patient data with new symptom info
389
+ if "new_symptoms" not in patient_data:
390
+ patient_data["new_symptoms"] = []
391
+
392
+ # Simple heuristic: if message contains key symptoms, store it
393
+ symptom_keywords = ["fever", "cough", "headache", "ache", "pain", "rash", "vomit", "nausea"]
394
+ if any(word in user_message.lower() for word in symptom_keywords):
395
+ patient_data["new_symptoms"].append(user_message)
396
+ save_patient_data(user_id, patient_data)
397
+
398
+ # Generate patient summary
399
+ persistent_summary = generate_patient_summary(patient_data) if patient_data else "No patient history available."
400
+
401
+ # Prepare messages for Gemini (convert to single prompt format)
402
+ system_context = f"""
403
+ {DOCTOR_SYSTEM_PROMPT}
404
+
405
+ You MUST always consider the following patient medical data when responding:
406
+
407
+ {persistent_summary}
408
+
409
+ Instructions:
410
+ 1. **Conversational Stage**:
411
+ - Start by acknowledging the patient's symptoms warmly.
412
+ - Ask **only one question at a time** to clarify their condition.
413
+ - Wait for the patient's answer before asking the next question.
414
+ - Limit clarifying questions to **3–4 total**, but ask them sequentially, not all at once.
415
+ - Example:
416
+ - "I'm sorry you're feeling unwell. How long have you had this fever?"
417
+ - Wait for response, then: "Are you experiencing any chills or body aches?"
418
+ - And so on.
419
+ 2. **Structured Guidance Stage**:
420
+ - Only after 3–4 clarifying questions, provide the structured advice in the FINAL RESPONSE FORMAT.
421
+
422
+ - Always factor in patient history (conditions, medications, allergies, labs).
423
+ - Keep tone warm, empathetic, professional.
424
+ - Never give definitive diagnoses; always use soft language.
425
+ """
426
+
427
+ # Build conversation prompt
428
+ conversation_prompt = system_context + "\n\n=== CONVERSATION HISTORY ===\n"
429
+
430
+ # Add previous chat history
431
+ for msg in chat_history:
432
+ role = "Patient" if msg["role"] == "user" else "Dr. HealBot"
433
+ conversation_prompt += f"\n{role}: {msg['content']}\n"
434
+
435
+ # Add current user message
436
+ conversation_prompt += f"\nPatient: {user_message}\n\nDr. HealBot:"
437
+
438
+ # Call Gemini API
439
+ model = genai.GenerativeModel('gemini-2.5-flash')
440
+ response = model.generate_content(
441
+ conversation_prompt,
442
+ generation_config=genai.types.GenerationConfig(
443
+ temperature=0.7,
444
+ max_output_tokens=1024,
445
+ )
446
+ )
447
+
448
+ reply_text = response.text.strip()
449
+
450
+ # Update chat history
451
+ chat_history.append({"role": "user", "content": user_message})
452
+ chat_history.append({"role": "assistant", "content": reply_text})
453
+ save_chat_history(user_id, chat_history)
454
+
455
+ return JSONResponse({
456
+ "reply": reply_text,
457
+ "user_id": user_id,
458
+ "message_count": len(chat_history)
459
+ })
460
+
461
+ except Exception as e:
462
+ print(f"Error in /chat: {str(e)}")
463
+ raise HTTPException(status_code=500, detail=str(e))
464
+
465
+ # ==================== CHAT HISTORY ENDPOINTS ====================
466
+ @app.get("/chat-history/{user_id}")
467
+ async def get_chat_history(user_id: str):
468
+ """Get chat history for a user"""
469
+ try:
470
+ history = load_chat_history(user_id)
471
+ return JSONResponse({
472
+ "user_id": user_id,
473
+ "chat_history": history,
474
+ "message_count": len(history)
475
+ })
476
+ except Exception as e:
477
+ raise HTTPException(status_code=500, detail=str(e))
478
+
479
+ @app.delete("/chat-history/{user_id}")
480
+ async def clear_chat_history(user_id: str):
481
+ """Clear chat history for a user"""
482
+ try:
483
+ delete_chat_history(user_id)
484
+ return JSONResponse({"message": "Chat history cleared", "user_id": user_id})
485
+ except Exception as e:
486
+ raise HTTPException(status_code=500, detail=str(e))
487
+
488
+ # ==================== PATIENT DATA ENDPOINTS ====================
489
+ @app.post("/patient-data/{user_id}")
490
+ async def save_patient(user_id: str, data: PatientData):
491
+ """Save patient profile and lab test results"""
492
+ try:
493
+ patient_info = {
494
+ "name": data.name,
495
+ "patient_profile": data.patient_profile,
496
+ "lab_test_results": data.lab_test_results,
497
+ "last_updated": datetime.now().isoformat()
498
+ }
499
+ save_patient_data(user_id, patient_info)
500
+ return JSONResponse({
501
+ "message": "Patient data saved successfully",
502
+ "user_id": user_id
503
+ })
504
+ except Exception as e:
505
+ raise HTTPException(status_code=500, detail=str(e))
506
+
507
+ @app.get("/patient-data/{user_id}")
508
+ async def get_patient(user_id: str):
509
+ """Get patient data"""
510
+ try:
511
+ data = load_patient_data(user_id)
512
+ if data:
513
+ return JSONResponse(data)
514
+ return JSONResponse({"message": "No patient data found"}, status_code=404)
515
+ except Exception as e:
516
+ raise HTTPException(status_code=500, detail=str(e))
517
+
518
+ @app.get("/patient-summary/{user_id}")
519
+ async def get_patient_summary(user_id: str, format: str = "markdown"):
520
+ """
521
+ Get formatted summary of patient's medical profile and lab results.
522
+ Supports Markdown (default) or HTML output.
523
+ """
524
+ try:
525
+ data = load_patient_data(user_id)
526
+ if not data:
527
+ return JSONResponse({"summary": "No patient data available"})
528
+
529
+ if format.lower() == "html":
530
+ summary = generate_patient_summary_html(data)
531
+ else:
532
+ summary = generate_patient_summary(data)
533
+
534
+ return JSONResponse({"summary": summary, "raw_data": data})
535
+ except Exception as e:
536
+ raise HTTPException(status_code=500, detail=str(e))
537
+
538
+
539
+ # ==================== TTS ENDPOINT ====================
540
+ @app.post("/tts")
541
+ async def text_to_speech(req: TTSRequest):
542
+ try:
543
+ # Remove emojis from the input text
544
+ clean_text = remove_emojis(req.text)
545
+
546
+ tmp_mp3 = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
547
+ tts = gTTS(text=clean_text, lang=req.language_code)
548
+ tts.save(tmp_mp3.name)
549
+
550
+ tmp_wav = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
551
+ subprocess.run(
552
+ ["ffmpeg", "-y", "-i", tmp_mp3.name, "-ar", "44100", "-ac", "2", tmp_wav.name],
553
+ stdout=subprocess.DEVNULL,
554
+ stderr=subprocess.DEVNULL
555
+ )
556
+
557
+ # Delete temporary mp3
558
+ os.remove(tmp_mp3.name)
559
+
560
+ return FileResponse(tmp_wav.name, media_type="audio/wav", filename="speech.wav")
561
+ except Exception as e:
562
+ raise HTTPException(status_code=500, detail=str(e))
563
+
564
+
565
+ # ==================== STT ENDPOINT ====================
566
+ # Initialize speech recognizer
567
+ recognizer = sr.Recognizer()
568
+
569
+ @app.post("/stt")
570
+ async def speech_to_text(file: UploadFile = File(...)):
571
+ try:
572
+ # Save uploaded file temporarily
573
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
574
+ tmp.write(await file.read())
575
+ tmp_path = tmp.name
576
+
577
+ # Use speech_recognition library with Google's free API
578
+ with sr.AudioFile(tmp_path) as source:
579
+ audio_data = recognizer.record(source)
580
+ # Use Google Speech Recognition (free, no API key needed)
581
+ transcript = recognizer.recognize_google(audio_data)
582
+
583
+ # Clean up temp file
584
+ os.remove(tmp_path)
585
+
586
+ return JSONResponse({"transcript": transcript})
587
+ except sr.UnknownValueError:
588
+ raise HTTPException(status_code=400, detail="Could not understand audio")
589
+ except sr.RequestError as e:
590
+ raise HTTPException(status_code=500, detail=f"Speech recognition service error: {str(e)}")
591
+ except Exception as e:
592
+ print(f"Error in STT: {str(e)}")
593
+ raise HTTPException(status_code=500, detail=str(e))
594
+