diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,285 +1,334 @@ """ -Rahbar v8.1 — Pakistan AI Civic Complaint Platform -- Gradio 6+ compatible (css in launch(), no type= in Chatbot) -- GPS via IP geolocation (requests → ipinfo.io, no JS/Selenium) -- Scattermap (not Scattermapbox) for Plotly -- English UI, other languages optional for report content -- PDF via ReportLab (professional, no grid lines) -- Map via gr.Plot (Plotly Scattermap) -- Voice input/output fully working -- Light + Dark mode CSS +Rahbar v9.0 — Pakistan AI Civic Complaint Platform +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +• Gradio 6+ compatible (css in launch, no type= on Chatbot) +• GPS: IP geolocation → shows city on map automatically +• Map: Plotly Scattermap, click-to-fill street/landmark box +• Full Pakistan coverage (not just big cities — any area) +• PDF via ReportLab (professional, no grid lines) +• Voice input/output fully working in chatbot +• Light + Dark mode CSS (auto + manual toggle) +• All UI in English; report content in selected language """ import os, io, re, uuid, base64, datetime, urllib.parse from PIL import Image import gradio as gr -# ── ReportLab imports ────────────────────────────────────────── -from reportlab.lib.pagesizes import A4 -from reportlab.lib import colors -from reportlab.lib.units import inch -from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle -from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT -from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, - Table, TableStyle, HRFlowable) - GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "") GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") - -complaint_log = [] +complaint_log = [] # ══════════════════════════════════════════════════════════════ -# GPS / IP GEOLOCATION (pure Python — no JS, no Selenium) +# IP GEOLOCATION (pure Python — no browser permissions needed) # ══════════════════════════════════════════════════════════════ def get_location_from_ip(): """ - Fetch approximate location using IP geolocation. - Returns (lat, lon, city, region) or None on failure. - Tries ipinfo.io first, then ip-api.com as fallback. + 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. """ - import requests - - # ── Provider 1: ipinfo.io ──────────────────────────────── try: - r = requests.get("https://ipinfo.io/json", timeout=5) + import requests + r = requests.get("https://ipinfo.io/json", timeout=6) if r.status_code == 200: - data = r.json() - loc = data.get("loc", "") + d = r.json() + loc = d.get("loc", "") if loc and "," in loc: lat, lon = map(float, loc.split(",")) - city = data.get("city", "Unknown") - region = data.get("region", "Unknown") - return lat, lon, city, region + return lat, lon, d.get("city","Unknown"), d.get("region","Unknown") except Exception: pass - - # ── Provider 2: ip-api.com (fallback) ─────────────────── try: - r = requests.get("http://ip-api.com/json/", timeout=5) + import requests + r = requests.get("http://ip-api.com/json/", timeout=6) if r.status_code == 200: - data = r.json() - if data.get("status") == "success": - return ( - float(data["lat"]), - float(data["lon"]), - data.get("city", "Unknown"), - data.get("regionName", "Unknown"), - ) + 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: pass + return None - return None # Both providers failed +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) + 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: + pass + return f"{lat:.5f}, {lon:.5f}" -def gps_locate_and_update(city_value): + +def gps_detect(city_hint): """ - Called when user clicks 'Detect My Location'. - Returns (map_figure, status_message, lat, lon). - If detection fails, falls back to selected city centre. + 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, detected_city, detected_region = result - status = ( - f"📍 Location detected via IP: **{detected_city}, {detected_region}** " - f"(lat {lat:.4f}, lon {lon:.4f}). " - f"*Note: IP geolocation is approximate (~city level).*" - ) - fig = create_map(city_value, detected_city, lat=lat, lon=lon) - return fig, status, lat, lon + 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 = CITY_COORDS.get(city_value, (31.5204, 74.3587)) - status = ( - "⚠️ Could not detect location automatically. " - "Showing city centre. Please enter your street/area manually." - ) - fig = create_map(city_value) - return fig, status, clat, clon + 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) # ══════════════════════════════════════════════════════════════ -# RAG KNOWLEDGE BASE +# PLOTLY MAP (Scattermap — Gradio 6 safe, no mapbox token) # ══════════════════════════════════════════════════════════════ -RAG_DOCUMENTS = [ - { - "id": "garbage_001", "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": "garbage_002","category": "Garbage", - "title": "Urban Solid Waste — City-level Responsibility", - "content": "Failure to collect garbage is a serious violation. EPA 1997 Section 11 prohibits pollution. 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 Lahore / KMC Karachi", - "response_time": "48 hours","fine": "Rs. 500 – 50,000", - }, - { - "id": "garbage_escalation","category": "Garbage", - "title": "Garbage Complaint Escalation Ladder", - "content": "If authority fails: 1.Contact Union Council 2.Apply at DC office 3.CM Cell 0800-02345 4.citizenportal.gov.pk 5.Federal Ombudsman 051-9204551 6.High Court Writ. Compensation possible 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": "pothole_001","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 covers LDA and C&W. Vehicle damage = compensation claim. NHA: 051-9032800. LDA: 042-99230215.", - "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": "pothole_002","category": "Pot Hole", - "title": "Road Accident Due to Pothole — Legal Recourse", - "content": "If accident: 1.File police report 2.Photograph with date 3.Written notice to NHA/LDA 4.Negligence claim under Tort Law 5.Federal Ombudsman 051-9204551 6.High Court Writ. Reports at nha.gov.pk.", - "laws": ["Tort Law Negligence","NHA Safety Ordinance 2000","Constitution Article 9"], - "hotline": "051-9204551","authority": "Federal Ombudsman / High Court", - "response_time": "Court timeline","fine": "Compensation for injury/damage", - }, - { - "id": "water_001","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. Supreme Court 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": "water_escalation","category": "Pipe Leakage", - "title": "WASA Did Not Act — Escalation Steps", - "content": "If WASA fails: 1.Call WASA helpline 2.Written application at WASA office 3.DC office 4.CM Cell 0800-02345 5.citizenportal.gov.pk 6.PWA 051-9246150 7.Federal Ombudsman 8.High Court. Keep evidence.", - "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": "rights_001","category": "General", - "title": "Fundamental Rights of Pakistani Citizens", - "content": "Article 9: Right to Life includes clean environment. Article 14: Dignity. Article 19A: Right to Information. Citizen Portal complaints 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": "rights_002","category": "General", - "title": "How to File a Civic Complaint — Complete Guide", - "content": "1.Photograph with date/time 2.Note exact location 3.Call helpline get number 4.If no action in 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": "rights_003","category": "General", - "title": "Federal Ombudsman — Role and Process", - "content": "The Federal Ombudsman (Wafaqi Mohtasib) hears complaints against government institutions. Free to file. Decision within 60 days. Phone: 051-9204551 | mohtasib.gov.pk. Can appeal to President of Pakistan.", - "laws": ["Federal Ombudsmen Institutional Reforms Act 2013"], - "hotline": "051-9204551","authority": "Federal Ombudsman (Mohtasib)", - "response_time": "60 days","fine": "Binding recommendations", - }, -] +PAKISTAN_CENTRE = (30.3753, 69.3451) + +def build_map(lat, lon, label="", zoom=14): + try: + import plotly.graph_objects as go + except ImportError: + 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) + # ══════════════════════════════════════════════════════════════ -# RAG ENGINE +# KNOWLEDGE BASE # ══════════════════════════════════════════════════════════════ -class RAGEngine: +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.documents = RAG_DOCUMENTS self.vectorizer = None self.doc_matrix = None - self._initialized = False + self._ready = False def initialize(self): - if self._initialized: - return True + if self._ready: return True try: from sklearn.feature_extraction.text import TfidfVectorizer corpus = [ - f"{d['title']} {d['content']} {' '.join(d.get('laws',[]))} " - f"{d.get('category','')} {d.get('hotline','')} {d.get('authority','')}" + 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 - ) + max_features=8000, sublinear_tf=True, min_df=1) self.doc_matrix = self.vectorizer.fit_transform(corpus) - self._initialized = True - return True + self._ready = True; return True except Exception as e: - print(f"RAG init error: {e}") - return False + print(f"KE init error: {e}"); return False def retrieve(self, query, top_k=3): - if not self._initialized: - if not self.initialize(): - return self._keyword_fallback(query, top_k) - 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] - top_idx = np.argsort(scores)[::-1][:top_k] - results = [] - for idx in top_idx: - if scores[idx] > 0.01: - doc = self.documents[idx].copy() - doc['relevance_score'] = float(scores[idx]) - results.append(doc) - return results if results else self._keyword_fallback(query, top_k) - except Exception: - return self._keyword_fallback(query, top_k) - - def _keyword_fallback(self, query, top_k=3): - q = query.lower() - keywords = { - "Garbage": ["garbage","waste","sanitation","trash","1139"], - "Pot Hole": ["pothole","pot hole","road","nha"], - "Pipe Leakage": ["water","wasa","pipe","leakage","contaminated"], + 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"], } - found_cat = None - for cat, kws in keywords.items(): - if any(kw in q for kw in kws): - found_cat = cat; break - matched = [d for d in self.documents if found_cat and d['category'] == found_cat] - for d in self.documents: - if d['category'] == 'General' and d not in matched: - matched.append(d) - return matched[:top_k] if matched else self.documents[:top_k] + 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 "" + if not docs: return "" ctx = "Relevant Legal Information:\n\n" - for i, doc in enumerate(docs, 1): - ctx += (f"[{i}] {doc['title']}\n" - f"Content: {doc['content'][:400]}\n" - f"Laws: {', '.join(doc['laws'][:2])}\n" - f"Helpline: {doc['hotline']} | Response: {doc['response_time']}\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 -rag_engine = RAGEngine() -rag_engine.initialize() +ke = KnowledgeEngine() +ke.initialize() # ══════════════════════════════════════════════════════════════ # STATIC DATA # ══════════════════════════════════════════════════════════════ -CITIES_AREAS = { - "Lahore": ["Model Town","DHA","Gulberg","Johar Town","Bahria Town","Township","Cantonment"], - "Karachi": ["Clifton","DHA","Gulshan-e-Iqbal","PECHS","Korangi","Saddar","North Nazimabad"], - "Islamabad": ["F-7","F-8","F-10","G-9","G-10","G-11","Blue Area"], - "Rawalpindi": ["Saddar","Bahria Town","Chaklala","Satellite Town","Murree Road"], - "Faisalabad": ["Jinnah Colony","Madina Town","Peoples Colony","Ghulam Muhammad Abad","Susan Road"], - "Multan": ["Shah Rukn-e-Alam","Cantt","Gulgasht Colony","New Multan","Bosan Road"], - "Peshawar": ["Hayatabad","University Town","Cantt","Saddar","Gulbahar"], - "Quetta": ["Satellite Town","Jinnah Town","Cantt","Sariab Road","Brewery Road"], -} - +# 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), @@ -289,77 +338,137 @@ CITY_COORDS = { "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} LEGAL_KB = { "Garbage": { - "laws": [ - "Punjab Waste Management Act 2014", - "Pakistan Environmental Protection Act 1997 (Section 11)", - "Punjab Local Government Act 2022 (Schedule II – Sanitation Duties)", - "Pakistan Penal Code Section 268 – Public Nuisance", - ], - "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 of Pakistan, Article 9 & 14)", - "Right to file FIR under PPC Section 268 if authority fails to act", - "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", - "dataset_ref": "Punjab SWMB | Urban Issues Dataset", + "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", }, "Pot Hole": { - "laws": [ - "National Highways Safety Ordinance 2000", - "Punjab Local Government Act 2022 (Section 54 – Road Maintenance)", - "Motor Vehicles Ordinance 1965 (Road Authority Liability)", - "Tort Law – Negligence (Pakistani courts)", - ], - "fine": "Authority liable for vehicle damage & personal injury", - "authority": "National Highway Authority (NHA) / C&W Department / LDA", - "hotline": "051-9032800", - "response": "72 hours", - "citizen_rights": [ - "Right to claim compensation for vehicle damage or personal injury", - "Right to lodge complaint with Federal Ombudsman", - "Right to file High Court writ petition for dereliction of duty", - "Right to written notice to NHA/LDA", - ], - "escalation": "Federal Ombudsman: 051-9204551 | nha.gov.pk", - "dataset_ref": "NHA Road Quality Reports | Road Issues Detection Dataset", + "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", }, "Pipe Leakage": { - "laws": [ - "Punjab Water Act 2019 (Section 23 – Supply Obligation)", - "WASA Act – Water & Sanitation Agency Bylaws", - "Pakistan Environmental Protection Act 1997 (Section 13)", - "Punjab Local Government Act 2022 (Water & Sewerage Schedules)", - "Constitution of Pakistan Article 9 – Right to Life", - ], - "fine": "Compensatory damages + Rs. 10,000 – 5,00,000", - "authority": "WASA / Pakistan Water Authority", - "hotline": "042-99200300", - "response": "24 hours", - "citizen_rights": [ - "Right to safe drinking water (Supreme Court ruling 2018 – PLD 2018 SC 1)", - "Right to compensation for property damage from water leakage", - "Right to disconnect billing if water supply is contaminated", - "Right to file complaint with Pakistan Water Authority (PWA)", - ], - "escalation": "Pakistan Water Authority: 051-9246150 | CM Portal: 0800-02345", - "dataset_ref": "WASA Annual Reports | Consumer Complaints Dataset", + "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", }, } -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} +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 ڪلاڪن ۾ ذميواري آهي."}, +} # ══════════════════════════════════════════════════════════════ # YOLO DETECTION @@ -370,27 +479,23 @@ def detect_with_yolo(image_pil, issue_type): import numpy as np model = YOLO("yolo26n.pt") results = model(np.array(image_pil), verbose=False) - result = results[0] - names = model.names - detected, severity = [], 1 + result = results[0]; names = model.names + detected, sev = [], 1 for box in result.boxes: - cls_id = int(box.cls[0]); conf = float(box.conf[0]) - detected.append(f"{names.get(cls_id, f'class_{cls_id}')} ({conf:.0%})") - if issue_type == "Garbage" and cls_id in WASTE_CLASS_IDS: - severity = min(10, severity + 2) - elif issue_type in ("Pot Hole", "Pipe Leakage"): - severity = min(10, severity + 1) - annotated = Image.fromarray(result.plot()) - summary = (f"Detected {len(detected)} object(s): {', '.join(detected[:5])}" - if detected else "No specific objects detected.") - return annotated, summary, max(severity, 3) + 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, "Object detection library not available.", 5 + return image_pil, "Detection library not available.", 5 except Exception as e: return image_pil, f"Detection error: {e}", 5 # ══════════════════════════════════════════════════════════════ -# GEMINI VISION +# GEMINI # ══════════════════════════════════════════════════════════════ def analyze_with_gemini(image_pil, issue, location, city, yolo_summary): if not GOOGLE_API_KEY: @@ -398,884 +503,637 @@ def analyze_with_gemini(image_pil, issue, location, city, yolo_summary): try: import google.generativeai as genai genai.configure(api_key=GOOGLE_API_KEY) - model = genai.GenerativeModel("gemini-3-flash-preview") - buf = io.BytesIO() - image_pil.save(buf, format="JPEG") - prompt = ( - f"You are a STRICT Pakistani Civic Issue Inspector.\n" - f"REPORTED ISSUE: '{issue}' | CITY: {city} | LOCATION: {location}\n" - f"DETECTION: {yolo_summary}\n" - f"Garbage=actual waste/litter, Pot Hole=visible road hole, Pipe Leakage=water from pipe.\n" - f"Respond ONLY in this format:\n" - f"STATUS: [APPROVED or REJECTED]\n" - f"REASON: [2-3 sentences]\n" - f"SEVERITY: [1-10]\n" - f"CONFIDENCE: [XX%]\n" - f"RECOMMENDED_ACTION: [one sentence]" - ) - image_part = {"mime_type": "image/jpeg", - "data": base64.b64encode(buf.getvalue()).decode()} - return model.generate_content([prompt, image_part]).text.strip() + 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_response(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"), - ]: +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) - r[key] = v.upper() if key == "status" else (int(v) if key == "severity" else 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() + 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 # ══════════════════════════════════════════════════════════════ -# LEGAL ADVICE (LLM) +# LEGAL ADVICE # ══════════════════════════════════════════════════════════════ -def analyze_with_llama(issue, location, city, yolo_summary, severity, language="English"): +def get_legal_advice(issue, location, city, yolo_s, severity, language="English"): kb = LEGAL_KB.get(issue, {}) - lang_map = { - "Urdu": "Respond entirely in Urdu script.", - "Punjabi": "Respond in Punjabi Shahmukhi script.", - "Sindhi": "Respond in Sindhi script.", - } - lang_instruction = lang_map.get(language, "Respond in clear professional English.") - + 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 ( - "Applicable Laws:\n" + "\n".join(f" • {l}" for l in kb.get("laws", [])) + - f"\n\nCitizen Rights:\n{rights}" - f"\n\nFine / Penalty: {kb.get('fine', 'N/A')}" - f"\nAuthority Helpline: {kb.get('hotline', 'N/A')}" - f"\nRequired Response Time: {kb.get('response', 'N/A')}" - f"\n\nEscalation: {kb.get('escalation', 'N/A')}" - "\n\n(Configure API key for AI-generated legal advice)" - ) + 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 - client = Groq(api_key=GROQ_API_KEY) - prompt = ( - f"You are a Pakistani civic law expert.\n" - f"{lang_instruction}\n" - f"Complaint: {issue} in {location}, {city} | Severity: {severity}/10\n" - f"Applicable Laws: {', '.join(kb.get('laws', []))}\n" - f"Required Response Time: {kb.get('response', '72 hours')}\n\n" - f"Provide:\n" - f"1. Specific legal rights (cite law names/sections)\n" - f"2. Exact numbered steps to file a formal complaint\n" - f"3. What to do if authority does not respond in time\n" - f"4. Possible compensation or legal action available\n" - f"5. Relevant helplines and escalation contacts\n" - f"Keep it concise and practical for an ordinary Pakistani citizen." - ) - resp = client.chat.completions.create( + 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 - ) + 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}" # ══════════════════════════════════════════════════════════════ -# RAG CHATBOT — Gradio 6 messages format +# CHATBOT # ══════════════════════════════════════════════════════════════ -def legal_chatbot_rag(user_message, history, language): - """ - history is a list of {"role": "user"|"assistant", "content": str} - (Gradio 6 messages format — no type= parameter needed on Chatbot). - """ - if history is None: - history = [] - if not user_message.strip(): - return history, "" - - retrieved_docs = rag_engine.retrieve(user_message, top_k=3) - rag_context = rag_engine.format_context(retrieved_docs) - - lang_map = { - "Urdu": "Respond entirely in Urdu script.", - "Punjabi": "Respond in Punjabi Shahmukhi script.", - "Sindhi": "Respond in Sindhi script.", - } - lang_instruction = lang_map.get(language, "Respond in clear professional English.") - - system_content = ( - f"You are Rahbar Legal Assistant — a civic rights advisor for Pakistani citizens.\n" - f"{lang_instruction}\n" - f"Only discuss: water, pipe leakage, WASA, garbage, roads, potholes, Pakistani civic law.\n" - f"Always cite specific laws and provide helpline numbers. Max 250 words per response.\n\n" - f"Knowledge Base:\n{rag_context}" - ) - +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: - if retrieved_docs: - doc = retrieved_docs[0] - answer = (f"**{doc['title']}**\n\n{doc['content'][:500]}\n\n" - f"Helpline: {doc['hotline']} | Response Time: {doc['response_time']}\n" - f"Laws: {', '.join(doc['laws'][:2])}\n\n" - f"_(Configure API key for full AI-powered responses)_") - else: - answer = "I can help with water, garbage, and road issues in Pakistan. Please ask a specific civic question." - new_history = history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": answer}, - ] - return new_history, "" - + 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 - client = Groq(api_key=GROQ_API_KEY) - api_messages = [{"role": "system", "content": system_content}] - # Replay last 8 turns - for msg in history[-16:]: - api_messages.append({"role": msg["role"], "content": msg["content"]}) - api_messages.append({"role": "user", "content": user_message}) - resp = client.chat.completions.create( - model="llama-3.3-70b-versatile", - messages=api_messages, - max_tokens=500 - ) - answer = resp.choices[0].message.content.strip() - if retrieved_docs: - refs = [f"[{d['title'][:40]}]" for d in retrieved_docs[:2]] - answer += f"\n\n_Sources: {' | '.join(refs)}_" + 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: - answer = f"Sorry, there was an error: {e}" - - new_history = history + [ - {"role": "user", "content": user_message}, - {"role": "assistant", "content": answer}, - ] - return new_history, "" + ans = f"Error: {e}" + return history+[{"role":"user","content":user_message},{"role":"assistant","content":ans}], "" -def chatbot_tts_output(history, language): - if not history: - return None - # history is list of dicts in messages format +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 msg.get("role") == "assistant": - text = re.sub(r'_Sources:.*?_', '', msg["content"], flags=re.DOTALL).strip() - return make_tts(text[:600], language) + 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, "" + # ══════════════════════════════════════════════════════════════ # TTS # ══════════════════════════════════════════════════════════════ def make_tts(text, language): try: from gtts import gTTS - lang_code = LANG_CODES.get(language, "en") - tts = gTTS(text=str(text)[:600], lang=lang_code, slow=False) + 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) path = f"/tmp/tts_{uuid.uuid4().hex[:8]}.mp3" - tts.save(path) - return path + 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 + 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(audio_file): - if audio_file is None: - return "No audio received. Please record or upload audio first." - - def ensure_wav(path): - if path.lower().endswith(".wav"): - return path +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 try: from pydub import AudioSegment - out = path + "_converted.wav" - AudioSegment.from_file(path).export(out, format="wav") - return out - except Exception: - return path - + out=p+"_c.wav"; AudioSegment.from_file(p).export(out,format="wav"); return out + except: return p if GROQ_API_KEY: try: from groq import Groq - client = Groq(api_key=GROQ_API_KEY) - wav_path = ensure_wav(audio_file) - with open(wav_path, "rb") as f: - result = client.audio.transcriptions.create( - model="whisper-large-v3", file=f, response_format="text" - ) - text = result if isinstance(result, str) else result.text - return text.strip() or "No speech detected in audio." - except Exception as e: - groq_err = str(e) - else: - groq_err = "API key not configured" - + 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" try: import speech_recognition as sr - wav_path = ensure_wav(audio_file) - recognizer = sr.Recognizer() - with sr.AudioFile(wav_path) as src: - recognizer.adjust_for_ambient_noise(src, duration=0.3) - audio_data = recognizer.record(src) - return recognizer.recognize_google(audio_data) + 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. Error: {groq_err}. Fallback: {e2}" + return f"Transcription failed. Primary: {groq_err}. Fallback: {e2}" # ══════════════════════════════════════════════════════════════ # LAW REFERENCE # ══════════════════════════════════════════════════════════════ def law_info(issue, language): - kb = LEGAL_KB.get(issue, {}) - rights = "\n".join(f" - {r}" for r in kb.get("citizen_rights", [])) - out = f"## Legal Reference: {issue}\n\n### Applicable Laws\n" - for law in kb.get("laws", []): - out += f" - {law}\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### Official Helpline\n**{kb.get('hotline','N/A')}**\n" - f"\n### Mandatory Response Time\n{kb.get('response','N/A')}\n" - f"\n### Citizen Rights\n{rights}\n" - f"\n### Escalation Path\n{kb.get('escalation','N/A')}\n" - f"\n---\n*Source: {kb.get('dataset_ref','Pakistani civic law databases')}*" - ) + 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 STATS +# ADMIN # ══════════════════════════════════════════════════════════════ def get_admin_stats(): - total = len(complaint_log) - if total == 0: - return "No complaints filed yet.", "" - counts = {"Garbage": 0, "Pot Hole": 0, "Pipe Leakage": 0} - cities, severities = {}, [] + 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: - issue = c.get("issue", "") - counts[issue] = counts.get(issue, 0) + 1 - city = c.get("city", "Unknown") - cities[city] = cities.get(city, 0) + 1 - severities.append(c.get("severity", 5)) - avg_sev = sum(severities) / len(severities) if severities else 0 - top_city = max(cities, key=cities.get) if cities else "N/A" - stats_md = ( - f"## Dashboard Summary\n" - f"| Metric | Value |\n|--------|-------|\n" - f"| Total Complaints | **{total}** |\n" - f"| Average Severity | **{avg_sev:.1f}/10** |\n" - f"| Most Active City | **{top_city}** |\n\n" - f"### By Issue Type\n| Issue | Count |\n|-------|-------|\n" - f"| Garbage | {counts['Garbage']} |\n" - f"| Pot Hole | {counts['Pot Hole']} |\n" - f"| Pipe Leakage | {counts['Pipe Leakage']} |\n\n" - f"### By City\n" - ) - for city, cnt in sorted(cities.items(), key=lambda x: -x[1]): - stats_md += f"| {city} | {cnt} |\n" - log_md = "## Recent Complaints\n\n" + 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_md += (f"**{c['id']}** | {c['timestamp']} | {c['city']}, {c['location']} | " - f"{c['issue']} | Severity {c['severity']}/10 | {c.get('name','N/A')}\n\n") - return stats_md, log_md + 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 -def severity_label(score): - if score <= 3: return "LOW" - if score <= 6: return "MEDIUM" - if score <= 8: return "HIGH" - return "CRITICAL" +def sev_label(s): return "LOW" if s<=3 else ("MEDIUM" if s<=6 else ("HIGH" if s<=8 else "CRITICAL")) def update_areas(city): - areas = CITIES_AREAS.get(city, ["Enter area"]) - return gr.Dropdown(choices=areas, value=areas[0]) + """Not used anymore — we use free-text location instead of fixed areas.""" + return city # ══════════════════════════════════════════════════════════════ -# PLOTLY MAP — Scattermap (not Scattermapbox, Gradio 6 safe) +# PDF GENERATION (ReportLab — professional, no grid lines) # ══════════════════════════════════════════════════════════════ -def create_map(city, location_text="", lat=None, lon=None): - """Return a Plotly figure using Scattermap (non-deprecated API).""" +def generate_pdf(cid, ts, name, cnic, phone, city, location, issue_type, + language, severity, g_status, g_reason, g_conf, kb, + description, advice): try: - import plotly.graph_objects as go - except ImportError: - return None - - clat, clon = CITY_COORDS.get(city, (31.5204, 74.3587)) - mlat = lat if lat is not None else clat - mlon = lon if lon is not None else clon - label = location_text if location_text.strip() else city - - fig = go.Figure(go.Scattermap( - lat=[mlat], - lon=[mlon], - mode="markers+text", - marker=dict(size=16, color="#e8410a"), - text=[label], - textposition="top right", - hovertemplate=f"{label}
Lat: {mlat:.4f}
Lon: {mlon:.4f}", - )) - fig.update_layout( - map=dict( - style="open-street-map", - center=dict(lat=mlat, lon=mlon), - zoom=13, - ), - margin=dict(r=0, t=0, l=0, b=0), - height=320, - paper_bgcolor="rgba(0,0,0,0)", - plot_bgcolor="rgba(0,0,0,0)", - ) - return fig - -def update_map_on_city(city): - return create_map(city) - -def update_map_on_location(city, area, location_text): - return create_map(city, location_text or area) - -# ══════════════════════════════════════════════════════════════ -# PDF GENERATION -# ══════════════════════════════════════════════════════════════ -def generate_pdf_report(complaint_id, timestamp, name, cnic, phone, city, location, - issue_type, language, severity, gemini_status, gemini_reason, - gemini_confidence, kb, description, llama_advice): - try: - pdf_path = f"/tmp/rahbar_report_{complaint_id}.pdf" - doc = SimpleDocTemplate( - pdf_path, pagesize=A4, - rightMargin=0.75*inch, leftMargin=0.75*inch, - topMargin=0.75*inch, bottomMargin=0.75*inch - ) - - C_DARK_GREEN = colors.HexColor("#1a5c3f") - C_MID_GREEN = colors.HexColor("#25a06b") - C_LIGHT_GREEN = colors.HexColor("#eaf5ef") - C_GOLD = colors.HexColor("#c8860a") - C_GOLD_LIGHT = colors.HexColor("#fef9ee") - C_TEXT = colors.HexColor("#0d2b1e") - C_MUTED = colors.HexColor("#5a8a6e") - C_WHITE = colors.white - SEV_COLORS = { - "LOW": colors.HexColor("#27ae60"), - "MEDIUM": colors.HexColor("#f39c12"), - "HIGH": colors.HexColor("#e67e22"), - "CRITICAL": colors.HexColor("#c0392b"), - } - - def PS(name, **kw): - return ParagraphStyle(name, **kw) - - sHeadWhite = PS("hw", fontName="Helvetica-Bold", fontSize=18, textColor=C_WHITE, - alignment=TA_CENTER, leading=24, spaceAfter=2) - sSubWhite = PS("sw", fontName="Helvetica", fontSize=10, textColor=colors.HexColor("#b8e8cc"), - alignment=TA_CENTER, leading=14, spaceAfter=2) - sRefWhite = PS("rw", fontName="Helvetica", fontSize=8, textColor=colors.HexColor("#a8d8c0"), - alignment=TA_CENTER, spaceAfter=0) - sSecHead = PS("sec", fontName="Helvetica-Bold", fontSize=10, textColor=C_WHITE, - leading=14, spaceAfter=0) - sSevBadge = PS("sev", fontName="Helvetica-Bold", fontSize=11, textColor=C_WHITE, - alignment=TA_CENTER, leading=16) - sLabel = PS("lbl", fontName="Helvetica-Bold", fontSize=8.5, textColor=C_MUTED, leading=12) - sValue = PS("val", fontName="Helvetica", fontSize=9.5, textColor=C_TEXT, leading=14) - sBody = PS("bod", fontName="Helvetica", fontSize=9, textColor=C_TEXT, leading=13, spaceAfter=3) - sBodyI = PS("bi", fontName="Helvetica-Oblique", fontSize=9, textColor=colors.HexColor("#2d5a3e"), leading=13) - sBullet = PS("bul", fontName="Helvetica", fontSize=9, textColor=C_TEXT, leading=13, leftIndent=12) - sGoldDir = PS("gd", fontName="Helvetica-Bold", fontSize=10, textColor=C_WHITE, alignment=TA_CENTER, leading=15) - sFooter = PS("ft", fontName="Helvetica", fontSize=7.5, textColor=C_WHITE, alignment=TA_CENTER, leading=11) - sDecl = PS("dc", fontName="Helvetica", fontSize=9, textColor=C_TEXT, leading=13) - - W = 7.0 * inch - - def sec_header(letter, title): - t = Table([[Paragraph(f" {letter}. {title.upper()}", sSecHead)]], colWidths=[W]) - t.setStyle(TableStyle([ - ("BACKGROUND", (0,0),(-1,-1), C_DARK_GREEN), - ("TOPPADDING", (0,0),(-1,-1), 6), - ("BOTTOMPADDING", (0,0),(-1,-1), 6), - ("LEFTPADDING", (0,0),(-1,-1), 10), - ])) + 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) + + 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 info_grid(pairs): - rows = [] - row = [] - for i, (lbl, val) in enumerate(pairs): - row.extend([Paragraph(lbl, sLabel), Paragraph(str(val), sValue)]) - if len(row) == 4 or i == len(pairs) - 1: - while len(row) < 4: - row.extend([Paragraph("", sLabel), Paragraph("", sValue)]) - rows.append(row) - row = [] - t = Table(rows, colWidths=[2.0*inch, 1.5*inch, 2.0*inch, 1.5*inch]) - t.setStyle(TableStyle([ - ("BACKGROUND", (0,0),(-1,-1), C_LIGHT_GREEN), - ("TOPPADDING", (0,0),(-1,-1), 5), - ("BOTTOMPADDING", (0,0),(-1,-1), 5), - ("LEFTPADDING", (0,0),(-1,-1), 6), - ("RIGHTPADDING", (0,0),(-1,-1), 6), - ("VALIGN", (0,0),(-1,-1), "TOP"), - ("ROWBACKGROUNDS",(0,0),(-1,-1), [C_LIGHT_GREEN, C_WHITE]), - ])) + 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 text_card(paras, bg=None): - bg = bg or C_LIGHT_GREEN - rows = [[p] for p in paras] - t = Table(rows, 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), - ("VALIGN", (0,0),(-1,-1), "TOP"), - ])) + 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) + def sp(h=0.15): return Spacer(1,h*inch) - story = [] - date_str = datetime.datetime.now().strftime("%d %B %Y") - time_str = datetime.datetime.now().strftime("%I:%M %p") - sev_lbl = severity_label(severity) + story=[] - header_rows = [ - [Paragraph("GOVERNMENT OF PAKISTAN", sHeadWhite)], - [Paragraph("CIVIC COMPLAINT REPORT", sHeadWhite)], - [Paragraph("Rahbar Digital Civic Redressal System", sSubWhite)], - [Paragraph(f"Reference: {complaint_id} | {date_str} at {time_str} | Language: {language}", sRefWhite)], - ] - h_t = Table(header_rows, colWidths=[W]) - h_t.setStyle(TableStyle([ - ("BACKGROUND", (0,0),(-1,-1), C_DARK_GREEN), - ("TOPPADDING", (0,0),(-1,-1), 10), - ("BOTTOMPADDING", (0,0),(-1,-1), 10), - ("LEFTPADDING", (0,0),(-1,-1), 14), - ("RIGHTPADDING", (0,0),(-1,-1), 14), - ])) - story += [h_t, sp(0.12)] - - sev_color = SEV_COLORS.get(sev_lbl, C_MID_GREEN) - sev_t = Table( - [[Paragraph(f"SEVERITY: {severity}/10 — {sev_lbl}", sSevBadge)]], - colWidths=[W] - ) - sev_t.setStyle(TableStyle([ - ("BACKGROUND", (0,0),(-1,-1), sev_color), - ("TOPPADDING", (0,0),(-1,-1), 8), - ("BOTTOMPADDING", (0,0),(-1,-1), 8), - ])) - story += [sev_t, sp(0.18)] - - story += [sec_header("A", "Complainant Information"), sp(0.08)] - story += [info_grid([ - ("Full Name", name), ("CNIC", cnic), - ("Phone", phone or "N/A"),("City", city), - ]), sp(0.15)] - - story += [sec_header("B", "Complaint Details"), sp(0.08)] - story += [info_grid([ - ("Issue Type", issue_type), ("Location", location), - ("Date Filed", date_str), ("Time Filed", time_str), - ])] + # 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), - text_card([Paragraph(f"Description: {description.strip()}", sBodyI)])] - story += [sp(0.15)] - - story += [sec_header("C", "Verification Results"), sp(0.08)] - ai_bg = colors.HexColor("#e6f7ed") if "APPROVED" in gemini_status else colors.HexColor("#fdecea") - story += [text_card([ - Paragraph(f"Status: {gemini_status} | Confidence: {gemini_confidence}", sBody), - Paragraph(f"Assessment: {gemini_reason}", sBody), - ], bg=ai_bg), sp(0.15)] - - story += [sec_header("D", "Legal Framework & Applicable Laws"), sp(0.08)] - story += [info_grid([ - ("Responsible Authority", kb.get("authority", "N/A")), - ("Official 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}. {law}", sBullet)] - for i, law in enumerate(kb.get("laws", []), 1)] + 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), C_LIGHT_GREEN), - ("TOPPADDING", (0,0),(-1,-1), 4), - ("BOTTOMPADDING", (0,0),(-1,-1), 4), - ("LEFTPADDING", (0,0),(-1,-1), 10), - ])) + 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.15)] - - story += [sec_header("E", "Citizen's Legal Rights"), sp(0.08)] - rights_rows = [[Paragraph(f"✓ {r}", sBullet)] - for r in kb.get("citizen_rights", [])] - if rights_rows: - rt = Table(rights_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), [C_WHITE, C_LIGHT_GREEN]), - ])) + 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), - text_card([Paragraph( - f"Escalation Path: {kb.get('escalation', 'CM Portal: 0800-02345')}", - sBodyI)], bg=C_GOLD_LIGHT), - sp(0.15)] - - story += [sec_header("F", f"Legal Advice ({language})"), sp(0.08)] - advice_paras = [Paragraph(line.strip(), sBody) - for line in llama_advice.strip().split("\n") if line.strip()] - if advice_paras: - at = Table([[p] for p in advice_paras], colWidths=[W]) - at.setStyle(TableStyle([ - ("BACKGROUND", (0,0),(-1,-1), C_LIGHT_GREEN), - ("TOPPADDING", (0,0),(-1,-1), 4), - ("BOTTOMPADDING", (0,0),(-1,-1), 4), - ("LEFTPADDING", (0,0),(-1,-1), 10), - ])) + 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.15)] - - story += [sec_header("G", "Mandatory Action Directive"), sp(0.08)] - dir_t = Table( - [[Paragraph(f"MANDATORY ACTION REQUIRED WITHIN: {kb.get('response','72 hours').upper()}", sGoldDir)]], - colWidths=[W] - ) - dir_t.setStyle(TableStyle([ - ("BACKGROUND", (0,0),(-1,-1), C_GOLD), - ("TOPPADDING", (0,0),(-1,-1), 9), - ("BOTTOMPADDING", (0,0),(-1,-1), 9), - ])) - story += [dir_t, sp(0.08)] - story += [info_grid([ - ("Responsible Authority", kb.get("authority","N/A")), - ("Official Helpline", kb.get("hotline","N/A")), - ("Citizen Portal", "citizenportal.gov.pk"), - ("CM Toll-Free", "0800-02345"), - ]), sp(0.18)] - - story += [sec_header("H", "Declaration & Official Use"), sp(0.08)] - inner_decl = [ - [Paragraph( - f"I, {name} (CNIC: {cnic}), declare that the information provided " - f"is true and correct to the best of my knowledge.", - sDecl)], + 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", sLabel), - Paragraph("Date", sLabel), - Paragraph("Reference No.", sLabel)], - [Paragraph("____________________________", sValue), - Paragraph(date_str, sValue), - Paragraph(complaint_id, sValue)], - ], colWidths=[2.5*inch, 2.5*inch, 2.0*inch])], + [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", sLabel), - Paragraph("Date of Receipt", sLabel), - Paragraph("Action Taken", sLabel), - Paragraph("Resolved On", sLabel)], - [Paragraph("______________", sValue), - Paragraph("______________", sValue), - Paragraph("______________", sValue), - Paragraph("______________", sValue)], - ], colWidths=[1.75*inch, 1.75*inch, 1.75*inch, 1.75*inch])], + [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)], ] - decl_outer = Table(inner_decl, colWidths=[W]) - decl_outer.setStyle(TableStyle([ - ("BACKGROUND", (0,0),(-1,-1), C_LIGHT_GREEN), - ("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_outer, sp(0.18)] - - foot_t = Table( - [[Paragraph( - f"Generated by Rahbar — Pakistan's Civic Redressal Platform | " - f"{timestamp} | {complaint_id}", - sFooter)]], - colWidths=[W] - ) - foot_t.setStyle(TableStyle([ - ("BACKGROUND", (0,0),(-1,-1), C_DARK_GREEN), - ("TOPPADDING", (0,0),(-1,-1), 7), - ("BOTTOMPADDING", (0,0),(-1,-1), 7), - ])) + 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) doc.build(story) - return pdf_path - + return path except Exception as e: import traceback; traceback.print_exc() print(f"PDF error: {e}") - return None + 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 # ══════════════════════════════════════════════════════════════ -# WHATSAPP LINK -# ══════════════════════════════════════════════════════════════ -def make_whatsapp_link(text): - return f"https://wa.me/?text={urllib.parse.quote(text[:1000])}" - -# ══════════════════════════════════════════════════════════════ -# MAIN REPORT FUNCTION +# MAIN REPORT # ══════════════════════════════════════════════════════════════ 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 of the issue.", "", "", None, "", None, None, None - if not location.strip(): - return None, "Please enter the complaint 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 - - complaint_id = f"RB-{uuid.uuid4().hex[:8].upper()}" - timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - annotated_img, yolo_summary, yolo_severity = detect_with_yolo(image, issue_type) - gemini_raw = analyze_with_gemini(image, issue_type, location, city, yolo_summary) - gemini_parsed = parse_gemini_response(gemini_raw) - gemini_status = gemini_parsed["status"] - gemini_reason = gemini_parsed["reason"] - - if gemini_status == "REJECTED": - return ( - annotated_img, - f"COMPLAINT REJECTED — Verification\n\nReason: {gemini_reason}\n" - f"Confidence: {gemini_parsed.get('confidence','N/A')}\n\n" - f"Please upload a clear image of the issue ({issue_type}).\n" - f"This complaint has NOT been saved.", - "", "", None, complaint_id, None, None, None - ) - - if gemini_status == "UNKNOWN" and "GOOGLE_API_KEY not set" in gemini_raw: - gemini_reason = "Verification skipped — API key not configured." - gemini_status = "APPROVED_WITH_WARNING" - - final_severity = gemini_parsed["severity"] if gemini_status == "APPROVED" else yolo_severity - kb = LEGAL_KB.get(issue_type, {}) - sev_lbl = severity_label(final_severity) - llama_advice = analyze_with_llama( - issue_type, location, city, yolo_summary, final_severity, language - ) + 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 - pdf_path = generate_pdf_report( - complaint_id, timestamp, name, cnic, phone, city, location, - issue_type, language, final_severity, - gemini_status, gemini_reason, gemini_parsed.get("confidence", "N/A"), - kb, description, llama_advice - ) + 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) + + sl = sev_label(final_sev) report = ( f"GOVERNMENT OF PAKISTAN — CIVIC COMPLAINT REPORT\n" f"Rahbar Digital Civic Redressal System\n" - f"{'='*55}\n" - f"Complaint Number : {complaint_id}\n" - f"Date : {datetime.datetime.now().strftime('%d %B %Y')}\n" - f"Time : {datetime.datetime.now().strftime('%I:%M %p')}\n" - f"Language : {language}\n\n" - f"SECTION A — COMPLAINANT INFORMATION\n" - f"Full Name : {name}\n" - f"CNIC : {cnic}\n" - f"Phone : {phone if phone else 'Not provided'}\n" - f"City : {city}\n" - f"Location : {location}\n\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 Type : {issue_type}\n" - f"Location : {location}, {city}\n" - f"Date/Time : {timestamp}\n" - f"Severity : {final_severity}/10 [{sev_lbl}]\n" - f"Description:\n{description.strip() if description.strip() else '[No additional details provided]'}\n\n" - f"SECTION C — VERIFICATION RESULTS\n" - f"Status : {gemini_status}\n" - f"Confidence : {gemini_parsed.get('confidence','N/A')}\n" - f"Assessment : {gemini_reason}\n\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"Laws:\n" + "\n".join(f" - {l}" for l in kb.get("laws",[])) + - f"\nAuthority : {kb.get('authority','N/A')}\n" - f"Helpline : {kb.get('hotline','N/A')}\n" - f"Response : {kb.get('response','N/A')}\n" - f"Penalty : {kb.get('fine','N/A')}\n\n" - f"SECTION E — CITIZEN'S RIGHTS\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"\nEscalation : {kb.get('escalation','CM Portal: 0800-02345')}\n\n" - f"MANDATORY ACTION REQUIRED 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 that the information provided is accurate.\n" - f"Reference: {complaint_id} | Generated: {timestamp}" + 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}" ) - wa_text = ( - f"Rahbar Civic Complaint\nID: {complaint_id}\nIssue: {issue_type}\n" - f"Location: {location}, {city}\nSeverity: {final_severity}/10\n" - f"Authority: {kb.get('authority','N/A')}\nHotline: {kb.get('hotline','N/A')}\nTime: {timestamp}" - ) - wa_md = f"[📲 Share on WhatsApp]({make_whatsapp_link(wa_text)})" + 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])})" - complaint_log.append({ - "id": complaint_id, "timestamp": timestamp, - "city": city, "location": location, "issue": issue_type, - "severity": final_severity, "language": language, - "name": name, "cnic": cnic, "phone": phone, - }) + 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}) - report_tts_path = None + report_tts=None if enable_tts: - tts_text = ( - f"Complaint {complaint_id} has been filed. " - f"Issue: {issue_type}. Location: {location}, {city}. " - f"Severity: {final_severity} out of 10. " - f"The responsible authority is {kb.get('authority','')}. " - f"Helpline: {kb.get('hotline','')}." - ) - report_tts_path = make_tts(tts_text, language) + 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) + + advice_tts = make_tts(advice[:600], language) + map_fig = build_map_city(city) - advice_tts_path = make_tts(llama_advice[:600], language) if llama_advice else None - map_fig = create_map(city, location) + return ann, report, wa_md, advice, report_tts, cid, advice_tts, pdf_path, map_fig - return (annotated_img, report, wa_md, llama_advice, - report_tts_path, complaint_id, advice_tts_path, pdf_path, map_fig) # ══════════════════════════════════════════════════════════════ -# CSS +# 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'); -:root { - --bg:#ffffff; --bg2:#f5f8f6; --bg3:#e8f3ec; --surface:#ffffff; - --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; +/* 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; + --radius:10px;--radius-lg:18px; --header-bg:linear-gradient(135deg,#14432e 0%,#0d2b1e 60%,#091a10 100%); } +/* System dark mode */ @media(prefers-color-scheme:dark){ :root{ - --bg:#0c1a10; --bg2:#132118; --bg3:#1a3024; --surface:#0c1a10; - --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; + --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%); } } -.dark-mode{ - --bg:#0c1a10; --bg2:#132118; --bg3:#1a3024; --surface:#0c1a10; - --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; +/* 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%); } *,*::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;} -.rh-header{background:var(--header-bg);padding:28px 20px 22px;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,3.2rem)!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.1rem);color:#a8e8c4;margin:4px 0 6px;} -.rh-tag{font-size:.78rem;color:#5de3a3;letter-spacing:.1em;text-transform:uppercase;} -.top-bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:8px 16px;background:var(--bg2);border-bottom:1px solid var(--border);gap:8px;} +body,.gradio-container{ + font-family:'Inter',sans-serif!important; + background:var(--bg)!important;color:var(--txt)!important; + transition:background .3s,color .3s; +} +/* 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:.68rem;font-weight:600;letter-spacing:.06em;padding:3px 10px;border-radius:20px;text-transform:uppercase;background:var(--surface);color:var(--green3);border:1px solid var(--border2);} +.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-btn{background:transparent;border:1px solid var(--border2);border-radius:20px;padding:4px 14px;cursor:pointer;color:var(--muted);font-size:.78rem;font-weight:500;font-family:'Inter',sans-serif;transition:all .2s;} -.dark-btn:hover{background:var(--bg3);color:var(--txt);} +.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:.84rem!important;color:var(--muted)!important;padding:12px 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;} -.sec-title{font-size:.68rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--green3);margin-bottom:10px;padding-bottom:7px;border-bottom:1px solid var(--border);} +.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(--surface)!important;border:1px solid var(--border2)!important;border-radius:var(--radius)!important;color:var(--txt)!important;font-family:'Inter',sans-serif!important;transition:border-color .2s,box-shadow .2s;} -.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(--surface)!important;border-color:var(--border2)!important;} -.gradio-container .block{background:var(--surface)!important;} -.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(--surface)!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 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-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);} -.gps-box{background:var(--bg3);border:1px solid var(--border2);border-left:4px solid var(--green3);border-radius:var(--radius);padding:10px 14px;font-size:.85rem;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;} +/* 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;} +.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:16px 12px;}.gradio-container .tab-nav button{padding:10px 10px!important;font-size:.74rem!important;}} +@media(max-width:640px){ + .rh-header{padding:14px 12px;} + .gradio-container .tab-nav button{padding:10px 10px!important;font-size:.74rem!important;} +} """ HEADER_HTML = """
-
Rahbar
-
Pakistan's AI-Powered Civic Complaint Platform
-
Serving Citizens — Enforcing Rights
+
Rahbar | رہبر
+
Pakistan's Civic Complaint Platform
+
Know Your Rights — File With Confidence
Image Verification - Object Detection Legal Assistant - Knowledge Base + Voice Support 4 Languages + PDF Export LIVE
- +