Spaces:
Running on Zero
Running on Zero
File size: 7,392 Bytes
10cf611 f00d669 10cf611 71d9edc 10cf611 f00d669 bd037ff f00d669 10cf611 f00d669 10cf611 f00d669 10cf611 f00d669 2c5a5d1 10cf611 2c5a5d1 f00d669 10cf611 1add89c 10cf611 f00d669 10cf611 1add89c f00d669 10cf611 2c5a5d1 10cf611 f00d669 2c5a5d1 f00d669 10cf611 2c5a5d1 10cf611 f00d669 10cf611 2c5a5d1 10cf611 f00d669 10cf611 2c5a5d1 10cf611 f00d669 10cf611 f00d669 10cf611 f00d669 10cf611 f00d669 b34c92a 1add89c b34c92a 10cf611 b34c92a f00d669 2b4da11 f00d669 10cf611 | 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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | """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)
|