WanderLust / app.py
coreprinciple's picture
Trace: log push failures + boot self-check for HF_TOKEN write access
2b4da11
Raw
History Blame Contribute Delete
7.39 kB
"""DiscoverRoute — gradio.Server entrypoint (Hugging Face Space app_file).
Off-Brand custom frontend: instead of Gradio's default column layout, this serves
a hand-built app-shell (ui/shell.py) from ``app.get("/")`` and exposes the planner
as ``@app.api`` endpoints called from the browser via ``@gradio/client`` (the
required path for ZeroGPU).
Run locally: python app.py
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
# The package lives in src/ and is not pip-installed on the Space container —
# put it on the path before any discoverroute import (also makes plain
# `python app.py` work locally without PYTHONPATH).
sys.path.insert(0, str(Path(__file__).resolve().parent / "src"))
from fastapi.responses import HTMLResponse
from gradio import Server
# ZeroGPU scans for @spaces.GPU functions AT STARTUP — import the module that
# defines one (cheap: model/torch loading stays lazy inside the function).
import discoverroute.narrate.llm # noqa: F401 (registers spaces.GPU)
from discoverroute.pipeline import plan_route
from discoverroute.ui import map as mapui
from discoverroute.ui import shell
N_ALTERNATIVES = 3
app = Server()
def _alt_label(i: int, alt, plain) -> str:
extra = round(alt.discovery.time_min + alt.discovery.dwell_s / 60.0 - plain.time_min)
from collections import Counter
top = Counter(p.category for p in alt.pois).most_common(2)
flavor = ", ".join(c.replace("_", " ") for c, _ in top)
n = len(alt.pois)
return (f"Option {i + 1} · {alt.discovery.distance_m / 1000:.1f} km · +{extra} min · "
f"{n} place{'' if n == 1 else 's'} ({flavor})")
@app.api(name="suggest")
def suggest(query: str = "") -> list:
"""Autocomplete Start/Destination from the local POI-name index."""
from discoverroute.routing.geocode import suggest as _suggest
typed = (query or "").strip()
matches = list(_suggest(typed))
if typed and typed not in matches:
matches = [typed] + matches
return matches or ([typed] if typed else [])
@app.api(name="plan")
def plan(start: str, dest: str, mode: str = "walk", budget: float = 0.5,
vibe: str = "", adventurousness: float = 0.3,
prefer_green: float = 0.0, prefer_quiet: float = 0.0,
profile: str = "", city: str = "") -> dict:
"""Plan a route and return everything the custom frontend renders."""
try:
profile_obj = json.loads(profile) if profile else {}
except Exception: # noqa: BLE001
profile_obj = {}
result = plan_route(
start_query=start, dest_query=dest, mode=mode, budget=budget, vibe=vibe,
adventurousness=adventurousness, prefer_green=prefer_green,
prefer_quiet=prefer_quiet, profile=profile_obj, n_alternatives=N_ALTERNATIVES,
city=city,
)
if result.error:
return {
"error": result.error,
"map_html": mapui.empty_map("Hmm — " + result.error.split(". ")[0].rstrip(".") + "."),
}
geo = {
"start": list(result.start) if result.start else None,
"end": list(result.end) if result.end else None,
"mode": (mode or "walk").lower(),
"start_label": start, "end_label": dest,
}
alts = result.alternatives or []
if not alts: # honest no-detour (or budget 0): plain route + stump state
return {
"error": None,
"no_detour": True,
"map_html": mapui.render_routes(
plain=result.plain, start=result.start, end=result.end),
"summary_md": result.summary_md,
"interpretation_md": result.interpretation_md,
"itinerary_md": result.itinerary_md,
"nodetour_html": _nodetour_html(),
"alternatives": [],
"last_cats": [],
"export": _export_data(result.plain, []),
**geo,
}
alternatives = []
for i, alt in enumerate(alts):
alternatives.append({
"label": _alt_label(i, alt, result.plain),
"map_html": mapui.render_routes(
plain=result.plain, discovery=alt.discovery, pois=alt.pois,
start=result.start, end=result.end),
"summary_md": alt.summary_md,
"itinerary_md": alt.itinerary_md,
"export": _export_data(alt.discovery, alt.pois),
})
return {
"error": None,
"no_detour": False,
"interpretation_md": result.interpretation_md,
"alternatives": alternatives,
"last_cats": [p.category for p in alts[0].pois],
**geo,
}
def _export_data(route, pois) -> dict:
"""Lat/lon waypoints + the real polyline, so the browser can build a Google/
Apple Maps link or a GPX file without re-planning."""
from discoverroute.data import taxonomy
return {
"waypoints": [
{"lat": round(p.lat, 6), "lon": round(p.lon, 6),
"name": taxonomy.display_label(p)}
for p in (pois or [])
],
"coords": [[round(c[0], 6), round(c[1], 6)]
for c in (getattr(route, "coords", None) or [])],
}
def _nodetour_html() -> str:
from discoverroute.ui import design
return design.NO_DETOUR_HTML
@app.get("/", response_class=HTMLResponse)
def homepage() -> str:
return shell.index_html()
def warmup() -> None:
"""Preload graph + CSR + POIs + the vibe embedder so the first request is fast."""
try:
from discoverroute.routing import graph as g
from discoverroute.routing import pois as poimod
g.load_graph()
g.graph_csr()
poimod.load_pois()
print("[warmup] routing graph + POIs ready", flush=True)
except Exception as exc: # noqa: BLE001
print(f"[warmup] graph FAILED: {exc}", flush=True)
# Pre-warm the secondary cities: pull each from the HF dataset (if not already
# local) and load it into memory NOW, at boot, so the first user to pick a city
# waits 0 s — and request-time stays fully offline (files are local by then).
from discoverroute import config
from discoverroute.routing import area as area_mod
for slug in config.PREWARM_CITIES:
try:
area_mod._city_area(slug) # downloads if needed + loads + caches
print(f"[warmup] city ready: {slug}", flush=True)
except Exception as exc: # noqa: BLE001 - one bad city must not block boot
print(f"[warmup] city {slug} skipped: {exc}", flush=True)
try:
from discoverroute.interpret import embed
embed.vibe_to_affinity("quiet green wander")
print("[warmup] vibe embedder ready", flush=True)
except Exception as exc: # noqa: BLE001
print(f"[warmup] embedder skipped: {exc}", flush=True)
# Trace-push self-check: prints whether HF_TOKEN is set + can write the dataset.
try:
from discoverroute.narrate import trace
trace.selftest()
except Exception as exc: # noqa: BLE001
print(f"[trace] selftest skipped: {exc}", flush=True)
if __name__ == "__main__":
warmup()
# _frontend=False: do NOT mount Gradio's default SPA at "/", so our custom
# app-shell route (@app.get("/")) wins. The @app.api endpoints + @gradio/client
# queue still work (that's the API engine, independent of the default UI).
app.launch(show_error=True, _frontend=False)