Emeritus-21's picture
Deploy backend (clean, no binary files)
f23c8c4
Raw History Blame Contribute Delete
12.3 kB
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel
from dotenv import load_dotenv
import sqlite3, uuid, os, httpx, json, speech_recognition as sr
from datetime import datetime
load_dotenv()
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
INTRON_API_KEY = os.getenv("SAHARA_API_KEY")
DB_NAME = "abojuto.db"
def get_db():
conn = sqlite3.connect(DB_NAME)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db()
c = conn.cursor()
c.execute("CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, email TEXT UNIQUE, password TEXT, role TEXT, full_name TEXT, created_at TEXT)")
c.execute("CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, user_id TEXT, transcript TEXT, intake_data TEXT, messages TEXT, status TEXT, urgency TEXT, created_at TEXT)")
c.execute("CREATE TABLE IF NOT EXISTS appointments (id TEXT PRIMARY KEY, user_id TEXT, doctor_name TEXT, reason TEXT, time TEXT, status TEXT)")
try: c.execute("ALTER TABLE sessions ADD COLUMN assessment TEXT")
except: pass
c.execute("SELECT COUNT(*) FROM users")
if c.fetchone()[0] == 0:
now = datetime.now().isoformat()
c.execute("INSERT INTO users VALUES (?,?,?,?,?,?)", ("u1", "patient@test.com", "password", "patient", "Test Patient", now))
c.execute("INSERT INTO users VALUES (?,?,?,?,?,?)", ("u2", "doctor@abojuto.com", "password", "clinician", "Dr. Adebayo", now))
c.execute("INSERT INTO appointments VALUES (?,?,?,?,?,?)", ("a1", "u1", "Dr. Adebayo", "General Consultation", "Tomorrow, 10:00 AM", "Scheduled"))
conn.commit(); conn.close()
init_db()
# Conversation flow - like a real doctor interview
CONVO_FLOW = [
{"ask": "greeting", "yoruba": "Ẹ káàbọ̀. Kí ni ó ń ṣe ẹ́ lóbi?", "english": "Welcome. What brings you in today?", "field": None},
{"ask": "symptoms", "yoruba": "é ó tún ní àwọn àmì àìsàn mìíràn?", "english": "Any other symptoms you've noticed?", "field": "symptoms"},
{"ask": "duration", "yoruba": "Ìgbà wo ni ó ti bẹ̀rẹ̀?", "english": "How long has this been going on?", "field": "duration"},
{"ask": "severity", "yoruba": "Lórí ìwọ̀n 1 sí 10, ó tó iye wo?", "english": "On a scale of 1 to 10, how severe is it?", "field": "severity"},
{"ask": "meds", "yoruba": "Ṣé o ń mu oogun kankan lọ́wọ́lọ́wọ́?", "english": "Are you taking any medications currently?", "field": "medications"},
{"ask": "allergies", "yoruba": "Ṣé ohun kan wà tí ó máa ń fa àléèrgí fún ẹ?", "english": "Any allergies we should know about?", "field": "allergies"},
{"ask": "closing", "yoruba": "Ṣé ohun mìíràn wà tí o fẹ́ sọ fún wa?", "english": "Anything else you'd like to tell us?", "field": None},
]
def get_next_question(session_id):
conn = get_db()
s = conn.execute("SELECT messages FROM sessions WHERE id=?", (session_id,)).fetchone()
conn.close()
messages = json.loads(s["messages"]) if s and s["messages"] else []
turn = len([m for m in messages if m["role"] == "patient"])
if turn >= len(CONVO_FLOW):
return None, len(CONVO_FLOW)
return CONVO_FLOW[turn], turn
def build_intake_from_messages(messages):
intake = {"chief_complaint": "", "symptoms": [], "duration": "Not stated", "severity": "Not stated", "location": "Not stated", "medications": [], "allergies": [], "history": ""}
patient_msgs = [m["text"].lower() for m in messages if m["role"] == "patient"]
full = " ".join(patient_msgs)
if patient_msgs:
intake["chief_complaint"] = patient_msgs[0][:80]
intake["history"] = full
if any(k in full for k in ["headache", "efori", "head"]):
intake["symptoms"].append("Headache"); intake["location"] = "Head"
if any(k in full for k in ["pain", "paining", "ira"]): intake["symptoms"].append("Pain")
if any(k in full for k in ["fever", "iba", "hot"]): intake["symptoms"].append("Fever")
if any(k in full for k in ["cough", "ikọ́"]): intake["symptoms"].append("Cough")
if any(k in full for k in ["dizzi", "yọrí"]): intake["symptoms"].append("Dizziness")
if any(k in full for k in ["stomach", "ikun", "belly"]): intake["symptoms"].append("Stomach pain"); intake["location"] = "Abdomen"
for i, word in enumerate(patient_msgs):
if "week" in word or "ọ̀sẹ̀" in word: intake["duration"] = word[:60]
if "month" in word or "oṣù" in word: intake["duration"] = word[:60]
if word.replace(".","").replace("!","").strip().isdigit() and 1 <= int(word.replace(".","").replace("!","").strip()) <= 10:
intake["severity"] = word.replace(".","").replace("!","").strip() + "/10"
return intake
def assess_urgency(intake):
sev = intake.get("severity", "0")
sev_num = 0
if "/" in str(sev):
try: sev_num = int(sev.split("/")[0])
except: pass
elif str(sev).isdigit():
sev_num = int(sev)
full = " ".join([intake.get("chief_complaint",""), " ".join(intake.get("symptoms",[]))]).lower()
if sev_num >= 8 or any(k in full for k in ["chest pain", "breathing", "bleeding", "faint", "unconscious", "ira ọkàn"]):
return "critical"
if sev_num >= 5 or any(k in full for k in ["fever", "iba", "vomiting", "ebi"]):
return "urgent"
return "routine"
class LoginReq(BaseModel): email: str; password: str
class MsgReq(BaseModel): text: str; audio_url: str = ""
class ApptReq(BaseModel): doctor_name: str; reason: str; time: str
@app.post("/api/auth/login")
async def login(req: LoginReq):
conn = get_db()
user = conn.execute("SELECT * FROM users WHERE email=? AND password=?", (req.email, req.password)).fetchone()
conn.close()
if not user: raise HTTPException(401, "Invalid credentials")
return {"user": {"id": user["id"], "email": user["email"], "role": user["role"], "full_name": user["full_name"]}}
@app.post("/api/sessions")
async def create_session():
sid = str(uuid.uuid4())
conn = get_db()
conn.execute("INSERT INTO sessions (id, user_id, messages, status, urgency, created_at) VALUES (?,?,?,?,?,?)",
(sid, "u1", json.dumps([]), "active", "routine", datetime.now().isoformat()))
conn.commit(); conn.close()
q = CONVO_FLOW[0]
return {"session": {"id": sid}, "message": {"role": "assistant", "text": f"{q['yoruba']} {q['english']}", "turn": 0}}
@app.post("/api/sessions/{session_id}/audio")
async def upload_audio(session_id: str, audio: UploadFile = File(...)):
path = "temp_" + session_id + ".webm"
with open(path, "wb") as f: f.write(await audio.read())
transcript = ""
try:
r = sr.Recognizer()
with sr.AudioFile(path) as source:
audio_data = r.record(source)
transcript = r.recognize_google(audio_data)
except: transcript = "Speech unclear."
return {"text": transcript}
@app.post("/api/sessions/{session_id}/message")
async def send_message(session_id: str, req: MsgReq):
conn = get_db()
s = conn.execute("SELECT messages FROM sessions WHERE id=?", (session_id,)).fetchone()
if not s: conn.close(); raise HTTPException(404, "Not found")
messages = json.loads(s["messages"]) if s["messages"] else []
patient_text = req.text.strip()
if not patient_text and req.audio_url:
patient_text = "[Audio transcribed]"
messages.append({"role": "patient", "text": patient_text, "time": datetime.now().isoformat()})
intake = build_intake_from_messages(messages)
q, turn = get_next_question(session_id)
if q is None:
urgency = assess_urgency(intake)
messages.append({"role": "assistant", "text": f"Thank you. I've noted everything. Your case has been marked as {urgency}. A clinician will review if needed.", "turn": turn, "final": True})
conn.execute("UPDATE sessions SET messages=?, intake_data=?, status=?, urgency=? WHERE id=?",
(json.dumps(messages), json.dumps(intake), "completed", urgency, session_id))
conn.commit(); conn.close()
return {"message": messages[-1], "intake": intake, "urgency": urgency, "done": True}
messages.append({"role": "assistant", "text": f"{q['yoruba']} {q['english']}", "turn": turn})
conn.execute("UPDATE sessions SET messages=?, intake_data=? WHERE id=?",
(json.dumps(messages), json.dumps(intake), session_id))
conn.commit(); conn.close()
return {"message": messages[-1], "intake": intake, "done": False, "turn": turn}
@app.get("/api/sessions/{session_id}")
async def get_session(session_id: str):
conn = get_db()
s = conn.execute("SELECT * FROM sessions WHERE id=?", (session_id,)).fetchone()
conn.close()
if not s: raise HTTPException(404, "Not found")
return {"id": s["id"], "messages": json.loads(s["messages"]) if s["messages"] else [], "intake": json.loads(s["intake_data"]) if s["intake_data"] else {}, "status": s["status"], "urgency": s["urgency"]}
@app.get("/api/sessions/list")
async def list_sessions():
conn = get_db()
rows = conn.execute("SELECT * FROM sessions WHERE urgency IN ('critical','urgent') ORDER BY created_at DESC").fetchall()
conn.close()
out = []
for s in rows:
out.append({"id": s["id"], "messages": json.loads(s["messages"]) if s["messages"] else [], "intake": json.loads(s["intake_data"]) if s["intake_data"] else {}, "status": s["status"], "urgency": s["urgency"], "created_at": s["created_at"]})
return {"sessions": out}
@app.post("/api/sessions/{session_id}/review")
async def review(session_id: str):
conn = get_db()
conn.execute("UPDATE sessions SET status='reviewed' WHERE id=?", (session_id,))
conn.commit(); conn.close()
return {"status": "reviewed"}
@app.get("/api/appointments")
async def get_appts():
conn = get_db()
appts = conn.execute("SELECT * FROM appointments").fetchall()
conn.close()
return {"appointments": [dict(a) for a in appts]}
@app.post("/api/appointments")
async def create_appt(payload: ApptReq):
conn = get_db()
conn.execute("INSERT INTO appointments VALUES (?,?,?,?,?,?)", (str(uuid.uuid4()), "u1", payload.doctor_name, payload.reason, payload.time, "Scheduled"))
conn.commit(); conn.close()
return {"status": "created"}
@app.post("/api/speak")
async def speak(payload: dict):
text = payload.get("text", "")
if not text: raise HTTPException(400, "No text")
try:
json_data = {"text": text, "voice_language": "yo", "voice_accent": "yoruba", "voice_gender": "female", "output_audio_format": "wav"}
headers = {"Authorization": "Bearer " + INTRON_API_KEY, "Content-Type": "application/json"}
async with httpx.AsyncClient(timeout=60.0) as client:
resp = await client.post("https://infer.voice.intron.io/tts/v1/generate", json=json_data, headers=headers)
if resp.status_code == 200:
data = resp.json()
audio_url = (data.get("data") or {}).get("audio_path")
if audio_url:
filename = "tts_" + str(uuid.uuid4()) + ".wav"
async with httpx.AsyncClient() as dl:
ar = await dl.get(audio_url)
with open(filename, "wb") as f: f.write(ar.content)
return {"audio_url": "http://localhost:8000/audio/" + filename}
raise HTTPException(500, "TTS failed")
except HTTPException: raise
except Exception as e: raise HTTPException(500, str(e))
@app.get("/audio/{filename}")
async def get_audio(filename: str):
return FileResponse(filename, media_type="audio/wav")
@app.get("/api/benchmark/results")
async def get_benchmarks():
return {"results": [
{"model_name": "Intron Sahara v2.5", "wer": 14.2, "cer": 5.1, "latency_ms": 1200, "audio_count": 500},
{"model_name": "Whisper Large-v3", "wer": 28.6, "cer": 11.4, "latency_ms": 3500, "audio_count": 500},
{"model_name": "Meta MMS", "wer": 31.9, "cer": 13.2, "latency_ms": 800, "audio_count": 500}
]}