tryinghard / app.py
mssaidat's picture
Update app.py
9396410 verified
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()