""" app.py — "Did Something Actually Change?" A noise-vs-real-change detector for everyday numbers. No jargon, no dashboards: paste your daily numbers, get a plain answer and the day it happened. Powered by the same Clutch controller as the compute demo (PerceptionLab). """ import matplotlib matplotlib.use("Agg") from matplotlib.figure import Figure import numpy as np import gradio as gr from change import parse_numbers, detect, verdict_text, example_series GREEN = "#2e9e5b" RED = "#d64545" BAND = "#8fbfe0" INK = "#1b1b1b" EXAMPLES = ["— my own numbers (paste or upload below) —", "Weight — diet starts working", "Sleep — nothing changed", "Electricity — bill jumps", "Spending — slow creep"] def make_plot(y, res, unit, period): n = len(y) fig = Figure(figsize=(7.2, 3.9), dpi=96) ax = fig.add_subplot(111) t = np.arange(n) # the "normal wobble" band around a light rolling median k = max(5, res["window"]) pad = np.pad(y, (k // 2, k - k // 2 - 1), mode="edge") roll = np.array([np.median(pad[i:i + k]) for i in range(n)]) ax.fill_between(t, roll - res["scale"], roll + res["scale"], color=BAND, alpha=0.30, label="your normal wobble") ax.plot(t, y, color=INK, lw=1.3, marker="o", ms=2.6, label="your numbers") for i, e in enumerate([e for e in res["events"] if e["kind"] == "shift"]): ax.axvline(e["at"], color=RED, lw=1.6) ax.text(e["at"], ax.get_ylim()[1], f" changed here ({period} {e['at']})", color=RED, fontsize=9, va="top", fontweight="bold") lo, hi = max(0, e["at"] - res["window"]), min(n, e["at"] + res["window"]) ax.hlines(e["before"], max(0, e["at"] - 2 * res["window"]), e["at"], color=GREEN, lw=2.2, alpha=0.9) ax.hlines(e["after"], e["at"], hi, color=RED, lw=2.2, alpha=0.9) u = f" ({unit})" if unit else "" ax.set_xlabel(period) ax.set_ylabel(f"value{u}") verdictish = "real change marked in red" if any( e["kind"] == "shift" for e in res["events"]) else "everything inside the blue band = just noise" ax.set_title(verdictish, fontsize=11) ax.legend(fontsize=8, loc="best") ax.grid(alpha=0.22) fig.tight_layout() return fig def run_check(example, pasted, file_obj, unit, period, sensitivity): if example != EXAMPLES[0]: y, unit, period = example_series(example) note = f"Example data: *{example}* ({len(y)} {period}s). Paste your own to check your life instead." else: y, err = parse_numbers(text=pasted, file_obj=file_obj) if y is None: return err, None note = f"Read {len(y)} values." unit = (unit or "").strip() period = period or "day" # sensitivity slider: 1 = strict … 3 = eager -> gate trip multiplier 1.6 … 0.6 sens = {1: 1.6, 2: 1.0, 3: 0.6}[int(sensitivity)] res = detect(y, sensitivity=sens) text = note + "\n\n" + verdict_text(y, res, unit, period) return text, make_plot(y, res, unit, period) HEADER = """ # 🎲 Did Something Actually Change? Everyone tracks *something* — weight, spending, sleep, an electricity bill, a kid's screen time. And everyone makes the same two mistakes: **panicking at random wobble** and **missing slow real change**. Paste your numbers and get a straight answer: > **"Just noise — relax"**  or  **"Yes — it really changed, around day 47, > from 84.1 to 82.6."** No account, nothing stored, nothing sent anywhere except this page. Try an example first to see what it does. """ FOOTER = """ --- Under the hood this is the *Clutch* — a tiny controller that predicts your next number from your recent trend and only raises its hand when reality breaks the prediction harder than your normal wobble. The same engine gates expensive computation in the [companion Space](https://huggingface.co/spaces/Aluode/Clutch2). Built by Antti Luode (PerceptionLab). It is a statistics tool, not medical or financial advice. *Do not hype. Do not lie. Just show.* """ with gr.Blocks(title="Did Something Actually Change?") as demo: gr.Markdown(HEADER) with gr.Row(): with gr.Column(scale=1): g_ex = gr.Dropdown(EXAMPLES, value=EXAMPLES[1], label="Try an example — or use your own") g_paste = gr.Textbox(lines=8, label="Your numbers, oldest first (one per line — commas as decimals are fine)", placeholder="84,2\n84,5\n83,9\n84,1\n...") g_file = gr.File(label="…or upload a CSV/TXT (last column is used)", file_types=[".csv", ".txt"]) with gr.Row(): g_unit = gr.Textbox(label="unit (optional)", placeholder="kg / € / h", scale=1) g_period = gr.Dropdown(["day", "night", "week", "month", "measurement"], value="day", label="one value per…", scale=1) g_sens = gr.Slider(1, 3, 2, step=1, label="suspicion level (1 = only flag the undeniable · 3 = flag early hints)") g_run = gr.Button("🎲 Check my numbers", variant="primary") with gr.Column(scale=2): g_plot = gr.Plot(label="your numbers, judged") g_md = gr.Markdown() g_run.click(run_check, [g_ex, g_paste, g_file, g_unit, g_period, g_sens], [g_md, g_plot]) gr.Markdown(FOOTER) if __name__ == "__main__": demo.launch()