diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,417 +1,1047 @@ """ -Rahbar v9.0 — Pakistan AI Civic Complaint Platform -Full Pakistan Coverage | GPS Location | Interactive Map +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 """ import os, io, re, uuid, base64, datetime, urllib.parse from PIL import Image import gradio as gr -GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") +# ── 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 = [] # ══════════════════════════════════════════════════════════════ -# KNOWLEDGE BASE +# GPS / IP GEOLOCATION (pure Python — no JS, no Selenium) # ══════════════════════════════════════════════════════════════ -ISSUE_TYPES = ["Garbage", "Pot Hole", "Pipe Leakage"] -LANGUAGES = ["English", "Urdu", "Punjabi", "Sindhi"] -LANG_CODES = {"English": "en", "Urdu": "ur", "Punjabi": "ur", "Sindhi": "ur"} +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. + """ + import requests -LEGAL_INFO = { - "Garbage": { - "laws": ["Punjab Waste Management Act 2014", "EPA 1997 Section 11"], - "fine": "Rs. 500-50,000", "authority": "Local Government / SWMB", - "hotline": "1139", "response": "48 hours", - "citizen_rights": ["Right to clean environment", "Right to file FIR", "Right to compensation"], - "escalation": "CM Cell: 0800-02345 | citizenportal.gov.pk" + # ── Provider 1: ipinfo.io ──────────────────────────────── + try: + r = requests.get("https://ipinfo.io/json", timeout=5) + if r.status_code == 200: + data = r.json() + loc = data.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 + except Exception: + pass + + # ── Provider 2: ip-api.com (fallback) ─────────────────── + try: + r = requests.get("http://ip-api.com/json/", timeout=5) + 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"), + ) + except Exception: + pass + + return None # Both providers failed + + +def gps_locate_and_update(city_value): + """ + Called when user clicks 'Detect My Location'. + Returns (map_figure, status_message, lat, lon). + If detection fails, falls back to selected city centre. + """ + 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 + 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 + + +# ═════════════════════════════════════════════════════════════�� +# RAG KNOWLEDGE BASE +# ══════════════════════════════════════════════════════════════ +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", }, - "Pot Hole": { - "laws": ["National Highways Safety Ordinance 2000", "Motor Vehicles Ordinance 1965"], - "fine": "Authority liable for damages", "authority": "NHA / C&W", - "hotline": "051-9032800", "response": "72 hours", - "citizen_rights": ["Right to compensation", "Right to Ombudsman complaint"], - "escalation": "Federal Ombudsman: 051-9204551" + { + "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", }, - "Pipe Leakage": { - "laws": ["Punjab Water Act 2019", "Constitution Article 9"], - "fine": "Rs. 10,000-500,000", "authority": "WASA / PWA", - "hotline": "042-99200300", "response": "24 hours", - "citizen_rights": ["Right to clean water", "Right to compensation"], - "escalation": "PWA: 051-9246150 | CM Portal: 0800-02345" - } -} - -LOCALIZED = { - "Garbage": {"English": "Dumping garbage is a criminal offence. Helpline: 1139", - "Urdu": "کچرا پھینکنا جرم ہے۔ ہیلپ لائن: 1139", - "Punjabi": "کچرا سُٹنا جرم اے۔", "Sindhi": "ڪچرو اڇلائڻ جرم آهي."}, - "Pot Hole": {"English": "Road repair required within 72 hours. NHA: 051-9032800", - "Urdu": "سڑک کی مرمت 72 گھنٹوں میں ضروری ہے۔", - "Punjabi": "سڑک دی مرمت 72 گھنٹیاں وچ ضروری اے۔", - "Sindhi": "سڙڪ جي مرمت 72 ڪلاڪن ۾ ضروري آهي."}, - "Pipe Leakage": {"English": "Pipe leakage repair within 24 hours. WASA: 042-99200300", - "Urdu": "پائپ لیکیج 24 گھنٹوں میں ٹھیک کرنا ضروری ہے۔", - "Punjabi": "پائپ لیکیج 24 گھنٹیاں وچ ٹھیک کرنا ضروری اے۔", - "Sindhi": "پائپ ليڪيج 24 ڪلاڪن ۾ مرمت ضروري آهي."} -} + { + "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", + }, +] # ══════════════════════════════════════════════════════════════ -# HTML with GPS and Interactive Map (Works everywhere in Pakistan) +# RAG ENGINE # ══════════════════════════════════════════════════════════════ -MAP_HTML = """ -
-
- 📍 Click "Get My Location" or click on map to set address -
-
- -
+class RAGEngine: + def __init__(self): + self.documents = RAG_DOCUMENTS + self.vectorizer = None + self.doc_matrix = None + self._initialized = False - - + def initialize(self): + if self._initialized: + 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','')}" + for d in self.documents + ] + self.vectorizer = TfidfVectorizer( + analyzer='char_wb', ngram_range=(2,5), + max_features=8000, sublinear_tf=True, min_df=1 + ) + self.doc_matrix = self.vectorizer.fit_transform(corpus) + self._initialized = True + return True + except Exception as e: + print(f"RAG init error: {e}") + return False - -""" + 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] + + def format_context(self, docs): + 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") + return ctx + +rag_engine = RAGEngine() +rag_engine.initialize() # ══════════════════════════════════════════════════════════════ -# IMAGE ANALYSIS +# STATIC DATA # ══════════════════════════════════════════════════════════════ -def analyze_image(image_pil, issue_type): - if image_pil is None: - return None, "No image", 5, "REJECTED", "Please upload an image", "", "0%", "" - return (image_pil, f"{issue_type} area identified", 6, "APPROVED", - "Image shows the reported issue", "Image analysis complete", "85%", - "Forward to relevant department for action") +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"], +} + +CITY_COORDS = { + "Lahore": (31.5204, 74.3587), + "Karachi": (24.8607, 67.0011), + "Islamabad": (33.6844, 73.0479), + "Rawalpindi": (33.5651, 73.0169), + "Faisalabad": (31.4181, 73.0776), + "Multan": (30.1575, 71.5249), + "Peshawar": (34.0151, 71.5249), + "Quetta": (30.1798, 66.9750), +} + +ISSUE_TYPES = ["Garbage", "Pot Hole", "Pipe Leakage"] +LANGUAGES = ["English", "Urdu", "Punjabi", "Sindhi"] + +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", + }, + "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", + }, + "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", + }, +} + +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 ADVICE +# YOLO DETECTION # ══════════════════════════════════════════════════════════════ -def get_legal_advice(issue, location, severity, language="English"): - info = LEGAL_INFO.get(issue, LEGAL_INFO.get("Garbage", {})) - rights = "\n".join(f"• {r}" for r in info.get("citizen_rights", [])) - local_msg = LOCALIZED.get(issue, {}).get(language, "") - - return f"""## Your Legal Rights for {issue} +def detect_with_yolo(image_pil, issue_type): + try: + from ultralytics import YOLO + import numpy as np + model = YOLO("yolo26n.pt") + results = model(np.array(image_pil), verbose=False) + result = results[0] + names = model.names + detected, severity = [], 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) + except ImportError: + return image_pil, "Object detection library not available.", 5 + except Exception as e: + return image_pil, f"Detection error: {e}", 5 -**Your Rights:** -{rights} +# ══════════════════════════════════════════════════════════════ +# GEMINI VISION +# ══════════════════════════════════════════════════════════════ +def analyze_with_gemini(image_pil, issue, location, city, yolo_summary): + if not GOOGLE_API_KEY: + return "WARNING: GOOGLE_API_KEY not set. Verification skipped." + try: + import google.generativeai as genai + genai.configure(api_key=GOOGLE_API_KEY) + model = genai.GenerativeModel("gemini-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() + except Exception as e: + return f"WARNING: Verification error: {e}" -**Responsible Authority:** {info.get('authority', 'N/A')} -**Helpline:** {info.get('hotline', 'N/A')} -**Response Time:** {info.get('response', 'N/A')} -**Fine/Penalty:** {info.get('fine', 'N/A')} +def 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"), + ]: + 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() + return r -**Escalation Path:** {info.get('escalation', 'CM Portal: 0800-02345')} +# ══════════════════════════════════════════════════════════════ +# LEGAL ADVICE (LLM) +# ══════════════════════════════════════════════════════════════ +def analyze_with_llama(issue, location, city, yolo_summary, 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.") ---- -*Notice in {language}:* {local_msg} -""" + 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)" + ) + 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( + model="llama-3.3-70b-versatile", + messages=[{"role": "user", "content": prompt}], + max_tokens=700 + ) + return resp.choices[0].message.content.strip() + except Exception as e: + return f"Legal advice error: {e}" # ══════════════════════════════════════════════════════════════ -# CHATBOT +# RAG CHATBOT — Gradio 6 messages format # ══════════════════════════════════════════════════════════════ -def legal_chatbot(message, history, language): +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 message or not message.strip(): + if not user_message.strip(): return history, "" - - response = """**Rahbar Legal Assistant** -I can help you with civic issues in Pakistan: + retrieved_docs = rag_engine.retrieve(user_message, top_k=3) + rag_context = rag_engine.format_context(retrieved_docs) -• **Garbage Complaints** - Punjab Waste Management Act 2014, Helpline: 1139 -• **Road/Pothole Complaints** - NHA helpline: 051-9032800 -• **Water/Pipe Leakage** - WASA helpline: 042-99200300 + 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.") -Please describe your specific issue for detailed guidance, including: -- What happened? -- When did it happen? -- Which authority have you contacted? + 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}" + ) -I'll provide your legal rights and the exact steps to file a complaint.""" - - history.append({"role": "user", "content": message}) - history.append({"role": "assistant", "content": response}) - return history, "" + 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, "" + + 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)}_" + 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, "" + + +def chatbot_tts_output(history, language): + if not history: + return None + # history is list of dicts in messages format + 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) + return None # ══════════════════════════════════════════════════════════════ -# VOICE FUNCTIONS +# TTS # ═════════════════��════════════════════════════════════════════ -def text_to_speech(text, language): - if not text: - return None +def make_tts(text, language): try: from gtts import gTTS - clean = re.sub(r'[*_#`]', '', str(text))[:500] - if not clean: - return None lang_code = LANG_CODES.get(language, "en") - tts = gTTS(text=clean, lang=lang_code, slow=False) + tts = gTTS(text=str(text)[:600], lang=lang_code, slow=False) path = f"/tmp/tts_{uuid.uuid4().hex[:8]}.mp3" tts.save(path) return path - except: + except Exception: try: - tts = gTTS(text=str(text)[:500], lang="en", slow=False) + 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: + except Exception: return None -def speech_to_text(audio_file): +# ══════════════════════════════════════════════════════════════ +# STT +# ══════════════════════════════════════════════════════════════ +def stt(audio_file): if audio_file is None: - return "No audio recorded" - + return "No audio received. Please record or upload audio first." + + def ensure_wav(path): + if path.lower().endswith(".wav"): + return path + try: + from pydub import AudioSegment + out = path + "_converted.wav" + AudioSegment.from_file(path).export(out, format="wav") + return out + except Exception: + return path + if GROQ_API_KEY: try: from groq import Groq - with open(audio_file, "rb") as f: - client = Groq(api_key=GROQ_API_KEY) + 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") - return result.strip() if result else "No speech detected" - except: - pass - + 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" + try: import speech_recognition as sr + wav_path = ensure_wav(audio_file) recognizer = sr.Recognizer() - with sr.AudioFile(audio_file) as source: - audio = recognizer.record(source) - return recognizer.recognize_google(audio) - except: - return "Could not transcribe. Please type your question." + 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) + except Exception as e2: + return f"Transcription failed. Error: {groq_err}. Fallback: {e2}" -def voice_to_chat(audio_file, history, language): - if audio_file is None: - return history or [], "" - transcribed = speech_to_text(audio_file) - if not transcribed or transcribed.startswith("Could not") or transcribed.startswith("No audio"): - return history or [], transcribed - new_hist, _ = legal_chatbot(transcribed, history or [], language) - return new_hist, "" - -def read_last_answer(history, language): - if not history: +# ══════════════════════════════════════════════════════════════ +# 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')}*" + ) + return out + +# ══════════════════════════════════════════════════════════════ +# ADMIN STATS +# ══════════════════════════════════════════════════════════════ +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 = {}, [] + 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" + 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 + +def severity_label(score): + if score <= 3: return "LOW" + if score <= 6: return "MEDIUM" + if score <= 8: return "HIGH" + return "CRITICAL" + +def update_areas(city): + areas = CITIES_AREAS.get(city, ["Enter area"]) + return gr.Dropdown(choices=areas, value=areas[0]) + +# ══════════════════════════════════════════════════════════════ +# PLOTLY MAP — Scattermap (not Scattermapbox, Gradio 6 safe) +# ══════════════════════════════════════════════════════════════ +def create_map(city, location_text="", lat=None, lon=None): + """Return a Plotly figure using Scattermap (non-deprecated API).""" + try: + import plotly.graph_objects as go + except ImportError: return None - for msg in reversed(history): - if isinstance(msg, dict) and msg.get("role") == "assistant": - content = msg.get("content", "") - if content: - clean = re.sub(r'[*_#`]', '', content[:500]) - return text_to_speech(clean, language) if clean else None - return None + + 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(cid, ts, name, cnic, phone, city, location, issue_type, language, - severity, status, reason, confidence, info, description): +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: - from reportlab.lib.pagesizes import A4 - from reportlab.lib import colors - from reportlab.lib.units import inch - from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet - from reportlab.lib.enums import TA_CENTER - from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle - - path = f"/tmp/Rahbar_{cid}.pdf" - doc = SimpleDocTemplate(path, pagesize=A4, - leftMargin=0.75*inch, rightMargin=0.75*inch, - topMargin=0.75*inch, bottomMargin=0.75*inch) - - styles = getSampleStyleSheet() - title_style = ParagraphStyle('Title', parent=styles['Heading1'], fontSize=14, alignment=TA_CENTER, textColor=colors.HexColor('#1a5c3f')) - body_style = ParagraphStyle('Body', parent=styles['Normal'], fontSize=9, leading=14) - + 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), + ])) + 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]), + ])) + 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"), + ])) + return t + + def sp(h=0.15): + return Spacer(1, h * inch) + story = [] date_str = datetime.datetime.now().strftime("%d %B %Y") - - story.append(Paragraph("GOVERNMENT OF PAKISTAN", title_style)) - story.append(Paragraph("CIVIC COMPLAINT REPORT", title_style)) - story.append(Spacer(1, 0.2*inch)) - - data = [ - ["Complaint ID:", cid, "Date:", date_str], - ["Name:", name, "CNIC:", cnic], - ["Issue:", issue_type, "Severity:", f"{severity}/10"], - ["Location:", f"{location}, {city}", "Status:", status], + time_str = datetime.datetime.now().strftime("%I:%M %p") + sev_lbl = severity_label(severity) + + 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)], ] - - t = Table(data, colWidths=[1.5*inch, 2.2*inch, 1.2*inch, 2.1*inch]) - t.setStyle(TableStyle([ - ('FONTNAME', (0,0), (0,-1), 'Helvetica-Bold'), - ('FONTSIZE', (0,0), (-1,-1), 9), - ('TOPPADDING', (0,0), (-1,-1), 6), - ('BOTTOMPADDING', (0,0), (-1,-1), 6), - ('GRID', (0,0), (-1,-1), 0.5, colors.grey), + 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.append(t) - story.append(Spacer(1, 0.2*inch)) - - story.append(Paragraph(f"Authority: {info.get('authority', 'N/A')}", body_style)) - story.append(Paragraph(f"Helpline: {info.get('hotline', 'N/A')}", body_style)) - story.append(Paragraph(f"Response Time: {info.get('response', 'N/A')}", body_style)) - story.append(Spacer(1, 0.2*inch)) - - story.append(Paragraph(f"Declaration: I, {name}, certify that the information is true.", body_style)) - story.append(Paragraph(f"Signature: ____________________", body_style)) - story.append(Paragraph(f"Reference: {cid}", body_style)) - + 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), + ])] + 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)] + 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), + ])) + 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.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.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)], + [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])], + [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])], + ] + 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), + ])) + story.append(foot_t) + doc.build(story) - return path + return pdf_path + except Exception as e: + import traceback; traceback.print_exc() print(f"PDF error: {e}") - path = f"/tmp/Rahbar_{cid}.txt" - with open(path, "w", encoding="utf-8") as f: - f.write(f"RAHBAR COMPLAINT REPORT\nID: {cid}\nIssue: {issue_type}\nLocation: {location}, {city}\nSeverity: {severity}/10\nName: {name}\nCNIC: {cnic}\nDate: {ts}") - return path + return None + +# ══════════════════════════════════════════════════════════════ +# WHATSAPP LINK +# ══════════════════════════════════════════════════════════════ +def make_whatsapp_link(text): + return f"https://wa.me/?text={urllib.parse.quote(text[:1000])}" # ══════════════════════════════════════════════════════════════ # MAIN REPORT FUNCTION @@ -419,331 +1049,544 @@ def generate_pdf(cid, ts, name, cnic, phone, city, location, issue_type, languag def make_report(image, issue_type, city, location, name, cnic, phone, description, language, enable_tts): if image is None: - return (None, "Please upload an image.", "", "", None, "", None, None) - if not location or not location.strip(): - return (None, "Please enter a location.", "", "", None, "", None, None) - if not name or not name.strip(): - return (None, "Please enter your full name.", "", "", None, "", None, None) - if not cnic or not cnic.strip(): - return (None, "Please enter your CNIC number.", "", "", None, "", None, None) - - cid = f"RB-{uuid.uuid4().hex[:8].upper()}" - ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - date_str = datetime.datetime.now().strftime("%d %B %Y") - - annotated_img, yolo_summary, severity, status, reason, reason_urdu, confidence, action = analyze_image(image, issue_type) - - info = LEGAL_INFO.get(issue_type, LEGAL_INFO.get("Garbage", {})) - legal_advice = get_legal_advice(issue_type, location, severity, language) - - severity_icon = "🟢" if severity <= 3 else ("🟡" if severity <= 6 else ("🟠" if severity <= 8 else "🔴")) - - report = f"""====================================================================== -GOVERNMENT OF PAKISTAN — CIVIC COMPLAINT REPORT -Rahbar Digital Civic Redressal System -====================================================================== -Complaint ID: {cid} -Date: {date_str} | Language: {language} -====================================================================== -SECTION A — COMPLAINANT INFORMATION ----------------------------------------------------------------------- -Full Name: {name} -CNIC: {cnic} -Phone: {phone or "Not Provided"} -City: {city} -Location: {location} -====================================================================== -SECTION B — COMPLAINT DETAILS ----------------------------------------------------------------------- -Issue Type: {issue_type} -Severity: {severity_icon} {severity}/10 -Description: {description.strip() if description else "[None provided]"} -====================================================================== -SECTION C — VERIFICATION RESULTS ----------------------------------------------------------------------- -Status: {status} -Confidence: {confidence} -Finding: {reason} -Action: {action} -====================================================================== -SECTION D — LEGAL FRAMEWORK ----------------------------------------------------------------------- -Authority: {info.get('authority', 'N/A')} -Helpline: {info.get('hotline', 'N/A')} -Response Time: {info.get('response', 'N/A')} -Fine/Penalty: {info.get('fine', 'N/A')} -====================================================================== -SECTION E — CITIZEN'S RIGHTS ----------------------------------------------------------------------- -{chr(10).join(f'• {r}' for r in info.get('citizen_rights', []))} - -Escalation Path: {info.get('escalation', 'CM Portal: 0800-02345')} -====================================================================== -MANDATORY ACTION WITHIN: {info.get('response', '72 hours').upper()} -Citizen Portal: citizenportal.gov.pk | CM: 0800-02345 -====================================================================== -DECLARATION -I, {name} (CNIC: {cnic}), declare that the information provided is true. -Signature: ______________________ -Reference: {cid} | {ts} -======================================================================""" + 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_log.append({ - "id": cid, "timestamp": ts, "city": city, "location": location, - "issue": issue_type, "severity": severity, "language": language, - "name": name, "cnic": cnic, "phone": phone - }) + complaint_id = f"RB-{uuid.uuid4().hex[:8].upper()}" + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - wa_text = f"Rahbar Complaint\nRef: {cid}\nIssue: {issue_type}\nLocation: {location}, {city}\nSeverity: {severity}/10\nAuthority: {info.get('authority', 'N/A')}\nHelpline: {info.get('hotline', 'N/A')}\nFiled: {ts}" - wa_md = f"[📲 Share on WhatsApp](https://wa.me/?text={urllib.parse.quote(wa_text[:1000])})" + 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"] - report_tts = text_to_speech(report[:800], language) if enable_tts else None - advice_tts = text_to_speech(legal_advice[:600], language) if enable_tts else None - pdf_path = generate_pdf(cid, ts, name, cnic, phone, city, location, issue_type, language, severity, status, reason, confidence, info, description or "") + 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 + ) - return (annotated_img, report, wa_md, legal_advice, report_tts, cid, advice_tts, pdf_path) + 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" -# ══════════════════════════════════════════════════════════════ -# HELPER FUNCTIONS -# ══════════════════════════════════════════════════════════════ -def law_info(issue, language): - info = LEGAL_INFO.get(issue, LEGAL_INFO.get("Garbage", {})) - rights = "\n".join(f"• {r}" for r in info.get("citizen_rights", [])) - local = LOCALIZED.get(issue, {}).get(language, "") - return f"""## Legal Reference: {issue} + 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 + ) -**Applicable Laws:** -{chr(10).join(f'• {l}' for l in info.get('laws', []))} + 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 + ) -**Your Rights:** -{rights} + 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"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"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" + + "\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}" + ) -**Authority:** {info.get('authority', 'N/A')} -**Helpline:** {info.get('hotline', 'N/A')} -**Response Time:** {info.get('response', 'N/A')} -**Escalation:** {info.get('escalation', 'CM Portal: 0800-02345')} + 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)})" ---- -*Notice in {language}:* {local} -""" + 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, + }) -def get_admin_stats(): - total = len(complaint_log) - if not total: - return "No complaints filed yet.", "" - counts = {"Garbage": 0, "Pot Hole": 0, "Pipe Leakage": 0} - cities = {} - for c in complaint_log: - issue = c.get('issue', '') - counts[issue] = counts.get(issue, 0) + 1 - city = c.get('city', 'Unknown') - cities[city] = cities.get(city, 0) + 1 - - stats = f"## Complaint Statistics\n**Total Complaints:** {total}\n\n### By Issue Type\n" - for k, v in counts.items(): - stats += f"- {k}: {v}\n" - stats += "\n### By City\n" - for c, n in sorted(cities.items(), key=lambda x: -x[1])[:10]: - stats += f"- {c}: {n}\n" - - logs = "## Recent Complaints\n" - for c in complaint_log[-10:]: - logs += f"- **{c['id']}** | {c['issue']} | {c['city']}, {c['location']} | Severity {c['severity']}/10\n" - return stats, logs + report_tts_path = 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) + + advice_tts_path = make_tts(llama_advice[:600], language) if llama_advice else None + map_fig = create_map(city, location) + + return (annotated_img, report, wa_md, llama_advice, + report_tts_path, complaint_id, advice_tts_path, pdf_path, map_fig) # ══════════════════════════════════════════════════════════════ # CSS # ══════════════════════════════════════════════════════════════ CSS = """ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); +@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; - --txt: #0d2b1e; - --border: #c0d9ca; - --green: #1f7a52; - --gold: #c8860a; + --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; + --shadow:0 2px 10px rgba(13,43,30,.10); + --radius:10px; --radius-lg:18px; + --header-bg:linear-gradient(135deg,#14432e 0%,#0d2b1e 60%,#091a10 100%); } - -@media (prefers-color-scheme: dark) { - :root { - --bg: #0c1a10; - --bg2: #132118; - --txt: #d5f0e0; - --border: #243d2d; - --green: #2a9460; - --gold: #f5a623; +@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; + --shadow:0 2px 14px rgba(0,0,0,.45); + --header-bg:linear-gradient(135deg,#091a10 0%,#060d08 60%,#040a06 100%); } } - -* { font-family: 'Inter', sans-serif !important; } - -.gradio-container { background: var(--bg) !important; } - -label, .gr-label { color: var(--txt) !important; } - -input, textarea, select { - background: var(--bg) !important; - border: 1px solid var(--border) !important; - color: var(--txt) !important; - border-radius: 8px !important; -} - -button.primary { - background: linear-gradient(135deg, var(--green), #25a06b) !important; - color: white !important; - border: none !important; - font-weight: 600 !important; -} - -.tab-nav { background: var(--bg2) !important; border-bottom: 2px solid var(--border) !important; } -.tab-nav button { color: var(--txt) !important; } -.tab-nav button.selected { color: var(--gold) !important; border-bottom: 2px solid var(--gold) !important; } - -.info-box { - background: var(--bg2); - border-left: 4px solid var(--green); - padding: 10px 15px; - border-radius: 8px; - margin: 10px 0; - font-size: 0.85rem; -} - -.sec-title { - font-size: 0.7rem; - font-weight: 700; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--green); - margin-bottom: 10px; - padding-bottom: 6px; - border-bottom: 1px solid var(--border); +.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; + --shadow:0 2px 14px rgba(0,0,0,.45); + --header-bg:linear-gradient(135deg,#091a10 0%,#060d08 60%,#040a06 100%); } - -.message.user { background: var(--bg2) !important; } -.message.bot { background: var(--bg) !important; border: 1px solid var(--border) !important; } +*,*::before,*::after{box-sizing:border-box;} +body,.gradio-container{font-family:'Inter',sans-serif!important;background:var(--bg)!important;color:var(--txt)!important;transition:background .3s,color .3s;} +.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;} +.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-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);} +.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);} +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 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;} +.gradio-container textarea{font-family:'JetBrains Mono',monospace!important;font-size:.82rem!important;line-height:1.7!important;} +.gradio-container .message.user{background:var(--bg3)!important;color:var(--txt)!important;} +.gradio-container .message.bot{background:var(--bg2)!important;color:var(--txt)!important;} +::-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;}} """ HEADER_HTML = """ -
-

Rahbar | رہبر

-

Pakistan's Civic Complaint System

+
+
Rahbar
+
Pakistan's AI-Powered Civic Complaint Platform
+
Serving Citizens — Enforcing Rights
+
+
+
+ Image Verification + Object Detection + Legal Assistant + Knowledge Base + 4 Languages + LIVE +
+
+ """ HOTLINES_HTML = """
- Emergency Helplines:
- 🗑️ Garbage: 1139 | 🕳️ Roads: 051-9032800 | 💧 WASA: 042-99200300 | 📞 CM Portal: 0800-02345 + Emergency Helplines:   + Garbage: 1139  + Roads / NHA: 051-9032800  + WASA Lahore: 042-99200300  + CM Portal: 0800-02345  + Federal Ombudsman: 051-9204551
""" -CITIES = [ - "Lahore", "Karachi", "Islamabad", "Rawalpindi", "Faisalabad", "Multan", - "Peshawar", "Quetta", "Gujranwala", "Sialkot", "Sukkur", "Hyderabad", - "Bahawalpur", "Sargodha", "Abbottabad", "Gilgit", "Skardu", "Gwadar", - "Mardan", "Dera Ismail Khan", "Muzaffarabad", "Mirpur", "Chitral" -] - # ══════════════════════════════════════════��═══════════════════ -# BUILD UI +# BUILD UI — Gradio 6+ compatible # ══════════════════════════════════════════════════════════════ def build_ui(): - with gr.Blocks(title="Rahbar | Pakistan Civic Complaint System") as demo: + default_map = create_map("Lahore") + + # State holders for GPS coords (used when submitting complaint) + gps_lat_state = gr.State(value=None) + gps_lon_state = gr.State(value=None) + + with gr.Blocks(title="Rahbar | AI Civic Complaint System") as demo: gr.HTML(HEADER_HTML) - + with gr.Tabs(): - # Tab 1 - File Complaint - with gr.Tab("📝 File a Complaint"): - with gr.Row(): - with gr.Column(scale=1): - gr.HTML('
Citizen Details
') - name_tb = gr.Textbox(label="Full Name", placeholder="e.g., Ali Raza") - cnic_tb = gr.Textbox(label="CNIC (no dashes)", placeholder="1234567890123") - phone_tb = gr.Textbox(label="Phone (optional)", placeholder="03xxxxxxxxx") - - gr.HTML('
Issue Photo
') - image_input = gr.Image(type="pil", label="Upload Photo", height=200) - - gr.HTML('
Complaint Details
') - issue_type = gr.Radio(choices=ISSUE_TYPES, label="Issue Type") - city_dd = gr.Dropdown(choices=sorted(CITIES), value="Lahore", label="City/District", allow_custom_value=True) - - gr.HTML('
Location Map
') - gr.HTML(MAP_HTML) - - location_tb = gr.Textbox(label="Street / Landmark / Area", placeholder="Enter exact location", elem_id="location-input") - - desc_tb = gr.Textbox(label="Description (optional)", lines=3) - language_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Language") - tts_cb = gr.Checkbox(label="🔊 Read report aloud", value=False) - submit_btn = gr.Button("Submit Complaint", variant="primary") - - with gr.Column(scale=1): - gr.HTML('
Verification Result
') - annotated_out = gr.Image(label="Analysis Result") - complaint_id_out = gr.Textbox(label="Complaint ID", interactive=False) - - gr.HTML('
Complaint Report
') - report_out = gr.Textbox(label="Report", lines=15, interactive=False) - pdf_out = gr.File(label="Download PDF Report") - wa_out = gr.Markdown() - report_tts_out = gr.Audio(label="Report Audio") - - gr.HTML('
Legal Advice
') - legal_out = gr.Markdown() - advice_tts_out = gr.Audio(label="Legal Advice Audio") - + + # ════════════════════════════════════════════════ + # TAB 1 — File Complaint + # ════════════════════════════════════════════════ + with gr.Tab("📝 File Complaint"): + with gr.Row(equal_height=False): + + # ── Left: Inputs ───────────────────────── + with gr.Column(scale=1, min_width=300): + + gr.HTML('
Citizen Information
') + name_tb = gr.Textbox(label="Full Name", placeholder="e.g. Ali Raza", lines=1) + cnic_tb = gr.Textbox(label="CNIC Number (no dashes)", placeholder="1234567890123", lines=1) + phone_tb = gr.Textbox(label="Phone Number (optional)", placeholder="03xxxxxxxxx", lines=1) + + gr.HTML('
Issue Photo
') + gr.HTML('
Upload or capture a clear photo of the issue.
') + image_input = gr.Image( + type="pil", label="Upload or Capture Photo", + sources=["webcam", "upload"], height=220 + ) + + gr.HTML('
Complaint Details
') + issue_type = gr.Radio(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="Issue Type") + city_dd = gr.Dropdown(choices=list(CITIES_AREAS.keys()), value="Lahore", label="City") + area_dd = gr.Dropdown(choices=CITIES_AREAS["Lahore"], value="Model Town", label="Area / Neighbourhood") + + gr.HTML('
Location Details
') + gr.HTML( + '
' + 'Select your city and area above. Click Detect My Location to ' + 'auto-fill coordinates via your internet connection (approximate, city-level). ' + 'Or type a specific street/landmark below.' + '
' + ) + location_tb = gr.Textbox( + label="Street / Landmark / Additional Location Detail", + placeholder="e.g. Near Park, Main Boulevard, Street 5", + lines=1 + ) + + # ── GPS Button + Status ─────────────── + gps_btn = gr.Button("📍 Detect My Location (IP-based)", variant="secondary") + gps_status = gr.Markdown( + value="*Click the button above to detect your approximate location.*", + elem_classes=["gps-box"] + ) + + gr.HTML('
Location Map
') + map_out = gr.Plot(label="Location Map", value=default_map) + + desc_tb = gr.Textbox(label="Additional Description (optional)", + placeholder="Describe the issue in detail...", lines=3) + language_dd = gr.Dropdown(choices=LANGUAGES, value="English", + label="Report & Voice Language") + tts_cb = gr.Checkbox(label="Read Report Aloud (Text-to-Speech)", value=False) + submit_btn = gr.Button("Submit Complaint", variant="primary", size="lg") + + # ── Right: Outputs ──────────────────────── + with gr.Column(scale=2, min_width=320): + + gr.HTML('
Detection Result
') + annotated_out = gr.Image(label="Detection Output", height=240) + complaint_id_out = gr.Textbox(label="Complaint Reference Number", interactive=False) + + gr.HTML('
Complaint Summary
') + report_out = gr.Textbox( + label="Official Summary", lines=12, interactive=False, + placeholder="Complaint summary will appear here after submission..." + ) + + gr.HTML('
Download PDF Report
') + gr.HTML('
Official complaint PDF — download and share via WhatsApp.
') + pdf_out = gr.File(label="📄 Download PDF Report", interactive=False) + wa_out = gr.Markdown() + report_tts_out = gr.Audio(label="Report Audio", autoplay=False) + + gr.HTML('
Legal Advice
') + gr.HTML('
Your rights and next steps under Pakistani civic law.
') + legal_advice_out = gr.Textbox( + label="Your Legal Rights & Steps", lines=12, interactive=False, + placeholder="Legal advice will appear here..." + ) + advice_tts_out = gr.Audio(label="Legal Advice Audio", autoplay=False) + + # ── GPS State (lat/lon hidden) ──────────────── + gps_lat = gr.State(value=None) + gps_lon = gr.State(value=None) + + # ── Event: GPS detect ───────────────────────── + def on_gps_click(city): + fig, status, lat, lon = gps_locate_and_update(city) + return fig, status, lat, lon + + gps_btn.click( + fn=on_gps_click, + inputs=[city_dd], + outputs=[map_out, gps_status, gps_lat, gps_lon] + ) + + # ── Events: city/area/location changes ──────── + city_dd.change(fn=update_areas, inputs=[city_dd], outputs=[area_dd]) + city_dd.change(fn=update_map_on_city, inputs=[city_dd], outputs=[map_out]) + area_dd.change(fn=update_map_on_location, inputs=[city_dd, area_dd, location_tb], outputs=[map_out]) + location_tb.change(fn=update_map_on_location,inputs=[city_dd, area_dd, location_tb], outputs=[map_out]) + + # ── Event: submit complaint ─────────────────── submit_btn.click( fn=make_report, - inputs=[image_input, issue_type, city_dd, location_tb, name_tb, cnic_tb, phone_tb, desc_tb, language_dd, tts_cb], - outputs=[annotated_out, report_out, wa_out, legal_out, report_tts_out, complaint_id_out, advice_tts_out, pdf_out] + inputs=[image_input, issue_type, city_dd, location_tb, + name_tb, cnic_tb, phone_tb, desc_tb, language_dd, tts_cb], + outputs=[annotated_out, report_out, wa_out, legal_advice_out, + report_tts_out, complaint_id_out, advice_tts_out, + pdf_out, map_out], ) - - # Tab 2 - Legal Rights - with gr.Tab("⚖️ Legal Rights"): - issue_dd = gr.Dropdown(choices=ISSUE_TYPES, label="Select Issue") - lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Language") - law_btn = gr.Button("Show Legal Information", variant="primary") + + # ════════════════════════════════════════════════ + # TAB 2 — Legal Reference & Chatbot + # ════════════════════════════════════════════════ + with gr.Tab("⚖️ Legal Reference & Chatbot"): + + gr.HTML('
Pakistani Civic Laws Database
') + with gr.Row(): + law_issue_dd = gr.Dropdown(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="Select Issue", scale=1) + law_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1) law_out = gr.Markdown() - law_btn.click(fn=law_info, inputs=[issue_dd, lang_dd], outputs=[law_out]) + gr.Button("Show Legal Details", variant="primary").click( + fn=law_info, inputs=[law_issue_dd, law_lang_dd], outputs=[law_out] + ) gr.HTML(HOTLINES_HTML) - - # Tab 3 - Chatbot - with gr.Tab("💬 Ask a Question"): - chat_lang = gr.Dropdown(choices=LANGUAGES, value="English", label="Response Language") - chatbot = gr.Chatbot(height=400) - msg = gr.Textbox(label="Your Question", placeholder="Ask about garbage, roads, water, or legal rights...") - send = gr.Button("Send", variant="primary") - audio_in = gr.Audio(type="filepath", label="Voice Input", sources=["microphone"]) - voice_send = gr.Button("🎤 Send Voice") - tts_btn = gr.Button("🔊 Read Last Answer") - tts_out = gr.Audio(label="Audio Answer") - - send.click(fn=legal_chatbot, inputs=[msg, chatbot, chat_lang], outputs=[chatbot, msg]) - msg.submit(fn=legal_chatbot, inputs=[msg, chatbot, chat_lang], outputs=[chatbot, msg]) - voice_send.click(fn=voice_to_chat, inputs=[audio_in, chatbot, chat_lang], outputs=[chatbot, msg]) - tts_btn.click(fn=read_last_answer, inputs=[chatbot, chat_lang], outputs=[tts_out]) - - # Tab 4 - Admin - with gr.Tab("📊 Admin"): - refresh = gr.Button("Refresh Stats", variant="primary") - stats = gr.Markdown() - logs = gr.Markdown() - refresh.click(fn=get_admin_stats, outputs=[stats, logs]) - + + gr.HTML('
Legal Chatbot
') + gr.HTML('
Ask any question about civic issues in Pakistan. Supports voice input and audio output.
') + + chat_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Response Language") + + # Gradio 6.13 — no type= parameter; uses {"role","content"} dicts natively + chatbot = gr.Chatbot( + label="Rahbar Legal Assistant", + height=400, + value=[], + ) + + with gr.Row(): + chat_input = gr.Textbox( + label="Your Question", + placeholder="e.g. WASA did not fix the pipe after 3 days — what are my rights?", + lines=2, scale=4 + ) + chat_send_btn = gr.Button("Send", variant="primary", scale=1) + + gr.HTML('
Voice Input
') + gr.HTML('
Record your question — it will be transcribed and sent automatically.
') + with gr.Row(): + chat_audio_in = gr.Audio(type="filepath", label="Record Question", + sources=["microphone","upload"], scale=3) + chat_voice_btn = gr.Button("🎤 Send Voice", variant="secondary", scale=1) + + gr.HTML('
Voice Output
') + with gr.Row(): + chat_tts_out = gr.Audio(label="Last Answer (Audio)", autoplay=False, scale=3) + chat_tts_btn = gr.Button("🔊 Play Answer", variant="secondary", scale=1) + + gr.Examples( + examples=[ + ["WASA did not fix the pipe leakage for 3 days — what are my legal rights?"], + ["Water in my area is contaminated — where should I complain?"], + ["Garbage has not been collected for a week — which law applies?"], + ["The authority ignored my complaint — what do I do next?"], + ["My car was damaged by a pothole — can I claim compensation?"], + ["How do I file a complaint on Pakistan Citizen Portal?"], + ], + inputs=chat_input, + label="Try These Sample Questions" + ) + + chat_send_btn.click( + fn=legal_chatbot_rag, + inputs=[chat_input, chatbot, chat_lang_dd], + outputs=[chatbot, chat_input] + ) + chat_input.submit( + fn=legal_chatbot_rag, + inputs=[chat_input, chatbot, chat_lang_dd], + outputs=[chatbot, chat_input] + ) + + def voice_then_send(audio_file, history, language): + if audio_file is None: + return history or [], "" + transcribed = stt(audio_file) + if (not transcribed or + transcribed.startswith("No audio") or + transcribed.startswith("Transcription")): + return history or [], transcribed + new_history, _ = legal_chatbot_rag(transcribed, history or [], language) + return new_history, "" + + chat_voice_btn.click( + fn=voice_then_send, + inputs=[chat_audio_in, chatbot, chat_lang_dd], + outputs=[chatbot, chat_input] + ) + chat_tts_btn.click( + fn=chatbot_tts_output, + inputs=[chatbot, chat_lang_dd], + outputs=[chat_tts_out] + ) + + # ════════════════════════════════════════════════ + # TAB 3 — Voice Tools + # ════════════════════════════════════════════════ + with gr.Tab("🎤 Voice Tools"): + + gr.HTML('
Speech to Text
') + gr.HTML('
Record your complaint. Transcription uses your API key or Google Speech as fallback. Supports English, Urdu, Punjabi, Sindhi.
') + gr.HTML('
Tip: Speak clearly. Copy the transcript into the complaint description field.
') + + audio_in = gr.Audio(type="filepath", label="Record or Upload Audio", sources=["microphone","upload"]) + stt_btn = gr.Button("Transcribe Audio", variant="primary") + stt_out = gr.Textbox(label="Transcript (editable)", lines=6, interactive=True, + placeholder="Transcribed text will appear here...") + stt_btn.click(fn=stt, inputs=[audio_in], outputs=[stt_out]) + + gr.HTML('
Text to Speech Test
') + gr.HTML('
Test audio output in any supported language.
') + with gr.Row(): + tts_text_in = gr.Textbox(label="Text to Speak", placeholder="Type something here...", scale=3) + tts_lang_in = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1) + tts_test_btn = gr.Button("▶ Play", variant="secondary") + tts_test_out = gr.Audio(label="Audio Output", autoplay=True) + tts_test_btn.click(fn=make_tts, inputs=[tts_text_in, tts_lang_in], outputs=[tts_test_out]) + + # ════════════════════════════════════════════════ + # TAB 4 — Admin Dashboard + # ════════════════════════════════════════════════ + with gr.Tab("📊 Admin Dashboard"): + + gr.HTML('
Complaint Statistics
') + refresh_btn = gr.Button("Refresh Statistics", variant="primary") + with gr.Row(): + stats_out = gr.Markdown() + log_out = gr.Markdown() + refresh_btn.click(fn=get_admin_stats, outputs=[stats_out, log_out]) + + gr.HTML("""
+Knowledge Base: Road Issues Detection Dataset | Urban Issues Dataset | Consumer Complaints Dataset
+Verification: Image analysis with computer vision and AI language model
+PDF Engine: ReportLab — Professional Government-style Reports
+Voice: Speech recognition (multilingual) + Text-to-speech in 4 languages
+GPS: IP-based geolocation via ipinfo.io / ip-api.com (no browser permissions needed) +
""") + return demo # ══════════════════════════════════════════════════════════════ # LAUNCH # ══════════════════════════════════════════════════════════════ if __name__ == "__main__": - print("Rahbar v9.0 starting...") + print("Rahbar v8.1 starting...") + print("RAG Engine:", "ready" if rag_engine._initialized else "initializing...") + + # Check Gradio version for compatibility info + try: + import gradio + gv = tuple(int(x) for x in gradio.__version__.split(".")[:2]) + print(f"Gradio version: {gradio.__version__}") + if gv < (5, 0): + print("WARNING: Gradio < 5 detected. Remove type='messages' from gr.Chatbot if you see errors.") + except Exception: + pass + demo = build_ui() demo.launch( server_name="0.0.0.0", server_port=7860, - css=CSS, - theme=gr.themes.Soft() + share=False, + theme=gr.themes.Base( + primary_hue=gr.themes.colors.green, + secondary_hue=gr.themes.colors.yellow, + ), + css=CSS, # <-- CSS now in launch() for Gradio 6+ compatibility ) \ No newline at end of file