File size: 12,252 Bytes
f23c8c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232

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}
    ]}