Spaces:
Runtime error
Runtime error
| # app.py β WaterWise Home (stable cache + robust handlers + light/dark-safe UI) | |
| import os | |
| from pathlib import Path | |
| import csv | |
| import re | |
| import numpy as np | |
| import gradio as gr | |
| # ----------------------- | |
| # HF cache: FIX PERMISSIONS (must be before importing HF libs) | |
| # ----------------------- | |
| os.environ["HF_HOME"] = "/home/user/.cache/huggingface" | |
| os.environ["HF_HUB_CACHE"] = "/home/user/.cache/huggingface/hub" | |
| os.environ["TRANSFORMERS_CACHE"] = "/home/user/.cache/huggingface/transformers" | |
| Path("/home/user/.cache/huggingface").mkdir(parents=True, exist_ok=True) | |
| # ----------------------- | |
| # Load tips CSV (strict) | |
| # ----------------------- | |
| CSV_PATH = "tips_dataset.csv" | |
| if not Path(CSV_PATH).exists(): | |
| raise FileNotFoundError( | |
| f"Missing {CSV_PATH}. Upload it next to app.py with the first line exactly 'tip'." | |
| ) | |
| tips: list[str] = [] | |
| with open(CSV_PATH, "r", encoding="utf-8") as f: | |
| reader = csv.reader(f) | |
| rows = list(reader) | |
| if not rows: | |
| raise ValueError("tips_dataset.csv is empty.") | |
| header = rows[0] | |
| if len(header) != 1 or header[0].strip().lower() != "tip": | |
| raise ValueError("First line of tips_dataset.csv must be exactly one column named 'tip'.") | |
| for r in rows[1:]: | |
| if not r: | |
| continue | |
| t = (r[0] or "").strip() | |
| if t: | |
| tips.append(t) | |
| if len(tips) != 1000: | |
| raise ValueError(f"Expected exactly 1000 tips, found {len(tips)}. Check tips_dataset.csv formatting.") | |
| # ----------------------- | |
| # Embedding model (robust) | |
| # ----------------------- | |
| from sentence_transformers import SentenceTransformer # noqa: E402 | |
| def load_embedder(): | |
| names = [ | |
| "sentence-transformers/all-MiniLM-L6-v2", # canonical repo | |
| "all-MiniLM-L6-v2", # alias fallback | |
| ] | |
| last_err = None | |
| for name in names: | |
| try: | |
| return SentenceTransformer(name, cache_folder="models") | |
| except Exception as e: | |
| last_err = e | |
| raise RuntimeError(f"Failed to load embedder. Last error: {last_err}") | |
| model = load_embedder() | |
| # Warmup so the first user click doesn't trigger a delayed download | |
| _ = model.encode(["warmup"], convert_to_tensor=False) | |
| # ----------------------- | |
| # Precompute tip embeddings | |
| # ----------------------- | |
| def _normalize(m: np.ndarray) -> np.ndarray: | |
| norms = np.linalg.norm(m, axis=1, keepdims=True) + 1e-12 | |
| return m / norms | |
| tip_embs = model.encode(tips, convert_to_numpy=True, show_progress_bar=False) | |
| tip_embs = _normalize(tip_embs.astype(np.float32)) | |
| # ----------------------- | |
| # Recommender | |
| # ----------------------- | |
| SEASON_KEYWORDS = { | |
| "Spring": ["spring", "rain", "garden", "flowers", "bloom"], | |
| "Summer": ["summer", "heat", "hot", "sprinkler", "pool", "evapor"], | |
| "Fall": ["fall", "autumn", "leaves", "harvest"], | |
| "Winter": ["winter", "cold", "freeze", "heater", "boiler", "pipe"], | |
| } | |
| def recommend(user_text: str, season: str): | |
| user_text = (user_text or "").strip() | |
| if not user_text: | |
| return ( | |
| "Tell me one thing you did with water today (e.g., 'long shower', 'ran dishwasher twice'). " | |
| "Choose a season, then hit the button π", | |
| "", | |
| ) | |
| # Embed query | |
| q = model.encode([user_text], convert_to_numpy=True).astype(np.float32) | |
| q = q / (np.linalg.norm(q, axis=1, keepdims=True) + 1e-12) | |
| # Cosine similarity | |
| sims = (tip_embs @ q.T).squeeze(1) | |
| # Gentle seasonal keyword boost | |
| kws = SEASON_KEYWORDS.get(season, []) | |
| if kws: | |
| boost = np.zeros_like(sims, dtype=np.float32) | |
| for i, tip in enumerate(tips): | |
| tl = tip.lower() | |
| if any(k in tl for k in kws): | |
| boost[i] = 0.03 | |
| sims = sims + boost | |
| top_idx = np.argsort(-sims)[:3].tolist() | |
| top_tips = [f"β’ {tips[i]}" for i in top_idx] | |
| summary = ( | |
| f"Based on: **β{user_text}β** Β· Season: **{season}**\n\n" | |
| "Here are 3 smart, doable tips to try next:" | |
| ) | |
| return summary, "\n".join(top_tips) | |
| # ----------------------- | |
| # Savings heuristics (safe) | |
| # ----------------------- | |
| def parse_minutes(text: str, default=10): | |
| if not text: | |
| return default | |
| m = re.search(r'(\d+)\s*(min|minute|minutes)\b', text.lower()) | |
| return int(m.group(1)) if m else default | |
| def estimate_saved_liters(text: str, season: str) -> float: | |
| """ | |
| Tiny heuristic. NEVER raises. Adjust numbers as you like. | |
| """ | |
| t = (text or "").lower() | |
| mins = parse_minutes(t, default=10) | |
| saved = 0.0 | |
| if "shower" in t: | |
| # suggest -2 minutes ~ 6 L/min => ~12 L saved | |
| saved += 12.0 | |
| if "dishwasher" in t: | |
| # suggest full loads | |
| saved += 6.0 | |
| if "garden" in t or "watering" in t or "lawn" in t: | |
| saved += 8.0 if season == "Summer" else 5.0 | |
| return float(max(0.0, saved)) | |
| # ----------------------- | |
| # Combined submit handler (robust) | |
| # ----------------------- | |
| def on_submit(user_text: str, season: str, state: dict | None): | |
| try: | |
| # State default | |
| if not isinstance(state, dict): | |
| state = {"saved_liters": 0.0, "history": []} | |
| # Recs | |
| summary_md, tips_md = recommend(user_text, season) | |
| # Savings | |
| delta = estimate_saved_liters(user_text, season) | |
| state["saved_liters"] = float(state.get("saved_liters", 0.0)) + float(delta) | |
| state["history"].append({"text": user_text, "season": season, "saved": float(delta)}) | |
| saved_headline = f"π§ {state['saved_liters']:.1f} liters saved" | |
| saved_detail = ( | |
| "Drops become streams, streams become rivers. π\n\n" | |
| f"β {state['saved_liters']/12.0:.1f} showers Β· " | |
| f"{state['saved_liters']/6.0:.1f} dishwasher loads Β· " | |
| f"{state['saved_liters']/10.0:.1f} watering cans" | |
| ) | |
| return summary_md, tips_md, saved_headline, saved_detail, state | |
| except Exception as e: | |
| import traceback; traceback.print_exc() | |
| err = f"Sorry β something went wrong: {e}" | |
| return err, "", "π§ 0.0 liters saved", "", state or {"saved_liters": 0.0, "history": []} | |
| def on_clear(state: dict | None): | |
| state = {"saved_liters": 0.0, "history": []} | |
| return "π§ 0.0 liters saved", "", state | |
| def set_example(text: str, season: str): | |
| return text, season | |
| # ----------------------- | |
| # UI (light/dark safe) | |
| # ----------------------- | |
| CUSTOM_CSS = """ | |
| :root { | |
| --water-blue: #1570ef; | |
| --water-blue-2: #2e90fa; | |
| --card-bg: #ffffff; | |
| } | |
| body { background: linear-gradient(180deg, #dfefff 0%, #0c2d4a 800px, #0c2d4a 100%); } | |
| .gradio-container { max-width: 980px !important; margin: 0 auto !important; } | |
| #title-card { background: #ffffff; border-radius: 24px; padding: 22px; box-shadow: 0 10px 30px rgba(16,56,112,0.12); } | |
| #subtitle { color: #1f3b67; } | |
| label, .gr-text, .gr-markdown { color: #0b2c56 !important; } | |
| button { border-radius: 999px !important; } | |
| .gr-button { background: var(--water-blue) !important; border: none !important; color: white !important; } | |
| .gr-button:hover { background: var(--water-blue-2) !important; } | |
| .input-card, .output-card { background: #1f2a36; color: #e6f1ff; border-radius: 16px; padding: 18px; box-shadow: 0 8px 24px rgba(0,0,0,0.25); } | |
| .centered { display: flex; justify-content: center; align-items: center; text-align: center; } | |
| /* ---- Force readable look in dark theme ---- */ | |
| [data-theme="dark"] body { background: linear-gradient(180deg, #dfefff 0%, #0c2d4a 800px, #0c2d4a 100%); } | |
| [data-theme="dark"] #title-card { background: #ffffff !important; } | |
| [data-theme="dark"] label, | |
| [data-theme="dark"] .gr-text, | |
| [data-theme="dark"] .gr-markdown { color: #e6f1ff !important; } | |
| [data-theme="dark"] .gr-button { background: var(--water-blue) !important; color: #ffffff !important; } | |
| """ | |
| theme = gr.themes.Soft(primary_hue="blue", neutral_hue="slate") | |
| with gr.Blocks(css=CUSTOM_CSS, theme=theme, title="WaterWise Home β Your smart water assistant") as demo: | |
| # Header | |
| with gr.Column(elem_id="title-card"): | |
| gr.Markdown("# π§ WaterWise Home", elem_classes=["centered"]) | |
| gr.Markdown( | |
| "I'm your smart chip! Tell me how you used water today, and I'll track your usage, give tips, " | |
| "and challenge you to save more.", | |
| elem_id="subtitle", | |
| elem_classes=["centered"], | |
| ) | |
| # Input / Output | |
| with gr.Row(): | |
| with gr.Column(scale=1, elem_classes=["input-card"]): | |
| user_text = gr.Textbox( | |
| label="Log your water use for today", | |
| placeholder="e.g., Took a 12-minute shower, ran the dishwasher twice, watered the lawn", | |
| lines=2, | |
| value="" | |
| ) | |
| season = gr.Radio( | |
| label="π€οΈ What season is it?", | |
| choices=["Winter", "Spring", "Summer", "Fall"], | |
| value="Winter", | |
| interactive=True, | |
| ) | |
| with gr.Column(scale=1, elem_classes=["output-card"]): | |
| summary_md = gr.Markdown() | |
| tips_md = gr.Markdown() | |
| # Examples row | |
| gr.Markdown("π **Try an example**") | |
| with gr.Row(): | |
| ex1 = gr.Button("π 10-min shower β example (Summer)") | |
| ex2 = gr.Button("π½οΈ Full dishwasher β example (Winter)") | |
| ex3 = gr.Button("π± Watered garden at noon β example (Summer)") | |
| gr.Markdown("**Selected:** βοΈ Winter") | |
| with gr.Row(): | |
| submit = gr.Button("π‘ Get My Water-Saving Tips", scale=1) | |
| clear_btn = gr.Button("π§Ή Clear History", scale=1) | |
| # Savings section | |
| gr.Markdown("π§ **Lifetime Water Saved**") | |
| saved_headline = gr.Markdown("π§ 0.0 liters saved") | |
| saved_detail = gr.Markdown("Drops become streams, streams become rivers. π\n\nβ 0.0 showers Β· 0.0 dishwasher loads Β· 0.0 watering cans") | |
| # App state | |
| state = gr.State({"saved_liters": 0.0, "history": []}) | |
| # Wiring | |
| submit.click( | |
| fn=on_submit, | |
| inputs=[user_text, season, state], | |
| outputs=[summary_md, tips_md, saved_headline, saved_detail, state], | |
| ) | |
| clear_btn.click( | |
| fn=on_clear, | |
| inputs=[state], | |
| outputs=[saved_headline, saved_detail, state], | |
| ) | |
| ex1.click(lambda: set_example("10 minute shower", "Summer"), None, [user_text, season]) | |
| ex2.click(lambda: set_example("Full dishwasher", "Winter"), None, [user_text, season]) | |
| ex3.click(lambda: set_example("Watered garden at noon", "Summer"), None, [user_text, season]) | |
| # Local dev | |
| if __name__ == "__main__": | |
| demo.launch() | |