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
-
-
-
- 📍 Get My Current Location (GPS)
-
-
+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
+
+
+
+ Image Verification
+ Object Detection
+ Legal Assistant
+ Knowledge Base
+ 4 Languages
+ LIVE
+
+
🌙 Dark Mode
+
"""
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