Liat2025's picture
Update app.py
8dde4c9 verified
# 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()