diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,30 +1,17 @@ """ -╔══════════════════════════════════════════════════════════════╗ -║ Rahbar | رہبر v6.0 ║ -║ Pakistan's AI-Powered Civic Complaint System ║ -╚══════════════════════════════════════════════════════════════╝ - -v6.0 Changes: - ✅ English as primary UI language (Urdu as optional output language) - ✅ GPS map fully working — uses gr.HTML with Leaflet + Python reverse geocode - ✅ GPS button triggers Python function → updates location textbox - ✅ PDF report download (ReportLab) - ✅ Voice input + TTS output in chatbot - ✅ CSS in launch() — Gradio 6 correct placement - ✅ No theme in gr.Blocks() — Gradio 6 correct placement - ✅ Chatbot uses plain list-of-dicts (no ChatMessage import needed) +Rahbar | رہبر — Pakistan's Civic Complaint System v7.0 +HuggingFace Spaces compatible · Gradio 6+ """ import os, io, re, uuid, base64, datetime, urllib.parse, json, urllib.request from PIL import Image import gradio as gr -# ─── ENV ────────────────────────────────────────────────────── GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "") GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") complaint_log = [] -# ─── RAG KNOWLEDGE BASE ─────────────────────────────────────── +# ─── KNOWLEDGE BASE ─────────────────────────────────────────── RAG_DOCUMENTS = [ {"id":"g1","category":"Garbage", "title":"Punjab Waste Management Act 2014 — Citizen Rights", @@ -33,58 +20,58 @@ RAG_DOCUMENTS = [ "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 in urban areas violates EPA 1997 Section 11. Garbage not collected for >1 week = Public Nuisance under PPC Section 268. Lahore: LWMC 042-111-222-888. Karachi: KMC 021-99231677.", + "content":"Failure to collect garbage in urban areas violates EPA 1997 Section 11. Garbage not collected for more than 1 week becomes Public Nuisance under 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 / Local SWMB","response_time":"48 hours","fine":"Rs. 500 – 50,000"}, {"id":"g3","category":"Garbage", "title":"Garbage Complaint Escalation Ladder", - "content":"If local authority does not act within 48 hours: 1. Union Council / Nazim 2. Deputy Commissioner office 3. CM Cell 0800-02345 4. Pakistan Citizen Portal citizenportal.gov.pk 5. Federal Ombudsman 051-9204551 6. High Court Writ Petition under Article 9. You can claim compensation if health was affected — EPA 1997 Section 14.", + "content":"If local authority does not act within 48 hours: 1. Union Council / Nazim 2. Deputy Commissioner office 3. CM Cell 0800-02345 4. Pakistan Citizen Portal citizenportal.gov.pk 5. Federal Ombudsman 051-9204551 6. High Court Writ Petition under Article 9. You can claim compensation if health was affected 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":"Road potholes are NHA responsibility. Under National Highways Safety Ordinance 2000 road repairs must be done within 72 hours. Punjab LGA 2022 Section 54 — LDA and C&W also responsible. Vehicle damaged? Claim compensation under Motor Vehicles Ordinance 1965. NHA Helpline: 051-9032800.", + "content":"Road potholes are NHA responsibility. Road repairs must be done within 72 hours under National Highways Safety Ordinance 2000. Punjab LGA 2022 Section 54 also applies. Vehicle damaged? Claim compensation under Motor Vehicles Ordinance 1965. NHA Helpline: 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 occurs due to pothole: 1. Police report 2. Photograph scene 3. Written notice to NHA or LDA 4. File Negligence claim under Tort Law 5. Federal Ombudsman 051-9204551 6. High Court Writ Petition. Pothole deeper than 10cm = immediate hazard.", + "content":"If accident occurs due to pothole: 1. File police report 2. Photograph scene 3. Written notice to NHA or LDA 4. File Negligence claim under Tort Law 5. Federal Ombudsman 051-9204551 6. High Court Writ Petition. Pothole deeper than 10cm is an immediate hazard.", "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":"Under Punjab Water Act 2019 Section 23, WASA must repair pipe leakage within 24 hours. If not repaired, file at DC office. WASA fine: Rs.10,000–5,00,000. WASA Lahore: 042-99200300. WASA Karachi (KWSB): 021-99231677. WASA Rawalpindi: 051-5594244. Supreme Court 2018: clean water is a fundamental right — PLD 2018 SC 1.", + "content":"Under Punjab Water Act 2019 Section 23, WASA must repair pipe leakage within 24 hours. If not repaired, file complaint at DC office. WASA fine: Rs.10,000–5,00,000. WASA Lahore: 042-99200300. WASA Karachi KWSB: 021-99231677. WASA Rawalpindi: 051-5594244. Supreme Court 2018: clean water is a fundamental right PLD 2018 SC 1.", "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 and Health — Legal Rights", - "content":"EPA 1997 Section 13 makes polluting water a criminal offence. National Drinking Water Policy 2009 mandates WHO-standard water. You can claim compensation if contaminated water causes illness. You can suspend billing if water is contaminated — WASA Bylaws. Wrong billing: Contact WASA Ombudsman.", + "content":"EPA 1997 Section 13 makes polluting water a criminal offence. National Drinking Water Policy 2009 mandates WHO-standard water. Claim compensation if contaminated water causes illness. You can suspend billing if water is contaminated under WASA Bylaws. Wrong billing: Contact WASA Ombudsman.", "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 to repair within 24 hours: 1. Call WASA again, note complaint number 2. Written application at WASA office 3. Deputy Commissioner office 4. CM Cell 0800-02345 5. Citizen Portal citizenportal.gov.pk 6. Pakistan Water Authority 051-9246150 7. Federal Ombudsman 051-9204551 8. High Court Article 9. Keep evidence: photo, date, complaint number.", + "content":"If WASA fails to repair within 24 hours: 1. Call WASA again and note complaint number 2. Written application at WASA office 3. Deputy Commissioner office 4. CM Cell 0800-02345 5. Citizen Portal citizenportal.gov.pk 6. Pakistan Water Authority 051-9246150 7. Federal Ombudsman 051-9204551 8. High Court Article 9. Keep evidence: photo, date, complaint number.", "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 water and clean environment (SC 2018). Article 14: Right to Dignity — polluted environment violates dignity. Article 19A: Right to Information — you can request info from any public body. Citizen Portal complaints must receive legal response. You have the right to file FIR if public body fails to act.", + "content":"Article 9: Right to Life includes clean water and clean environment SC 2018. Article 14: Right to Dignity means polluted environment violates your dignity. Article 19A: Right to Information allows you to request info from any public body. Citizen Portal complaints must receive legal response. You have the right to file FIR if public body fails to act.", "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. Photograph issue with date/time. 2. Note exact location. 3. Call relevant helpline, get complaint number. 4. If no action in 48-72 hours, use CM Portal 0800-02345. 5. Pakistan Citizen Portal citizenportal.gov.pk is most effective. 6. Share on WhatsApp for awareness. Garbage 1139 | Roads 051-9032800 | WASA 042-99200300 | CM Portal 0800-02345", + "content":"1. Photograph issue with date and time. 2. Note exact location. 3. Call relevant helpline and get complaint number. 4. If no action in 48-72 hours use CM Portal 0800-02345. 5. Pakistan Citizen Portal citizenportal.gov.pk is most effective. 6. Share on WhatsApp for awareness. Garbage 1139 Roads 051-9032800 WASA 042-99200300 CM Portal 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 (Wafaqi Mohtasib) — Role and Process", - "content":"The Federal Ombudsman is an independent body that hears complaints against government institutions including NHA, WASA, LDA, Local Government. Phone: 051-9204551. Website: mohtasib.gov.pk. Filing is FREE, decision within 60 days. If unsatisfied, appeal to the President of Pakistan.", + "title":"Federal Ombudsman — Role and Process", + "content":"The Federal Ombudsman is an independent body that hears complaints against NHA, WASA, LDA, Local Government. Phone: 051-9204551. Website: mohtasib.gov.pk. Filing is FREE. Decision within 60 days. If unsatisfied, appeal to the President of Pakistan.", "laws":["Federal Ombudsmen Institutional Reforms Act 2013"], "hotline":"051-9204551","authority":"Federal Ombudsman (Mohtasib)","response_time":"60 days","fine":"Binding recommendations"}, ] -# ─── RAG ENGINE (sklearn TF-IDF, no faiss needed) ───────────── -class RAGEngine: +# ─── KNOWLEDGE RETRIEVAL ENGINE ─────────────────────────────── +class KnowledgeEngine: def __init__(self): self.documents = RAG_DOCUMENTS self.vectorizer = None @@ -95,17 +82,21 @@ class RAGEngine: if self._ready: return True try: from sklearn.feature_extraction.text import TfidfVectorizer - corpus = [f"{d['title']} {d['content']} {' '.join(d['laws'])} {d['category']}" for d in self.documents] - self.vectorizer = TfidfVectorizer(analyzer='char_wb', ngram_range=(2,4), max_features=6000, sublinear_tf=True) + corpus = [f"{d['title']} {d['content']} {' '.join(d['laws'])} {d['category']}" + for d in self.documents] + self.vectorizer = TfidfVectorizer( + analyzer='char_wb', ngram_range=(2, 4), + max_features=6000, sublinear_tf=True) self.doc_matrix = self.vectorizer.fit_transform(corpus) self._ready = True return True except Exception as e: - print(f"RAG init error: {e}") + print(f"Knowledge engine init error: {e}") return False def retrieve(self, query, top_k=3): - if not self._ready: self.initialize() + if not self._ready: + self.initialize() if self._ready: try: from sklearn.metrics.pairwise import cosine_similarity @@ -113,30 +104,36 @@ class RAGEngine: q_vec = self.vectorizer.transform([query]) scores = cosine_similarity(q_vec, self.doc_matrix)[0] idxs = np.argsort(scores)[::-1][:top_k] - return [dict(self.documents[i], score=float(scores[i])) for i in idxs if scores[i]>0.01] + return [dict(self.documents[i], score=float(scores[i])) + for i in idxs if scores[i] > 0.01] 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"], - "Pot Hole":["pothole","road","nha","sadak"], - "Pipe Leakage":["water","wasa","pipe","leakage","pani"]} + kw = { + "Garbage": ["garbage","waste","trash","kachra","1139","sanitation"], + "Pot Hole": ["pothole","road","nha","sadak","gara"], + "Pipe Leakage": ["water","wasa","pipe","leakage","pani","contaminated"], + } found = next((cat for cat, keys in kw.items() if any(k in q for k in keys)), None) - matched = [d for d in self.documents if found and d['category']==found] - matched += [d for d in self.documents if d['category']=='General' and d not in matched] + matched = [d for d in self.documents if found and d['category'] == found] + matched += [d for d in self.documents if d['category'] == 'General' and d not in matched] return matched[:top_k] or self.documents[:top_k] def format_context(self, docs): if not docs: return "" - ctx = "Relevant legal information from knowledge base:\n\n" + ctx = "Relevant legal information:\n\n" for i, d in enumerate(docs, 1): - ctx += f"[{i}] {d['title']}\n{d['content'][:350]}\nLaws: {', '.join(d['laws'][:2])}\nHelpline: {d['hotline']} | Response: {d['response_time']}\n\n" + 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 = RAGEngine() -rag.initialize() + +kb_engine = KnowledgeEngine() +kb_engine.initialize() # ─── STATIC DATA ────────────────────────────────────────────── CITIES_AREAS = { @@ -149,23 +146,80 @@ CITIES_AREAS = { "Peshawar": ["Hayatabad","University Town","Cantt","Saddar","Gulbahar"], "Quetta": ["Satellite Town","Jinnah Town","Cantt","Sariab Road","Brewery Road"], } -ISSUE_TYPES = ["🗑️ Garbage","🕳️ Pot Hole","💧 Pipe Leakage"] -LANGUAGES = ["English","اردو (Urdu)","پنجابی (Punjabi)","سندھی (Sindhi)"] +ISSUE_TYPES = ["🗑️ Garbage", "🕳️ Pot Hole", "💧 Pipe Leakage"] +LANGUAGES = ["English", "اردو (Urdu)", "پنجابی (Punjabi)", "سندھی (Sindhi)"] LANG_CODES = {"English":"en","اردو (Urdu)":"ur","پنجابی (Punjabi)":"ur","سندھی (Sindhi)":"ur"} LEGAL_KB = { - "Garbage":{"laws":["Punjab Waste Management Act 2014","EPA 1997 Section 11","Punjab LGA 2022 Schedule II","PPC Section 268"],"fine":"Rs. 500–50,000","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","dataset_ref":"Punjab SWMB | Urban Issues Dataset"}, - "Pot Hole":{"laws":["National Highways Safety Ordinance 2000","Punjab LGA 2022 Section 54","Motor Vehicles Ordinance 1965","Tort Law – Negligence"],"fine":"Authority liable for vehicle damage & 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: 051-9204551","Right to file writ petition in High Court","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","WASA Act Bylaws","EPA 1997 Section 13","Punjab LGA 2022","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","dataset_ref":"WASA Service Quality Reports | Consumer Complaints Dataset"}, + "Garbage": { + "laws": ["Punjab Waste Management Act 2014","EPA 1997 Section 11", + "Punjab LGA 2022 Schedule II","PPC Section 268"], + "fine":"Rs. 500–50,000","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", + "dataset_ref":"Punjab SWMB | Urban Issues Data", + }, + "Pot Hole": { + "laws": ["National Highways Safety Ordinance 2000","Punjab LGA 2022 Section 54", + "Motor Vehicles Ordinance 1965","Tort Law – Negligence"], + "fine":"Authority liable for vehicle damage & 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: 051-9204551", + "Right to file writ petition in High Court", + "Right to written notice to NHA/LDA", + ], + "escalation":"Federal Ombudsman: 051-9204551 | nha.gov.pk", + "dataset_ref":"NHA Road Quality Reports", + }, + "Pipe Leakage": { + "laws": ["Punjab Water Act 2019 Section 23","WASA Act Bylaws", + "EPA 1997 Section 13","Constitution Article 9"], + "fine":"Rs. 10,000–5,00,000 under PWA 2019", + "authority":"WASA / Pakistan Water Authority", + "hotline":"042-99200300","response":"24 hours", + "citizen_rights": [ + "Right to safe drinking water (SC 2018 – PLD 2018 SC 1)", + "Right to compensation for property damage", + "Right to stop billing if water is contaminated", + "Right to file complaint with Pakistan Water Authority", + ], + "escalation":"Pakistan Water Authority: 051-9246150 | CM Portal: 0800-02345", + "dataset_ref":"WASA Service Quality Reports", + }, } LOCALIZED = { - "Garbage": {"English":"Dumping garbage is a criminal offence under Punjab Waste Management Act 2014. Fine: Rs.500-50,000. Helpline: 1139","اردو (Urdu)":"کچرا پھینکنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 کے تحت جرم ہے۔ جرمانہ: 500-50,000 روپے۔ ہیلپ لائن: 1139","پنجابی (Punjabi)":"کچرا سُٹنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 دے تحت جرم اے۔ جرمانہ 500 توں 50,000 روپے۔","سندھی (Sindhi)":"ڪچرو اڇلائڻ پنجاب ويسٽ مئنيجمينٽ ايڪٽ 2014 تحت جرم آهي. جرمانو 500 کان 50,000 رپيا."}, - "Pot Hole": {"English":"Road repair is the government's legal obligation within 72 hours under Punjab LGA 2022. NHA: 051-9032800","اردو (Urdu)":"سڑک کی مرمت 72 گھنٹوں میں حکومت کی ذمہ داری ہے۔ NHA: 051-9032800","پنجابی (Punjabi)":"سڑک دی مرمت 72 گھنٹیاں وچ سرکار دی ذمہ واری اے۔","سندھی (Sindhi)":"سڙڪ جي مرمت 72 ڪلاڪن ۾ حڪومت جي ذميواري آهي."}, - "Pipe Leakage":{"English":"Pipe leakage must be repaired within 24 hours by WASA under Punjab Water Act 2019. WASA: 042-99200300","اردو (Urdu)":"پائپ لیکیج WASA کی 24 گھنٹوں میں ذمہ داری ہے۔ WASA: 042-99200300","پنجابی (Punjabi)":"پائپ لیکیج دی مرمت WASA دی 24 گھنٹیاں وچ ذمہ واری اے۔","سندھی (Sindhi)":"پائپ ليڪيج جي مرمت WASA جي 24 ڪلاڪن ۾ ذميواري آهي."}, + "Garbage": { + "English": "Dumping garbage is a criminal offence under Punjab Waste Management Act 2014. Fine: Rs.500–50,000. Helpline: 1139", + "اردو (Urdu)": "کچرا پھینکنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 کے تحت جرم ہے۔ جرمانہ: 500–50,000 روپے۔ ہیلپ لائن: 1139", + "پنجابی (Punjabi)": "کچرا سُٹنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 دے تحت جرم اے۔ جرمانہ 500 توں 50,000 روپے۔", + "سندھی (Sindhi)": "ڪچرو اڇلائڻ پنجاب ويسٽ مئنيجمينٽ ايڪٽ 2014 تحت جرم آهي. جرمانو 500 کان 50,000 رپيا.", + }, + "Pot Hole": { + "English": "Road repair is the government's legal obligation within 72 hours under Punjab LGA 2022. NHA: 051-9032800", + "اردو (Urdu)": "سڑک کی مرمت 72 گھنٹوں میں حکومت کی ذمہ داری ہے۔ NHA: 051-9032800", + "پنجابی (Punjabi)": "سڑک دی مرمت 72 گھنٹیاں وچ سرکار دی ذمہ واری اے۔", + "سندھی (Sindhi)": "سڙڪ جي مرمت 72 ڪلاڪن ۾ حڪومت جي ذميواري آهي.", + }, + "Pipe Leakage": { + "English": "Pipe leakage must be repaired within 24 hours by WASA under Punjab Water Act 2019. WASA: 042-99200300", + "اردو (Urdu)": "پائپ لیکیج WASA کی 24 گھنٹوں میں ذمہ داری ہے۔ WASA: 042-99200300", + "پنجابی (Punjabi)": "پائپ لیکیج دی مرمت WASA دی 24 گھنٹیاں وچ ذمہ واری اے۔", + "سندھی (Sindhi)": "پائپ ليڪيج جي مرمت WASA جي 24 ڪلاڪن ۾ ذميواري آهي.", + }, } 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} + # ─── YOLO ───────────────────────────────────────────────────── def detect_with_yolo(image_pil, issue_type): try: @@ -173,435 +227,566 @@ 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 + result = results[0]; names = model.names detected, sev = [], 1 - clean = issue_type.split(" ",1)[-1] + clean = issue_type.split(" ", 1)[-1] for box in result.boxes: cid = int(box.cls[0]); conf = float(box.conf[0]) - detected.append(f"{names.get(cid,f'cls{cid}')} ({conf:.0%})") - if clean=="Garbage" and cid in WASTE_CLASS_IDS: sev=min(10,sev+2) - elif clean in("Pot Hole","Pipe Leakage"): sev=min(10,sev+1) + detected.append(f"{names.get(cid, f'cls{cid}')} ({conf:.0%})") + if clean == "Garbage" and cid in WASTE_CLASS_IDS: sev = min(10, sev + 2) + elif clean 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 by YOLO." - return ann, summ, max(sev,3) + 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,"YOLO not installed.",5 + return image_pil, "Detection module not installed.", 5 except Exception as e: - return image_pil,f"YOLO error: {e}",5 + return image_pil, f"Detection error: {e}", 5 -# ─── GEMINI ─────────────────────────────────────────────────── + +# ─── GEMINI IMAGE ANALYSIS ──────────────────────────────────── def analyze_with_gemini(image_pil, issue, location, city, yolo_summary): - if not GOOGLE_API_KEY: return "WARNING: GOOGLE_API_KEY not set." + if not GOOGLE_API_KEY: + return "WARNING: GOOGLE_API_KEY not set." 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"""Strict Pakistani Civic Issue Inspector AI. -ISSUE: '{issue}' CITY: {city} LOCATION: {location} YOLO: {yolo_summary} -Garbage=actual waste, Pot Hole=visible road hole, Pipe Leakage=water from pipe. Clean/indoor/vegetation=REJECT. -Respond ONLY: -STATUS: [APPROVED or REJECTED] -REASON: [2-3 sentences English] -REASON_URDU: [2-3 sentences Urdu] -SEVERITY: [1-10] -CONFIDENCE: [XX%] -RECOMMENDED_ACTION: [one sentence]""" - img_part = {"mime_type":"image/jpeg","data":base64.b64encode(buf.getvalue()).decode()} - return model.generate_content([prompt,img_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} YOLO: {yolo_summary}\n" + f"Garbage=actual waste, Pot Hole=visible road hole, Pipe Leakage=water from pipe. " + f"Clean/indoor/vegetation=REJECT.\n" + f"Respond ONLY:\nSTATUS: [APPROVED or REJECTED]\n" + f"REASON: [2-3 sentences English]\nREASON_URDU: [2-3 sentences Urdu]\n" + f"SEVERITY: [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: Gemini error: {e}" + return f"WARNING: Analysis error: {e}" + def parse_gemini(text): - r={"status":"UNKNOWN","reason":"Could not parse.","reason_urdu":"","severity":5,"confidence":"0%","action":""} + r = {"status":"UNKNOWN","reason":"Could not parse.","reason_urdu":"", + "severity":5,"confidence":"0%","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) + for pat, key in [(r"STATUS:\s*(APPROVED|REJECTED)", "status"), + (r"SEVERITY:\s*(\d+)", "severity"), + (r"CONFIDENCE:\s*(\d+%)", "confidence")]: + m = re.search(pat, text, re.IGNORECASE) if m: - v=m.group(1) - if key=="status": r[key]=v.upper() - elif key=="severity": r[key]=int(v) - else: r[key]=v - for pat,key in[(r"REASON:\s*(.+?)(?=REASON_URDU:|SEVERITY:|$)","reason"),(r"REASON_URDU:\s*(.+?)(?=SEVERITY:|$)","reason_urdu"),(r"RECOMMENDED_ACTION:\s*(.+?)$","action")]: - m=re.search(pat,text,re.DOTALL|re.IGNORECASE) - if m: r[key]=m.group(1).strip() + v = m.group(1) + if key == "status": r[key] = v.upper() + elif key == "severity": r[key] = int(v) + else: r[key] = v + for pat, key in [(r"REASON:\s*(.+?)(?=REASON_URDU:|SEVERITY:|$)", "reason"), + (r"REASON_URDU:\s*(.+?)(?=SEVERITY:|$)", "reason_urdu"), + (r"RECOMMENDED_ACTION:\s*(.+?)$", "action")]: + m = re.search(pat, text, re.DOTALL | re.IGNORECASE) + if m: r[key] = m.group(1).strip() return r -# ─── LLAMA 3 LEGAL ADVICE ───────────────────────────────────── -def analyze_with_llama(issue, location, city, yolo_s, severity, language="English"): - clean = issue.split(" ",1)[-1]; kb = LEGAL_KB.get(clean,{}) - lang_inst = {"اردو (Urdu)":"Respond entirely in Urdu.","پنجابی (Punjabi)":"Respond in Punjabi Shahmukhi.","سندھی (Sindhi)":"Respond in Sindhi."}.get(language,"Respond in clear professional English.") + +# ─── LEGAL ADVICE (AI) ──────────────────────────────────────── +def get_legal_advice(issue, location, city, yolo_s, severity, language="English"): + clean = issue.split(" ", 1)[-1] + kb = LEGAL_KB.get(clean, {}) + lang_inst = { + "اردو (Urdu)": "Respond entirely in Urdu.", + "پنجابی (Punjabi)": "Respond in Punjabi Shahmukhi.", + "سندھی (Sindhi)": "Respond in Sindhi.", + }.get(language, "Respond in clear professional English.") + if not GROQ_API_KEY: - rights = "\n".join(f" * {r}" for r in kb.get("citizen_rights",[])) - return (f"Applicable Laws:\n"+"\n".join(f" * {l}" for l in kb.get("laws",[]))+ - f"\n\nCitizen Rights:\n{rights}\n\nFine: {kb.get('fine','N/A')}" - f"\nHelpline: {kb.get('hotline','N/A')}\nResponse Time: {kb.get('response','N/A')}" - f"\nEscalation: {kb.get('escalation','N/A')}\n\n(Set GROQ_API_KEY for AI legal advice)") + rights = "\n".join(f" • {r}" for r in kb.get("citizen_rights", [])) + return (f"Applicable Laws:\n" + + "\n".join(f" • {l}" for l in kb.get("laws", [])) + + f"\n\nYour Rights:\n{rights}" + + f"\n\nFine / Penalty: {kb.get('fine','N/A')}" + + f"\nHelpline: {kb.get('hotline','N/A')}" + + f"\nRequired Response Time: {kb.get('response','N/A')}" + + f"\nEscalation: {kb.get('escalation','N/A')}\n\n" + "(Set GROQ_API_KEY in Space secrets for detailed AI legal advice)") try: from groq import Groq - client = Groq(api_key=GROQ_API_KEY) - prompt = f"""You are a Pakistani civic law expert for the Rahbar platform. -{lang_inst} -Complaint: {issue} at {location}, {city} | Severity: {severity}/10 -Laws: {', '.join(kb.get('laws',[]))} | Response Time: {kb.get('response','72 hours')} -Provide: 1.Specific legal rights (cite law/section) 2.Exact numbered steps to file complaint 3.What to do if authority doesn't respond in time 4.Possible compensation 5.Helplines and escalation contacts. Concise and practical.""" + prompt = (f"You are a Pakistani civic law expert.\n{lang_inst}\n" + f"Complaint: {issue} at {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/section) " + f"2.Exact numbered steps to file complaint " + f"3.What to do if authority doesn't respond in time " + f"4.Possible compensation 5.Helplines and escalation. Concise and practical.") resp = Groq(api_key=GROQ_API_KEY).chat.completions.create( - model="llama-3.3-70b-versatile", messages=[{"role":"user","content":prompt}], max_tokens=700) + 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"Llama 3 error: {e}" + return f"Legal advice error: {e}" -# ─── RAG CHATBOT — plain dict format ───────────────────────── -def legal_chatbot_rag(user_message, history, language): + +# ─── LEGAL CHATBOT ──────────────────────────────────────────── +def legal_chatbot(user_message, history, language): if history is None: history = [] if not user_message.strip(): return history, "" - retrieved = rag.retrieve(user_message, top_k=3) - ctx = rag.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 Rahbar Legal Assistant — a civic rights advisor for Pakistani citizens. -{lang_inst} -Specialise ONLY in: water, WASA, pipe leakage, garbage, roads, potholes, Pakistani civic law. -Always cite laws and provide helplines. Max 250 words. -{ctx}""" + + retrieved = kb_engine.retrieve(user_message, top_k=3) + ctx = kb_engine.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.\n{lang_inst}\n" + f"Only discuss: water, WASA, pipe leakage, garbage, roads, potholes, Pakistani civic law.\n" + f"Always cite specific laws and provide helpline numbers. Max 250 words.\n{ctx}") + if not GROQ_API_KEY: d = retrieved[0] if retrieved else None - answer = (f"**{d['title']}**\n\n{d['content'][:400]}\n\nHelpline: {d['hotline']} | Response: {d['response_time']}\nLaws: {', '.join(d['laws'][:2])}\n\n_(Set GROQ_API_KEY for full AI answers)_" if d else - "I can help with water, garbage, and road issues in Pakistan. Please ask about a specific civic issue.") - return history + [{"role":"user","content":user_message},{"role":"assistant","content":answer}], "" + answer = (f"**{d['title']}**\n\n{d['content'][:400]}\n\n" + f"Helpline: {d['hotline']} | Response: {d['response_time']}\n" + f"Laws: {', '.join(d['laws'][:2])}\n\n" + f"_(Set GROQ_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":answer}], "" try: from groq import Groq msgs = [{"role":"system","content":system}] - for msg in history[-16:]: msgs.append({"role":msg["role"],"content":msg["content"]}) + for msg in history[-16:]: + msgs.append({"role": msg["role"], "content": msg["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) + resp = Groq(api_key=GROQ_API_KEY).chat.completions.create( + model="llama-3.3-70b-versatile", messages=msgs, max_tokens=500) answer = resp.choices[0].message.content.strip() - if retrieved: answer += f"\n\n_Sources: {' | '.join(d['title'][:30]+'…' for d in retrieved[:2])}_" except Exception as e: answer = f"Error: {e}" - return history + [{"role":"user","content":user_message},{"role":"assistant","content":answer}], "" + + return history + [{"role":"user","content":user_message}, + {"role":"assistant","content":answer}], "" + # ─── TTS ────────────────────────────────────────────────────── def make_tts(text, language): try: from gtts import gTTS - code = LANG_CODES.get(language,"en") - tts = gTTS(text=str(text)[:600], lang=code, slow=False) - path = f"/tmp/tts_{uuid.uuid4().hex[:8]}.mp3" - tts.save(path); return path + code = LANG_CODES.get(language, "en") + # Strip markdown/source references + 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 except Exception as e: - print(f"TTS error: {e}"); return None + print(f"TTS error: {e}") + return None + # ─── STT ────────────────────────────────────────────────────── -def stt(audio_file): - if audio_file is None: return "No audio provided. Please record or upload audio first." +def stt_transcribe(audio_file): + if audio_file is None: + return "No audio provided. Please record or upload audio first." + def to_wav(p): if p.lower().endswith(".wav"): return p try: from pydub import AudioSegment - out = p+"_c.wav"; AudioSegment.from_file(p).export(out,format="wav"); return out + 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 wav = to_wav(audio_file) - with open(wav,"rb") as f: - result = Groq(api_key=GROQ_API_KEY).audio.transcriptions.create(model="whisper-large-v3",file=f,response_format="text") - return (result if isinstance(result,str) else result.text).strip() or "No speech detected." - except Exception as e: groq_err=str(e) - else: groq_err="GROQ_API_KEY not set" + with open(wav, "rb") as f: + result = Groq(api_key=GROQ_API_KEY).audio.transcriptions.create( + model="whisper-large-v3", file=f, response_format="text") + text = (result if isinstance(result, str) else result.text).strip() + return text or "No speech detected." + except Exception as e: + groq_err = str(e) + else: + groq_err = "GROQ_API_KEY not set" + try: import speech_recognition as sr wav = to_wav(audio_file) rec = sr.Recognizer() with sr.AudioFile(wav) as src: - rec.adjust_for_ambient_noise(src,duration=0.3); data=rec.record(src) - try: return rec.recognize_google(data,language="ur-PK") + 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.\nGroq error: {groq_err}\nFallback error: {e2}\nTip: Set GROQ_API_KEY for best results." - -def voice_then_chat(audio_file, history, language): - if audio_file is None: return history, "" - txt = stt(audio_file) - if not txt or txt.startswith("Transcription failed") or txt.startswith("No audio"): return history, txt - return legal_chatbot_rag(txt, history, language)[0], "" + return (f"Transcription failed.\n" + f"Primary error: {groq_err}\n" + f"Fallback error: {e2}\n" + f"Tip: Add GROQ_API_KEY in Space secrets for best results.") + + +# ─── VOICE → CHATBOT pipeline ───────────────────────────────── +def voice_to_chat(audio_file, history, language): + """Transcribe audio then send to chatbot, return updated history.""" + if audio_file is None: + return history, "No audio recorded." + text = stt_transcribe(audio_file) + if not text or text.startswith("Transcription failed") or text.startswith("No audio"): + return history, text + new_history, _ = legal_chatbot(text, history, language) + return new_history, "" + + +def read_last_answer(history, language): + """Find last assistant message and convert to speech.""" + if not history: + return None + # Walk backwards to find last assistant turn + for msg in reversed(history): + if isinstance(msg, dict) and msg.get("role") == "assistant": + content = msg.get("content", "") + if content.strip(): + return make_tts(content, language) + return None -def chatbot_tts(history, language): - if not history: return None - last = history[-1] - txt = last.get("content","") if isinstance(last,dict) else "" - txt = re.sub(r'_Sources:.*?_','',txt,flags=re.DOTALL).strip() - return make_tts(txt[:600], language) # ─── LAW REFERENCE ──────────────────────────────────────────── def law_info(issue, language): - clean = issue.split(" ",1)[-1]; kb = LEGAL_KB.get(clean,{}); local = LOCALIZED.get(clean,{}).get(language,"") - rights = "\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\n### Responsible Authority\n{kb.get('authority','N/A')}\n" - f"\n### Helpline\n**{kb.get('hotline','N/A')}**\n\n### Response Time\n{kb.get('response','N/A')}\n" - f"\n### Citizen Rights\n{rights}\n\n### Escalation Path\n{kb.get('escalation','N/A')}\n") - if local: out+=f"\n---\n### Localized Message ({language})\n> {local}\n" - out+=f"\n---\n*Source: {kb.get('dataset_ref','Pakistani civic law databases')}*" + clean = issue.split(" ", 1)[-1] + kb = LEGAL_KB.get(clean, {}) + local = LOCALIZED.get(clean, {}).get(language, "") + rights = "\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### Emergency Helpline\n**{kb.get('hotline','N/A')}**\n" + f"\n### Required Government Response Time\n{kb.get('response','N/A')}\n" + f"\n### Your Rights\n{rights}\n" + f"\n### If Ignored — Escalate To\n{kb.get('escalation','N/A')}\n") + if local: + out += f"\n---\n### Notice in {language}\n> {local}\n" return out + # ─── ADMIN ──────────────────────────────────────────────────── def get_admin_stats(): - total=len(complaint_log) - if not total: return "No complaints filed yet.","" - counts={"Garbage":0,"Pot Hole":0,"Pipe Leakage":0}; cities={}; sevs=[] + total = len(complaint_log) + if not total: return "No complaints filed yet.", "" + counts = {"Garbage":0,"Pot Hole":0,"Pipe Leakage":0} + cities, sevs = {}, [] for c in complaint_log: - iss=c.get("issue","").split(" ",1)[-1]; 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"## Admin Dashboard\n|Metric|Value|\n|---|---|\n|Total|**{total}**|\n|Avg Severity|**{avg:.1f}/10**|\n|Top City|**{top}**|\n\n### By Issue Type\n|Issue|Count|\n|---|---|\n" - for k,v in counts.items(): stats+=f"|{k}|{v}|\n" - stats+="\n### 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" + iss = c.get("issue","").split(" ",1)[-1] + 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" + f"|Total Complaints|**{total}**|\n" + f"|Average Severity|**{avg:.1f}/10**|\n" + f"|Most Active City|**{top}**|\n\n" + f"### By Issue\n|Issue|Count|\n|---|---|\n") + for k, v in counts.items(): stats += f"|{k}|{v}|\n" + stats += "\n### By City\n|City|Count|\n|---|---|\n" + for c, n in sorted(cities.items(), key=lambda x: -x[1]): stats += f"|{c}|{n}|\n" + log = "## Recent Complaints\n\n" for c in reversed(complaint_log[-10:]): - log+=(f"**{c['id']}** | {c['timestamp']} | {c['city']}, {c['location']} | " - f"{c['issue']} | Sev {c['severity']}/10 | {c.get('name','?')}\n\n") - return stats,log - -def sev_icon(s): return "🟢" if s<=3 else("🟡" if s<=6 else("🟠" if s<=8 else "🔴")) - -# ─── PDF REPORT GENERATOR ───────────────────────────────────── -def generate_pdf_report(complaint_id, timestamp, name, cnic, phone, city, location, - issue_type, language, severity, yolo_summary, gemini_status, - gemini_reason, gemini_confidence, gemini_action, kb, - description, local_msg, report_text): - """Generate professional PDF report using ReportLab""" + log += (f"**{c['id']}** | {c['timestamp']} | {c['city']}, {c['location']} | " + f"{c['issue']} | Severity {c['severity']}/10 | {c.get('name','?')}\n\n") + return stats, log + + +def sev_icon(s): + return "🟢" if s <= 3 else ("🟡" if s <= 6 else ("🟠" if s <= 8 else "🔴")) + + +# ─── PDF REPORT ─────────────────────────────────────────────── +def generate_pdf(cid, ts, name, cnic, phone, city, loc, issue_type, language, + severity, yolo_s, g_status, g_reason, g_conf, g_action, + kb, description, local_msg): try: from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import cm from reportlab.lib import colors - from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable - from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT + from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, + Table, TableStyle, HRFlowable) + from reportlab.lib.enums import TA_CENTER, TA_LEFT buf = io.BytesIO() doc = SimpleDocTemplate(buf, pagesize=A4, rightMargin=2*cm, leftMargin=2*cm, topMargin=2*cm, bottomMargin=2*cm) + GREEN_DARK = colors.HexColor('#0d2b1e') + GREEN_MID = colors.HexColor('#1f7a52') + GREEN_LIGHT = colors.HexColor('#e8f3ed') + GOLD = colors.HexColor('#d4870e') + GRAY_LIGHT = colors.HexColor('#f4f8f5') + styles = getSampleStyleSheet() - # Custom styles - title_style = ParagraphStyle('Title', parent=styles['Normal'], fontSize=14, fontName='Helvetica-Bold', alignment=TA_CENTER, spaceAfter=4, textColor=colors.HexColor('#0d2b1e')) - sub_style = ParagraphStyle('Sub', parent=styles['Normal'], fontSize=11, fontName='Helvetica-Bold', alignment=TA_CENTER, spaceAfter=8, textColor=colors.HexColor('#1f7a52')) - section_style = ParagraphStyle('Section', parent=styles['Normal'], fontSize=10, fontName='Helvetica-Bold', spaceAfter=6, textColor=colors.HexColor('#1f7a52'), spaceBefore=12) - body_style = ParagraphStyle('Body', parent=styles['Normal'], fontSize=9, fontName='Helvetica', spaceAfter=3, leading=14) - small_style = ParagraphStyle('Small', parent=styles['Normal'], fontSize=8, fontName='Helvetica', textColor=colors.gray) - label_style = ParagraphStyle('Label', parent=styles['Normal'], fontSize=9, fontName='Helvetica-Bold', spaceAfter=2) + + def S(name, **kw): + return ParagraphStyle(name, parent=styles['Normal'], **kw) + + title_s = S('T', fontSize=15, fontName='Helvetica-Bold', + alignment=TA_CENTER, textColor=GREEN_DARK, spaceAfter=2) + sub_s = S('Su', fontSize=11, fontName='Helvetica-Bold', + alignment=TA_CENTER, textColor=GREEN_MID, spaceAfter=2) + urdu_s = S('U', fontSize=10, alignment=TA_CENTER, + textColor=GOLD, spaceAfter=6) + section_s = S('Sec', fontSize=10, fontName='Helvetica-Bold', + textColor=GREEN_MID, spaceBefore=10, spaceAfter=4) + body_s = S('B', fontSize=9, fontName='Helvetica', + spaceAfter=3, leading=14) + label_s = S('L', fontSize=9, fontName='Helvetica-Bold', + spaceAfter=2) + small_s = S('Sm', fontSize=7.5, fontName='Helvetica', + textColor=colors.gray) date_str = datetime.datetime.now().strftime("%d %B %Y") time_str = datetime.datetime.now().strftime("%I:%M %p") - - story = [] - - # ── Header ── - GREEN = colors.HexColor('#0d2b1e') - header_data = [[Paragraph("GOVERNMENT OF PAKISTAN", title_style)], - [Paragraph("CIVIC COMPLAINT REPORT", sub_style)], - [Paragraph("Rahbar Digital Civic Redressal System | رہبر", body_style)]] - header_table = Table(header_data, colWidths=[17*cm]) - header_table.setStyle(TableStyle([ - ('BACKGROUND', (0,0), (-1,-1), colors.HexColor('#e8f3ed')), - ('BOX', (0,0), (-1,-1), 1, colors.HexColor('#1f7a52')), - ('TOPPADDING', (0,0), (-1,-1), 8), - ('BOTTOMPADDING',(0,0),(-1,-1), 8), - ('LEFTPADDING', (0,0), (-1,-1), 12), + story = [] + + # ── Banner ── + banner = Table( + [[Paragraph("GOVERNMENT OF PAKISTAN", title_s)], + [Paragraph("CIVIC COMPLAINT REPORT", sub_s)], + [Paragraph("Rahbar Digital Civic Redressal System | رہبر", urdu_s)]], + colWidths=[17*cm] + ) + banner.setStyle(TableStyle([ + ('BACKGROUND', (0,0),(-1,-1), GREEN_LIGHT), + ('BOX', (0,0),(-1,-1), 1.5, GREEN_MID), + ('TOPPADDING', (0,0),(-1,-1), 10), + ('BOTTOMPADDING', (0,0),(-1,-1), 10), + ('LEFTPADDING', (0,0),(-1,-1), 16), ])) - story.append(header_table) - story.append(Spacer(1, 0.3*cm)) - - # ── Ref box ── - ref_data = [ - ["Complaint Ref No.", complaint_id, "Date of Filing", date_str], - ["Time of Filing", time_str, "Language", language], - ["Platform", "Rahbar AI (Verified)", "", ""], - ] - ref_table = Table(ref_data, colWidths=[4.25*cm, 4.25*cm, 4.25*cm, 4.25*cm]) - ref_table.setStyle(TableStyle([ - ('FONTNAME', (0,0), (0,-1), 'Helvetica-Bold'), - ('FONTNAME', (2,0), (2,-1), 'Helvetica-Bold'), - ('FONTSIZE', (0,0), (-1,-1), 8), - ('BACKGROUND', (0,0), (0,-1), colors.HexColor('#e8f3ed')), - ('BACKGROUND', (2,0), (2,-1), colors.HexColor('#e8f3ed')), - ('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#b8d9c5')), - ('TOPPADDING', (0,0), (-1,-1), 4), - ('BOTTOMPADDING',(0,0),(-1,-1), 4), - ('LEFTPADDING', (0,0), (-1,-1), 6), + story.extend([banner, Spacer(1, 0.3*cm)]) + + # ── Reference box ── + ref = Table([ + ["Complaint Ref No.", cid, "Date of Filing", date_str], + ["Time of Filing", time_str, "Language", language], + ["Filing Method", "Rahbar Digital Platform (AI-Verified)", "", ""], + ], colWidths=[4.25*cm]*4) + ref.setStyle(TableStyle([ + ('FONTNAME', (0,0),(0,-1), 'Helvetica-Bold'), + ('FONTNAME', (2,0),(2,-1), 'Helvetica-Bold'), + ('FONTSIZE', (0,0),(-1,-1), 8), + ('BACKGROUND', (0,0),(0,-1), GRAY_LIGHT), + ('BACKGROUND', (2,0),(2,-1), GRAY_LIGHT), + ('GRID', (0,0),(-1,-1), 0.5, colors.HexColor('#b8d9c5')), + ('TOPPADDING', (0,0),(-1,-1), 4), + ('BOTTOMPADDING', (0,0),(-1,-1), 4), + ('LEFTPADDING', (0,0),(-1,-1), 6), ])) - story.append(ref_table) - story.append(Spacer(1, 0.3*cm)) + story.extend([ref, Spacer(1, 0.25*cm)]) def section(title): - story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor('#1f7a52'))) - story.append(Paragraph(f"◆ {title}", section_style)) + story.append(HRFlowable(width="100%", thickness=1, color=GREEN_MID)) + story.append(Paragraph(f"◆ {title}", section_s)) def row(label, value): - story.append(Paragraph(f"{label}: {value}", body_style)) + story.append(Paragraph(f"{label}: {value}", body_s)) - # ── Section A ── + # ── A: Complainant ── section("SECTION A — COMPLAINANT INFORMATION") - for lbl, val in [("Full Name",name),("CNIC",cnic),("Phone",phone or "Not Provided"),("City",city),("Area / Location",location)]: + for lbl, val in [("Full Name", name), ("CNIC", cnic), + ("Phone", phone or "Not Provided"), + ("City", city), ("Area / Location", loc)]: row(lbl, val) - # ── Section B ── + # ── B: Complaint ── section("SECTION B — COMPLAINT DETAILS") - for lbl, val in [("Issue Type",issue_type),("Location",f"{location}, {city}"),("Date & Time",f"{date_str}, {time_str}"),("Severity",f"{sev_icon(severity)} {severity} / 10")]: + for lbl, val in [("Issue Type", issue_type), + ("Location", f"{loc}, {city}"), + ("Date & Time", f"{date_str}, {time_str}"), + ("Severity", f"{sev_icon(severity)} {severity} / 10")]: row(lbl, val) - story.append(Paragraph(f"Description: {description.strip() or '[None provided]'}", body_style)) - - # ── Section C ── - section("SECTION C — AI VERIFICATION RESULTS") - for lbl, val in [("AI Status",gemini_status),("Confidence",gemini_confidence),("Finding",gemini_reason),("Recommended Action",gemini_action or "Immediate field inspection required."),("YOLO Detection",yolo_summary)]: + story.append(Paragraph( + f"Description: {description.strip() or '[None provided]'}", body_s)) + + # ── C: AI Verification ── + section("SECTION C — VERIFICATION RESULTS") + for lbl, val in [("Status", g_status), ("Confidence", g_conf), + ("Finding", g_reason), + ("Recommended Action", g_action or "Immediate field inspection required."), + ("Image Analysis", yolo_s)]: row(lbl, val) - # ── Section D ── + # ── D: Legal ── section("SECTION D — LEGAL FRAMEWORK") - story.append(Paragraph("Applicable Legislation:", label_style)) - for l in kb.get("laws",[]): story.append(Paragraph(f" • {l}", body_style)) - for lbl, val in [("Responsible Authority",kb.get('authority','N/A')),("Official Helpline",kb.get('hotline','N/A')),("Statutory Response Time",kb.get('response','N/A')),("Fine / Penalty",kb.get('fine','N/A'))]: + story.append(Paragraph("Applicable Legislation:", label_s)) + for l in kb.get("laws", []): + story.append(Paragraph(f" • {l}", body_s)) + for lbl, val in [ + ("Responsible Authority", kb.get('authority','N/A')), + ("Official Helpline", kb.get('hotline','N/A')), + ("Statutory Response Time", kb.get('response','N/A')), + ("Fine / Penalty", kb.get('fine','N/A')), + ]: row(lbl, val) - # ── Section E ── + # ── E: Rights ── section("SECTION E — CITIZEN'S LEGAL RIGHTS") - for r in kb.get("citizen_rights",[]): story.append(Paragraph(f" • {r}", body_style)) - row("Escalation Path", kb.get('escalation','CM Portal: 0800-02345')) + for right in kb.get("citizen_rights", []): + story.append(Paragraph(f" • {right}", body_s)) + row("Escalation Path", kb.get('escalation', 'CM Portal: 0800-02345')) - # ── Section F ── - section(f"SECTION F — LOCALIZED NOTICE ({language})") - story.append(Paragraph(local_msg or "N/A", body_style)) + # ── F: Localized ── + section(f"SECTION F — NOTICE IN {language.upper()}") + story.append(Paragraph(local_msg or "N/A", body_s)) - # ── Section G ── + # ── G: Directive ── section("SECTION G — ACTION DIRECTIVE") - story.append(Paragraph(f"MANDATORY ACTION REQUIRED WITHIN: {kb.get('response','72 hours').upper()}", label_style)) + story.append(Paragraph( + f"MANDATORY ACTION REQUIRED WITHIN: " + f"{kb.get('response','72 hours').upper()}", label_s)) row("Authority", kb.get('authority','N/A')) row("Helpline", kb.get('hotline','N/A')) - row("Portal", "citizenportal.gov.pk | CM: 0800-02345") + row("Citizen Portal", "citizenportal.gov.pk | CM Helpline: 0800-02345") # ── Declaration ── section("DECLARATION") - story.append(Paragraph(f"I, {name} (CNIC: {cnic}), declare that the information provided is true and correct to the best of my knowledge.", body_style)) + story.append(Paragraph( + f"I, {name} (CNIC: {cnic}), declare that the information provided " + f"is true and correct to the best of my knowledge.", body_s)) story.append(Spacer(1, 0.4*cm)) - sig_data = [["Signature: ________________________ (Digital)", f"Date: {date_str}", f"Ref: {complaint_id}"]] - sig_table = Table(sig_data, colWidths=[6*cm, 5.5*cm, 5.5*cm]) - sig_table.setStyle(TableStyle([('FONTSIZE',(0,0),(-1,-1),8),('TOPPADDING',(0,0),(-1,-1),4)])) - story.append(sig_table) - # ── Official use ── + sig = Table( + [[f"Signature: ________________________ (Digital)", + f"Date: {date_str}", f"Ref: {cid}"]], + colWidths=[6.5*cm, 5*cm, 5.5*cm] + ) + sig.setStyle(TableStyle([('FONTSIZE',(0,0),(-1,-1),8)])) + story.append(sig) + + # ── Official Use ── section("FOR OFFICIAL USE ONLY") - for lbl in ["Received By","Date of Receipt","Action Taken","Resolved On"]: - story.append(Paragraph(f"{lbl}: _____________________________", body_style)) + for lbl in ["Received By", "Date of Receipt", "Action Taken", "Resolved On"]: + story.append(Paragraph( + f"{lbl}: ___________________________________", body_s)) # ── Footer ── - story.append(Spacer(1, 0.4*cm)) - story.append(HRFlowable(width="100%", thickness=0.5, color=colors.gray)) - story.append(Paragraph(f"Generated by: Rahbar — Pakistan's AI Civic Redressal Platform | Timestamp: {timestamp}", small_style)) + story.extend([ + Spacer(1, 0.4*cm), + HRFlowable(width="100%", thickness=0.5, color=colors.gray), + Paragraph( + f"Generated by Rahbar — Pakistan's Civic Complaint System | " + f"Complaint ID: {cid} | Timestamp: {ts}", small_s), + ]) doc.build(story) buf.seek(0) - path = f"/tmp/rahbar_report_{complaint_id}.pdf" + path = f"/tmp/Rahbar_Report_{cid}.pdf" with open(path, "wb") as f: f.write(buf.read()) return path + except Exception as e: - print(f"PDF generation error: {e}") - # Fallback: plain text in a temp file - path = f"/tmp/rahbar_report_{complaint_id}.txt" - with open(path,"w",encoding="utf-8") as f: f.write(report_text) + print(f"PDF error: {e}") + path = f"/tmp/Rahbar_Report_{cid}.txt" + with open(path, "w", encoding="utf-8") as f: + f.write(f"RAHBAR COMPLAINT REPORT\nID: {cid}\nIssue: {issue_type}\n" + f"Location: {loc}, {city}\nSeverity: {severity}/10\n" + f"Name: {name} CNIC: {cnic}\nTimestamp: {ts}\n") return path + # ─── TEXT REPORT ────────────────────────────────────────────── -def build_text_report(cid, ts, name, cnic, phone, city, loc, issue, lang, sev, si, - yolo_s, g_status, g_reason, g_conf, g_action, kb, desc, local): - d=datetime.datetime.now().strftime("%d %B %Y"); t=datetime.datetime.now().strftime("%I:%M %p") - laws="".join(f"\n - {l}" for l in kb.get("laws",[])) - rights="".join(f"\n - {r}" for r in kb.get("citizen_rights",[])) +def build_text_report(cid, ts, name, cnic, phone, city, loc, issue_type, + lang, sev, si, yolo_s, g_status, g_reason, + g_conf, g_action, kb, desc, local): + d = datetime.datetime.now().strftime("%d %B %Y") + t = datetime.datetime.now().strftime("%I:%M %p") + laws = "".join(f"\n - {l}" for l in kb.get("laws", [])) + rights = "".join(f"\n - {r}" for r in kb.get("citizen_rights", [])) return f""" -=========================================================================== - GOVERNMENT OF PAKISTAN — CIVIC COMPLAINT REPORT - Rahbar Digital Civic Redressal System | رہبر -=========================================================================== +====================================================================== + GOVERNMENT OF PAKISTAN — CIVIC COMPLAINT REPORT + Rahbar Digital Civic Redressal System | رہبر +====================================================================== Complaint Ref : {cid} Date / Time : {d} {t} - Platform : Rahbar AI (AI-Verified) + Platform : Rahbar (AI-Verified) Language : {lang} -=========================================================================== +====================================================================== SECTION A — COMPLAINANT -=========================================================================== - Full Name : {name} - CNIC : {cnic} - Phone : {phone or "Not Provided"} - City : {city} - Location : {loc} -=========================================================================== +====================================================================== + Full Name : {name} + CNIC : {cnic} + Phone : {phone or "Not Provided"} + City : {city} + Location : {loc} +====================================================================== SECTION B — COMPLAINT DETAILS -=========================================================================== - Issue : {issue} - Location : {loc}, {city} - Severity : {si} {sev} / 10 - Description: {desc.strip() or "[None provided]"} -=========================================================================== - SECTION C — AI VERIFICATION -=========================================================================== - Status : {g_status} - Confidence : {g_conf} - Finding : {g_reason} - Action : {g_action or "Immediate field inspection required."} - YOLO : {yolo_s} -=========================================================================== +====================================================================== + Issue : {issue_type} + Location : {loc}, {city} + Severity : {si} {sev} / 10 + Description : {desc.strip() or "[None provided]"} +====================================================================== + SECTION C — VERIFICATION RESULTS +====================================================================== + Status : {g_status} + Confidence : {g_conf} + Finding : {g_reason} + Action : {g_action or "Immediate field inspection required."} + Detection : {yolo_s} +====================================================================== SECTION D — LEGAL FRAMEWORK -=========================================================================== - Laws :{laws} - - Authority : {kb.get('authority','N/A')} - Helpline : {kb.get('hotline','N/A')} - Response : {kb.get('response','N/A')} - Fine : {kb.get('fine','N/A')} -=========================================================================== - SECTION E — CITIZEN'S RIGHTS -=========================================================================== +====================================================================== + Laws :{laws} + + Authority : {kb.get('authority','N/A')} + Helpline : {kb.get('hotline','N/A')} + Response : {kb.get('response','N/A')} + Fine : {kb.get('fine','N/A')} +====================================================================== + SECTION E — YOUR RIGHTS +====================================================================== {rights} - Escalation : {kb.get('escalation','CM Portal: 0800-02345')} -=========================================================================== - SECTION F — LOCALIZED NOTICE ({lang}) -=========================================================================== + Escalation : {kb.get('escalation','CM Portal: 0800-02345')} +====================================================================== + SECTION F — NOTICE ({lang}) +====================================================================== {local} -=========================================================================== +====================================================================== SECTION G — ACTION DIRECTIVE -=========================================================================== +====================================================================== MANDATORY ACTION WITHIN: {kb.get('response','72 hours').upper()} - Authority: {kb.get('authority','?')} | Helpline: {kb.get('hotline','?')} - Portal: citizenportal.gov.pk | CM: 0800-02345 -=========================================================================== + Authority : {kb.get('authority','?')} + Helpline : {kb.get('hotline','?')} + Portal : citizenportal.gov.pk | CM: 0800-02345 +====================================================================== DECLARATION -=========================================================================== - I, {name} (CNIC: {cnic}), declare this information is true and correct. - Signature: ________________________ (Digital) Date: {d} +====================================================================== + I, {name} (CNIC: {cnic}), declare this information is true. + Signature: ______________________ (Digital) Date: {d} Reference: {cid} -=========================================================================== +====================================================================== FOR OFFICIAL USE ONLY -=========================================================================== - Received By : _________________________ - Date Received: _________________________ - Action Taken : _________________________ - Resolved On : _________________________ - Generated: Rahbar Pakistan AI Civic Platform | {ts} -=========================================================================== +====================================================================== + Received By : _______________________ + Date Received : _______________________ + Action Taken : _______________________ + Resolved On : _______________________ + Generated: Rahbar Pakistan Civic Platform | {ts} +====================================================================== """ + # ─── MAIN REPORT FUNCTION ───────────────────────────────────── def make_report(image, issue_type, city, location, name, cnic, phone, description, language, enable_tts): - if image is None: return (None,"Please upload an image.","","",None,"",None,None) - if not location.strip(): return (None,"Please enter a location.","","",None,"",None,None) - if not name.strip(): return (None,"Please enter your full name.","","",None,"",None,None) - if not cnic.strip(): return (None,"Please enter your CNIC number.","","",None,"",None,None) + if image is None: return (None,"Please upload an image.","","",None,"",None,None) + if not location.strip(): return (None,"Please enter a location.","","",None,"",None,None) + if not name.strip(): return (None,"Please enter your full name.","","",None,"",None,None) + if 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") @@ -611,493 +796,622 @@ def make_report(image, issue_type, city, location, name, cnic, phone, 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 BY AI\n\nReason: {gem['reason']}\nConfidence: {gem['confidence']}\n\nPlease upload a clear image showing the reported issue ({issue_type}).\nYour complaint has NOT been logged.", - "","",None,cid,None,None) - - if gem["status"]=="UNKNOWN" and "not set" in gem_raw: - gem["reason"]="Gemini key missing — accepted via YOLO."; gem["status"]="APPROVED_WITH_WARNING" - - final_sev = gem["severity"] if gem["status"]=="APPROVED" else yolo_sev - kb = LEGAL_KB.get(clean,{}); local = LOCALIZED.get(clean,{}).get(language,""); si = sev_icon(final_sev) - - advice = analyze_with_llama(issue_type, location, city, yolo_s, final_sev, language) - report = build_text_report(cid, ts, name, cnic, phone, city, location, issue_type, language, - final_sev, si, yolo_s, gem["status"], gem["reason"], - gem["confidence"], gem["action"], kb, description, local) + if gem["status"] == "REJECTED": + return (ann, + f"COMPLAINT REJECTED\n\nReason: {gem['reason']}\n" + f"Confidence: {gem['confidence']}\n\n" + f"Please upload a clear image showing the reported issue ({issue_type}).\n" + "This complaint has NOT been logged.", + "", "", None, cid, None, None) + + if gem["status"] == "UNKNOWN" and "not set" in gem_raw: + gem["reason"] = "Image verification skipped (API key missing)." + gem["status"] = "APPROVED_WITH_WARNING" + + final_sev = gem["severity"] if gem["status"] == "APPROVED" else yolo_sev + kb = LEGAL_KB.get(clean, {}) + local = LOCALIZED.get(clean, {}).get(language, "") + si = sev_icon(final_sev) + + advice = get_legal_advice(issue_type, location, city, yolo_s, final_sev, language) + report = build_text_report(cid, ts, name, cnic, phone, city, location, issue_type, + language, final_sev, si, yolo_s, gem["status"], + gem["reason"], gem["confidence"], gem["action"], + kb, description, local) 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}) - wa_text = f"Rahbar Complaint\nRef: {cid}\nIssue: {issue_type}\nLocation: {location}, {city}\nSeverity: {final_sev}/10\nAuthority: {kb.get('authority','N/A')}\nHelpline: {kb.get('hotline','N/A')}\nFiled: {ts}" - wa_md = f"[📲 Share on WhatsApp](https://wa.me/?text={urllib.parse.quote(wa_text[:1000])})" + wa_text = (f"Rahbar Complaint\nRef: {cid}\nIssue: {issue_type}\n" + f"Location: {location}, {city}\nSeverity: {final_sev}/10\n" + f"Authority: {kb.get('authority','N/A')}\n" + f"Helpline: {kb.get('hotline','N/A')}\nFiled: {ts}") + wa_md = f"[📲 Share on WhatsApp](https://wa.me/?text={urllib.parse.quote(wa_text[:1000])})" report_tts = None if enable_tts: - report_tts = make_tts(f"Complaint {cid} filed. Issue: {issue_type} at {location} in {city}. Severity: {final_sev} out of 10. {local}", language) + report_tts = make_tts( + f"Complaint {cid} has been filed. Issue: {issue_type} " + f"at {location} in {city}. Severity: {final_sev} out of 10. {local}", language) advice_tts = make_tts(advice[:600], language) - # Generate PDF - pdf_path = generate_pdf_report(cid, ts, name, cnic, phone, city, location, - issue_type, language, final_sev, yolo_s, - gem["status"], gem["reason"], gem["confidence"], - gem["action"], kb, description, local, report) + pdf_path = generate_pdf(cid, ts, name, cnic, phone, city, location, issue_type, + language, final_sev, yolo_s, gem["status"], gem["reason"], + gem["confidence"], gem["action"], kb, description, local) return ann, report, wa_md, advice, report_tts, cid, advice_tts, pdf_path + def update_areas(city): - areas = CITIES_AREAS.get(city,["Enter area manually"]) + areas = CITIES_AREAS.get(city, ["Enter area manually"]) return gr.Dropdown(choices=areas, value=areas[0]) -# ─── GPS REVERSE GEOCODE (Python / server-side) ─────────────── -def reverse_geocode_py(lat_str, lng_str): - """Python-side reverse geocoding — called by GPS button click""" - try: - lat = float(lat_str); lng = float(lng_str) - except: return "Invalid coordinates" - try: - url = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lng}&zoom=17&addressdetails=1" - req = urllib.request.Request(url, headers={"User-Agent":"Rahbar/6.0"}) - with urllib.request.urlopen(req, timeout=6) as resp: - data = json.loads(resp.read()) - a = data.get("address",{}); parts=[] - if a.get("road") or a.get("pedestrian"): parts.append(a.get("road") or a.get("pedestrian")) - if a.get("suburb") or a.get("neighbourhood"): parts.append(a.get("suburb") or a.get("neighbourhood")) - if a.get("city_district") or a.get("county"): parts.append(a.get("city_district") or a.get("county")) - if not parts and data.get("display_name"): parts=data["display_name"].split(",")[:3] - return ", ".join(p.strip() for p in parts if p.strip()) or f"GPS:{lat:.5f},{lng:.5f}" - except: return f"GPS:{lat:.5f},{lng:.5f}" - -# ─── CSS ────────────────────────────────────────────────────── + +# ─── CSS — light AND dark mode ──────────────────────────────── CSS = """ @import url('https://fonts.googleapis.com/css2?family=Noto+Nastaliq+Urdu:wght@400;700&family=Playfair+Display:wght@700;900&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap'); -:root{ - --g9:#0d2b1e;--g8:#14432e;--g7:#1a5c3f;--g6:#1f7a52;--g5:#25a06b;--g4:#2ec97f; - --a5:#f5a623;--a4:#d4870e; - --surface:#ffffff;--surface2:#f4f8f5;--surface3:#e8f3ed; - --text-primary:#0d2b1e;--text-secondary:#2d5a3e;--text-muted:#5a8a6e; - --border:#b8d9c5;--border-strong:#1f7a52; - --shadow:0 2px 8px rgba(13,43,30,.10);--radius:12px;--radius-lg:20px; + +/* ── Light mode tokens ── */ +:root { + --g9:#0d2b1e; --g8:#14432e; --g7:#1a5c3f; --g6:#1f7a52; --g5:#25a06b; --g4:#2ec97f; + --a5:#f5a623; --a4:#d4870e; + --surface:#ffffff; --surface2:#f4f8f5; --surface3:#e8f3ed; + --text-primary:#0d2b1e; --text-secondary:#2d5a3e; --text-muted:#5a8a6e; + --border:#b8d9c5; --border-strong:#1f7a52; + --shadow:0 2px 8px rgba(13,43,30,.10); --radius:12px; --radius-lg:20px; --header-bg:linear-gradient(135deg,#14432e 0%,#0d2b1e 60%,#0a1f14 100%); - --warn-bg:#fffbf0;--warn-border:rgba(245,166,35,.5); - --info-bg:#f0faf4;--info-border:#1f7a52; + --warn-bg:#fffbf0; --warn-border:rgba(245,166,35,.5); + --info-bg:#f0faf4; --info-border:#1f7a52; + --map-bg:#e8f5e9; --map-status-bg:#f4f8f5; --map-status-border:#b8d9c5; } + +/* ── Dark mode tokens ── */ @media(prefers-color-scheme:dark){ :root{ - --g9:#d0f0df;--g8:#1e6644;--g7:#28885a;--g6:#30aa72;--g5:#3dcf8a;--g4:#5de3a3; - --a5:#f7bc57;--a4:#f5a623; - --surface:#0f1f16;--surface2:#162b1e;--surface3:#1c3828; - --text-primary:#d0f0df;--text-secondary:#8fcfae;--text-muted:#5a9a78; - --border:#2a5c3e;--border-strong:#30aa72; + --g9:#d0f0df; --g8:#1e6644; --g7:#28885a; --g6:#30aa72; --g5:#3dcf8a; --g4:#5de3a3; + --a5:#f7bc57; --a4:#f5a623; + --surface:#0f1f16; --surface2:#162b1e; --surface3:#1c3828; + --text-primary:#d0f0df; --text-secondary:#8fcfae; --text-muted:#5a9a78; + --border:#2a5c3e; --border-strong:#30aa72; --shadow:0 2px 12px rgba(0,0,0,.4); --header-bg:linear-gradient(135deg,#0a1f14 0%,#071510 60%,#050f0a 100%); - --warn-bg:#1e1800;--warn-border:rgba(247,188,87,.4); - --info-bg:#0e1f16;--info-border:#30aa72; + --warn-bg:#1e1800; --warn-border:rgba(247,188,87,.4); + --info-bg:#0e1f16; --info-border:#30aa72; + --map-bg:#1a3a25; --map-status-bg:#162b1e; --map-status-border:#2a5c3e; } } + *,*::before,*::after{box-sizing:border-box} -body,.gradio-container{font-family:'DM Sans',sans-serif!important;background:var(--surface)!important;color:var(--text-primary)!important} -.rh-header{background:var(--header-bg);border-bottom:2px solid var(--g6);padding:24px 20px 20px;text-align:center;position:relative;overflow:hidden} -.rh-header::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 60% 60% at 50% 0%,rgba(37,160,107,.18),transparent);pointer-events:none} -.rh-header h1{font-family:'Playfair Display',serif!important;font-size:clamp(1.8rem,5vw,3rem)!important;font-weight:900!important;color:#fafdf8!important;margin:0 0 4px!important;line-height:1.1} -.rh-header .rh-sub{font-family:'Noto Nastaliq Urdu',serif;font-size:clamp(1rem,3vw,1.5rem);color:#f7bc57;direction:rtl;margin:4px 0 8px} -.rh-header .rh-tag{font-size:clamp(.75rem,2vw,.9rem);color:#5de3a3;letter-spacing:.08em;text-transform:uppercase} -.badge-strip{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;padding:10px 16px;background:var(--surface2);border-bottom:1px solid var(--border)} +body,.gradio-container{ + font-family:'DM Sans',sans-serif!important; + background:var(--surface)!important; + color:var(--text-primary)!important; +} + +/* Header */ +.rh-header{ + background:var(--header-bg);border-bottom:2px solid var(--g6); + padding:24px 20px 20px;text-align:center;position:relative;overflow:hidden; +} +.rh-header::before{ + content:'';position:absolute;inset:0; + background:radial-gradient(ellipse 60% 60% at 50% 0%,rgba(37,160,107,.18),transparent); + pointer-events:none; +} +.rh-header h1{ + font-family:'Playfair Display',serif!important; + font-size:clamp(1.8rem,5vw,3rem)!important;font-weight:900!important; + color:#fafdf8!important;margin:0 0 4px!important;line-height:1.1; +} +.rh-header .rh-sub{ + font-family:'Noto Nastaliq Urdu',serif;font-size:clamp(1rem,3vw,1.5rem); + color:#f7bc57;direction:rtl;margin:4px 0 8px; +} +.rh-header .rh-tag{ + font-size:clamp(.75rem,2vw,.9rem);color:#5de3a3;letter-spacing:.08em;text-transform:uppercase; +} + +/* Badge strip */ +.badge-strip{ + display:flex;flex-wrap:wrap;gap:8px;justify-content:center; + padding:10px 16px;background:var(--surface2);border-bottom:1px solid var(--border); +} .badge{font-size:.72rem;font-weight:600;letter-spacing:.06em;padding:4px 12px;border-radius:20px;text-transform:uppercase} -.badge-ai{background:var(--surface);color:var(--g4);border:1px solid var(--border-strong)} -.badge-pk{background:var(--surface);color:var(--a4);border:1px solid var(--warn-border)} +.badge-ai {background:var(--surface);color:var(--g4);border:1px solid var(--border-strong)} +.badge-pk {background:var(--surface);color:var(--a4);border:1px solid var(--warn-border)} .badge-live{background:var(--surface);color:#ff8080;border:1px solid rgba(232,83,83,.4)} + +/* Tabs */ .tab-nav{background:var(--surface2)!important;border-bottom:2px solid var(--border)!important} -.tab-nav button{font-family:'DM Sans',sans-serif!important;font-weight:500!important;font-size:.85rem!important;color:var(--text-muted)!important;padding:12px 18px!important;border-radius:0!important;transition:all .2s!important} -.tab-nav button.selected,.tab-nav button[aria-selected="true"]{color:var(--a4)!important;border-bottom:3px solid var(--a5)!important;background:transparent!important} -.card-title{font-size:.7rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--g4);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border)} +.tab-nav button{ + font-family:'DM Sans',sans-serif!important;font-weight:500!important;font-size:.85rem!important; + color:var(--text-muted)!important;padding:12px 18px!important;border-radius:0!important;transition:all .2s!important; +} +.tab-nav button.selected,.tab-nav button[aria-selected="true"]{ + color:var(--a4)!important;border-bottom:3px solid var(--a5)!important;background:transparent!important; +} + +/* Card title */ +.card-title{ + font-size:.7rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase; + color:var(--g4);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--border); +} + +/* Form elements */ label,.gradio-container .label-wrap span{color:var(--text-primary)!important} -.gradio-container input,.gradio-container textarea{background:var(--surface)!important;border:1px solid var(--border-strong)!important;border-radius:var(--radius)!important;color:var(--text-primary)!important;font-family:'DM Sans',sans-serif!important} -.gradio-container input:focus,.gradio-container textarea:focus{border-color:var(--a5)!important;outline:none!important;box-shadow:0 0 0 3px rgba(245,166,35,.15)!important} +.gradio-container input,.gradio-container textarea{ + background:var(--surface)!important;border:1px solid var(--border-strong)!important; + border-radius:var(--radius)!important;color:var(--text-primary)!important; + font-family:'DM Sans',sans-serif!important; +} +.gradio-container input:focus,.gradio-container textarea:focus{ + border-color:var(--a5)!important;outline:none!important; + box-shadow:0 0 0 3px rgba(245,166,35,.15)!important; +} .gradio-container .wrap{background:var(--surface)!important;border-color:var(--border-strong)!important} .gradio-container .block{background:var(--surface)!important} -.gradio-container button.primary{background:linear-gradient(135deg,var(--g6),var(--g5))!important;color:#fafdf8!important;border:none!important;border-radius:var(--radius)!important;font-weight:600!important;font-size:.9rem!important;padding:12px 24px!important;cursor:pointer!important;box-shadow:var(--shadow)!important;transition:all .2s!important} -.gradio-container button.primary:hover{background:linear-gradient(135deg,var(--g5),var(--g4))!important;transform:translateY(-1px)!important} -.gradio-container button.secondary{background:var(--surface)!important;border:1px solid var(--border-strong)!important;color:var(--g4)!important} -.gradio-container [data-testid="image"]{border:2px dashed var(--border-strong)!important;border-radius:var(--radius-lg)!important;background:var(--surface2)!important} + +/* Buttons */ +.gradio-container button.primary{ + background:linear-gradient(135deg,var(--g6),var(--g5))!important;color:#fafdf8!important; + border:none!important;border-radius:var(--radius)!important;font-weight:600!important; + font-size:.9rem!important;padding:12px 24px!important;cursor:pointer!important; + box-shadow:var(--shadow)!important;transition:all .2s!important; +} +.gradio-container button.primary:hover{ + background:linear-gradient(135deg,var(--g5),var(--g4))!important;transform:translateY(-1px)!important; +} +.gradio-container button.secondary{ + background:var(--surface)!important;border:1px solid var(--border-strong)!important;color:var(--g4)!important; +} + +/* Image upload */ +.gradio-container [data-testid="image"]{ + border:2px dashed var(--border-strong)!important;border-radius:var(--radius-lg)!important; + background:var(--surface2)!important; +} + +/* Markdown headings */ .gradio-container .prose h2,.gradio-container .prose h3{color:var(--a4)!important} .gradio-container audio{width:100%!important;border-radius:var(--radius)!important} -.info-box{background:var(--info-bg);border:1px solid var(--info-border);border-left:4px solid var(--g5);border-radius:var(--radius);padding:12px 16px;font-size:.88rem;line-height:1.6;margin-bottom:8px;color:var(--text-secondary)} -.warn-box{background:var(--warn-bg);border:1px solid var(--warn-border);border-left:4px solid var(--a5);border-radius:var(--radius);padding:12px 16px;font-size:.88rem;margin-bottom:8px;color:var(--text-secondary)} -.hotline-pill{display:inline-block;background:var(--surface2);color:var(--a4);border:1px solid var(--warn-border);border-radius:20px;padding:2px 12px;font-size:.8rem;font-weight:600} -.gradio-container textarea{font-family:'DM Mono','Courier New',monospace!important;font-size:.82rem!important;line-height:1.7!important} -.gradio-container select,.gradio-container [data-testid="dropdown"]{background:var(--surface)!important;color:var(--text-primary)!important;border-color:var(--border-strong)!important} + +/* Info / warn boxes */ +.info-box{ + background:var(--info-bg);border:1px solid var(--info-border);border-left:4px solid var(--g5); + border-radius:var(--radius);padding:12px 16px;font-size:.88rem;line-height:1.6; + margin-bottom:8px;color:var(--text-secondary); +} +.warn-box{ + background:var(--warn-bg);border:1px solid var(--warn-border);border-left:4px solid var(--a5); + border-radius:var(--radius);padding:12px 16px;font-size:.88rem; + margin-bottom:8px;color:var(--text-secondary); +} +.hotline-pill{ + display:inline-block;background:var(--surface2);color:var(--a4); + border:1px solid var(--warn-border);border-radius:20px;padding:2px 12px; + font-size:.8rem;font-weight:600; +} + +/* Report textarea */ +.gradio-container textarea{ + font-family:'DM Mono','Courier New',monospace!important; + font-size:.82rem!important;line-height:1.7!important; +} + +/* Dropdowns */ +.gradio-container select,.gradio-container [data-testid="dropdown"]{ + background:var(--surface)!important;color:var(--text-primary)!important;border-color:var(--border-strong)!important; +} + +/* Chatbot messages */ .gradio-container .message.user{background:var(--surface3)!important;color:var(--text-primary)!important} -.gradio-container .message.bot{background:var(--surface2)!important;color:var(--text-primary)!important} +.gradio-container .message.bot {background:var(--surface2)!important;color:var(--text-primary)!important} + +/* Scrollbar */ ::-webkit-scrollbar{width:6px;height:6px} ::-webkit-scrollbar-track{background:var(--surface2)} ::-webkit-scrollbar-thumb{background:var(--g6);border-radius:3px} -@media(max-width:640px){.rh-header{padding:16px 12px}.tab-nav button{padding:10px 12px!important;font-size:.78rem!important}} + +/* GPS map elements */ +#rb-gps-status{ + font-size:.82rem;min-height:26px;margin-bottom:8px;font-weight:500; + padding:7px 12px;border-radius:8px; + background:var(--map-status-bg);border:1px solid var(--map-status-border); + color:#25a06b; +} +#rb-map-container{ + height:240px;width:100%;border-radius:12px; + border:2px solid var(--border-strong);margin-bottom:6px; + background:var(--map-bg);position:relative;overflow:hidden; +} + +@media(max-width:640px){ + .rh-header{padding:16px 12px} + .tab-nav button{padding:10px 12px!important;font-size:.78rem!important} +} """ HEADER_HTML = """
- Click map or drag pin to change location · GPS button auto-fills address +
+ Click the map or drag the pin to fine-tune · GPS button fills your address automatically
""" + # ─── BUILD UI ───────────────────────────────────────────────── def build_ui(): - # gr.Blocks with NO css/theme (Gradio 6: these go in launch()) with gr.Blocks(title="Rahbar | رہبر") as demo: gr.HTML(HEADER_HTML) with gr.Tabs(): - # ══ TAB 1: Report Issue ═══════════════════════════ - with gr.Tab("📸 Report Issue"): + # ══════════════════════════════════════════════ + # TAB 1 — FILE A COMPLAINT + # ══════════════════════════════════════════════ + with gr.Tab("📸 File a Complaint"): with gr.Row(equal_height=False): - # ── Left column ─────────────────────────── + # ── Left: inputs ── with gr.Column(scale=1, min_width=300): - gr.HTML('