from __future__ import annotations import base64 import io import os from datetime import date from gradio import Server from fastapi.responses import HTMLResponse import logic import storage import vision app = Server() HERE = os.path.dirname(os.path.abspath(__file__)) def _fmt_qty(q): if q is None: return None return int(q) if float(q).is_integer() else q def _payload() -> dict: meds = storage.load_meds() rows = [] for i, m in enumerate(meds): rows.append({ "i": i, "name": m.drug_name, "dose": m.dose, "instruction": m.frequency_text, "quantity": _fmt_qty(m.quantity), "times": [logic.format_time_12h(t) for t in (m.schedule_times or [])], "run_out": m.run_out_date, "refill_or_expiry": m.refill_or_expiry_date, "warning": m.refill_warning, "on_track": not m.refill_warning, }) timed, as_needed = logic.build_daily_agenda(meds) schedule = [{"time": logic.format_time_12h(t), "items": items} for t, items in timed] today = date.today().strftime("%A, %B %d, %Y").replace(" 0", " ") return {"meds": rows, "schedule": schedule, "as_needed": as_needed, "today": today} def _decode(data_url: str): if not data_url or "," not in data_url: return None try: raw = base64.b64decode(data_url.split(",", 1)[1]) from PIL import Image return Image.open(io.BytesIO(raw)).convert("RGB") except Exception: return None @app.api(name="state") def state() -> dict: return _payload() @app.api(name="read_label") def read_label(image_data: str = "") -> dict: img = _decode(image_data) if img is None: return {"ok": False, "message": "Upload a photo of the label first."} try: raw = vision.extract_label(img) data = logic.safe_parse(raw) except Exception as exc: return {"ok": False, "message": f"Couldn't read the label ({exc}). Enter the details below."} if not data: return {"ok": False, "message": "Couldn't read the label clearly — try a sharper, well-lit photo, or enter it below."} return {"ok": True, "fields": { "drug_name": data.get("drug_name") or "", "dose": data.get("dose") or "", "frequency_text": data.get("frequency_text") or "", "quantity": _fmt_qty(data.get("quantity")) or "", "refill_or_expiry_date": data.get("refill_or_expiry_date") or ""}} @app.api(name="save_med") def save_med(drug_name: str = "", dose: str = "", frequency_text: str = "", quantity: str = "", refill_or_expiry_date: str = "", index: int = -1) -> dict: if not (drug_name or "").strip(): return {"ok": False, "message": "A medication name is required.", **_payload()} data = {"drug_name": drug_name.strip(), "dose": (dose or "").strip() or None, "frequency_text": (frequency_text or "").strip() or None, "quantity": logic._coerce_quantity(quantity), "refill_or_expiry_date": (refill_or_expiry_date or "").strip() or None} med = logic.make_medication(data) meds = storage.load_meds() if 0 <= int(index) < len(meds): meds[int(index)] = med else: meds.append(med) storage.save_meds(meds) return {"ok": True, **_payload()} @app.api(name="delete_med") def delete_med(index: int = -1) -> dict: meds = storage.load_meds() if 0 <= int(index) < len(meds): meds.pop(int(index)); storage.save_meds(meds) return {"ok": True, **_payload()} @app.api(name="clear_all") def clear_all() -> dict: storage.save_meds([]) return {"ok": True, **_payload()} @app.get("/", response_class=HTMLResponse) async def homepage(): with open(os.path.join(HERE, "index.html"), "r", encoding="utf-8") as f: return f.read() if __name__ == "__main__": os.environ.setdefault("GRADIO_SERVER_NAME", "0.0.0.0") os.environ.setdefault("GRADIO_SERVER_PORT", "7860") app.launch(show_error=True)