import gradio as gr import requests import pandas as pd import os # ===== CONFIG ===== GEOAPIFY_KEY = os.getenv("GEOAPIFY_KEY", "YOUR_GEOAPIFY_API_KEY") # https://myprojects.geoapify.com OVERPASS_URL = "https://overpass-api.de/api/interpreter" # Ottawa center (lon, lat) and a city-sized radius (meters) OTTAWA_LON, OTTAWA_LAT = -75.6972, 45.4215 SEARCH_RADIUS_M = 12000 # ~12km # ===== OPTIONAL: HF text gen (summary blurb) ===== USE_HF = False # set True to enable generation HF_MODEL = "openai/gpt-oss-20b" HF_FALLBACK = "gpt2" # CPU-friendly fallback GEN_TEMP = 0.6 def _gen_blurb(kind: str, names: list[str]) -> str: if not USE_HF or not names: return "" try: from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline tok = AutoTokenizer.from_pretrained(HF_MODEL) mdl = AutoModelForCausalLM.from_pretrained(HF_MODEL, device_map="auto", torch_dtype="auto") pipe = pipeline("text-generation", model=mdl, tokenizer=tok) except Exception: from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline tok = AutoTokenizer.from_pretrained(HF_FALLBACK) mdl = AutoModelForCausalLM.from_pretrained(HF_FALLBACK) pipe = pipeline("text-generation", model=mdl, tokenizer=tok) title = "Top Hotels in Ottawa" if kind == "hotel" else "Top Attractions in Ottawa" lines = "\n".join([f"- {n}" for n in names[:5]]) prompt = ( f"Write a concise, friendly 3-sentence intro for a travel app.\n" f"Title: {title}\nPlaces:\n{lines}\n" f"Audience: families and solo travelers. Avoid hype; be helpful." ) out = pipe(prompt, max_new_tokens=100, temperature=GEN_TEMP, do_sample=True, top_p=0.92) return out[0]["generated_text"].split("Places:")[-1].strip() # ===== PRIMARY: GEOAPIFY ===== def search_geoapify(kind: str, query: str | None): """ kind: 'hotel' or 'attraction' """ if GEOAPIFY_KEY in (None, "", "YOUR_GEOAPIFY_API_KEY"): return None # force fallback if key not set # Categories per Geoapify taxonomy if kind == "hotel": categories = "accommodation.hotel" else: # cast a wider net for attractions categories = ",".join([ "tourism.attraction", "entertainment.museum", "entertainment.planetarium", "entertainment.zoo", "heritage.sights", "entertainment.theme_park", "leisure.park", "natural.sights" ]) url = "https://api.geoapify.com/v2/places" params = { "categories": categories, "filter": f"circle:{OTTAWA_LON},{OTTAWA_LAT},{SEARCH_RADIUS_M}", "bias": f"proximity:{OTTAWA_LON},{OTTAWA_LAT}", "limit": 20, "apiKey": GEOAPIFY_KEY } if query: params["text"] = query r = requests.get(url, params=params, timeout=30) if r.status_code != 200: return None data = r.json() feats = data.get("features") or [] if not feats: return None rows = [] for f in feats: p = f.get("properties", {}) name = p.get("name") or "N/A" addr = p.get("formatted") or "N/A" rating = p.get("rating") lat, lon = p.get("lat"), p.get("lon") rows.append({ "Name": name, "Address": addr, "Rating": rating if rating is not None else "β€”", "Map Link": f"https://www.google.com/maps/search/?api=1&query={lat},{lon}", "Source": "Geoapify" }) # Sort: rating desc (if present), then keep Geoapify order (which uses internal rank) def _key(row): r = row["Rating"] return (float(r) if isinstance(r, (int, float, str)) and str(r).replace('.', '', 1).isdigit() else -1.0) rows.sort(key=_key, reverse=True) return rows[:5] # ===== FALLBACK: OVERPASS (OSM) ===== def search_overpass(kind: str, query: str | None): # Ottawa bbox: south,west,north,east bbox = "45.20,-75.90,45.53,-75.40" if kind == "hotel": body = f""" [out:json][timeout:30]; ( node["tourism"="hotel"]({bbox}); way["tourism"="hotel"]({bbox}); relation["tourism"="hotel"]({bbox}); ); out center tags; """ else: body = f""" [out:json][timeout:35]; ( node["tourism"="attraction"]({bbox}); way["tourism"="attraction"]({bbox}); relation["tourism"="attraction"]({bbox}); node["tourism"="museum"]({bbox}); way["tourism"="museum"]({bbox}); relation["tourism"="museum"]({bbox}); node["amenity"="museum"]({bbox}); way["amenity"="museum"]({bbox}); relation["amenity"="museum"]({bbox}); node["tourism"="gallery"]({bbox}); way["tourism"="gallery"]({bbox}); relation["tourism"="gallery"]({bbox}); node["tourism"="theme_park"]({bbox}); node["tourism"="zoo"]({bbox}); node["tourism"="viewpoint"]({bbox}); node["historic"]({bbox}); ); out center tags; """ r = requests.post(OVERPASS_URL, data={'data': body}, timeout=60) if r.status_code != 200: return None data = r.json() elems = data.get("elements") or [] if not elems: return None def pick_name(tags): return tags.get("name") or tags.get("official_name") or tags.get("alt_name") or "Unnamed" # Simple heuristic score to approximate "top" def score(tags): s = 0 if "name" in tags: s += 2 if "wikidata" in tags: s += 3 if "wikipedia" in tags: s += 3 if "website" in tags: s += 2 if "brand" in tags: s += 1 if "operator" in tags: s += 1 if kind == "hotel" and "stars" in tags: try: s += 2 * float(tags["stars"]) except: pass return s rows_scored = [] for e in elems: tags = e.get("tags", {}) nm = pick_name(tags) if "center" in e: lat, lon = e["center"]["lat"], e["center"]["lon"] else: lat, lon = e.get("lat"), e.get("lon") if query and nm and query.lower() not in nm.lower(): # lightweight name filter when a query is provided continue rows_scored.append(( score(tags), { "Name": nm, "Address": ", ".join(filter(None, [ tags.get("addr:housenumber"), tags.get("addr:street"), tags.get("addr:city"), tags.get("addr:postcode") ])) or tags.get("addr:city") or "N/A", "Rating": (tags.get("stars") + "β˜…") if tags.get("stars") else "β€”", "Map Link": f"https://www.google.com/maps?q={lat},{lon}", "Source": "Overpass(OSM)" } )) rows_scored.sort(key=lambda t: t[0], reverse=True) rows = [r for _, r in rows_scored[:5]] return rows or None # ===== WRAPPER ===== def find_places(kind: str, query: str): """ kind: 'Hotels' or 'Attractions' query: optional text (can be empty to just list top 5) """ k = "hotel" if kind.lower().startswith("hotel") else "attraction" data = search_geoapify(k, query.strip() or None) if not data: data = search_overpass(k, query.strip() or None) if not data: return "No results found.", pd.DataFrame([{"Message": "No results found"}]) # Optional summary blurb via HF blurb = _gen_blurb(k, [row["Name"] for row in data]) return blurb, pd.DataFrame(data) # ===== GRADIO UI ===== demo = gr.Interface( fn=find_places, inputs=[ gr.Radio(choices=["Hotels", "Attractions"], value="Hotels", label="What to search"), gr.Textbox(placeholder="(Optional) e.g. 'downtown', 'museum', 'spa' β€” leave empty for top 5", label="Filter text") ], outputs=[ gr.Markdown(label="Summary"), gr.Dataframe(headers=["Name", "Address", "Rating", "Map Link", "Source"], wrap=True) ], title="πŸ—ΊοΈ Ottawa Hotels & Attractions β€” Free API Edition", description="Primary: Geoapify Places (free tier) β€’ Fallback: Overpass (OpenStreetMap). Toggle HF text gen inside the code." ) if __name__ == "__main__": demo.launch()