File size: 4,037 Bytes
adb39f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9bade7f
 
0802195
 
 
9bade7f
 
 
 
 
 
 
 
adb39f0
 
 
 
0258ba4
adb39f0
 
 
 
 
 
 
 
 
 
 
 
0258ba4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
adb39f0
 
 
 
0258ba4
 
adb39f0
 
 
 
 
 
 
 
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
from __future__ import annotations
import os
import gradio as gr
from whatchanged.db import init_db
from whatchanged.inference import LlamaCppBackend
from whatchanged.ui.log_tab import build_log_tab
from whatchanged.ui.trends_tab import build_trends_tab
from whatchanged.ui.report_tab import build_report_tab

DB_PATH = os.environ.get("WC_DB", "data/whatchanged.db")
MODEL_PATH = os.environ.get("WC_MODEL", "")  # local GGUF path (wins if it exists)
HF_REPO = os.environ.get("WC_HF_REPO", "zeon01/what-changed-1b")  # else pull from the Hub
HF_FILE = os.environ.get("WC_HF_FILE", "MiniCPM5-1B.Q8_0.gguf")


def resolve_model_path() -> str:
    """Local WC_MODEL wins; otherwise download the published GGUF from the Hub (public,
    no token needed) so the Space is self-contained on first boot."""
    if MODEL_PATH and os.path.exists(MODEL_PATH):
        return MODEL_PATH
    if HF_REPO:
        try:
            from huggingface_hub import hf_hub_download
            return hf_hub_download(repo_id=HF_REPO, filename=HF_FILE)
        except Exception as e:  # noqa: BLE001
            print(f"[model] could not fetch {HF_REPO}/{HF_FILE}: {e}")
    return ""


def make_backend():
    path = resolve_model_path()
    if not path:
        return None
    # Pin threads (WC_THREADS) so llama.cpp doesn't oversubscribe the host's core count on a
    # 2-vCPU Space (the usual cause of absurdly slow CPU inference).
    backend = LlamaCppBackend(path, n_threads=(int(os.environ.get("WC_THREADS", "0")) or None))
    try:
        backend._ensure()  # warm-load now so a bad wheel/model degrades, never crashes mid-click
    except Exception as e:  # noqa: BLE001 — a model load failure must not 500 the UI
        print(f"[model] load failed ({type(e).__name__}: {e}); using deterministic fallback")
        return None
    return backend
    # When None: extraction is a no-op and the report narrative uses a deterministic
    # fallback (bullet summaries joined into a plain sentence).


def maybe_seed(conn) -> None:
    """On the demo Space (WC_SEED_DEMO=1) only, populate an empty DB with a realistic
    60-day decline arc so Trends/Report are non-empty on first open."""
    if os.environ.get("WC_SEED_DEMO") != "1":
        return
    if conn.execute("SELECT COUNT(*) FROM entries").fetchone()[0] > 0:
        return
    try:
        from scripts.seed_demo import seed
        seed(conn)
        print("[seed] demo data loaded")
    except Exception as e:  # noqa: BLE001
        print(f"[seed] skipped: {e}")


# --- Off-Brand custom look: calm health palette, Inter, branded header, no Gradio footer ---
THEME = gr.themes.Soft(
    primary_hue="teal", secondary_hue="cyan", neutral_hue="slate",
    font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
)
CSS = """
.gradio-container {max-width: 880px !important; margin: 0 auto !important;}
#wc-header {background: linear-gradient(135deg,#0d9488,#0e7490); color:#fff;
  padding:22px 26px; border-radius:16px; margin-bottom:6px;}
#wc-header h1 {margin:0; font-size:26px; font-weight:700; color:#fff;}
#wc-header p {margin:6px 0 0; opacity:.92; font-size:14px; color:#fff;}
#wc-header .wc-pill {display:inline-block; background:rgba(255,255,255,.18);
  padding:3px 11px; border-radius:999px; font-size:12px; margin-top:11px;}
footer {display:none !important;}
"""
HEADER = (
    '<div id="wc-header"><h1>📋 What Changed</h1>'
    "<p>A private, on-device Parkinson's diary — describe the day, and a local fine-tuned 1B "
    "structures it into a doctor-ready summary.</p>"
    '<span class="wc-pill">Runs 100% locally · fine-tuned 1B · llama.cpp · not medical advice</span>'
    "</div>"
)


def build_app():
    conn = init_db(DB_PATH)
    maybe_seed(conn)
    backend = make_backend()
    with gr.Blocks(title="What Changed", theme=THEME, css=CSS) as demo:
        gr.HTML(HEADER)
        build_log_tab(conn, backend)
        build_trends_tab(conn)
        build_report_tab(conn, backend)
    return demo


if __name__ == "__main__":
    build_app().launch()