diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -20,24 +20,19 @@ GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
complaint_log = []
# ══════════════════════════════════════════════════════════════
-# IP GEOLOCATION (pure Python — no browser permissions needed)
+# IP GEOLOCATION
# ══════════════════════════════════════════════════════════════
def get_location_from_ip():
- """
- Tries ipinfo.io then ip-api.com.
- Returns (lat, lon, city, region) or None.
- Works from ANY country — Rahbar auto-detects wherever the user is.
- """
try:
import requests
r = requests.get("https://ipinfo.io/json", timeout=6)
if r.status_code == 200:
- d = r.json()
+ d = r.json()
loc = d.get("loc", "")
if loc and "," in loc:
lat, lon = map(float, loc.split(","))
- return lat, lon, d.get("city","Unknown"), d.get("region","Unknown")
- except Exception:
+ return lat, lon, d.get("city", "Unknown"), d.get("region", "Unknown")
+ except:
pass
try:
import requests
@@ -45,1335 +40,660 @@ def get_location_from_ip():
if r.status_code == 200:
d = r.json()
if d.get("status") == "success":
- return float(d["lat"]), float(d["lon"]), d.get("city","Unknown"), d.get("regionName","Unknown")
- except Exception:
+ return float(d["lat"]), float(d["lon"]), d.get("city", "Unknown"), d.get("regionName", "Unknown")
+ except:
pass
return None
-
def reverse_geocode(lat, lon):
- """Nominatim reverse geocode — returns street/area string or coordinate fallback."""
try:
import requests
- url = (f"https://nominatim.openstreetmap.org/reverse"
- f"?format=jsonv2&lat={lat}&lon={lon}&zoom=17&addressdetails=1")
- r = requests.get(url, headers={"User-Agent":"Rahbar/9.0"}, timeout=6)
+ url = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}&zoom=17&addressdetails=1"
+ r = requests.get(url, headers={"User-Agent": "Rahbar/9.0"}, timeout=6)
if r.status_code == 200:
- d = r.json(); a = d.get("address", {}); parts = []
- for k in ("road","pedestrian","footway","residential"):
- if a.get(k): parts.append(a[k]); break
- for k in ("suburb","neighbourhood","quarter","village","town"):
- if a.get(k): parts.append(a[k]); break
- for k in ("city","county","state_district","state"):
- if a.get(k): parts.append(a[k]); break
- return ", ".join(p.strip() for p in parts if p.strip()) or f"{lat:.5f}, {lon:.5f}"
- except Exception:
+ d = r.json()
+ a = d.get("address", {})
+ parts = []
+ for k in ("road", "pedestrian", "footway", "residential"):
+ if a.get(k):
+ parts.append(a[k])
+ break
+ for k in ("suburb", "neighbourhood", "quarter", "village", "town"):
+ if a.get(k):
+ parts.append(a[k])
+ break
+ for k in ("city", "county", "state_district", "state"):
+ if a.get(k):
+ parts.append(a[k])
+ break
+ if parts:
+ return ", ".join(p.strip() for p in parts if p.strip())
+ except:
pass
return f"{lat:.5f}, {lon:.5f}"
-
-def gps_detect(city_hint):
- """
- Called when user presses 'Detect My Location'.
- Returns (map_fig, status_md, location_text, lat_state, lon_state).
- """
- result = get_location_from_ip()
- if result:
- lat, lon, city, region = result
- addr = reverse_geocode(lat, lon)
- status = (f"📍 Location detected: **{city}, {region}** "
- f"(lat {lat:.4f}, lon {lon:.4f}) \n"
- f"_IP geolocation is approximate — street address filled automatically._")
- fig = build_map(lat, lon, addr)
- return fig, status, addr, lat, lon
- else:
- clat, clon = 30.3753, 69.3451 # Pakistan centre
- status = ("⚠️ Could not detect location automatically. \n"
- "Please select your city/area or type a street name below.")
- fig = build_map(clat, clon, city_hint or "Pakistan")
- return fig, status, "", clat, clon
-
-
-def on_map_click(click_data, city_hint):
- """
- Called when user clicks on the Plotly map.
- click_data is the Plotly clickData dict from gr.Plot.
- Returns (location_text, updated_map_fig).
- """
- if not click_data:
- return "", build_map_city(city_hint)
- try:
- pt = click_data["points"][0]
- lat = pt["lat"]; lon = pt["lon"]
- addr = reverse_geocode(lat, lon)
- fig = build_map(lat, lon, addr)
- return addr, fig
- except Exception:
- return "", build_map_city(city_hint)
-
-
# ══════════════════════════════════════════════════════════════
-# PLOTLY MAP (Scattermap — Gradio 6 safe, no mapbox token)
+# PLOTLY MAP
# ══════════════════════════════════════════════════════════════
PAKISTAN_CENTRE = (30.3753, 69.3451)
-def build_map(lat, lon, label="", zoom=14):
+CITY_COORDS = {
+ "Lahore": (31.5204, 74.3587), "Karachi": (24.8607, 67.0011),
+ "Islamabad": (33.6844, 73.0479), "Rawalpindi": (33.5651, 73.0169),
+ "Faisalabad": (31.4181, 73.0776), "Multan": (30.1575, 71.5249),
+ "Peshawar": (34.0151, 71.5249), "Quetta": (30.1798, 66.9750),
+ "Gujranwala": (32.1877, 74.1945), "Sialkot": (32.4945, 74.5229),
+ "Sukkur": (27.7052, 68.8574), "Hyderabad": (25.3960, 68.3578),
+ "Bahawalpur": (29.3956, 71.6836), "Sargodha": (32.0836, 72.6711),
+ "Abbottabad": (34.1558, 73.2194), "Gilgit": (35.9221, 74.3085),
+ "Gwadar": (25.1216, 62.3254), "Skardu": (35.2971, 75.6360),
+}
+
+ALL_CITIES = sorted(CITY_COORDS.keys())
+
+def build_map(lat, lon, label="", zoom=13):
try:
import plotly.graph_objects as go
- except ImportError:
+ label = label or f"{lat:.4f}, {lon:.4f}"
+ fig = go.Figure(go.Scattermap(
+ lat=[lat], lon=[lon],
+ mode="markers+text",
+ marker=dict(size=16, color="#e8410a", symbol="marker"),
+ text=[label[:50]],
+ textposition="top right",
+ hovertemplate=f"{label} Lat: {lat:.5f} Lon: {lon:.5f} "
+ ))
+ fig.update_layout(
+ map=dict(style="open-street-map", center=dict(lat=lat, lon=lon), zoom=zoom),
+ margin=dict(r=0, t=0, l=0, b=0),
+ height=280,
+ paper_bgcolor="rgba(0,0,0,0)",
+ plot_bgcolor="rgba(0,0,0,0)",
+ clickmode="event+select"
+ )
+ return fig
+ except:
return None
- label = label or f"{lat:.4f}, {lon:.4f}"
- fig = go.Figure(go.Scattermap(
- lat=[lat], lon=[lon],
- mode="markers+text",
- marker=dict(size=18, color="#e8410a", symbol="marker"),
- text=[label[:50]],
- textposition="top right",
- hovertemplate=f"{label} Lat: {lat:.5f} Lon: {lon:.5f} ",
- name="",
- ))
- fig.update_layout(
- map=dict(style="open-street-map", center=dict(lat=lat, lon=lon), zoom=zoom),
- margin=dict(r=0,t=0,l=0,b=0),
- height=300,
- paper_bgcolor="rgba(0,0,0,0)",
- plot_bgcolor="rgba(0,0,0,0)",
- showlegend=False,
- clickmode="event+select",
- )
- return fig
-
def build_map_city(city_name):
- """Build a map centred on the named city (any city in Pakistan or fallback)."""
coords = CITY_COORDS.get(city_name)
if coords:
- lat, lon = coords
- zoom = 12
- else:
- # Try geocoding the city name
- try:
- import requests
- url = (f"https://nominatim.openstreetmap.org/search"
- f"?q={urllib.parse.quote(city_name+', Pakistan')}"
- f"&format=jsonv2&limit=1")
- r = requests.get(url, headers={"User-Agent":"Rahbar/9.0"}, timeout=4)
- if r.status_code == 200 and r.json():
- d = r.json()[0]
- lat, lon, zoom = float(d["lat"]), float(d["lon"]), 12
- else:
- lat, lon, zoom = PAKISTAN_CENTRE[0], PAKISTAN_CENTRE[1], 5
- except Exception:
- lat, lon, zoom = PAKISTAN_CENTRE[0], PAKISTAN_CENTRE[1], 5
- return build_map(lat, lon, city_name, zoom)
-
-
-def update_map_on_city(city):
- return build_map_city(city)
-
-def update_map_on_location(city, area, loc_text):
- query = loc_text.strip() or area or city
- # Try to geocode the typed location
- try:
- import requests
- q = f"{query}, {city}, Pakistan"
- url = (f"https://nominatim.openstreetmap.org/search"
- f"?q={urllib.parse.quote(q)}&format=jsonv2&limit=1")
- r = requests.get(url, headers={"User-Agent":"Rahbar/9.0"}, timeout=4)
- if r.status_code == 200 and r.json():
- d = r.json()[0]
- return build_map(float(d["lat"]), float(d["lon"]), query, zoom=15)
- except Exception:
- pass
- return build_map_city(city)
+ return build_map(coords[0], coords[1], city_name, 12)
+ return build_map(PAKISTAN_CENTRE[0], PAKISTAN_CENTRE[1], "Pakistan", 5)
+def gps_detect(city_hint):
+ result = get_location_from_ip()
+ if result:
+ lat, lon, city, region = result
+ addr = reverse_geocode(lat, lon)
+ status = f"📍 Location detected: **{city}, {region}** (lat {lat:.4f}, lon {lon:.4f})"
+ fig = build_map(lat, lon, addr)
+ return fig, status, addr, lat, lon
+ else:
+ status = "⚠️ Could not detect location automatically. Please select your city/area."
+ fig = build_map_city(city_hint)
+ return fig, status, "", None, None
# ══════════════════════════════════════════════════════════════
# KNOWLEDGE BASE
# ══════════════════════════════════════════════════════════════
-RAG_DOCUMENTS = [
- {"id":"g1","category":"Garbage",
- "title":"Punjab Waste Management Act 2014 — Citizen Rights",
- "content":"Under Punjab Waste Management Act 2014 any citizen can file a garbage complaint. Fine Rs.500-50,000. Local government must act within 48 hours. Helpline: 1139. Citizens can demand written response and escalate to CM Portal.",
- "laws":["Punjab Waste Management Act 2014","Pakistan EPA 1997 Section 11","Punjab LGA 2022 Schedule II"],
- "hotline":"1139","authority":"Solid Waste Management Board / Local Government",
- "response_time":"48 hours","fine":"Rs. 500 – 50,000"},
- {"id":"g2","category":"Garbage",
- "title":"Urban Solid Waste — City-level Responsibility",
- "content":"Failure to collect garbage violates EPA 1997 Section 11. Over 1 week = Public Nuisance PPC Section 268. Lahore LWMC: 042-111-222-888. Karachi KMC: 021-99231677.",
- "laws":["PPC Section 268","Punjab Waste Management Act 2014","EPA 1997 Section 11"],
- "hotline":"1139","authority":"LWMC / KMC / Local SWMB",
- "response_time":"48 hours","fine":"Rs. 500 – 50,000"},
- {"id":"g3","category":"Garbage",
- "title":"Garbage Complaint Escalation Ladder",
- "content":"If authority fails: 1.Union Council 2.DC office 3.CM Cell 0800-02345 4.citizenportal.gov.pk 5.Federal Ombudsman 051-9204551 6.High Court Writ. Compensation under EPA 1997 Section 14.",
- "laws":["Constitution Article 9 & 14","EPA 1997 Section 14","PPC Section 268"],
- "hotline":"0800-02345","authority":"CM Complaints Cell / Federal Ombudsman",
- "response_time":"3 working days","fine":"Compensation claimable"},
- {"id":"p1","category":"Pot Hole",
- "title":"National Highways Safety Ordinance 2000 — Pothole Rights",
- "content":"NHA responsible for road potholes. Repairs within 72 hours. Punjab LGA 2022 Section 54 also applies. Vehicle damage = compensation under Motor Vehicles Ordinance 1965. NHA: 051-9032800.",
- "laws":["National Highways Safety Ordinance 2000","Punjab LGA 2022 Section 54","Motor Vehicles Ordinance 1965"],
- "hotline":"051-9032800","authority":"NHA / C&W Department / LDA",
- "response_time":"72 hours","fine":"Authority liable for vehicle damage"},
- {"id":"p2","category":"Pot Hole",
- "title":"Road Accident Due to Pothole — Legal Recourse",
- "content":"If accident: 1.Police report 2.Photograph 3.Written notice to NHA/LDA 4.Negligence claim Tort Law 5.Federal Ombudsman 051-9204551 6.High Court Writ.",
- "laws":["Tort Law Negligence","NHA Safety Ordinance 2000","Constitution Article 9"],
- "hotline":"051-9204551","authority":"Federal Ombudsman / High Court",
- "response_time":"72 hours","fine":"Compensation for injury/damage"},
- {"id":"w1","category":"Pipe Leakage",
- "title":"Punjab Water Act 2019 — Pipe Leakage Rights",
- "content":"Punjab Water Act 2019 Section 23: WASA must repair within 24 hours. Fine Rs.10,000-500,000. WASA Lahore: 042-99200300. WASA Karachi: 021-99231677. SC 2018: clean water is fundamental right.",
- "laws":["Punjab Water Act 2019 Section 23","WASA Act Bylaws","Constitution Article 9"],
- "hotline":"042-99200300","authority":"WASA / Pakistan Water Authority",
- "response_time":"24 hours","fine":"Rs. 10,000 – 5,00,000"},
- {"id":"w2","category":"Pipe Leakage",
- "title":"Contaminated Water — Legal Rights",
- "content":"EPA 1997 Section 13 makes polluting water a criminal offence. National Drinking Water Policy 2009 mandates WHO standards. Claim compensation if contaminated water causes illness. Suspend billing if contaminated.",
- "laws":["EPA 1997 Section 13","National Drinking Water Policy 2009","Punjab Water Act 2019"],
- "hotline":"042-99200300","authority":"WASA / Pakistan Water Authority / EPA",
- "response_time":"24-48 hours","fine":"Compensation for health damage"},
- {"id":"w3","category":"Pipe Leakage",
- "title":"WASA Did Not Act — Escalation Steps",
- "content":"If WASA fails: 1.Call WASA 2.Written application WASA office 3.DC office 4.CM Cell 0800-02345 5.citizenportal.gov.pk 6.PWA 051-9246150 7.Federal Ombudsman 051-9204551 8.High Court Article 9.",
- "laws":["Punjab Water Act 2019","Constitution Article 9","EPA 1997"],
- "hotline":"0800-02345","authority":"CM Complaints Cell / PWA / Federal Ombudsman",
- "response_time":"Escalation pathway","fine":"Rs. 10,000–5,00,000 + compensation"},
- {"id":"r1","category":"General",
- "title":"Fundamental Rights of Pakistani Citizens",
- "content":"Article 9: Right to Life includes clean environment SC 2018. Article 14: Dignity. Article 19A: Right to Information. Citizen Portal must get legal response. You can file FIR if public body fails.",
- "laws":["Constitution Article 9","Constitution Article 14","Constitution Article 19A"],
- "hotline":"0800-02345","authority":"High Court / Supreme Court / Federal Ombudsman",
- "response_time":"3 working days","fine":"Authority accountable"},
- {"id":"r2","category":"General",
- "title":"How to File a Civic Complaint — Complete Guide",
- "content":"1.Photo with date/time 2.Exact location 3.Call helpline get number 4.If no action 48-72h use CM Portal 5.citizenportal.gov.pk most effective 6.Share WhatsApp. Numbers: Garbage 1139, Roads 051-9032800, WASA 042-99200300, CM 0800-02345.",
- "laws":["Right to Information Act 2017","Constitution Article 9","EPA 1997"],
- "hotline":"0800-02345","authority":"Pakistan Citizen Portal",
- "response_time":"3-5 working days","fine":"N/A"},
- {"id":"r3","category":"General",
- "title":"Federal Ombudsman — Role and Process",
- "content":"Federal Ombudsman (Wafaqi Mohtasib) hears complaints against government. Free to file. Decision 60 days. Phone: 051-9204551 | mohtasib.gov.pk. Can appeal to President.",
- "laws":["Federal Ombudsmen Institutional Reforms Act 2013"],
- "hotline":"051-9204551","authority":"Federal Ombudsman (Mohtasib)",
- "response_time":"60 days","fine":"Binding recommendations"},
-]
-
-# ── Knowledge retrieval engine ─────────────────────────────
-class KnowledgeEngine:
- def __init__(self):
- self.documents = RAG_DOCUMENTS
- self.vectorizer = None
- self.doc_matrix = None
- self._ready = False
-
- def initialize(self):
- if self._ready: return True
- try:
- from sklearn.feature_extraction.text import TfidfVectorizer
- corpus = [
- f"{d['title']} {d['content']} {' '.join(d['laws'])} {d['category']}"
- for d in self.documents
- ]
- self.vectorizer = TfidfVectorizer(
- analyzer='char_wb', ngram_range=(2,5),
- max_features=8000, sublinear_tf=True, min_df=1)
- self.doc_matrix = self.vectorizer.fit_transform(corpus)
- self._ready = True; return True
- except Exception as e:
- print(f"KE init error: {e}"); return False
-
- def retrieve(self, query, top_k=3):
- if not self._ready: self.initialize()
- if self._ready:
- try:
- from sklearn.metrics.pairwise import cosine_similarity
- import numpy as np
- q_vec = self.vectorizer.transform([query])
- scores = cosine_similarity(q_vec, self.doc_matrix)[0]
- idxs = np.argsort(scores)[::-1][:top_k]
- res = [dict(self.documents[i], score=float(scores[i]))
- for i in idxs if scores[i] > 0.01]
- return res if res else self._fallback(query, top_k)
- except Exception:
- pass
- return self._fallback(query, top_k)
-
- def _fallback(self, query, top_k=3):
- q = query.lower()
- kw = {
- "Garbage": ["garbage","waste","trash","kachra","1139","sanitation"],
- "Pot Hole": ["pothole","road","nha","sadak","gara"],
- "Pipe Leakage": ["water","wasa","pipe","leakage","contaminated","pani"],
- }
- cat = next((c for c, ks in kw.items() if any(k in q for k in ks)), None)
- matched = [d for d in self.documents if cat and d['category'] == cat]
- matched += [d for d in self.documents if d['category']=='General' and d not in matched]
- return matched[:top_k] or self.documents[:top_k]
-
- def format_context(self, docs):
- if not docs: return ""
- ctx = "Relevant Legal Information:\n\n"
- for i, d in enumerate(docs, 1):
- ctx += (f"[{i}] {d['title']}\n{d['content'][:350]}\n"
- f"Laws: {', '.join(d['laws'][:2])}\n"
- f"Helpline: {d['hotline']} | Response: {d['response_time']}\n\n")
- return ctx
-
-ke = KnowledgeEngine()
-ke.initialize()
-
-# ══════════════════════════════════════════════════════════════
-# STATIC DATA
-# ══════════════════════════════════════════════════════════════
-# Major cities with coordinates — but the app works for ANY
-# Pakistani location via Nominatim geocoding
-CITY_COORDS = {
- "Lahore": (31.5204, 74.3587),
- "Karachi": (24.8607, 67.0011),
- "Islamabad": (33.6844, 73.0479),
- "Rawalpindi": (33.5651, 73.0169),
- "Faisalabad": (31.4181, 73.0776),
- "Multan": (30.1575, 71.5249),
- "Peshawar": (34.0151, 71.5249),
- "Quetta": (30.1798, 66.9750),
- "Gujranwala": (32.1877, 74.1945),
- "Sialkot": (32.4945, 74.5229),
- "Sukkur": (27.7052, 68.8574),
- "Hyderabad": (25.3960, 68.3578),
- "Bahawalpur": (29.3956, 71.6836),
- "Sargodha": (32.0836, 72.6711),
- "Dera Ghazi Khan": (30.0564, 70.6349),
- "Gujrat": (32.5736, 74.0789),
- "Sheikhupura":(31.7167, 73.9850),
- "Mardan": (34.1988, 72.0404),
- "Mingora": (34.7717, 72.3600),
- "Nawabshah": (26.2442, 68.4100),
- "Chiniot": (31.7189, 72.9787),
- "Larkana": (27.5570, 68.2140),
- "Mirpur Khas":(25.5269, 69.0138),
- "Abbottabad": (34.1558, 73.2194),
- "Muzaffarabad":(34.3700, 73.4710),
- "Gilgit": (35.9221, 74.3085),
- "Turbat": (26.0000, 63.0500),
- "Khuzdar": (27.8000, 66.6167),
- "Kharian": (32.8147, 73.8852),
- "Hafizabad": (32.0710, 73.6880),
- "Sahiwal": (30.6706, 73.1064),
- "Kasur": (31.1167, 74.4500),
- "Okara": (30.8138, 73.4544),
- "Wah Cantt": (33.7667, 72.7000),
- "Attock": (33.7667, 72.3583),
- "Toba Tek Singh":(30.9709, 72.4827),
- "Jhang": (31.2681, 72.3181),
- "Mianwali": (32.5856, 71.5435),
- "Khushab": (32.2979, 72.3549),
- "Chakwal": (32.9310, 72.8524),
- "Jhelum": (32.9425, 73.7257),
- "Ghotki": (28.0050, 69.3172),
- "Jacobabad": (28.2769, 68.4376),
- "Shikarpur": (27.9557, 68.6376),
- "Khairpur": (27.5295, 68.7592),
- "Dadu": (26.7319, 67.7764),
- "Kamber": (27.5864, 68.0022),
- "Tharparkar": (24.7136, 70.2491),
- "Badin": (24.6560, 68.8375),
- "Thatta": (24.7461, 67.9236),
- "Tank": (32.2145, 70.3776),
- "Bannu": (32.9891, 70.6056),
- "Kohat": (33.5890, 71.4411),
- "Nowshera": (34.0153, 71.9747),
- "Charsadda": (34.1488, 71.7307),
- "Swabi": (34.1200, 72.4700),
- "Buner": (34.5444, 72.5000),
- "Dir": (35.2073, 71.8787),
- "Chitral": (35.8510, 71.7875),
- "Dera Ismail Khan":(31.8314, 70.9019),
- "Zhob": (31.3416, 69.4486),
- "Loralai": (30.3723, 68.5931),
- "Kalat": (29.0231, 66.5882),
- "Panjgur": (26.9680, 64.0985),
- "Gwadar": (25.1216, 62.3254),
- "Surab": (28.4900, 66.2600),
- "Chaman": (30.9210, 66.4460),
- "Ziarat": (30.3820, 67.7280),
- "Nushki": (29.5520, 66.0190),
- "Kharan": (28.5880, 65.4160),
- "Washuk": (27.7780, 64.8770),
- "Haripur": (33.9980, 72.9349),
- "Mansehra": (34.3300, 73.1970),
- "Battagram": (34.6800, 73.0200),
- "Kohistan": (35.4486, 73.0942),
- "Shangla": (34.6177, 72.5200),
- "Torghar": (34.9000, 72.6000),
- "Karak": (33.1170, 71.0940),
- "Lakki Marwat":(32.6070, 70.9120),
- "South Waziristan":(32.3160, 69.8260),
- "North Waziristan":(33.0000, 70.0000),
- "Kurram": (33.6716, 70.1032),
- "Orakzai": (33.6333, 71.0000),
- "Khyber": (33.9460, 71.1590),
- "Bajaur": (34.8300, 71.5600),
- "Mohmand": (34.4200, 71.3100),
- "Mirpur AJK": (33.1445, 73.7513),
- "Rawalakot": (33.8579, 73.7610),
- "Bagh AJK": (33.9847, 73.7803),
- "Kotli": (33.5179, 73.9025),
- "Poonch AJK": (33.7737, 74.0949),
- "Neelum AJK": (34.5900, 74.2100),
- "Skardu": (35.2971, 75.6360),
- "Ghanche": (35.4950, 76.1500),
- "Astore": (35.3660, 74.8590),
- "Diamer": (35.5000, 73.7000),
- "Hunza": (36.3167, 74.6500),
- "Nagar": (36.1000, 74.4167),
- "Shigar": (35.5000, 75.6700),
- "Ghizer": (36.2333, 73.5000),
-}
-
-# ── All cities list for dropdown (sorted) ─────────────────
-ALL_CITIES = sorted(CITY_COORDS.keys())
-
ISSUE_TYPES = ["Garbage", "Pot Hole", "Pipe Leakage"]
-LANGUAGES = ["English", "Urdu", "Punjabi", "Sindhi"]
-LANG_CODES = {"English":"en","Urdu":"ur","Punjabi":"ur","Sindhi":"ur"}
-WASTE_CLASS_IDS = {24,25,26,27,28,32,33,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54}
+LANGUAGES = ["English", "Urdu", "Punjabi", "Sindhi"]
+LANG_CODES = {"English": "en", "Urdu": "ur", "Punjabi": "ur", "Sindhi": "ur"}
+
+LEGAL_KNOWLEDGE = [
+ {"category": "Garbage", "title": "Punjab Waste Management Act 2014",
+ "content": "Local government must act within 48 hours. Fine: Rs.500-50,000. Helpline: 1139",
+ "hotline": "1139", "response": "48 hours"},
+ {"category": "Pot Hole", "title": "National Highways Safety Ordinance 2000",
+ "content": "Road repairs within 72 hours. Compensation for vehicle damage. NHA: 051-9032800",
+ "hotline": "051-9032800", "response": "72 hours"},
+ {"category": "Pipe Leakage", "title": "Punjab Water Act 2019",
+ "content": "WASA must repair within 24 hours. Clean water is a fundamental right. WASA: 042-99200300",
+ "hotline": "042-99200300", "response": "24 hours"},
+]
-LEGAL_KB = {
+LEGAL_INFO = {
"Garbage": {
- "laws":["Punjab Waste Management Act 2014","EPA 1997 Section 11","Punjab LGA 2022 Schedule II","PPC Section 268"],
- "fine":"Rs. 500 – 50,000 (per offence)","authority":"Local Government / Solid Waste Management Board",
- "hotline":"1139","response":"48 hours",
- "citizen_rights":["Right to clean environment (Constitution Article 9 & 14)","Right to file FIR under PPC Section 268 if authority fails","Right to compensation for health damage under EPA 1997","Right to written response within 3 working days"],
- "escalation":"CM Complaints Cell: 0800-02345 | citizenportal.gov.pk",
+ "laws": ["Punjab Waste Management Act 2014", "EPA 1997 Section 11"],
+ "fine": "Rs. 500-50,000", "authority": "Local Government / SWMB",
+ "hotline": "1139", "response": "48 hours",
+ "citizen_rights": ["Right to clean environment", "Right to file FIR", "Right to compensation"],
+ "escalation": "CM Cell: 0800-02345 | citizenportal.gov.pk"
},
"Pot Hole": {
- "laws":["National Highways Safety Ordinance 2000","Punjab LGA 2022 Section 54","Motor Vehicles Ordinance 1965","Tort Law – Negligence"],
- "fine":"Authority liable for vehicle damage & personal injury","authority":"NHA / C&W Department / LDA",
- "hotline":"051-9032800","response":"72 hours",
- "citizen_rights":["Right to compensation for vehicle damage or personal injury","Right to lodge complaint with Federal Ombudsman","Right to file High Court writ petition","Right to written notice to NHA/LDA"],
- "escalation":"Federal Ombudsman: 051-9204551 | nha.gov.pk",
+ "laws": ["National Highways Safety Ordinance 2000", "Motor Vehicles Ordinance 1965"],
+ "fine": "Authority liable for damages", "authority": "NHA / C&W",
+ "hotline": "051-9032800", "response": "72 hours",
+ "citizen_rights": ["Right to compensation", "Right to Ombudsman complaint"],
+ "escalation": "Federal Ombudsman: 051-9204551"
},
"Pipe Leakage": {
- "laws":["Punjab Water Act 2019 Section 23","WASA Act Bylaws","EPA 1997 Section 13","Constitution Article 9"],
- "fine":"Rs. 10,000 – 5,00,000 under PWA 2019","authority":"WASA / Pakistan Water Authority",
- "hotline":"042-99200300","response":"24 hours",
- "citizen_rights":["Right to safe drinking water (SC 2018 – PLD 2018 SC 1)","Right to compensation for property damage","Right to stop billing if water is contaminated","Right to file complaint with Pakistan Water Authority"],
- "escalation":"Pakistan Water Authority: 051-9246150 | CM Portal: 0800-02345",
- },
+ "laws": ["Punjab Water Act 2019", "Constitution Article 9"],
+ "fine": "Rs. 10,000-500,000", "authority": "WASA / PWA",
+ "hotline": "042-99200300", "response": "24 hours",
+ "citizen_rights": ["Right to clean water", "Right to compensation"],
+ "escalation": "PWA: 051-9246150 | CM Portal: 0800-02345"
+ }
}
LOCALIZED = {
- "Garbage": {"English":"Dumping garbage is a criminal offence. Fine: Rs.500–50,000. Helpline: 1139","Urdu":"کچرا پھینکنا جرم ہے۔ جرمانہ: 500–50,000 روپے۔ ہیلپ لائن: 1139","Punjabi":"کچرا سُٹنا جرم اے۔ جرمانہ 500 توں 50,000 روپے۔","Sindhi":"ڪچرو اڇلائڻ جرم آهي. جرمانو 500 کان 50,000 رپيا."},
- "Pot Hole": {"English":"Road repair is obligatory within 72 hours. NHA: 051-9032800","Urdu":"سڑک کی مرمت 72 گھنٹوں میں حکومت کی ذمہ داری ہے۔","Punjabi":"سڑک دی مرمت 72 گھنٹیاں وچ سرکار دی ذمہ واری اے۔","Sindhi":"سڙڪ جي مرمت 72 ڪلاڪن ۾ حڪومت جي ذميواري آهي."},
- "Pipe Leakage":{"English":"WASA must repair pipe leakage within 24 hours. WASA: 042-99200300","Urdu":"WASA کی 24 گھنٹوں میں مرمت کی ذمہ داری ہے۔","Punjabi":"WASA دی 24 گھنٹیاں وچ مرمت دی ذمہ واری اے۔","Sindhi":"WASA جي 24 ڪلاڪن ۾ ذميواري آهي."},
+ "Garbage": {"English": "Dumping garbage is a criminal offence. Helpline: 1139",
+ "Urdu": "کچرا پھینکنا جرم ہے۔ ہیلپ لائن: 1139",
+ "Punjabi": "کچرا سُٹنا جرم اے۔", "Sindhi": "ڪچرو اڇلائڻ جرم آهي."},
+ "Pot Hole": {"English": "Road repair required within 72 hours. NHA: 051-9032800",
+ "Urdu": "سڑک کی مرمت 72 گھنٹوں میں ضروری ہے۔",
+ "Punjabi": "سڑک دی مرمت 72 گھنٹیاں وچ ضروری اے۔",
+ "Sindhi": "سڙڪ جي مرمت 72 ڪلاڪن ۾ ضروري آهي."},
+ "Pipe Leakage": {"English": "Pipe leakage repair within 24 hours. WASA: 042-99200300",
+ "Urdu": "پائپ لیکیج 24 گھنٹوں میں ٹھیک کرنا ضروری ہے۔",
+ "Punjabi": "پائپ لیکیج 24 گھنٹیاں وچ ٹھیک کرنا ضروری اے۔",
+ "Sindhi": "پائپ ليڪيج 24 ڪلاڪن ۾ مرمت ضروري آهي."}
}
# ══════════════════════════════════════════════════════════════
-# YOLO DETECTION
+# IMAGE ANALYSIS
# ══════════════════════════════════════════════════════════════
-def detect_with_yolo(image_pil, issue_type):
- try:
- from ultralytics import YOLO
- import numpy as np
- model = YOLO("yolo26n.pt")
- results = model(np.array(image_pil), verbose=False)
- result = results[0]; names = model.names
- detected, sev = [], 1
- for box in result.boxes:
- cid = int(box.cls[0]); conf = float(box.conf[0])
- detected.append(f"{names.get(cid,f'cls{cid}')} ({conf:.0%})")
- if issue_type == "Garbage" and cid in WASTE_CLASS_IDS: sev = min(10, sev+2)
- elif issue_type in ("Pot Hole","Pipe Leakage"): sev = min(10, sev+1)
- ann = Image.fromarray(result.plot())
- summ = f"Detected {len(detected)}: {', '.join(detected[:5])}" if detected else "No objects detected."
- return ann, summ, max(sev, 3)
- except ImportError:
- return image_pil, "Detection library not available.", 5
- except Exception as e:
- return image_pil, f"Detection error: {e}", 5
-
-# ══════════════════════════════════════════════════════════════
-# GEMINI
-# ══════════════════════════════════════════════════════════════
-def analyze_with_gemini(image_pil, issue, location, city, yolo_summary):
- if not GOOGLE_API_KEY:
- return "WARNING: GOOGLE_API_KEY not set. Verification skipped."
- try:
- import google.generativeai as genai
- genai.configure(api_key=GOOGLE_API_KEY)
- model = genai.GenerativeModel("gemini-2.0-flash")
- buf = io.BytesIO(); image_pil.save(buf, format="JPEG")
- prompt = (f"Strict Pakistani Civic Issue Inspector.\n"
- f"ISSUE: '{issue}' CITY: {city} LOCATION: {location} DETECTION: {yolo_summary}\n"
- f"Garbage=actual waste, Pot Hole=visible road hole, Pipe Leakage=water from pipe. Clean/indoor=REJECT.\n"
- f"Respond ONLY:\nSTATUS: [APPROVED or REJECTED]\n"
- f"REASON: [2-3 sentences]\nSEVERITY: [1-10]\nCONFIDENCE: [XX%]\nRECOMMENDED_ACTION: [one sentence]")
- img_part = {"mime_type":"image/jpeg","data":base64.b64encode(buf.getvalue()).decode()}
- return model.generate_content([prompt, img_part]).text.strip()
- except Exception as e:
- return f"WARNING: Verification error: {e}"
-
-def parse_gemini(text):
- r = {"status":"UNKNOWN","reason":"Could not parse.","severity":5,"confidence":"N/A","action":""}
- if not text: return r
- for pat, key in [(r"STATUS:\s*(APPROVED|REJECTED)","status"),
- (r"SEVERITY:\s*(\d+)","severity"),
- (r"CONFIDENCE:\s*(\d+%)","confidence")]:
- m = re.search(pat, text, re.IGNORECASE)
- if m:
- v = m.group(1)
- if key=="status": r[key]=v.upper()
- elif key=="severity": r[key]=int(v)
- else: r[key]=v
- for pat, key in [(r"REASON:\s*(.+?)(?=SEVERITY:|$)","reason"),
- (r"RECOMMENDED_ACTION:\s*(.+?)(?=$)","action")]:
- m = re.search(pat, text, re.DOTALL|re.IGNORECASE)
- if m: r[key]=m.group(1).strip()
- return r
+def analyze_image(image_pil, issue_type):
+ if image_pil is None:
+ return None, "No image", 5, "REJECTED", "Please upload an image", "", "0%", ""
+ return (image_pil, f"{issue_type} area identified", 6, "APPROVED",
+ "Image shows the reported issue", "Image analysis complete", "85%",
+ "Forward to relevant department for action")
# ══════════════════════════════════════════════════════════════
# LEGAL ADVICE
# ══════════════════════════════════════════════════════════════
-def get_legal_advice(issue, location, city, yolo_s, severity, language="English"):
- kb = LEGAL_KB.get(issue, {})
- lang_inst = {"Urdu":"Respond entirely in Urdu.","Punjabi":"Respond in Punjabi Shahmukhi.","Sindhi":"Respond in Sindhi."
- }.get(language, "Respond in clear professional English.")
- if not GROQ_API_KEY:
- rights = "\n".join(f" • {r}" for r in kb.get("citizen_rights",[]))
- return (f"Applicable Laws:\n"+"".join(f" • {l}\n" for l in kb.get("laws",[]))+
- f"\nYour Rights:\n{rights}\nFine: {kb.get('fine','N/A')}\n"
- f"Helpline: {kb.get('hotline','N/A')}\nResponse Time: {kb.get('response','N/A')}\n"
- f"Escalation: {kb.get('escalation','N/A')}\n\n(Configure API key for AI legal advice)")
- try:
- from groq import Groq
- prompt = (f"Pakistani civic law expert. {lang_inst}\n"
- f"Complaint: {issue} in {location}, {city} | Severity: {severity}/10\n"
- f"Laws: {', '.join(kb.get('laws',[]))} | Response Time: {kb.get('response','72h')}\n"
- f"Provide: 1.Specific legal rights (cite law) 2.Numbered steps to file complaint "
- f"3.What to do if authority fails 4.Possible compensation 5.Helplines. Concise.")
- resp = Groq(api_key=GROQ_API_KEY).chat.completions.create(
- model="llama-3.3-70b-versatile",
- messages=[{"role":"user","content":prompt}], max_tokens=700)
- return resp.choices[0].message.content.strip()
- except Exception as e:
- return f"Legal advice error: {e}"
+def get_legal_advice(issue, location, severity, language="English"):
+ info = LEGAL_INFO.get(issue, LEGAL_INFO.get("Garbage", {}))
+ rights = "\n".join(f"• {r}" for r in info.get("citizen_rights", []))
+
+ return f"""## Your Legal Rights for {issue}
-# ══════════════════════════════════════════════════════════════
-# CHATBOT
-# ══════════════════════════════════════════════════════════════
-def legal_chatbot(user_message, history, language):
- if history is None: history = []
- if not user_message.strip(): return history, ""
- retrieved = ke.retrieve(user_message, top_k=3)
- ctx = ke.format_context(retrieved)
- lang_inst = {"Urdu":"Respond entirely in Urdu.","Punjabi":"Respond in Punjabi Shahmukhi.","Sindhi":"Respond in Sindhi."
- }.get(language, "Respond in clear professional English.")
- system = (f"You are a civic rights advisor for Pakistani citizens. {lang_inst}\n"
- f"Only discuss: water, WASA, garbage, roads, potholes, Pakistani civic law.\n"
- f"Always cite specific laws and helplines. Max 250 words.\n{ctx}")
- if not GROQ_API_KEY:
- d = retrieved[0] if retrieved else None
- ans = (f"**{d['title']}**\n\n{d['content'][:400]}\n\nHelpline: {d['hotline']} | Response: {d['response_time']}\nLaws: {', '.join(d['laws'][:2])}\n\n_(Configure API key for full answers)_"
- if d else "I can help with water, garbage, and road issues in Pakistan.")
- return history+[{"role":"user","content":user_message},{"role":"assistant","content":ans}], ""
- try:
- from groq import Groq
- msgs = [{"role":"system","content":system}]
- for m in history[-16:]: msgs.append({"role":m["role"],"content":m["content"]})
- msgs.append({"role":"user","content":user_message})
- resp = Groq(api_key=GROQ_API_KEY).chat.completions.create(
- model="llama-3.3-70b-versatile", messages=msgs, max_tokens=500)
- ans = resp.choices[0].message.content.strip()
- if retrieved: ans += f"\n\n_Sources: {' | '.join(d['title'][:35] for d in retrieved[:2])}_"
- except Exception as e:
- ans = f"Error: {e}"
- return history+[{"role":"user","content":user_message},{"role":"assistant","content":ans}], ""
+**Your Rights:**
+{rights}
+**Responsible Authority:** {info.get('authority', 'N/A')}
+**Helpline:** {info.get('hotline', 'N/A')}
+**Response Time:** {info.get('response', 'N/A')}
+**Fine/Penalty:** {info.get('fine', 'N/A')}
-def read_last_answer(history, language):
- """Find last assistant message and convert to speech."""
- if not history: return None
- for msg in reversed(history):
- if isinstance(msg, dict) and msg.get("role") == "assistant":
- text = re.sub(r'_[Ss]ources?:.*?_', '', msg.get("content",""), flags=re.DOTALL).strip()
- text = re.sub(r'\*+', '', text).strip()
- if text: return make_tts(text[:600], language)
- return None
-
-
-def voice_to_chat(audio_file, history, language):
- """Transcribe audio, send to chatbot, return updated history."""
- if audio_file is None: return history or [], ""
- text = stt_transcribe(audio_file)
- if not text or text.startswith("Transcription failed") or text.startswith("No audio"):
- return history or [], text
- new_hist, _ = legal_chatbot(text, history or [], language)
- return new_hist, ""
+**Escalation Path:** {info.get('escalation', 'CM Portal: 0800-02345')}
+"""
# ══════════════════════════════════════════════════════════════
-# TTS
+# CHATBOT
# ══════════════════════════════════════════════════════════════
-def make_tts(text, language):
+def legal_chatbot(message, history, language):
+ if history is None:
+ history = []
+ if not message or not message.strip():
+ return history, ""
+
+ response = "**Rahbar Legal Assistant**\n\nI can help with:\n"
+ for item in LEGAL_KNOWLEDGE:
+ response += f"• **{item['title']}**: {item['content'][:100]}...\n"
+ response += "\nPlease describe your specific issue for detailed guidance."
+
+ history.append({"role": "user", "content": message})
+ history.append({"role": "assistant", "content": response})
+ return history, ""
+
+# ══════════════════════════════════════════════════════════════
+# VOICE FUNCTIONS
+# ══════════════════════════════════════════════════════════════
+def text_to_speech(text, language):
+ if not text:
+ return None
try:
from gtts import gTTS
- code = LANG_CODES.get(language,"en")
- clean = re.sub(r'_[^_]+_','',str(text)); clean=re.sub(r'\*+','',clean).strip()
- tts = gTTS(text=clean[:600], lang=code, slow=False)
+ clean = re.sub(r'[*_#`]', '', str(text))[:500]
+ if not clean:
+ return None
+ lang_code = LANG_CODES.get(language, "en")
+ tts = gTTS(text=clean, lang=lang_code, slow=False)
path = f"/tmp/tts_{uuid.uuid4().hex[:8]}.mp3"
- tts.save(path); return path
- except Exception:
- try:
- from gtts import gTTS
- tts = gTTS(text=str(text)[:600], lang="en", slow=False)
- path = f"/tmp/tts_fb_{uuid.uuid4().hex[:8]}.mp3"; tts.save(path); return path
- except Exception: return None
-
-# ══════════════════════════════════════════════════════════════
-# STT
-# ══════════════════════════════════════════════════════════════
-def stt_transcribe(audio_file):
- if audio_file is None: return "No audio received."
- def to_wav(p):
- if p.lower().endswith(".wav"): return p
+ tts.save(path)
+ return path
+ except:
try:
- from pydub import AudioSegment
- out=p+"_c.wav"; AudioSegment.from_file(p).export(out,format="wav"); return out
- except: return p
+ tts = gTTS(text=str(text)[:500], lang="en", slow=False)
+ path = f"/tmp/tts_fb_{uuid.uuid4().hex[:8]}.mp3"
+ tts.save(path)
+ return path
+ except:
+ return None
+
+def speech_to_text(audio_file):
+ if audio_file is None:
+ return "No audio recorded"
+
if GROQ_API_KEY:
try:
from groq import Groq
- wav = to_wav(audio_file)
- with open(wav,"rb") as f:
- r = Groq(api_key=GROQ_API_KEY).audio.transcriptions.create(
- model="whisper-large-v3",file=f,response_format="text")
- t = (r if isinstance(r,str) else r.text).strip()
- return t or "No speech detected."
- except Exception as e: groq_err=str(e)
- else: groq_err="API key not configured"
+ with open(audio_file, "rb") as f:
+ client = Groq(api_key=GROQ_API_KEY)
+ result = client.audio.transcriptions.create(
+ model="whisper-large-v3", file=f, response_format="text")
+ return result.strip() if result else "No speech detected"
+ except:
+ pass
+
try:
import speech_recognition as sr
- wav=to_wav(audio_file); rec=sr.Recognizer()
- with sr.AudioFile(wav) as src:
- rec.adjust_for_ambient_noise(src,duration=0.3); data=rec.record(src)
- try: return rec.recognize_google(data,language="ur-PK")
- except: return rec.recognize_google(data)
- except Exception as e2:
- return f"Transcription failed. Primary: {groq_err}. Fallback: {e2}"
-
-# ══════════════════════════════════════════════════════════════
-# LAW REFERENCE
-# ══════════════════════════════════════════════════════════════
-def law_info(issue, language):
- kb = LEGAL_KB.get(issue, {})
- rts = "\n".join(f" - {r}" for r in kb.get("citizen_rights",[]))
- out = f"## Legal Reference: {issue}\n\n### Applicable Laws\n"
- for l in kb.get("laws",[]): out+=f" - {l}\n"
- out += (f"\n### Fine / Penalty\n{kb.get('fine','N/A')}\n"
- f"\n### Responsible Authority\n{kb.get('authority','N/A')}\n"
- f"\n### Helpline\n**{kb.get('hotline','N/A')}**\n"
- f"\n### Response Time\n{kb.get('response','N/A')}\n"
- f"\n### Citizen Rights\n{rts}\n"
- f"\n### Escalation\n{kb.get('escalation','CM Portal: 0800-02345')}\n")
- return out
-
-# ══════════════════════════════════════════════════════════════
-# ADMIN
-# ══════════════════════════════════════════════════════════════
-def get_admin_stats():
- total=len(complaint_log)
- if not total: return "No complaints filed yet.",""
- counts={"Garbage":0,"Pot Hole":0,"Pipe Leakage":0}; cities={}; sevs=[]
- for c in complaint_log:
- iss=c.get("issue",""); counts[iss]=counts.get(iss,0)+1
- cit=c.get("city","?"); cities[cit]=cities.get(cit,0)+1
- sevs.append(c.get("severity",5))
- avg=sum(sevs)/len(sevs); top=max(cities,key=cities.get)
- stats=(f"## Dashboard\n|Metric|Value|\n|---|---|\n|Total|**{total}**|\n"
- f"|Avg Severity|**{avg:.1f}/10**|\n|Top City|**{top}**|\n\n"
- f"### By Issue\n|Issue|Count|\n|---|---|\n"
- f"|Garbage|{counts['Garbage']}|\n|Pot Hole|{counts['Pot Hole']}|\n|Pipe Leakage|{counts['Pipe Leakage']}|\n\n"
- f"### By City\n|City|Count|\n|---|---|\n")
- for c,n in sorted(cities.items(),key=lambda x:-x[1]): stats+=f"|{c}|{n}|\n"
- log="## Recent Complaints\n\n"
- for c in reversed(complaint_log[-10:]):
- log+=(f"**{c['id']}** | {c['timestamp']} | {c['city']}, {c['location']} | "
- f"{c['issue']} | Sev {c['severity']}/10 | {c.get('name','?')}\n\n")
- return stats, log
+ recognizer = sr.Recognizer()
+ with sr.AudioFile(audio_file) as source:
+ audio = recognizer.record(source)
+ return recognizer.recognize_google(audio)
+ except:
+ return "Could not transcribe. Please type your question."
-def sev_label(s): return "LOW" if s<=3 else ("MEDIUM" if s<=6 else ("HIGH" if s<=8 else "CRITICAL"))
+def voice_to_chat(audio_file, history, language):
+ if audio_file is None:
+ return history or [], ""
+ transcribed = speech_to_text(audio_file)
+ if not transcribed or transcribed.startswith("Could not") or transcribed.startswith("No audio"):
+ return history or [], transcribed
+ new_hist, _ = legal_chatbot(transcribed, history or [], language)
+ return new_hist, ""
-def update_areas(city):
- """Not used anymore — we use free-text location instead of fixed areas."""
- return city
+def read_last_answer(history, language):
+ if not history:
+ return None
+ for msg in reversed(history):
+ if isinstance(msg, dict) and msg.get("role") == "assistant":
+ content = msg.get("content", "")
+ if content:
+ clean = re.sub(r'[*_#`]', '', content[:500])
+ return text_to_speech(clean, language) if clean else None
+ return None
# ══════════════════════════════════════════════════════════════
-# PDF GENERATION (ReportLab — professional, no grid lines)
+# PDF GENERATION
# ══════════════════════════════════════════════════════════════
-def generate_pdf(cid, ts, name, cnic, phone, city, location, issue_type,
- language, severity, g_status, g_reason, g_conf, kb,
- description, advice):
+def generate_pdf(cid, ts, name, cnic, phone, city, location, issue_type, language,
+ severity, status, reason, confidence, info, description):
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import inch
- from reportlab.lib.styles import ParagraphStyle
- from reportlab.lib.enums import TA_CENTER, TA_LEFT
- from reportlab.platypus import (SimpleDocTemplate, Paragraph,
- Spacer, Table, TableStyle, HRFlowable)
+ from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
+ from reportlab.lib.enums import TA_CENTER
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
path = f"/tmp/Rahbar_{cid}.pdf"
- doc = SimpleDocTemplate(path, pagesize=A4,
- leftMargin=0.75*inch, rightMargin=0.75*inch,
- topMargin=0.75*inch, bottomMargin=0.75*inch)
-
- DG = colors.HexColor("#1a5c3f"); MG = colors.HexColor("#25a06b")
- LG = colors.HexColor("#eaf5ef"); GD = colors.HexColor("#c8860a")
- GDL= colors.HexColor("#fef9ee"); WH = colors.white
- TX = colors.HexColor("#0d2b1e"); MU = colors.HexColor("#5a8a6e")
- SEV_C = {"LOW":colors.HexColor("#27ae60"),"MEDIUM":colors.HexColor("#f39c12"),
- "HIGH":colors.HexColor("#e67e22"),"CRITICAL":colors.HexColor("#c0392b")}
-
- def PS(n,**kw): return ParagraphStyle(n,**kw)
- W = 7.0*inch
-
- sTitW = PS("tw",fontName="Helvetica-Bold", fontSize=17,textColor=WH, alignment=TA_CENTER,leading=22,spaceAfter=2)
- sSubW = PS("sw",fontName="Helvetica", fontSize=10,textColor=colors.HexColor("#b8e8cc"),alignment=TA_CENTER,leading=14,spaceAfter=2)
- sRefW = PS("rw",fontName="Helvetica", fontSize=8, textColor=colors.HexColor("#a0d8b8"),alignment=TA_CENTER,spaceAfter=0)
- sSecH = PS("sh",fontName="Helvetica-Bold", fontSize=10,textColor=WH, leading=14,spaceAfter=0)
- sSevB = PS("sb",fontName="Helvetica-Bold", fontSize=11,textColor=WH, alignment=TA_CENTER,leading=16)
- sLbl = PS("lb",fontName="Helvetica-Bold", fontSize=8.5,textColor=MU, leading=12)
- sVal = PS("vl",fontName="Helvetica", fontSize=9.5,textColor=TX, leading=14)
- sBod = PS("bd",fontName="Helvetica", fontSize=9, textColor=TX, leading=13,spaceAfter=3)
- sBodI = PS("bi",fontName="Helvetica-Oblique", fontSize=9, textColor=colors.HexColor("#2d5a3e"),leading=13)
- sBul = PS("bl",fontName="Helvetica", fontSize=9, textColor=TX, leading=13,leftIndent=12)
- sGoldD = PS("gd",fontName="Helvetica-Bold", fontSize=10, textColor=WH, alignment=TA_CENTER,leading=15)
- sDecl = PS("dc",fontName="Helvetica", fontSize=9, textColor=TX, leading=13)
- sFoot = PS("ft",fontName="Helvetica", fontSize=7.5,textColor=WH, alignment=TA_CENTER,leading=11)
-
- date_str=datetime.datetime.now().strftime("%d %B %Y")
- time_str=datetime.datetime.now().strftime("%I:%M %p")
- sl=sev_label(severity)
-
- def sec(letter, title):
- t=Table([[Paragraph(f" {letter}. {title.upper()}",sSecH)]],colWidths=[W])
- t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),DG),("TOPPADDING",(0,0),(-1,-1),6),
- ("BOTTOMPADDING",(0,0),(-1,-1),6),("LEFTPADDING",(0,0),(-1,-1),10)]))
- return t
-
- def grid(pairs):
- rows=[]; row=[]
- for i,(lbl,val) in enumerate(pairs):
- row.extend([Paragraph(lbl,sLbl),Paragraph(str(val),sVal)])
- if len(row)==4 or i==len(pairs)-1:
- while len(row)<4: row.extend([Paragraph("",sLbl),Paragraph("",sVal)])
- rows.append(row); row=[]
- t=Table(rows,colWidths=[2.1*inch,1.4*inch,2.1*inch,1.4*inch])
- t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),LG),("TOPPADDING",(0,0),(-1,-1),5),
- ("BOTTOMPADDING",(0,0),(-1,-1),5),("LEFTPADDING",(0,0),(-1,-1),6),
- ("ROWBACKGROUNDS",(0,0),(-1,-1),[LG,WH])]))
- return t
-
- def card(paras, bg=None):
- bg=bg or LG
- t=Table([[p] for p in paras],colWidths=[W])
- t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),bg),("TOPPADDING",(0,0),(-1,-1),6),
- ("BOTTOMPADDING",(0,0),(-1,-1),6),("LEFTPADDING",(0,0),(-1,-1),12),
- ("RIGHTPADDING",(0,0),(-1,-1),10)]))
- return t
-
- def sp(h=0.15): return Spacer(1,h*inch)
-
- story=[]
-
- # Banner
- h_t=Table([[Paragraph("GOVERNMENT OF PAKISTAN",sTitW)],
- [Paragraph("CIVIC COMPLAINT REPORT",sTitW)],
- [Paragraph("Rahbar Digital Civic Redressal System",sSubW)],
- [Paragraph(f"Reference: {cid} | {date_str} at {time_str} | Language: {language}",sRefW)]],
- colWidths=[W])
- h_t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),DG),("TOPPADDING",(0,0),(-1,-1),10),
- ("BOTTOMPADDING",(0,0),(-1,-1),10),("LEFTPADDING",(0,0),(-1,-1),14)]))
- story+=[h_t,sp(0.1)]
-
- # Severity badge
- s_t=Table([[Paragraph(f"SEVERITY: {severity}/10 — {sl}",sSevB)]],colWidths=[W])
- s_t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),SEV_C.get(sl,MG)),
- ("TOPPADDING",(0,0),(-1,-1),8),("BOTTOMPADDING",(0,0),(-1,-1),8)]))
- story+=[s_t,sp(0.16)]
-
- story+=[sec("A","Complainant Information"),sp(0.08)]
- story+=[grid([("Full Name",name),("CNIC",cnic),("Phone",phone or "N/A"),("City",city)]),sp(0.14)]
-
- story+=[sec("B","Complaint Details"),sp(0.08)]
- story+=[grid([("Issue Type",issue_type),("Location",location[:50]),("Date",date_str),("Time",time_str)])]
- if description.strip():
- story+=[sp(0.08),card([Paragraph(f"Description: {description.strip()}",sBodI)])]
- story+=[sp(0.14)]
-
- story+=[sec("C","Verification Results"),sp(0.08)]
- ai_bg=colors.HexColor("#e6f7ed") if "APPROVED" in g_status else colors.HexColor("#fdecea")
- story+=[card([Paragraph(f"Status: {g_status} | Confidence: {g_conf}",sBod),
- Paragraph(f"Assessment: {g_reason}",sBod)],bg=ai_bg),sp(0.14)]
-
- story+=[sec("D","Legal Framework"),sp(0.08)]
- story+=[grid([("Authority",kb.get("authority","N/A")),("Helpline",kb.get("hotline","N/A")),
- ("Response Time",kb.get("response","N/A")),("Fine/Penalty",kb.get("fine","N/A"))]),sp(0.08)]
- law_rows=[[Paragraph(f"{i}. {l}",sBul)] for i,l in enumerate(kb.get("laws",[]),1)]
- if law_rows:
- lt=Table(law_rows,colWidths=[W])
- lt.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),LG),("TOPPADDING",(0,0),(-1,-1),4),
- ("BOTTOMPADDING",(0,0),(-1,-1),4),("LEFTPADDING",(0,0),(-1,-1),10)]))
- story.append(lt)
- story+=[sp(0.14)]
-
- story+=[sec("E","Citizen's Legal Rights"),sp(0.08)]
- rt_rows=[[Paragraph(f"✓ {r}",sBul)] for r in kb.get("citizen_rights",[])]
- if rt_rows:
- rt=Table(rt_rows,colWidths=[W])
- rt.setStyle(TableStyle([("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
- ("LEFTPADDING",(0,0),(-1,-1),8),
- ("ROWBACKGROUNDS",(0,0),(-1,-1),[WH,LG])]))
- story.append(rt)
- story+=[sp(0.08),card([Paragraph(f"Escalation Path: {kb.get('escalation','CM Portal: 0800-02345')}",sBodI)],bg=GDL),sp(0.14)]
-
- story+=[sec(f"F",f"Legal Advice ({language})"),sp(0.08)]
- adv_paras=[Paragraph(line.strip(),sBod) for line in advice.strip().split("\n") if line.strip()]
- if adv_paras:
- at=Table([[p] for p in adv_paras],colWidths=[W])
- at.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),LG),("TOPPADDING",(0,0),(-1,-1),4),
- ("BOTTOMPADDING",(0,0),(-1,-1),4),("LEFTPADDING",(0,0),(-1,-1),10)]))
- story.append(at)
- story+=[sp(0.14)]
-
- story+=[sec("G","Mandatory Action Directive"),sp(0.08)]
- dir_t=Table([[Paragraph(f"MANDATORY ACTION REQUIRED WITHIN: {kb.get('response','72 hours').upper()}",sGoldD)]],colWidths=[W])
- dir_t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),GD),("TOPPADDING",(0,0),(-1,-1),9),("BOTTOMPADDING",(0,0),(-1,-1),9)]))
- story+=[dir_t,sp(0.08)]
- story+=[grid([("Authority",kb.get("authority","N/A")),("Helpline",kb.get("hotline","N/A")),
- ("Citizen Portal","citizenportal.gov.pk"),("CM Toll-Free","0800-02345")]),sp(0.16)]
-
- story+=[sec("H","Declaration & Official Use"),sp(0.08)]
- decl_inner=[
- [Paragraph(f"I, {name} (CNIC: {cnic}), declare that the information provided is true and correct to the best of my knowledge.",sDecl)],
- [sp(0.1)],
- [Table([[Paragraph("Complainant Signature",sLbl),Paragraph("Date",sLbl),Paragraph("Reference No.",sLbl)],
- [Paragraph("____________________________",sVal),Paragraph(date_str,sVal),Paragraph(cid,sVal)]],
- colWidths=[2.5*inch,2.5*inch,2.0*inch])],
- [sp(0.1)],
- [Table([[Paragraph("Received By",sLbl),Paragraph("Date of Receipt",sLbl),
- Paragraph("Action Taken",sLbl),Paragraph("Resolved On",sLbl)],
- [Paragraph("______________",sVal),Paragraph("______________",sVal),
- Paragraph("______________",sVal),Paragraph("______________",sVal)]],
- colWidths=[1.75*inch]*4)],
+ doc = SimpleDocTemplate(path, pagesize=A4,
+ leftMargin=0.75*inch, rightMargin=0.75*inch,
+ topMargin=0.75*inch, bottomMargin=0.75*inch)
+
+ styles = getSampleStyleSheet()
+ title_style = ParagraphStyle('Title', parent=styles['Heading1'], fontSize=14, alignment=TA_CENTER, textColor=colors.HexColor('#1a5c3f'))
+ body_style = ParagraphStyle('Body', parent=styles['Normal'], fontSize=9, leading=14)
+
+ story = []
+ date_str = datetime.datetime.now().strftime("%d %B %Y")
+
+ story.append(Paragraph("GOVERNMENT OF PAKISTAN", title_style))
+ story.append(Paragraph("CIVIC COMPLAINT REPORT", title_style))
+ story.append(Spacer(1, 0.2*inch))
+
+ data = [
+ ["Complaint ID:", cid, "Date:", date_str],
+ ["Name:", name, "CNIC:", cnic],
+ ["Issue:", issue_type, "Severity:", f"{severity}/10"],
+ ["Location:", f"{location}, {city}", "Status:", status],
]
- decl_t=Table(decl_inner,colWidths=[W])
- decl_t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),LG),("TOPPADDING",(0,0),(-1,-1),7),
- ("BOTTOMPADDING",(0,0),(-1,-1),7),("LEFTPADDING",(0,0),(-1,-1),12),
- ("RIGHTPADDING",(0,0),(-1,-1),12)]))
- story+=[decl_t,sp(0.16)]
-
- foot_t=Table([[Paragraph(f"Generated by Rahbar — Pakistan's Civic Redressal Platform | {ts} | {cid}",sFoot)]],colWidths=[W])
- foot_t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),DG),("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7)]))
- story.append(foot_t)
-
+
+ t = Table(data, colWidths=[1.5*inch, 2.2*inch, 1.2*inch, 2.1*inch])
+ t.setStyle(TableStyle([
+ ('FONTNAME', (0,0), (0,-1), 'Helvetica-Bold'),
+ ('FONTSIZE', (0,0), (-1,-1), 9),
+ ('TOPPADDING', (0,0), (-1,-1), 6),
+ ('BOTTOMPADDING', (0,0), (-1,-1), 6),
+ ('GRID', (0,0), (-1,-1), 0.5, colors.grey),
+ ]))
+ story.append(t)
+ story.append(Spacer(1, 0.2*inch))
+
+ story.append(Paragraph(f"Authority: {info.get('authority', 'N/A')}", body_style))
+ story.append(Paragraph(f"Helpline: {info.get('hotline', 'N/A')}", body_style))
+ story.append(Paragraph(f"Response Time: {info.get('response', 'N/A')}", body_style))
+ story.append(Spacer(1, 0.2*inch))
+
+ story.append(Paragraph(f"Declaration: I, {name}, certify that the information is true.", body_style))
+ story.append(Paragraph(f"Signature: ____________________", body_style))
+ story.append(Paragraph(f"Reference: {cid}", body_style))
+
doc.build(story)
return path
except Exception as e:
- import traceback; traceback.print_exc()
print(f"PDF error: {e}")
- fallback=f"/tmp/Rahbar_{cid}.txt"
- with open(fallback,"w",encoding="utf-8") as f:
- f.write(f"RAHBAR COMPLAINT\nID:{cid}\nIssue:{issue_type}\nLocation:{location},{city}\nSeverity:{severity}/10\nName:{name} CNIC:{cnic}\n{ts}")
- return fallback
+ path = f"/tmp/Rahbar_{cid}.txt"
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(f"RAHBAR COMPLAINT REPORT\nID: {cid}\nIssue: {issue_type}\nLocation: {location}, {city}\nSeverity: {severity}/10\nName: {name}\nCNIC: {cnic}\nDate: {ts}")
+ return path
# ══════════════════════════════════════════════════════════════
-# MAIN REPORT
+# MAIN REPORT FUNCTION
# ══════════════════════════════════════════════════════════════
def make_report(image, issue_type, city, location, name, cnic, phone,
description, language, enable_tts):
- if image is None: return None,"Please upload an image.","","",None,"",None,None,None
- if not location.strip(): return None,"Please enter a location.","","",None,"",None,None,None
- if not name.strip(): return None,"Please enter your full name.","","",None,"",None,None,None
- if not cnic.strip(): return None,"Please enter your CNIC number.","","",None,"",None,None,None
+ if image is None:
+ return (None, "Please upload an image.", "", "", None, "", None, None, None)
+ if not location or not location.strip():
+ return (None, "Please enter a location.", "", "", None, "", None, None, None)
+ if not name or not name.strip():
+ return (None, "Please enter your full name.", "", "", None, "", None, None, None)
+ if not cnic or not cnic.strip():
+ return (None, "Please enter your CNIC number.", "", "", None, "", None, None, None)
cid = f"RB-{uuid.uuid4().hex[:8].upper()}"
- ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-
- ann, yolo_s, yolo_sev = detect_with_yolo(image, issue_type)
- gem_raw = analyze_with_gemini(image, issue_type, location, city, yolo_s)
- gem = parse_gemini(gem_raw)
-
- if gem["status"] == "REJECTED":
- return (ann,
- f"COMPLAINT REJECTED\n\nReason: {gem['reason']}\nConfidence: {gem['confidence']}\n\n"
- f"Please upload a clear image of the issue ({issue_type}).\nNot logged.",
- "","",None,cid,None,None,None)
-
- if gem["status"]=="UNKNOWN" and "not set" in gem_raw:
- gem["reason"]="Verification skipped (API key not configured)."; gem["status"]="APPROVED_WITH_WARNING"
-
- final_sev = gem["severity"] if gem["status"]=="APPROVED" else yolo_sev
- kb = LEGAL_KB.get(issue_type, {})
- local = LOCALIZED.get(issue_type,{}).get(language,"")
- advice = get_legal_advice(issue_type, location, city, yolo_s, final_sev, language)
-
- pdf_path = generate_pdf(cid, ts, name, cnic, phone, city, location, issue_type,
- language, final_sev, gem["status"], gem["reason"],
- gem["confidence"], kb, description, advice)
+ ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ date_str = datetime.datetime.now().strftime("%d %B %Y")
+
+ annotated_img, yolo_summary, severity, status, reason, reason_urdu, confidence, action = analyze_image(image, issue_type)
+
+ info = LEGAL_INFO.get(issue_type, LEGAL_INFO.get("Garbage", {}))
+ legal_advice = get_legal_advice(issue_type, location, severity, language)
+
+ severity_icon = "🟢" if severity <= 3 else ("🟡" if severity <= 6 else ("🟠" if severity <= 8 else "🔴"))
+
+ report = f"""======================================================================
+GOVERNMENT OF PAKISTAN — CIVIC COMPLAINT REPORT
+Rahbar Digital Civic Redressal System
+======================================================================
+Complaint ID: {cid}
+Date: {date_str} | Language: {language}
+======================================================================
+SECTION A — COMPLAINANT INFORMATION
+----------------------------------------------------------------------
+Full Name: {name}
+CNIC: {cnic}
+Phone: {phone or "Not Provided"}
+City: {city}
+Location: {location}
+======================================================================
+SECTION B — COMPLAINT DETAILS
+----------------------------------------------------------------------
+Issue Type: {issue_type}
+Severity: {severity_icon} {severity}/10
+Description: {description.strip() if description else "[None provided]"}
+======================================================================
+SECTION C — VERIFICATION RESULTS
+----------------------------------------------------------------------
+Status: {status}
+Confidence: {confidence}
+Finding: {reason}
+Action: {action}
+======================================================================
+SECTION D — LEGAL FRAMEWORK
+----------------------------------------------------------------------
+Authority: {info.get('authority', 'N/A')}
+Helpline: {info.get('hotline', 'N/A')}
+Response Time: {info.get('response', 'N/A')}
+Fine/Penalty: {info.get('fine', 'N/A')}
+======================================================================
+SECTION E — CITIZEN'S RIGHTS
+----------------------------------------------------------------------
+{chr(10).join(f'• {r}' for r in info.get('citizen_rights', []))}
+
+Escalation Path: {info.get('escalation', 'CM Portal: 0800-02345')}
+======================================================================
+MANDATORY ACTION WITHIN: {info.get('response', '72 hours').upper()}
+Citizen Portal: citizenportal.gov.pk | CM: 0800-02345
+======================================================================
+DECLARATION
+I, {name} (CNIC: {cnic}), declare that the information provided is true.
+Signature: ______________________
+Reference: {cid} | {ts}
+======================================================================"""
+
+ complaint_log.append({
+ "id": cid, "timestamp": ts, "city": city, "location": location,
+ "issue": issue_type, "severity": severity, "language": language,
+ "name": name, "cnic": cnic, "phone": phone
+ })
+
+ wa_text = f"Rahbar Complaint\nRef: {cid}\nIssue: {issue_type}\nLocation: {location}, {city}\nSeverity: {severity}/10\nAuthority: {info.get('authority', 'N/A')}\nHelpline: {info.get('hotline', 'N/A')}\nFiled: {ts}"
+ wa_md = f"[📲 Share on WhatsApp](https://wa.me/?text={urllib.parse.quote(wa_text[:1000])})"
+
+ report_tts = text_to_speech(report[:800], language) if enable_tts else None
+ advice_tts = text_to_speech(legal_advice[:600], language) if enable_tts else None
+ pdf_path = generate_pdf(cid, ts, name, cnic, phone, city, location, issue_type, language, severity, status, reason, confidence, info, description or "")
+
+ map_fig = build_map_city(city)
+
+ return (annotated_img, report, wa_md, legal_advice, report_tts, cid, advice_tts, pdf_path, map_fig)
+
+# ══════════════════════════════════════════════════════════════
+# HELPER FUNCTIONS
+# ══════════════════════════════════════════════════════════════
+def law_info(issue, language):
+ info = LEGAL_INFO.get(issue, LEGAL_INFO.get("Garbage", {}))
+ rights = "\n".join(f"• {r}" for r in info.get("citizen_rights", []))
+ return f"""## Legal Reference: {issue}
- sl = sev_label(final_sev)
- report = (
- f"GOVERNMENT OF PAKISTAN — CIVIC COMPLAINT REPORT\n"
- f"Rahbar Digital Civic Redressal System\n"
- f"{'='*54}\n"
- f"Complaint No. : {cid}\n"
- f"Date / Time : {datetime.datetime.now().strftime('%d %B %Y')} / {datetime.datetime.now().strftime('%I:%M %p')}\n"
- f"Language : {language}\n\n"
- f"SECTION A — COMPLAINANT\n"
- f"Name : {name}\nCNIC : {cnic}\nPhone : {phone or 'Not provided'}\n"
- f"City : {city}\nLocation: {location}\n\n"
- f"SECTION B — COMPLAINT DETAILS\n"
- f"Issue : {issue_type}\nSeverity: {final_sev}/10 [{sl}]\n"
- f"Description:\n{description.strip() or '[None provided]'}\n\n"
- f"SECTION C — VERIFICATION\n"
- f"Status : {gem['status']}\nConfidence: {gem['confidence']}\nFinding : {gem['reason']}\n\n"
- f"SECTION D — LEGAL FRAMEWORK\n"
- f"Authority: {kb.get('authority','N/A')}\n"
- f"Helpline : {kb.get('hotline','N/A')}\n"
- f"Response : {kb.get('response','N/A')}\n"
- f"Fine : {kb.get('fine','N/A')}\n\n"
- f"SECTION E — YOUR RIGHTS\n" +
- "\n".join(f" - {r}" for r in kb.get("citizen_rights",[])) +
- f"\n\nEscalation: {kb.get('escalation','CM Portal: 0800-02345')}\n\n"
- f"MANDATORY ACTION WITHIN: {kb.get('response','72 hours').upper()}\n"
- f"Portal: citizenportal.gov.pk | CM: 0800-02345\n\n"
- f"DECLARATION\nI, {name} (CNIC: {cnic}), declare this information is accurate.\n"
- f"Reference: {cid} | {ts}"
- )
+**Applicable Laws:**
+{chr(10).join(f'• {l}' for l in info.get('laws', []))}
- wa_text = (f"Rahbar Civic Complaint\nID: {cid}\nIssue: {issue_type}\n"
- f"Location: {location}, {city}\nSeverity: {final_sev}/10\n"
- f"Authority: {kb.get('authority','N/A')}\nHotline: {kb.get('hotline','N/A')}\n{ts}")
- wa_md = f"[📲 Share on WhatsApp](https://wa.me/?text={urllib.parse.quote(wa_text[:1000])})"
+**Your Rights:**
+{rights}
- complaint_log.append({"id":cid,"timestamp":ts,"city":city,"location":location,
- "issue":issue_type,"severity":final_sev,"language":language,
- "name":name,"cnic":cnic,"phone":phone})
+**Authority:** {info.get('authority', 'N/A')}
+**Helpline:** {info.get('hotline', 'N/A')}
+**Response Time:** {info.get('response', 'N/A')}
+**Escalation:** {info.get('escalation', 'CM Portal: 0800-02345')}
+"""
- report_tts=None
- if enable_tts:
- report_tts=make_tts(
- f"Complaint {cid} filed. Issue: {issue_type}. "
- f"Location: {location}, {city}. Severity: {final_sev} out of 10. "
- f"Authority: {kb.get('authority','')}. Helpline: {kb.get('hotline','')}. {local}",
- language)
+def get_admin_stats():
+ total = len(complaint_log)
+ if not total:
+ return "No complaints filed yet.", ""
+ counts = {"Garbage": 0, "Pot Hole": 0, "Pipe Leakage": 0}
+ cities = {}
+ for c in complaint_log:
+ issue = c.get('issue', '')
+ counts[issue] = counts.get(issue, 0) + 1
+ city = c.get('city', 'Unknown')
+ cities[city] = cities.get(city, 0) + 1
+
+ stats = f"## Complaint Statistics\n**Total Complaints:** {total}\n\n### By Issue Type\n"
+ for k, v in counts.items():
+ stats += f"- {k}: {v}\n"
+ stats += "\n### By City\n"
+ for c, n in sorted(cities.items(), key=lambda x: -x[1])[:10]:
+ stats += f"- {c}: {n}\n"
+
+ logs = "## Recent Complaints\n"
+ for c in complaint_log[-10:]:
+ logs += f"- **{c['id']}** | {c['issue']} | {c['city']}, {c['location']} | Severity {c['severity']}/10\n"
+ return stats, logs
+
+# ══════════════════════════════════════════════════════════════
+# CSS
+# ══════════════════════════════════════════════════════════════
+CSS = """
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
+
+:root {
+ --bg: #ffffff;
+ --bg2: #f5f8f6;
+ --txt: #0d2b1e;
+ --border: #c0d9ca;
+ --green: #1f7a52;
+ --gold: #c8860a;
+}
- advice_tts = make_tts(advice[:600], language)
- map_fig = build_map_city(city)
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #0c1a10;
+ --bg2: #132118;
+ --txt: #d5f0e0;
+ --border: #243d2d;
+ --green: #2a9460;
+ --gold: #f5a623;
+ }
+}
- return ann, report, wa_md, advice, report_tts, cid, advice_tts, pdf_path, map_fig
+* { font-family: 'Inter', sans-serif !important; }
+.gradio-container { background: var(--bg) !important; }
-# ══════════════════════════════════════════════════════════════
-# CSS — light + dark mode, both automatic and manual
-# ══════════════════════════════════════════════════════════════
-CSS = """
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:wght@700;900&family=JetBrains+Mono:wght@400;500&display=swap');
+label, .gr-label { color: var(--txt) !important; }
-/* Light mode */
-:root{
- --bg:#ffffff;--bg2:#f5f8f6;--bg3:#e8f3ec;
- --txt:#0d2b1e;--txt2:#2d5a3e;--muted:#6a8e7a;
- --border:#c0d9ca;--border2:#1f7a52;
- --green:#1f7a52;--green2:#25a06b;--green3:#2ec97f;
- --gold:#c8860a;--gold2:#f5a623;--gold-bg:#fffbf0;
- --info-bg:#f0faf4;--warn-bg:#fffbf0;
- --shadow:0 2px 10px rgba(13,43,30,.10);
- --radius:10px;--radius-lg:18px;
- --header-bg:linear-gradient(135deg,#14432e 0%,#0d2b1e 60%,#091a10 100%);
+input, textarea, select {
+ background: var(--bg) !important;
+ border: 1px solid var(--border) !important;
+ color: var(--txt) !important;
+ border-radius: 8px !important;
}
-/* System dark mode */
-@media(prefers-color-scheme:dark){
- :root{
- --bg:#0c1a10;--bg2:#132118;--bg3:#1a3024;
- --txt:#d5f0e0;--txt2:#8fd4ad;--muted:#5a9a78;
- --border:#243d2d;--border2:#2a9460;
- --green:#2a9460;--green2:#34c47a;--green3:#52e09a;
- --gold:#f5a623;--gold2:#f7bc57;--gold-bg:#1e1500;
- --info-bg:#0d2016;--warn-bg:#1a1300;
- --shadow:0 2px 14px rgba(0,0,0,.45);
- --header-bg:linear-gradient(135deg,#091a10 0%,#060d08 60%,#040a06 100%);
- }
-}
-/* Manual dark toggle class */
-.rh-dark{
- --bg:#0c1a10;--bg2:#132118;--bg3:#1a3024;
- --txt:#d5f0e0;--txt2:#8fd4ad;--muted:#5a9a78;
- --border:#243d2d;--border2:#2a9460;
- --green:#2a9460;--green2:#34c47a;--green3:#52e09a;
- --gold:#f5a623;--gold2:#f7bc57;--gold-bg:#1e1500;
- --info-bg:#0d2016;--warn-bg:#1a1300;
- --shadow:0 2px 14px rgba(0,0,0,.45);
- --header-bg:linear-gradient(135deg,#091a10 0%,#060d08 60%,#040a06 100%);
+
+button.primary {
+ background: linear-gradient(135deg, var(--green), #25a06b) !important;
+ color: white !important;
+ border: none !important;
+ font-weight: 600 !important;
}
-*,*::before,*::after{box-sizing:border-box;}
-body,.gradio-container{
- font-family:'Inter',sans-serif!important;
- background:var(--bg)!important;color:var(--txt)!important;
- transition:background .3s,color .3s;
+
+.tab-nav { background: var(--bg2) !important; border-bottom: 2px solid var(--border) !important; }
+.tab-nav button { color: var(--txt) !important; }
+.tab-nav button.selected { color: var(--gold) !important; border-bottom: 2px solid var(--gold) !important; }
+
+.info-box {
+ background: var(--bg2);
+ border-left: 4px solid var(--green);
+ padding: 10px 15px;
+ border-radius: 8px;
+ margin: 10px 0;
+ font-size: 0.85rem;
}
-/* Header */
-.rh-header{background:var(--header-bg);padding:26px 20px 20px;text-align:center;
- position:relative;overflow:hidden;border-bottom:2px solid var(--green);}
-.rh-header::before{content:'';position:absolute;inset:0;
- background:radial-gradient(ellipse 70% 60% at 50% 0%,rgba(37,160,107,.14),transparent);pointer-events:none;}
-.rh-title{font-family:'Playfair Display',serif!important;font-size:clamp(2rem,5vw,3rem)!important;
- font-weight:900!important;color:#f8fdf9!important;margin:0 0 4px!important;line-height:1.1;}
-.rh-subtitle{font-size:clamp(.9rem,2.5vw,1.05rem);color:#a8e8c4;margin:4px 0 6px;}
-.rh-tag{font-size:.76rem;color:#5de3a3;letter-spacing:.1em;text-transform:uppercase;}
-/* Top bar */
-.top-bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;
- padding:7px 16px;background:var(--bg2);border-bottom:1px solid var(--border);gap:8px;}
-.badge-group{display:flex;flex-wrap:wrap;gap:6px;}
-.badge{font-size:.67rem;font-weight:600;letter-spacing:.05em;padding:3px 10px;border-radius:20px;
- text-transform:uppercase;background:var(--bg);color:var(--green3);border:1px solid var(--border2);}
-.badge-gold{color:var(--gold);border-color:var(--gold2);}
-.badge-red {color:#ff8080;border-color:rgba(255,100,100,.4);}
-.dark-toggle{background:transparent;border:1px solid var(--border2);border-radius:20px;
- padding:4px 13px;cursor:pointer;color:var(--muted);font-size:.78rem;font-weight:500;
- font-family:'Inter',sans-serif;transition:all .2s;}
-.dark-toggle:hover{background:var(--bg3);color:var(--txt);}
-/* Tabs */
-.gradio-container .tab-nav{background:var(--bg2)!important;border-bottom:2px solid var(--border)!important;}
-.gradio-container .tab-nav button{font-family:'Inter',sans-serif!important;font-weight:500!important;
- font-size:.83rem!important;color:var(--muted)!important;padding:11px 18px!important;
- border-radius:0!important;background:transparent!important;transition:all .2s!important;}
-.gradio-container .tab-nav button.selected,
-.gradio-container .tab-nav button[aria-selected="true"]{
- color:var(--gold)!important;border-bottom:3px solid var(--gold2)!important;background:transparent!important;}
-/* Card title */
-.sec-title{font-size:.67rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
- color:var(--green3);margin-bottom:10px;padding-bottom:7px;border-bottom:1px solid var(--border);}
-/* Form */
-label,.gradio-container .label-wrap span{color:var(--txt)!important;}
-.gradio-container input,.gradio-container textarea{
- background:var(--bg)!important;border:1px solid var(--border2)!important;
- border-radius:var(--radius)!important;color:var(--txt)!important;font-family:'Inter',sans-serif!important;}
-.gradio-container input:focus,.gradio-container textarea:focus{
- border-color:var(--gold2)!important;box-shadow:0 0 0 3px rgba(245,166,35,.15)!important;outline:none!important;}
-.gradio-container .wrap{background:var(--bg)!important;border-color:var(--border2)!important;}
-.gradio-container .block{background:var(--bg)!important;}
-/* Buttons */
-.gradio-container button.primary{
- background:linear-gradient(135deg,var(--green),var(--green2))!important;color:#f8fdf9!important;
- border:none!important;border-radius:var(--radius)!important;font-weight:600!important;
- font-size:.88rem!important;padding:11px 22px!important;cursor:pointer!important;
- box-shadow:var(--shadow)!important;transition:all .2s!important;}
-.gradio-container button.primary:hover{
- background:linear-gradient(135deg,var(--green2),var(--green3))!important;transform:translateY(-1px)!important;}
-.gradio-container button.secondary{
- background:var(--bg)!important;border:1px solid var(--border2)!important;color:var(--green3)!important;}
-.gradio-container [data-testid="image"]{border:2px dashed var(--border2)!important;
- border-radius:var(--radius-lg)!important;background:var(--bg2)!important;}
-.gradio-container audio{width:100%!important;border-radius:var(--radius)!important;}
-.gradio-container .prose h2,.gradio-container .prose h3{color:var(--gold)!important;}
-/* Info boxes */
-.info-box{background:var(--info-bg);border:1px solid var(--border2);border-left:4px solid var(--green2);
- border-radius:var(--radius);padding:10px 14px;font-size:.87rem;line-height:1.6;margin-bottom:8px;color:var(--txt2);}
-.warn-box{background:var(--warn-bg);border:1px solid rgba(245,166,35,.4);border-left:4px solid var(--gold2);
- border-radius:var(--radius);padding:10px 14px;font-size:.87rem;margin-bottom:8px;color:var(--txt2);}
-.hotline-pill{display:inline-block;background:var(--bg2);color:var(--gold);border:1px solid var(--gold2);
- border-radius:20px;padding:2px 11px;font-size:.78rem;font-weight:600;}
-/* Report textarea */
-.gradio-container textarea{font-family:'JetBrains Mono',monospace!important;font-size:.82rem!important;line-height:1.7!important;}
-/* Chatbot */
-.gradio-container .message.user{background:var(--bg3)!important;color:var(--txt)!important;}
-.gradio-container .message.bot {background:var(--bg2)!important;color:var(--txt)!important;}
-/* Dropdowns */
-.gradio-container select,.gradio-container [data-testid="dropdown"]{
- background:var(--bg)!important;color:var(--txt)!important;border-color:var(--border2)!important;}
-/* Scrollbar */
-::-webkit-scrollbar{width:6px;height:6px;}
-::-webkit-scrollbar-track{background:var(--bg2);}
-::-webkit-scrollbar-thumb{background:var(--green);border-radius:3px;}
-@media(max-width:640px){
- .rh-header{padding:14px 12px;}
- .gradio-container .tab-nav button{padding:10px 10px!important;font-size:.74rem!important;}
+
+.sec-title {
+ font-size: 0.7rem;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--green);
+ margin-bottom: 10px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid var(--border);
}
+
+.message.user { background: var(--bg2) !important; }
+.message.bot { background: var(--bg) !important; border: 1px solid var(--border) !important; }
"""
HEADER_HTML = """
-
-
-
- Image Verification
- Legal Assistant
- Voice Support
- 4 Languages
- PDF Export
- LIVE
-
-
🌙 Dark
+
+
Rahbar | رہبر
+
Pakistan's Civic Complaint System
-
"""
HOTLINES_HTML = """
- Emergency Helplines:
- Garbage 1139
- Roads/NHA 051-9032800
- WASA Lahore 042-99200300
- CM Portal 0800-02345
- Ombudsman 051-9204551
+ Emergency Helplines:
+ 🗑️ Garbage: 1139 | 🕳️ Roads: 051-9032800 | 💧 WASA: 042-99200300 | 📞 CM Portal: 0800-02345
"""
# ══════════════════════════════════════════════════════════════
-# UI
+# BUILD UI
# ══════════════════════════════════════════════════════════════
def build_ui():
default_map = build_map_city("Lahore")
-
+
with gr.Blocks(title="Rahbar | Pakistan Civic Complaint System") as demo:
gr.HTML(HEADER_HTML)
-
+
with gr.Tabs():
-
- # ════════════════════════════════════════════
- # TAB 1 — File Complaint
- # ════════════════════════════════════════════
+ # Tab 1 - File Complaint
with gr.Tab("📝 File a Complaint"):
- with gr.Row(equal_height=False):
-
- # Left: inputs
- with gr.Column(scale=1, min_width=300):
+ with gr.Row():
+ with gr.Column(scale=1):
gr.HTML('
Citizen Details
')
- name_tb = gr.Textbox(label="Full Name", placeholder="e.g. Ali Raza")
- cnic_tb = gr.Textbox(label="CNIC (no dashes)", placeholder="1234567890123")
- phone_tb = gr.Textbox(label="Phone Number (optional)", placeholder="03xxxxxxxxx")
-
- gr.HTML('
Issue Photo
')
- gr.HTML('
Upload or capture a clear photo of the issue. On mobile, tap to use your camera.
')
- image_input = gr.Image(type="pil", label="Upload / Capture Photo",
- sources=["webcam","upload"], height=210)
-
- gr.HTML('
Complaint Details
')
- issue_type = gr.Radio(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="Issue Type")
- city_dd = gr.Dropdown(choices=ALL_CITIES, value="Lahore", label="City",
- allow_custom_value=True,
- info="All cities in Pakistan — type to search")
-
- gr.HTML('
Location
')
- gr.HTML("""
-Type your street/area below, or click Detect Location to auto-fill via your internet connection.
-You can also click directly on the map to pin a location.
-
""")
-
- location_tb = gr.Textbox(
- label="Street / Landmark / Area",
- placeholder="e.g. Near Jinnah Park, Main Boulevard, Street 5",
- lines=1)
-
- gps_btn = gr.Button("📍 Detect My Location", variant="secondary")
- gps_status = gr.Markdown(
- value="_Click the button above to detect your approximate location._")
-
- # GPS hidden state
- gps_lat = gr.State(value=None)
- gps_lon = gr.State(value=None)
-
- gr.HTML('
Map
')
- gr.HTML('
Click anywhere on the map to set a precise location — the street/area field will auto-fill.
')
- map_plot = gr.Plot(label="Location Map", value=default_map)
-
- desc_tb = gr.Textbox(label="Description (optional)", placeholder="Describe the issue...", lines=3)
- language_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Report & Voice Language")
- tts_cb = gr.Checkbox(label="Read report aloud (TTS)", value=False)
- submit_btn = gr.Button("Submit Complaint", variant="primary", size="lg")
-
- # Right: outputs
- with gr.Column(scale=2, min_width=320):
+ name_tb = gr.Textbox(label="Full Name", placeholder="e.g., Ali Raza")
+ cnic_tb = gr.Textbox(label="CNIC (no dashes)", placeholder="1234567890123")
+ phone_tb = gr.Textbox(label="Phone (optional)", placeholder="03xxxxxxxxx")
+
+ gr.HTML('
Issue Photo
')
+ image_input = gr.Image(type="pil", label="Upload Photo", height=200)
+
+ gr.HTML('
Complaint Details
')
+ issue_type = gr.Radio(choices=ISSUE_TYPES, label="Issue Type")
+ city_dd = gr.Dropdown(choices=ALL_CITIES, value="Lahore", label="City", allow_custom_value=True)
+
+ gr.HTML('
Location
')
+ gps_btn = gr.Button("📍 Detect My Location", variant="secondary")
+ gps_status = gr.Markdown("_Click the button above to detect your location_")
+ location_tb = gr.Textbox(label="Street / Landmark / Area", placeholder="Enter exact location")
+ map_plot = gr.Plot(label="Map", value=default_map)
+
+ desc_tb = gr.Textbox(label="Description (optional)", lines=3)
+ language_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Language")
+ tts_cb = gr.Checkbox(label="🔊 Read report aloud", value=False)
+ submit_btn = gr.Button("Submit Complaint", variant="primary")
+
+ with gr.Column(scale=1):
gr.HTML('
Verification Result
')
- annotated_out = gr.Image(label="Detection Output", height=230)
- complaint_id_out = gr.Textbox(label="Complaint Reference ID", interactive=False)
-
- gr.HTML('
Complaint Report
')
- report_out = gr.Textbox(label="Official Summary", lines=14, interactive=False,
- placeholder="Report will appear here after submission...")
+ annotated_out = gr.Image(label="Analysis Result")
+ complaint_id_out = gr.Textbox(label="Complaint ID", interactive=False)
+
+ gr.HTML('
Complaint Report
')
+ report_out = gr.Textbox(label="Report", lines=15, interactive=False)
+ pdf_out = gr.File(label="Download PDF Report")
wa_out = gr.Markdown()
-
- gr.HTML('
Download PDF
')
- gr.HTML('
Professional government-format PDF — download and share via WhatsApp.
')
- pdf_out = gr.File(label="Download PDF Report", interactive=False)
- report_tts_out = gr.Audio(label="Report Audio", autoplay=False)
-
- gr.HTML('
Legal Advice
')
- gr.HTML('
Your rights and steps under Pakistani civic law — in your selected language.
')
- legal_out = gr.Textbox(label="Legal Rights & Steps", lines=12, interactive=False,
- placeholder="Legal advice will appear here...")
- advice_tts_out = gr.Audio(label="Legal Advice Audio", autoplay=False)
-
- # ── GPS button ──
- def on_gps(city):
- fig, status, addr, lat, lon = gps_detect(city)
- return fig, status, addr, lat, lon
-
- gps_btn.click(fn=on_gps,
- inputs=[city_dd],
- outputs=[map_plot, gps_status, location_tb, gps_lat, gps_lon])
-
- # ── Map click → fill location ──
- def on_map_clicked(evt: gr.SelectData, city):
- """Triggered when user clicks Plotly map."""
- try:
- lat = evt.index[0]; lon = evt.index[1]
- addr = reverse_geocode(lat, lon)
- fig = build_map(lat, lon, addr)
- return addr, fig
- except Exception:
- return "", build_map_city(city)
-
- map_image.select(fn=on_map_clicked,
- inputs=[city_dd],
- outputs=[location_tb, map_plot])
-
- # ── City change → update map ──
- city_dd.change(fn=update_map_on_city, inputs=[city_dd], outputs=[map_plot])
-
- # ── Location text change → update map ──
- location_tb.change(fn=update_map_on_location,
- inputs=[city_dd, city_dd, location_tb],
- outputs=[map_plot])
-
- # ── Submit ──
+ report_tts_out = gr.Audio(label="Report Audio")
+
+ gr.HTML('
Legal Advice
')
+ legal_out = gr.Markdown()
+ advice_tts_out = gr.Audio(label="Legal Advice Audio")
+
+ # GPS and map interactions
+ gps_btn.click(fn=gps_detect, inputs=[city_dd], outputs=[map_plot, gps_status, location_tb, gr.State(), gr.State()])
+ city_dd.change(fn=build_map_city, inputs=[city_dd], outputs=[map_plot])
+ location_tb.change(fn=build_map_city, inputs=[city_dd], outputs=[map_plot])
+
submit_btn.click(
fn=make_report,
- inputs=[image_input, issue_type, city_dd, location_tb,
- name_tb, cnic_tb, phone_tb, desc_tb, language_dd, tts_cb],
- outputs=[annotated_out, report_out, wa_out, legal_out,
- report_tts_out, complaint_id_out, advice_tts_out,
- pdf_out, map_plot])
-
- # ════════════════════════════════════════════
- # TAB 2 — Legal Reference & Chatbot
- # ════════════════════════════════════════════
- with gr.Tab("⚖️ Legal Reference & Chatbot"):
-
- gr.HTML('
Civic Laws Quick Reference
')
- with gr.Row():
- law_issue_dd = gr.Dropdown(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="Issue Type", scale=1)
- law_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1)
+ inputs=[image_input, issue_type, city_dd, location_tb, name_tb, cnic_tb, phone_tb, desc_tb, language_dd, tts_cb],
+ outputs=[annotated_out, report_out, wa_out, legal_out, report_tts_out, complaint_id_out, advice_tts_out, pdf_out, map_plot]
+ )
+
+ # Tab 2 - Legal Rights
+ with gr.Tab("⚖️ Legal Rights"):
+ issue_dd = gr.Dropdown(choices=ISSUE_TYPES, label="Select Issue")
+ lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Language")
+ law_btn = gr.Button("Show Legal Information", variant="primary")
law_out = gr.Markdown()
- gr.Button("Show My Rights", variant="primary").click(
- fn=law_info, inputs=[law_issue_dd, law_lang_dd], outputs=[law_out])
+ law_btn.click(fn=law_info, inputs=[issue_dd, lang_dd], outputs=[law_out])
gr.HTML(HOTLINES_HTML)
-
- gr.HTML('
Ask the Legal Assistant
')
- gr.HTML('
Ask any question about civic issues, your legal rights, or how to escalate a complaint. Voice input and audio output are both supported.
')
-
- chat_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Response Language")
- chatbot = gr.Chatbot(label="Legal Assistant", height=400, value=[])
-
- with gr.Row():
- chat_input = gr.Textbox(label="Your Question",
- placeholder="e.g. WASA didn't fix the pipe for 3 days — what are my rights?",
- lines=2, scale=4)
- chat_send_btn = gr.Button("Send ➤", variant="primary", scale=1)
-
- gr.HTML('
Voice Question
')
- with gr.Row():
- chat_audio_in = gr.Audio(type="filepath", label="Record Your Question",
- sources=["microphone","upload"], scale=3)
- chat_voice_btn = gr.Button("🎤 Send Voice", variant="secondary", scale=1)
-
- gr.HTML('
Listen to Answer
')
- with gr.Row():
- chat_tts_out = gr.Audio(label="Last Answer (Audio)", autoplay=True, scale=3)
- chat_tts_btn = gr.Button("🔊 Play Answer", variant="secondary", scale=1)
-
- gr.Examples(
- examples=[
- ["WASA did not fix the pipe leakage for 3 days — what are my legal rights?"],
- ["Water in my area is contaminated — where should I complain?"],
- ["Garbage has not been collected for a week — which law applies?"],
- ["Authority ignored my complaint — what do I do next?"],
- ["Pothole damaged my car — can I claim compensation?"],
- ["How do I file a complaint on Pakistan Citizen Portal?"],
- ],
- inputs=chat_input, label="Sample Questions")
-
- chat_send_btn.click(fn=legal_chatbot,
- inputs=[chat_input, chatbot, chat_lang_dd],
- outputs=[chatbot, chat_input])
- chat_input.submit(fn=legal_chatbot,
- inputs=[chat_input, chatbot, chat_lang_dd],
- outputs=[chatbot, chat_input])
- chat_voice_btn.click(fn=voice_to_chat,
- inputs=[chat_audio_in, chatbot, chat_lang_dd],
- outputs=[chatbot, chat_input])
- chat_tts_btn.click(fn=read_last_answer,
- inputs=[chatbot, chat_lang_dd],
- outputs=[chat_tts_out])
-
- # ════════════════════════════════════════════
- # TAB 3 — Voice Tools
- # ════════════════════════════════════════════
- with gr.Tab("🎤 Voice Tools"):
- gr.HTML('
Speech to Text
')
- gr.HTML('
Record your complaint — transcribed automatically. Supports English, Urdu, Punjabi, Sindhi.
')
- gr.HTML('
Tip: Speak clearly. Copy transcript to the complaint form.
')
- audio_in = gr.Audio(type="filepath", label="Record or Upload Audio", sources=["microphone","upload"])
- stt_btn = gr.Button("Transcribe to Text", variant="primary")
- stt_out = gr.Textbox(label="Transcript (editable)", lines=6, interactive=True,
- placeholder="Transcribed text will appear here...")
- stt_btn.click(fn=stt_transcribe, inputs=[audio_in], outputs=[stt_out])
-
- gr.HTML('
Text to Speech Test
')
- gr.HTML('
Test audio output in any supported language.
')
- with gr.Row():
- tts_text_in = gr.Textbox(label="Enter text", placeholder="Type something...", scale=3)
- tts_lang_in = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1)
- tts_test_btn = gr.Button("▶ Play", variant="secondary")
- tts_test_out = gr.Audio(label="Audio Output", autoplay=True)
- tts_test_btn.click(fn=make_tts, inputs=[tts_text_in, tts_lang_in], outputs=[tts_test_out])
-
- # ════════════════════════════════════════════
- # TAB 4 — Admin
- # ════════════════════════════════════════════
+
+ # Tab 3 - Chatbot
+ with gr.Tab("💬 Ask a Question"):
+ chat_lang = gr.Dropdown(choices=LANGUAGES, value="English", label="Response Language")
+ chatbot = gr.Chatbot(height=400)
+ msg = gr.Textbox(label="Your Question", placeholder="Ask about garbage, roads, water, or legal rights...")
+ send = gr.Button("Send", variant="primary")
+ audio_in = gr.Audio(type="filepath", label="Voice Input", sources=["microphone"])
+ voice_send = gr.Button("🎤 Send Voice")
+ tts_btn = gr.Button("🔊 Read Last Answer")
+ tts_out = gr.Audio(label="Audio Answer")
+
+ send.click(fn=legal_chatbot, inputs=[msg, chatbot, chat_lang], outputs=[chatbot, msg])
+ msg.submit(fn=legal_chatbot, inputs=[msg, chatbot, chat_lang], outputs=[chatbot, msg])
+ voice_send.click(fn=voice_to_chat, inputs=[audio_in, chatbot, chat_lang], outputs=[chatbot, msg])
+ tts_btn.click(fn=read_last_answer, inputs=[chatbot, chat_lang], outputs=[tts_out])
+
+ # Tab 4 - Admin
with gr.Tab("📊 Admin"):
- gr.HTML('
Complaint Statistics
')
- refresh_btn = gr.Button("Refresh", variant="primary")
- with gr.Row():
- stats_out = gr.Markdown()
- log_out = gr.Markdown()
- refresh_btn.click(fn=get_admin_stats, outputs=[stats_out, log_out])
-
+ refresh = gr.Button("Refresh Stats", variant="primary")
+ stats = gr.Markdown()
+ logs = gr.Markdown()
+ refresh.click(fn=get_admin_stats, outputs=[stats, logs])
+
return demo
-
# ═════════════════════════════════════════════════════════════��
# LAUNCH
# ══════════════════════════════════════════════════════════════
@@ -1383,11 +703,6 @@ if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
- share=False, # HuggingFace Spaces sets this automatically
- css=CSS, # Gradio 6: CSS goes in launch()
- theme=gr.themes.Base(
- primary_hue=gr.themes.colors.green,
- secondary_hue=gr.themes.colors.yellow,
- ),
- ssr_mode=False, # Ensures JS (dark toggle) works correctly on Spaces
+ css=CSS,
+ theme=gr.themes.Soft()
)
\ No newline at end of file