mssaidat commited on
Commit
ecaada8
·
verified ·
1 Parent(s): c69172c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +265 -0
app.py CHANGED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import math
3
+ import json
4
+ import requests
5
+ import streamlit as st
6
+ from datetime import datetime
7
+
8
+ # Optional: transformers for gpt-oss-20b (graceful fallback to tiny model)
9
+ USE_HF = st.sidebar.toggle("Use Hugging Face model for summaries", value=False, help="Turns on text generation via openai/gpt-oss-20b (requires lots of RAM/GPU).")
10
+ HF_MODEL_PRIMARY = "openai/gpt-oss-20b"
11
+ HF_MODEL_FALLBACK = "gpt2" # small CPU-friendly fallback
12
+ GEN_TEMP = st.sidebar.slider("Gen temperature", 0.0, 1.0, 0.5, 0.05)
13
+
14
+ # ---------- OSM / Overpass helpers ----------
15
+ HEADERS = {
16
+ "User-Agent": "OttawaHotelsAttractionsApp/1.0 (contact: you@example.com)" # Replace with your email/domain
17
+ }
18
+
19
+ def nominatim_bbox(city="Ottawa", country="Canada"):
20
+ """Get bounding box for the city using OSM Nominatim (free)."""
21
+ url = "https://nominatim.openstreetmap.org/search"
22
+ params = {"city": city, "country": country, "format": "json", "limit": 1}
23
+ r = requests.get(url, params=params, headers=HEADERS, timeout=20)
24
+ r.raise_for_status()
25
+ data = r.json()
26
+ if not data:
27
+ return None
28
+ # OSM returns [south, north, west, east]
29
+ south, north, west, east = map(float, data[0]["boundingbox"])
30
+ return (south, west, north, east)
31
+
32
+ def overpass(query):
33
+ url = "https://overpass-api.de/api/interpreter"
34
+ r = requests.post(url, data={"data": query}, headers=HEADERS, timeout=60)
35
+ r.raise_for_status()
36
+ return r.json()
37
+
38
+ def build_overpass_query(bbox, kind="hotel"):
39
+ s, w, n, e = bbox
40
+ if kind == "hotel":
41
+ # tourism=hotel is the standard OSM tag for hotels
42
+ body = f"""
43
+ [out:json][timeout:25];
44
+ (
45
+ node["tourism"="hotel"]({s},{w},{n},{e});
46
+ way["tourism"="hotel"]({s},{w},{n},{e});
47
+ relation["tourism"="hotel"]({s},{w},{n},{e});
48
+ );
49
+ out center tags;
50
+ """
51
+ elif kind == "attraction":
52
+ # Mix of common attraction types
53
+ body = f"""
54
+ [out:json][timeout:30];
55
+ (
56
+ node["tourism"="attraction"]({s},{w},{n},{e});
57
+ way["tourism"="attraction"]({s},{w},{n},{e});
58
+ relation["tourism"="attraction"]({s},{w},{n},{e});
59
+ node["tourism"="museum"]({s},{w},{n},{e});
60
+ way["tourism"="museum"]({s},{w},{n},{e});
61
+ relation["tourism"="museum"]({s},{w},{n},{e});
62
+ node["amenity"="museum"]({s},{w},{n},{e});
63
+ way["amenity"="museum"]({s},{w},{n},{e});
64
+ relation["amenity"="museum"]({s},{w},{n},{e});
65
+ node["tourism"="gallery"]({s},{w},{n},{e});
66
+ way["tourism"="gallery"]({s},{w},{n},{e});
67
+ relation["tourism"="gallery"]({s},{w},{n},{e});
68
+ node["tourism"="theme_park"]({s},{w},{n},{e});
69
+ node["tourism"="zoo"]({s},{w},{n},{e});
70
+ node["tourism"="viewpoint"]({s},{w},{n},{e});
71
+ node["historic"]({s},{w},{n},{e});
72
+ );
73
+ out center tags;
74
+ """
75
+ else:
76
+ raise ValueError("Unsupported kind")
77
+ return body
78
+
79
+ def node_name(tags):
80
+ return tags.get("name") or tags.get("alt_name") or tags.get("official_name") or tags.get("brand") or "Unnamed"
81
+
82
+ def to_osm_link(elem):
83
+ et = elem.get("type", "node")
84
+ eid = elem.get("id")
85
+ return f"https://www.openstreetmap.org/{et}/{eid}"
86
+
87
+ def score_poi(elem, is_hotel=False):
88
+ """Heuristic score to approximate 'top' using tag richness."""
89
+ tags = elem.get("tags", {})
90
+ score = 0
91
+ if "name" in tags: score += 2
92
+ if "wikidata" in tags: score += 3
93
+ if "wikipedia" in tags: score += 3
94
+ if "website" in tags: score += 2
95
+ if "brand" in tags: score += 1
96
+ if "operator" in tags: score += 1
97
+
98
+ if is_hotel:
99
+ # Hotels sometimes have 'stars' tag
100
+ stars = tags.get("stars")
101
+ try:
102
+ if stars:
103
+ score += 2 * float(stars)
104
+ except:
105
+ pass
106
+
107
+ # Chain hotels often provide contact info
108
+ for k in ("phone", "email"):
109
+ if k in tags:
110
+ score += 0.5
111
+
112
+ # Slight boost for nodes with precise location
113
+ if "lat" in elem.get("center", {}) or "lat" in elem:
114
+ score += 0.5
115
+
116
+ return score
117
+
118
+ def prepare_results(overpass_json, is_hotel=False):
119
+ elements = overpass_json.get("elements", [])
120
+ # Normalize center lat/lon
121
+ for e in elements:
122
+ if "center" in e:
123
+ e["lat"], e["lon"] = e["center"]["lat"], e["center"]["lon"]
124
+ else:
125
+ e["lat"], e["lon"] = e.get("lat"), e.get("lon")
126
+
127
+ # Score & filter out nameless entries when possible
128
+ scored = []
129
+ for e in elements:
130
+ tags = e.get("tags", {})
131
+ nm = node_name(tags)
132
+ if not nm or nm.lower().startswith("unnamed"):
133
+ # keep only if it has website/wikidata
134
+ if not (tags.get("website") or tags.get("wikidata")):
135
+ continue
136
+ scored.append((score_poi(e, is_hotel=is_hotel), e))
137
+
138
+ scored.sort(key=lambda x: x[0], reverse=True)
139
+ top5 = [e for _, e in scored[:5]]
140
+
141
+ # Convert to compact dicts
142
+ compact = []
143
+ for e in top5:
144
+ t = e.get("tags", {})
145
+ compact.append({
146
+ "name": node_name(t),
147
+ "lat": e.get("lat"),
148
+ "lon": e.get("lon"),
149
+ "website": t.get("website"),
150
+ "address": t.get("addr:full") or ", ".join(
151
+ filter(None, [
152
+ t.get("addr:housenumber"),
153
+ t.get("addr:street"),
154
+ t.get("addr:city"),
155
+ t.get("addr:postcode")
156
+ ])
157
+ ) or t.get("addr:city"),
158
+ "stars": t.get("stars") if is_hotel else None,
159
+ "phone": t.get("phone"),
160
+ "osm_link": to_osm_link(e)
161
+ })
162
+ return compact
163
+
164
+ # ---------- Hugging Face text generation (optional) ----------
165
+ def gen_blurb(items, title, model_name):
166
+ try:
167
+ from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
168
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
169
+ model = AutoModelForCausalLM.from_pretrained(
170
+ model_name,
171
+ device_map="auto",
172
+ torch_dtype="auto"
173
+ )
174
+ pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
175
+ except Exception as e:
176
+ # fallback tiny model
177
+ from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
178
+ tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_FALLBACK)
179
+ model = AutoModelForCausalLM.from_pretrained(HF_MODEL_FALLBACK)
180
+ pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
181
+
182
+ place_lines = []
183
+ for i, p in enumerate(items, 1):
184
+ line = f"{i}. {p['name']}"
185
+ if p.get("stars"):
186
+ line += f" ({p['stars']}★)"
187
+ place_lines.append(line)
188
+
189
+ prompt = (
190
+ f"Write a warm, vivid 3–4 sentence blurb introducing the list below for a travel app, "
191
+ f"focusing on Ottawa and speaking to families and solo travelers. Avoid exaggeration.\n"
192
+ f"Title: {title}\n"
193
+ f"List:\n" + "\n".join(place_lines)
194
+ )
195
+ out = pipe(prompt, max_new_tokens=120, temperature=GEN_TEMP, do_sample=True, top_p=0.92)
196
+ return out[0]["generated_text"].split("List:")[-1].strip()
197
+
198
+ # ---------- UI ----------
199
+ st.set_page_config(page_title="Ottawa Hotels & Attractions Finder", page_icon="🗺️", layout="wide")
200
+ st.title("🗺️ Ottawa Hotels & Attractions Finder")
201
+ st.caption("Powered by OpenStreetMap (Nominatim + Overpass). Optional summaries by Hugging Face `openai/gpt-oss-20b`.")
202
+
203
+ with st.form("search_form"):
204
+ col1, col2 = st.columns([2,1])
205
+ with col1:
206
+ city = st.text_input("City", value="Ottawa", help="You can change this if you like.")
207
+ country = st.text_input("Country", value="Canada")
208
+ with col2:
209
+ submit = st.form_submit_button("Find Top 5 Hotels & Attractions", use_container_width=True)
210
+
211
+ if submit:
212
+ try:
213
+ bbox = nominatim_bbox(city, country)
214
+ if not bbox:
215
+ st.error("Could not find that city via Nominatim.")
216
+ else:
217
+ st.success(f"Found {city}, {country}. Searching OpenStreetMap…")
218
+ q_hotels = build_overpass_query(bbox, "hotel")
219
+ q_attr = build_overpass_query(bbox, "attraction")
220
+
221
+ hotels_raw = overpass(q_hotels)
222
+ attrs_raw = overpass(q_attr)
223
+
224
+ hotels = prepare_results(hotels_raw, is_hotel=True)
225
+ attrs = prepare_results(attrs_raw, is_hotel=False)
226
+
227
+ colA, colB = st.columns(2, vertical_alignment="start")
228
+
229
+ with colA:
230
+ st.subheader("🏨 Top 5 Hotels")
231
+ if USE_HF and hotels:
232
+ with st.spinner("Writing hotel blurb with Hugging Face…"):
233
+ blurb = gen_blurb(hotels, "Top Hotels in Ottawa", HF_MODEL_PRIMARY)
234
+ st.write(blurb)
235
+ for h in hotels:
236
+ st.markdown(f"**{h['name']}**" + (f" — {h['stars']}★" if h.get('stars') else ""))
237
+ if h["address"]:
238
+ st.write(h["address"])
239
+ meta = []
240
+ if h["website"]: meta.append(f"[Website]({h['website']})")
241
+ if h["osm_link"]: meta.append(f"[OSM]({h['osm_link']})")
242
+ if meta: st.write(" • ".join(meta))
243
+ st.write("")
244
+
245
+ with colB:
246
+ st.subheader("📍 Top 5 Attractions")
247
+ if USE_HF and attrs:
248
+ with st.spinner("Writing attractions blurb with Hugging Face…"):
249
+ blurb = gen_blurb(attrs, "Top Attractions in Ottawa", HF_MODEL_PRIMARY)
250
+ st.write(blurb)
251
+ for a in attrs:
252
+ st.markdown(f"**{a['name']}**")
253
+ if a["address"]:
254
+ st.write(a["address"])
255
+ meta = []
256
+ if a["website"]: meta.append(f"[Website]({a['website']})")
257
+ if a["osm_link"]: meta.append(f"[OSM]({a['osm_link']})")
258
+ if meta: st.write(" • ".join(meta))
259
+ st.write("")
260
+
261
+ st.caption("Notes: Results come from OpenStreetMap community data. Scores approximate ‘top’ via tag richness (stars, website, wikidata, etc.).")
262
+ except requests.HTTPError as e:
263
+ st.error(f"HTTP error: {e}")
264
+ except Exception as ex:
265
+ st.error(f"Something went wrong: {ex}")