CivicLens / app.py
Noshal's picture
Update app.py
a25f2c9 verified
"""
Rahbar v9.2 β€” Pakistan AI Civic Complaint Platform
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIXES vs v9.0:
βœ… MAP FIXED β€” Plotly Scattermapbox + gr.Plot() (Leaflet scripts were blocked)
βœ… SEND VOICE β€” status label, null-audio guard, clear feedback messages
βœ… TTS FIXED β€” gTTS (works online; pyttsx3 removed β€” was causing silent audio)
βœ… STT FIXED β€” proper error messages, Groq Whisper -> Google SR fallback
βœ… City change -> map re-centres instantly via Plotly figure update
"""
import os, io, re, uuid, base64, datetime, urllib.parse
from PIL import Image
import gradio as gr
import plotly.graph_objects as go
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import inch
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.enums import TA_CENTER
from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer,
Table, TableStyle, Image as RLImage)
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "")
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
complaint_log = []
# ══════════════════════════════════════════════════════════════
# PAKISTAN β€” Full Geographic Data
# ══════════════════════════════════════════════════════════════
PAKISTAN_GEO = {
"Punjab": {
"Lahore": ["Model Town","DHA","Gulberg","Johar Town","Bahria Town","Township","Cantonment","Iqbal Town","Garden Town","Allama Iqbal Town","Shadman","Samanabad","Ravi Road","Walled City","Shalimar","Wagah Border","Manga Mandi"],
"Faisalabad": ["Jinnah Colony","Madina Town","Peoples Colony","Ghulam Muhammad Abad","Susan Road","Satiana Road","D-Ground","Canal Road","Sargodha Road"],
"Rawalpindi": ["Saddar","Bahria Town","Chaklala","Satellite Town","Murree Road","Westridge","Dhoke Hassu","Adiala Road","Gunjmandi"],
"Gujranwala": ["Satellite Town","Rahwali","Trust Colony","Gondlanwala Road","Sialkot Road","Peoples Colony"],
"Multan": ["Shah Rukn-e-Alam","Cantt","Gulgasht Colony","New Multan","Bosan Road","Vehari Road","Khanewal Road","Chungi No. 9"],
"Sialkot": ["Cantt","Saidpur Road","Pasrur","Paris Road","Daska","Sambrial","Wazirabad Road"],
"Bahawalpur": ["Model Town","Satellite Town","Cantt","Farid Gate","Circular Road","Baghdad ul Jadeed"],
"Sargodha": ["University Road","Satellite Town","Sillanwali","Bhalwal","Kot Momin","Shahpur"],
"Sheikhupura": ["Farooqabad","Muridke","Safdarabad","Ferozewala","Nankana Road"],
"Jhang": ["Chiniot Road","Shorkot","Ahmed Pur Sial","Chiniot"],
"Rahim Yar Khan":["Khanpur","Sadiqabad","Liaquatpur","Ahmadpur East"],
"Kasur": ["Chunian","Pattoki","Kot Radha Kishan","Phool Nagar"],
"Okara": ["Renala Khurd","Depalpur","Haveli Lakha","Dipalpur"],
"Sahiwal": ["Chichawatni","Pakpattan","Arifwala"],
"Gujrat": ["Kharian","Lalamusa","Gujrat City","Kunjah"],
"Attock": ["Hazro","Fateh Jang","Jand","Pindi Gheb","Hassan Abdal"],
"Jhelum": ["Pind Dadan Khan","Sohawa","Dina","Mangla"],
},
"Sindh": {
"Karachi": ["Clifton","DHA","Gulshan-e-Iqbal","PECHS","Korangi","Saddar","North Nazimabad","Malir","Lyari","Orangi Town","Baldia","Keamari","Landhi","Shah Faisal Colony","Federal B Area","Gulistan-e-Jauhar","Nazimabad","New Karachi","Liaquatabad"],
"Hyderabad": ["Latifabad","Qasimabad","Hirabad","City","Kotri","Hatri","Tando Jam"],
"Sukkur": ["Rohri","Shikarpur","Pano Aqil","New Sukkur","Airport Area"],
"Larkana": ["Dokri","Ratodero","Kambar","Naudero"],
"Nawabshah": ["Sakrand","Sanghar","Shahdadpur","Nawabshah City"],
"Mirpurkhas": ["Umerkot","Digri","Tando Allahyar","Mithi Road"],
},
"KPK": {
"Peshawar": ["Hayatabad","University Town","Cantt","Saddar","Gulbahar","Dabgari Gardens","Kohat Road","Warsak Road","Charsadda Road","Peshawar City"],
"Mardan": ["Swabi Road","Shergarh","Katlang","Rustam","Takht Bhai"],
"Abbottabad": ["Mandian","Nawan Shehr","Havelian","Haripur","Kakul"],
"Swat": ["Mingora","Saidu Sharif","Kanju","Matta","Charbagh","Bahrain","Madyan","Kalam"],
"Kohat": ["Lachi","Darra Adam Khel","Hangu","Kohat City"],
"Bannu": ["Lakki Marwat","Domel","Bannu City"],
"Dera Ismail Khan":["Kulachi","Darazminda","Tank","D.I. Khan City"],
"Chitral": ["Booni","Drosh","Mastuj","Chitral City","Garam Chashma"],
},
"Balochistan": {
"Quetta": ["Satellite Town","Jinnah Town","Cantt","Sariab Road","Brewery Road","Airport Road","Zarghoon Road","Sirki Road"],
"Gwadar": ["Pasni","Ormara","Jiwani","Gwadar Port Area","Pishukan"],
"Turbat": ["Tump","Mand","Turbat City"],
"Khuzdar": ["Wadh","Ornach","Khuzdar City"],
"Chaman": ["Spin Boldak area","Kuchlak","Chaman City"],
},
"Islamabad": {
"Islamabad": ["F-6","F-7","F-8","F-10","F-11","G-6","G-7","G-8","G-9","G-10","G-11","G-13","I-8","I-9","I-10","I-11","E-7","E-11","D-12","Blue Area","Bahria Town","DHA Phase 1","DHA Phase 2","Bani Gala","Margalla Hills","Saidpur Village"],
},
"AJK": {
"Muzaffarabad": ["Chattar","Hattian Bala","Neelum Road","Patika","Leepa"],
"Mirpur": ["New Mirpur","Chakswari","Dadyal","Jatlan"],
"Rawalakot": ["Bagh","Sudhanoti","Hajira"],
"Kotli": ["Sehnsa","Charhoi","Kotli City"],
"Neelum Valley": ["Kel","Sharda","Athmuqam","Kutton"],
},
"Gilgit-Baltistan": {
"Gilgit": ["Jutial","Konodas","Nomal","Naltar","Danyore","Aliabad"],
"Skardu": ["Shigar","Khaplu","Rondu","Skardu City","Satpara"],
"Hunza": ["Karimabad","Altit","Baltit","Ganesh","Aliabad"],
"Ghizer": ["Gahkuch","Phander","Yasin","Ishkoman"],
"Diamer": ["Chilas","Darel","Tangir"],
},
}
PROVINCES = sorted(PAKISTAN_GEO.keys())
def get_cities(province):
return sorted(PAKISTAN_GEO.get(province, {}).keys())
def get_areas(province, city):
return PAKISTAN_GEO.get(province, {}).get(city, ["City Centre", "Main Bazaar", "Old City"])
# City centre coordinates
CITY_COORDS = {
# Punjab
"Lahore":(31.5204,74.3587),"Faisalabad":(31.4181,73.0776),"Rawalpindi":(33.5651,73.0169),
"Gujranwala":(32.1617,74.1883),"Multan":(30.1575,71.5249),"Sialkot":(32.4945,74.5229),
"Bahawalpur":(29.3956,71.6836),"Sargodha":(32.0836,72.6711),"Sheikhupura":(31.7131,73.9856),
"Jhang":(31.2782,72.3180),"Rahim Yar Khan":(28.4202,70.2952),"Kasur":(31.1167,74.4500),
"Okara":(30.8138,73.4455),"Sahiwal":(30.6706,73.1064),"Gujrat":(32.5736,74.0779),
"Attock":(33.7667,72.3500),"Jhelum":(32.9333,73.7333),
# Sindh
"Karachi":(24.8607,67.0011),"Hyderabad":(25.3960,68.3578),"Sukkur":(27.7052,68.8574),
"Larkana":(27.5570,68.2251),"Nawabshah":(26.2442,68.4100),"Mirpurkhas":(25.5259,69.0144),
# KPK
"Peshawar":(34.0151,71.5249),"Mardan":(34.1985,72.0404),"Abbottabad":(34.1463,73.2117),
"Swat":(35.2227,72.4258),"Kohat":(33.5869,71.4414),"Bannu":(32.9856,70.6044),
"Dera Ismail Khan":(31.8334,70.9020),"Chitral":(35.8514,71.7908),
# Balochistan
"Quetta":(30.1798,66.9750),"Gwadar":(25.1264,62.3225),"Turbat":(26.0022,63.0440),
"Khuzdar":(27.8119,66.6170),"Chaman":(30.9197,66.4503),
# Islamabad
"Islamabad":(33.6844,73.0479),
# AJK
"Muzaffarabad":(34.3600,73.4700),"Mirpur":(33.1500,73.7500),"Rawalakot":(33.8575,73.7619),
"Kotli":(33.5167,73.9000),"Neelum Valley":(34.5500,74.0167),
# Gilgit-Baltistan
"Gilgit":(35.9208,74.3142),"Skardu":(35.2973,75.6333),"Hunza":(36.3167,74.6500),
"Ghizer":(36.2167,73.6833),"Diamer":(35.4167,74.6500),
}
# ══════════════════════════════════════════════════════════════
# βœ… FIX 1: PLOTLY MAP (replaces broken Leaflet gr.HTML)
# Uses go.Scattermapbox + gr.Plot() β€” renders 100% in Gradio
# ══════════════════════════════════════════════════════════════
def make_plotly_map(city="Islamabad", highlight_area=None, location_text=""):
"""Return a Plotly figure centred on the selected city."""
clat, clon = CITY_COORDS.get(city, (30.3753, 69.3451))
# All Pakistan city markers (background dots)
all_lats = [v[0] for v in CITY_COORDS.values()]
all_lons = [v[1] for v in CITY_COORDS.values()]
all_names = list(CITY_COORDS.keys())
fig = go.Figure()
# Background: all cities (small green dots)
fig.add_trace(go.Scattermapbox(
lat=all_lats,
lon=all_lons,
mode="markers",
marker=dict(size=6, color="#25a06b", opacity=0.55),
text=all_names,
hovertemplate="<b>%{text}</b><extra></extra>",
name="Pakistan Cities",
))
# Foreground: selected city (big red pin)
label = location_text.strip() or highlight_area or city
fig.add_trace(go.Scattermapbox(
lat=[clat],
lon=[clon],
mode="markers+text",
marker=dict(size=18, color="#e74c3c"),
text=[label],
textposition="top right",
textfont=dict(size=11, color="#e74c3c"),
hovertemplate=f"<b>{label}</b><br>πŸ“ {city}<extra></extra>",
name="Complaint Location",
))
fig.update_layout(
mapbox_style="open-street-map",
mapbox=dict(
center=go.layout.mapbox.Center(lat=clat, lon=clon),
zoom=12,
),
hovermode="closest",
margin=dict(l=0, r=0, t=0, b=0),
height=360,
legend=dict(
bgcolor="rgba(13,43,30,0.7)",
font=dict(color="#d5f0e0", size=10),
x=0, y=1,
),
paper_bgcolor="#0c1a10",
plot_bgcolor="#0c1a10",
)
return fig
def on_province_change(province):
cities = get_cities(province)
city = cities[0] if cities else "Islamabad"
areas = get_areas(province, city)
return (
gr.Dropdown(choices=cities, value=city),
gr.Dropdown(choices=areas, value=areas[0]),
make_plotly_map(city=city),
)
def on_city_change(province, city):
areas = get_areas(province, city)
return (
gr.Dropdown(choices=areas, value=areas[0]),
make_plotly_map(city=city),
)
def on_location_change(province, city, area, loc_text):
return make_plotly_map(city=city, highlight_area=area, location_text=loc_text)
# ══════════════════════════════════════════════════════════════
# TTS β€” gTTS (online, works on HuggingFace / any server)
# ══════════════════════════════════════════════════════════════
# gTTS supported codes only β€” Sindhi not supported by gTTS, mapped to Urdu
# Punjabi (pa) is supported. Full list: gtts.lang.tts_langs()
LANG_CODES = {"English": "en", "Urdu": "ur", "Punjabi": "pa", "Sindhi": "ur"}
def make_tts(text, language="English"):
"""Convert text to speech using gTTS. Falls back to English on error."""
if not text or not str(text).strip():
return None
text = str(text).strip()[:600]
lang_code = LANG_CODES.get(language, "en")
path = f"/tmp/rahbar_tts_{uuid.uuid4().hex[:8]}.mp3"
try:
from gtts import gTTS
tts = gTTS(text=text, lang=lang_code, slow=False)
tts.save(path)
if os.path.exists(path) and os.path.getsize(path) > 200:
return path
except Exception as e:
print(f"gTTS error ({language}): {e} β€” retrying with English")
try:
from gtts import gTTS
tts = gTTS(text=text, lang="en", slow=False)
fb_path = f"/tmp/rahbar_tts_en_{uuid.uuid4().hex[:8]}.mp3"
tts.save(fb_path)
if os.path.exists(fb_path) and os.path.getsize(fb_path) > 200:
return fb_path
except Exception as e2:
print(f"gTTS fallback error: {e2}")
return None
# ══════════════════════════════════════════════════════════════
# 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.",
"laws":["Punjab Waste Management Act 2014","Pakistan EPA 1997 Section 11"],
"hotline":"1139","authority":"Solid Waste Management Board","response_time":"48 hours","fine":"Rs. 500 – 50,000"},
{"id":"garbage_002","category":"Garbage","title":"Urban Solid Waste β€” City-level Responsibility",
"content":"Failure to collect garbage is a serious violation. EPA 1997 Section 11 prohibits pollution. Over 1 week = Public Nuisance PPC Section 268. LWMC: 042-111-222-888.",
"laws":["PPC Section 268","Punjab Waste Management Act 2014","EPA 1997 Section 11"],
"hotline":"1139","authority":"LWMC Lahore / KMC Karachi","response_time":"48 hours","fine":"Rs. 500 – 50,000"},
{"id":"garbage_escalation","category":"Garbage","title":"Garbage Complaint Escalation Ladder",
"content":"If authority fails: 1.Contact Union Council 2.Apply at DC office 3.CM Cell 0800-02345 4.citizenportal.gov.pk 5.Federal Ombudsman 051-9204551 6.High Court Writ.",
"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.",
"laws":["National Highways Safety Ordinance 2000","Punjab LGA 2022 Section 54"],
"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 6.High Court Writ.",
"laws":["Tort Law Negligence","NHA Safety Ordinance 2000","Constitution Article 9"],
"hotline":"051-9204551","authority":"Federal Ombudsman / High Court","response_time":"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. Supreme Court 2018: clean water is fundamental right.",
"laws":["Punjab Water Act 2019 Section 23","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.",
"laws":["Punjab Water Act 2019","Constitution Article 9"],
"hotline":"0800-02345","authority":"CM Complaints Cell / PWA","response_time":"Escalation pathway","fine":"Rs. 10,000 – 5,00,000"},
{"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.",
"laws":["Constitution Article 9","Constitution Article 14","Constitution Article 19A"],
"hotline":"0800-02345","authority":"High Court / Supreme Court","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 4.If no action in 48-72h use CM Portal 5.citizenportal.gov.pk most effective.",
"laws":["Right to Information Act 2017","Constitution Article 9"],
"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 hears complaints against government institutions. Free to file. Decision within 60 days. Phone: 051-9204551 | mohtasib.gov.pk.",
"laws":["Federal Ombudsmen Institutional Reforms Act 2013"],
"hotline":"051-9204551","authority":"Federal Ombudsman (Mohtasib)","response_time":"60 days","fine":"Binding recommendations"},
]
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',[]))} {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
def retrieve(self,query,top_k=3):
if not self._initialized:
if not self.initialize(): return self._keyword_fallback(query,top_k)
try:
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
q_vec=self.vectorizer.transform([query])
scores=cosine_similarity(q_vec,self.doc_matrix)[0]
top_idx=np.argsort(scores)[::-1][:top_k]
results=[{**self.documents[i],'relevance_score':float(scores[i])} for i in top_idx if scores[i]>0.01]
return results if results else self._keyword_fallback(query,top_k)
except: return self._keyword_fallback(query,top_k)
def _keyword_fallback(self,query,top_k=3):
q=query.lower()
kws={"Garbage":["garbage","waste","sanitation","trash"],"Pot Hole":["pothole","road","nha"],"Pipe Leakage":["water","wasa","pipe","leakage"]}
found_cat=next((c for c,k in kws.items() if any(w in q for w in k)),None)
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']}\nContent: {doc['content'][:400]}\nLaws: {', '.join(doc['laws'][:2])}\nHelpline: {doc['hotline']} | Response: {doc['response_time']}\n\n"
return ctx
rag_engine = RAGEngine()
rag_engine.initialize()
# ══════════════════════════════════════════════════════════════
# LEGAL KB
# ══════════════════════════════════════════════════════════════
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 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 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",
},
"Pipe Leakage": {
"laws":["Punjab Water Act 2019 (Section 23 – Supply Obligation)","WASA Act – Water & Sanitation Agency Bylaws","Pakistan Environmental Protection Act 1997 (Section 13)","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",
},
}
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:
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'cls_{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
# ══════════════════════════════════════════════════════════════
# 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-2.0-flash")
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}"
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
# ══════════════════════════════════════════════════════════════
# LEGAL ADVICE (LLaMA / Groq)
# ══════════════════════════════════════════════════════════════
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.","Sindhi":"Respond in Sindhi script."}
lang_instruction=lang_map.get(language,"Respond in clear professional English.")
if not GROQ_API_KEY:
rights="\n".join(f" β€’ {r}" for r in kb.get("citizen_rights",[]))
return ("Applicable Laws:\n"+"\n".join(f" β€’ {l}" for l in kb.get("laws",[]))+
f"\n\nCitizen Rights:\n{rights}\n\nFine: {kb.get('fine','N/A')}\nHelpline: {kb.get('hotline','N/A')}\nResponse Time: {kb.get('response','N/A')}\nEscalation: {kb.get('escalation','N/A')}\n\n(Set GROQ_API_KEY for AI legal advice)")
try:
from groq import Groq
client=Groq(api_key=GROQ_API_KEY)
prompt=(f"You are a Pakistani civic law expert.\n{lang_instruction}\n"
f"Complaint: {issue} in {location}, {city} | Severity: {severity}/10\n"
f"Laws: {', '.join(kb.get('laws',[]))}\nResponse Time: {kb.get('response','72 hours')}\n\n"
f"Provide: 1.Legal rights 2.Steps to file complaint 3.If authority ignores 4.Compensation options 5.Helplines\nBe concise and practical.")
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}"
# ══════════════════════════════════════════════════════════════
# βœ… FIX 2: CHATBOT + VOICE SEND β€” robust null-audio guard + status
# ══════════════════════════════════════════════════════════════
def legal_chatbot_rag(user_message, history, language):
if history is None: history = []
if not user_message or not str(user_message).strip():
return history, ""
retrieved_docs = rag_engine.retrieve(user_message, top_k=3)
rag_context = rag_engine.format_context(retrieved_docs)
lang_map = {"Urdu":"Respond entirely in Urdu script.","Punjabi":"Respond in Punjabi.","Sindhi":"Respond in Sindhi script."}
lang_instruction = lang_map.get(language, "Respond in clear professional English.")
system_content = (f"You are Rahbar Legal Assistant β€” civic rights advisor for Pakistani citizens.\n{lang_instruction}\n"
f"Only discuss: water, garbage, roads, Pakistani civic law. Cite laws and helplines. Max 250 words.\n\nKnowledge Base:\n{rag_context}")
if not GROQ_API_KEY:
if retrieved_docs:
doc = retrieved_docs[0]
answer = (f"**{doc['title']}**\n\n{doc['content'][:500]}\n\nHelpline: {doc['hotline']} | Response: {doc['response_time']}\nLaws: {', '.join(doc['laws'][:2])}\n\n_(Set GROQ_API_KEY for AI responses)_")
else:
answer = "I can help with water, garbage, and road issues in Pakistan. Please ask a specific question."
return history + [{"role":"user","content":user_message}, {"role":"assistant","content":answer}], ""
try:
from groq import Groq
client = Groq(api_key=GROQ_API_KEY)
api_messages = [{"role":"system","content":system_content}]
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"Error: {e}"
return history + [{"role":"user","content":user_message}, {"role":"assistant","content":answer}], ""
def chatbot_tts_output(history, language):
if not history: return None
for msg in reversed(history):
if msg.get("role") == "assistant":
text = re.sub(r'_Sources:.*?_', '', extract_text(msg["content"]), flags=re.DOTALL).strip()
return make_tts(text[:600], language)
return None
def stt(audio_file):
"""Speech-to-text: Groq Whisper -> Google SR fallback."""
if audio_file is None:
return "No audio received. Please record or upload audio first."
def ensure_wav(path):
if path.lower().endswith(".wav"):
return path
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
client = Groq(api_key=GROQ_API_KEY)
wav_path = ensure_wav(audio_file)
with open(wav_path, "rb") as f:
result = client.audio.transcriptions.create(
model="whisper-large-v3", file=f, response_format="text"
)
text = result if isinstance(result, str) else result.text
return text.strip() or "No speech detected in audio."
except Exception as e:
groq_err = str(e)
else:
groq_err = "GROQ_API_KEY not configured"
try:
import speech_recognition as sr
wav_path = ensure_wav(audio_file)
recognizer = sr.Recognizer()
with sr.AudioFile(wav_path) as src:
recognizer.adjust_for_ambient_noise(src, duration=0.3)
audio_data = recognizer.record(src)
return recognizer.recognize_google(audio_data)
except Exception as e2:
return f"Transcription failed. Groq: {groq_err}. Fallback: {e2}"
def voice_then_send(audio_file, history, language):
"""
βœ… FIXED Send Voice:
1. Guard against None audio (user clicked without recording)
2. Transcribe via STT
3. Feed to chatbot
4. Return (updated_history, status_message)
"""
if history is None:
history = []
# Guard: no audio recorded / uploaded
if audio_file is None or audio_file == "":
return history, "⚠️ No audio recorded β€” please press the mic button and speak first."
# Transcribe
transcribed = stt(audio_file)
if not transcribed or not transcribed.strip():
return history, "⚠️ Could not hear speech. Please try again in a quiet place."
if transcribed.startswith("Transcription failed"):
return history, f"⚠️ {transcribed}"
# Send to chatbot
new_history, _ = legal_chatbot_rag(transcribed, history, language)
return new_history, f"🎀 Sent: \"{transcribed[:80]}{'...' if len(transcribed)>80 else ''}\""
# ══════════════════════════════════════════════════════════════
# 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:
counts[c.get("issue","")] = counts.get(c.get("issue",""), 0) + 1
cities[c.get("city","Unknown")] = cities.get(c.get("city","Unknown"), 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| Metric | Value |\n|--------|-------|\n"
f"| Total Complaints | **{total}** |\n| Average Severity | **{avg_sev:.1f}/10** |\n"
f"| Most Active City | **{top_city}** |\n\n"
f"### By Issue\n| Issue | Count |\n|-------|-------|\n"
f"| Garbage | {counts['Garbage']} |\n| Pot Hole | {counts['Pot Hole']} |\n| Pipe Leakage | {counts['Pipe Leakage']} |\n\n### 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 make_whatsapp_link(text):
return f"https://wa.me/?text={urllib.parse.quote(text[:1000])}"
# ══════════════════════════════════════════════════════════════
# PDF REPORT
# ══════════════════════════════════════════════════════════════
def generate_pdf_report(complaint_id, timestamp, name, cnic, phone, province, city, location,
issue_type, language, severity, gemini_status, gemini_reason,
gemini_confidence, kb, description, llama_advice, image_pil=None):
try:
pdf_path = f"/tmp/rahbar_{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 = colors.HexColor("#1a5c3f"); C_MID = colors.HexColor("#25a06b")
C_LIGHT = colors.HexColor("#eaf5ef"); C_GOLD = colors.HexColor("#c8860a")
C_GOLD_L= colors.HexColor("#fef9ee"); C_TEXT = colors.HexColor("#0d2b1e")
C_MUTED = colors.HexColor("#5a8a6e"); C_WHITE = colors.white
SEV_C = {"LOW":colors.HexColor("#27ae60"),"MEDIUM":colors.HexColor("#f39c12"),
"HIGH":colors.HexColor("#e67e22"),"CRITICAL":colors.HexColor("#c0392b")}
def PS(n, **kw): return ParagraphStyle(n, **kw)
sH = PS("h", fontName="Helvetica-Bold", fontSize=18, textColor=C_WHITE, alignment=TA_CENTER, leading=24)
sSub = PS("s", fontName="Helvetica", fontSize=10, textColor=colors.HexColor("#b8e8cc"), alignment=TA_CENTER, leading=14)
sRef = PS("r", fontName="Helvetica", fontSize=8, textColor=colors.HexColor("#a8d8c0"), alignment=TA_CENTER)
sSec = PS("sc", fontName="Helvetica-Bold", fontSize=10, textColor=C_WHITE, leading=14)
sSev = PS("sv", fontName="Helvetica-Bold", fontSize=11, textColor=C_WHITE, alignment=TA_CENTER, leading=16)
sLbl = PS("lb", fontName="Helvetica-Bold", fontSize=8.5,textColor=C_MUTED, leading=12)
sVal = PS("vl", fontName="Helvetica", fontSize=9.5,textColor=C_TEXT, leading=14)
sBod = PS("bd", fontName="Helvetica", fontSize=9, textColor=C_TEXT, leading=13, spaceAfter=3)
sBodI= PS("bi", fontName="Helvetica-Oblique",fontSize=9, textColor=colors.HexColor("#2d5a3e"), leading=13)
sBul = PS("bl", fontName="Helvetica", fontSize=9, textColor=C_TEXT, leading=13, leftIndent=12)
sGD = PS("gd", fontName="Helvetica-Bold", fontSize=10, textColor=C_WHITE, alignment=TA_CENTER, leading=15)
sFt = PS("ft", fontName="Helvetica", fontSize=7.5,textColor=C_WHITE, alignment=TA_CENTER, leading=11)
sDcl = PS("dc", fontName="Helvetica", fontSize=9, textColor=C_TEXT, leading=13)
W = 7.0 * inch
def sec_hdr(ltr, title):
t = Table([[Paragraph(f" {ltr}. {title.upper()}", sSec)]], colWidths=[W])
t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_DARK),("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, sLbl), Paragraph(str(val), sVal)])
if len(row) == 4 or i == len(pairs)-1:
while len(row) < 4: row.extend([Paragraph("", sLbl), Paragraph("", sVal)])
rows.append(row); row = []
t = Table(rows, colWidths=[2.0*inch,1.5*inch,2.0*inch,1.5*inch])
t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LIGHT),("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),("LEFTPADDING",(0,0),(-1,-1),6),("VALIGN",(0,0),(-1,-1),"TOP"),("ROWBACKGROUNDS",(0,0),(-1,-1),[C_LIGHT,C_WHITE])]))
return t
def card(paras, bg=None):
bg = bg or C_LIGHT
t = Table([[p] for p in paras], colWidths=[W])
t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),bg),("TOPPADDING",(0,0),(-1,-1),6),("BOTTOMPADDING",(0,0),(-1,-1),6),("LEFTPADDING",(0,0),(-1,-1),12),("RIGHTPADDING",(0,0),(-1,-1),10)]))
return t
def sp(h=0.15): return Spacer(1, h*inch)
story = []; date_str = datetime.datetime.now().strftime("%d %B %Y")
time_str = datetime.datetime.now().strftime("%I:%M %p"); sev_lbl = severity_label(severity)
h_t = Table([[Paragraph("GOVERNMENT OF PAKISTAN", sH)],[Paragraph("CIVIC COMPLAINT REPORT", sH)],
[Paragraph("Rahbar Digital Civic Redressal System", sSub)],
[Paragraph(f"Reference: {complaint_id} | {date_str} at {time_str} | Language: {language}", sRef)]], colWidths=[W])
h_t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_DARK),("TOPPADDING",(0,0),(-1,-1),10),("BOTTOMPADDING",(0,0),(-1,-1),10)]))
story += [h_t, sp(0.12)]
sev_t = Table([[Paragraph(f"SEVERITY: {severity}/10 β€” {sev_lbl}", sSev)]], colWidths=[W])
sev_t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),SEV_C.get(sev_lbl, C_MID)),("TOPPADDING",(0,0),(-1,-1),8),("BOTTOMPADDING",(0,0),(-1,-1),8)]))
story += [sev_t, sp(0.18)]
story += [sec_hdr("A","Complainant Information"), sp(0.08)]
story += [info_grid([("Full Name",name),("CNIC",cnic),("Phone",phone or "N/A"),("Province",province),("City",city),("Location",location[:60])]), sp(0.15)]
story += [sec_hdr("B","Complaint Details"), sp(0.08)]
story += [info_grid([("Issue Type",issue_type),("Date Filed",date_str),("Time Filed",time_str),("Severity",f"{severity}/10 [{sev_lbl}]")])]
if description.strip():
story += [sp(0.06), card([Paragraph(f"<b>Description:</b> {description.strip()}", sBodI)])]
if image_pil is not None:
try:
img_buf = io.BytesIO()
img_copy = image_pil.copy(); img_copy.thumbnail((600,500), Image.LANCZOS)
img_copy.save(img_buf, format="JPEG", quality=85); img_buf.seek(0)
rl_img = RLImage(img_buf, width=min(3.5*inch, img_copy.width*inch/96), height=min(2.8*inch, img_copy.height*inch/96))
img_table = Table([[rl_img]], colWidths=[W])
img_table.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LIGHT),("ALIGN",(0,0),(-1,-1),"CENTER"),("TOPPADDING",(0,0),(-1,-1),8),("BOTTOMPADDING",(0,0),(-1,-1),8)]))
story += [sp(0.08), Paragraph("<b>Complaint Photo Evidence:</b>", sBodI), sp(0.05), img_table]
except Exception as img_err:
print(f"PDF image embed error: {img_err}")
story += [sp(0.15)]
story += [sec_hdr("C","Verification Results"), sp(0.08)]
ai_bg = colors.HexColor("#e6f7ed") if "APPROVED" in gemini_status else colors.HexColor("#fdecea")
story += [card([Paragraph(f"<b>Status:</b> {gemini_status} | <b>Confidence:</b> {gemini_confidence}", sBod),
Paragraph(f"<b>Assessment:</b> {gemini_reason}", sBod)], bg=ai_bg), sp(0.15)]
story += [sec_hdr("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)]
if kb.get("laws"):
lt = Table([[Paragraph(f"{i}. {law}", sBul)] for i,law in enumerate(kb["laws"],1)], colWidths=[W])
lt.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LIGHT),("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_hdr("E","Citizen's Legal Rights"), sp(0.08)]
if kb.get("citizen_rights"):
rt = Table([[Paragraph(f"βœ“ {r}", sBul)] for r in kb["citizen_rights"]], 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])]))
story.append(rt)
story += [sp(0.08), card([Paragraph(f"<b>Escalation Path:</b> {kb.get('escalation','CM Portal: 0800-02345')}", sBodI)], bg=C_GOLD_L), sp(0.15)]
story += [sec_hdr("F",f"Legal Advice ({language})"), sp(0.08)]
advice_paras = [Paragraph(l.strip(), sBod) for l in llama_advice.strip().split("\n") if l.strip()]
if advice_paras:
at = Table([[p] for p in advice_paras], colWidths=[W])
at.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LIGHT),("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_hdr("G","Mandatory Action Directive"), sp(0.08)]
dir_t = Table([[Paragraph(f"MANDATORY ACTION REQUIRED WITHIN: {kb.get('response','72 hours').upper()}", sGD)]], 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_hdr("H","Declaration & Official Use"), sp(0.08)]
decl_inner = [
[Paragraph(f"I, <b>{name}</b> (CNIC: {cnic}), solemnly declare that all information provided is true and correct to the best of my knowledge.", sDcl)],
[sp(0.1)],
[Table([[Paragraph("Complainant Signature",sLbl),Paragraph("Date",sLbl),Paragraph("Reference No.",sLbl)],
[Paragraph("____________________________",sVal),Paragraph(date_str,sVal),Paragraph(complaint_id,sVal)]],colWidths=[2.5*inch,2.5*inch,2.0*inch])],
[sp(0.1)],
[Table([[Paragraph("Received By",sLbl),Paragraph("Date of Receipt",sLbl),Paragraph("Action Taken",sLbl),Paragraph("Resolved On",sLbl)],
[Paragraph("______________",sVal),Paragraph("______________",sVal),Paragraph("______________",sVal),Paragraph("______________",sVal)]],colWidths=[1.75*inch]*4)],
]
decl_outer = Table(decl_inner, colWidths=[W])
decl_outer.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_LIGHT),("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 | {timestamp} | {complaint_id}", sFt)]], colWidths=[W])
foot_t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),C_DARK),("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7)]))
story.append(foot_t)
doc.build(story)
return pdf_path
except Exception as e:
import traceback; traceback.print_exc(); return None
# ══════════════════════════════════════════════════════════════
# MAIN REPORT FUNCTION
# ══════════════════════════════════════════════════════════════
def make_report(image, issue_type, province, 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 the complaint 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
complaint_id = f"RB-{uuid.uuid4().hex[:8].upper()}"
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
annotated_img, yolo_summary, yolo_severity = detect_with_yolo(image, issue_type)
gemini_raw = analyze_with_gemini(image, issue_type, location, city, yolo_summary)
gemini_parsed = parse_gemini_response(gemini_raw)
gemini_status = gemini_parsed["status"]; gemini_reason = gemini_parsed["reason"]
if gemini_status == "REJECTED":
return (annotated_img,
f"COMPLAINT REJECTED\n\nReason: {gemini_reason}\nConfidence: {gemini_parsed.get('confidence','N/A')}\n\nPlease upload a clear image of the {issue_type}.",
"","",None,complaint_id,None,None)
if gemini_status == "UNKNOWN" and "GOOGLE_API_KEY not set" in gemini_raw:
gemini_reason = "Verification skipped β€” API key not configured."
gemini_status = "APPROVED_WITH_WARNING"
final_severity = gemini_parsed["severity"] if gemini_status == "APPROVED" else yolo_severity
kb = LEGAL_KB.get(issue_type, {}); sev_lbl = severity_label(final_severity)
llama_advice = analyze_with_llama(issue_type, location, city, yolo_summary, final_severity, language)
pdf_path = generate_pdf_report(
complaint_id, timestamp, name, cnic, phone, province, city, location,
issue_type, language, final_severity, gemini_status, gemini_reason,
gemini_parsed.get("confidence","N/A"), kb, description, llama_advice, image)
report = (f"GOVERNMENT OF PAKISTAN β€” CIVIC COMPLAINT REPORT\n"
f"Rahbar Digital Civic Redressal System\n{'='*55}\n"
f"Complaint Number : {complaint_id}\n"
f"Date / Time : {datetime.datetime.now().strftime('%d %B %Y %I:%M %p')}\n"
f"Language : {language}\n\n"
f"── COMPLAINANT ──\nName : {name}\nCNIC : {cnic}\nPhone : {phone or 'N/A'}\n"
f"Province : {province}\nCity : {city}\nLocation : {location}\n\n"
f"── COMPLAINT ──\nIssue : {issue_type}\nSeverity : {final_severity}/10 [{sev_lbl}]\n"
f"Description: {description.strip() or '[None]'}\n\n"
f"── VERIFICATION ──\nStatus : {gemini_status}\n"
f"Confidence : {gemini_parsed.get('confidence','N/A')}\nAssessment : {gemini_reason}\n\n"
f"── LEGAL FRAMEWORK ──\n" + "\n".join(f" - {l}" for l in kb.get("laws",[])) +
f"\nAuthority : {kb.get('authority','N/A')}\nHelpline : {kb.get('hotline','N/A')}\n"
f"Response : {kb.get('response','N/A')}\nPenalty : {kb.get('fine','N/A')}\n\n"
f"── CITIZEN 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 WITHIN: {kb.get('response','72 hours').upper()}\n"
f"Portal: citizenportal.gov.pk | CM: 0800-02345\n\n"
f"I, {name} (CNIC: {cnic}), declare the above information is accurate.\n"
f"Reference: {complaint_id} | Generated: {timestamp}")
wa_text = (f"Rahbar Civic Complaint\nID: {complaint_id}\nIssue: {issue_type}\n"
f"Location: {location}, {city}, {province}\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)})"
complaint_log.append({"id":complaint_id,"timestamp":timestamp,"province":province,
"city":city,"location":location,"issue":issue_type,"severity":final_severity,
"language":language,"name":name,"cnic":cnic,"phone":phone})
report_tts_path = None
if enable_tts:
tts_text = (f"Complaint {complaint_id} filed. Issue: {issue_type}. "
f"Location: {location}, {city}, {province}. Severity: {final_severity} out of 10. "
f"Responsible authority: {kb.get('authority','')}. 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
return (annotated_img, report, wa_md, llama_advice,
report_tts_path, complaint_id, advice_tts_path, pdf_path)
# ══════════════════════════════════════════════════════════════
# CSS
# ══════════════════════════════════════════════════════════════
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:wght@700;900&family=JetBrains+Mono:wght@400;500&display=swap');
:root{--bg:#fff;--bg2:#f5f8f6;--bg3:#e8f3ec;--surface:#fff;--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;--hbg:linear-gradient(135deg,#14432e 0%,#0d2b1e 60%,#091a10 100%);}
@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;--info-bg:#0d2016;--warn-bg:#1a1300;--shadow:0 2px 14px rgba(0,0,0,.45);--hbg:linear-gradient(135deg,#091a10 0%,#060d08 100%);}}
*,*::before,*::after{box-sizing:border-box;}
body,.gradio-container{font-family:'Inter',sans-serif!important;background:var(--bg)!important;color:var(--txt)!important;}
.rh-header{background:var(--hbg);padding:28px 20px 22px;text-align:center;border-bottom:2px solid var(--green);}
.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);}
.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;}
.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;}
.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;}
.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;padding:11px 22px!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:18px!important;}
.gradio-container audio{width:100%!important;border-radius:var(--radius)!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);}
.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;}
.voice-status{font-size:.82rem;padding:6px 10px;border-radius:8px;background:var(--bg2);border:1px solid var(--border);color:var(--txt2);min-height:32px;}
"""
HEADER_HTML = """
<div class="rh-header">
<div class="rh-title">Rahbar</div>
<div class="rh-subtitle">Pakistan's AI-Powered Civic Complaint Platform</div>
<div class="rh-tag">All of Pakistan β€” Every City, Town & Village</div>
</div>
<div class="top-bar">
<div class="badge-group">
<span class="badge">7 Provinces</span>
<span class="badge">Plotly Map</span>
<span class="badge">Legal Assistant</span>
<span class="badge">PDF + Photo</span>
<span class="badge badge-gold">4 Languages</span>
<span class="badge badge-red">LIVE</span>
</div>
</div>
"""
HOTLINES_HTML = """
<div class="info-box">
<strong>Emergency Helplines:</strong>&nbsp;&nbsp;
Garbage: <span class="hotline-pill">1139</span>&nbsp;
Roads/NHA: <span class="hotline-pill">051-9032800</span>&nbsp;
WASA Lahore: <span class="hotline-pill">042-99200300</span>&nbsp;
CM Portal: <span class="hotline-pill">0800-02345</span>&nbsp;
Fed. Ombudsman: <span class="hotline-pill">051-9204551</span>
</div>
"""
# ══════════════════════════════════════════════════════════════
# BUILD UI
# ══════════════════════════════════════════════════════════════
def build_ui():
default_province = "Punjab"
default_city = "Lahore"
default_areas = get_areas(default_province, default_city)
default_cities = get_cities(default_province)
with gr.Blocks(title="Rahbar | Pakistan Civic Complaint System") as demo:
gr.HTML(HEADER_HTML)
with gr.Tabs():
# ════════════════════════════════════════════════
# TAB 1 β€” File Complaint
# ════════════════════════════════════════════════
with gr.Tab("πŸ“ File Complaint"):
with gr.Row(equal_height=False):
# ── Left column ───────────────────────────
with gr.Column(scale=1, min_width=310):
gr.HTML('<div class="sec-title">Citizen Information</div>')
name_tb = gr.Textbox(label="Full Name", placeholder="e.g. Ali Hassan", lines=1)
cnic_tb = gr.Textbox(label="CNIC (no dashes)", placeholder="1234567890123", lines=1)
phone_tb = gr.Textbox(label="Phone (optional)", placeholder="03xxxxxxxxx", lines=1)
gr.HTML('<div class="sec-title" style="margin-top:14px">Issue Photo</div>')
gr.HTML('<div class="info-box">Upload or capture a clear photo. Photo will be embedded in PDF report.</div>')
image_input = gr.Image(type="pil", label="Upload / Capture Photo",
sources=["webcam","upload"], height=200)
gr.HTML('<div class="sec-title" style="margin-top:14px">Complaint Details</div>')
issue_type_rb = gr.Radio(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="Issue Type")
gr.HTML('<div class="sec-title" style="margin-top:10px">Location β€” Full Pakistan Coverage</div>')
gr.HTML('<div class="info-box">Select Province β†’ City β†’ Area. Map updates automatically. Type street/landmark below.</div>')
province_dd = gr.Dropdown(choices=PROVINCES, value=default_province, label="Province / Territory")
city_dd = gr.Dropdown(choices=default_cities, value=default_city, label="City / Town")
area_dd = gr.Dropdown(choices=default_areas, value=default_areas[0], label="Area / Neighbourhood")
gr.HTML('<div class="sec-title" style="margin-top:10px">Street / Landmark</div>')
location_tb = gr.Textbox(
label="Street / Landmark (type or paste GPS coords)",
placeholder="e.g. Near Govt Hospital, Main Bazaar, Street 5",
lines=1
)
desc_tb = gr.Textbox(label="Additional Description (optional)",
placeholder="Describe the issue...", lines=3)
language_dd = gr.Dropdown(choices=LANGUAGES, value="English",
label="Report & Voice Language")
tts_cb = gr.Checkbox(label="πŸ”Š Read Report Aloud (TTS)", value=False)
submit_btn = gr.Button("πŸš€ Submit Complaint", variant="primary", size="lg")
# ── Right column ──────────────────────────
with gr.Column(scale=2, min_width=320):
# βœ… PLOTLY MAP β€” gr.Plot() replaces gr.HTML() β€” works in Gradio sandbox
gr.HTML('<div class="sec-title">πŸ“ Live Location Map</div>')
gr.HTML('<div class="info-box">Map centres on your selected city automatically. Red pin = complaint location.</div>')
map_plot = gr.Plot(
value=make_plotly_map(city=default_city),
label="Complaint Location Map"
)
gr.HTML('<div class="sec-title" style="margin-top:14px">Detection Result</div>')
annotated_out = gr.Image(label="Detection Output", height=230)
complaint_id_out = gr.Textbox(label="Complaint Reference Number", interactive=False)
gr.HTML('<div class="sec-title" style="margin-top:14px">Official Complaint Summary</div>')
report_out = gr.Textbox(label="Summary", lines=14, interactive=False,
placeholder="Summary appears here after submission...")
gr.HTML('<div class="sec-title" style="margin-top:12px">Download Official PDF Report</div>')
gr.HTML('<div class="info-box">Professional PDF with your photo, legal framework, and complaint ID.</div>')
pdf_out = gr.File(label="πŸ“„ Download PDF Report", interactive=False)
wa_out = gr.Markdown()
report_tts_out = gr.Audio(label="πŸ”Š Report Audio (TTS)", autoplay=False)
gr.HTML('<div class="sec-title" style="margin-top:16px">Legal Advice</div>')
legal_advice_out = gr.Textbox(label="Legal Rights & Steps", lines=12, interactive=False,
placeholder="Legal advice appears after submission...")
advice_tts_out = gr.Audio(label="πŸ”Š Legal Advice Audio", autoplay=False)
# ── Event wiring ──────────────────────────────
# Province β†’ update cities, areas, map
province_dd.change(
fn=on_province_change,
inputs=[province_dd],
outputs=[city_dd, area_dd, map_plot]
)
# City β†’ update areas, map
city_dd.change(
fn=on_city_change,
inputs=[province_dd, city_dd],
outputs=[area_dd, map_plot]
)
# Area / location text β†’ update map pin label
area_dd.change(
fn=on_location_change,
inputs=[province_dd, city_dd, area_dd, location_tb],
outputs=[map_plot]
)
location_tb.change(
fn=on_location_change,
inputs=[province_dd, city_dd, area_dd, location_tb],
outputs=[map_plot]
)
# Submit
submit_btn.click(
fn=make_report,
inputs=[image_input, issue_type_rb, province_dd, 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],
)
# ════════════════════════════════════════════════
# TAB 2 β€” Legal Reference & Chatbot
# ════════════════════════════════════════════════
with gr.Tab("βš–οΈ Legal Reference & Chatbot"):
gr.HTML('<div class="sec-title">Pakistani Civic Laws Database</div>')
with gr.Row():
law_issue_dd = gr.Dropdown(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="Issue", scale=1)
law_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1)
law_out = gr.Markdown()
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)
gr.HTML('<div class="sec-title" style="margin-top:24px">Legal Chatbot (RAG-powered)</div>')
gr.HTML('<div class="info-box">Ask anything about civic issues in Pakistan. Supports voice input and 4 language output.</div>')
chat_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Response Language")
chatbot = gr.Chatbot(label="Rahbar Legal Assistant", height=420, value=[])
with gr.Row():
chat_input = gr.Textbox(
label="Your Question",
placeholder="e.g. WASA did not fix the pipe for 3 days β€” what are my rights?",
lines=2, scale=4)
chat_send_btn = gr.Button("Send ➀", variant="primary", scale=1)
gr.HTML('<div class="sec-title" style="margin-top:12px">🎀 Voice Input</div>')
gr.HTML('<div class="info-box">β‘  Press the mic button and speak your question β‘‘ Click <b>🎀 Send Voice</b> β€” transcript will be sent automatically.</div>')
gr.HTML('<div class="warn-box">πŸ’‘ Make sure to record first, then click Send Voice. Status shows below.</div>')
with gr.Row():
chat_audio_in = gr.Audio(
type="filepath",
label="πŸŽ™οΈ Record your question here",
sources=["microphone", "upload"],
scale=3
)
chat_voice_btn = gr.Button("🎀 Send Voice", variant="primary", scale=1)
# βœ… FIXED: Status label shows what happened (success / error / no-audio)
voice_status_md = gr.Markdown(
value="",
elem_classes=["voice-status"]
)
gr.HTML('<div class="sec-title" style="margin-top:10px">πŸ”Š Voice Output</div>')
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?"],
["What are my rights under Article 9 of the Constitution?"],
["How to contact Federal Ombudsman?"],
],
inputs=chat_input, label="Sample Questions"
)
# Text send
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]
)
# βœ… FIXED Send Voice β€” outputs to chatbot + status label
chat_voice_btn.click(
fn=voice_then_send,
inputs=[chat_audio_in, chatbot, chat_lang_dd],
outputs=[chatbot, voice_status_md]
)
# Play Answer TTS
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('<div class="sec-title">Speech to Text</div>')
gr.HTML('<div class="info-box">Record your complaint. Uses Whisper (Groq API) or Google Speech fallback.</div>')
gr.HTML('<div class="warn-box">πŸ’‘ Speak clearly in a quiet place. Copy transcript to the complaint description field.</div>')
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 appears here...")
stt_btn.click(fn=stt, inputs=[audio_in], outputs=[stt_out])
gr.HTML('<div class="sec-title" style="margin-top:24px">Text to Speech (Offline)</div>')
gr.HTML('<div class="info-box">Offline TTS via espeak-ng - works without internet. Supports Urdu, Punjabi, Sindhi, English.</div>')
with gr.Row():
tts_text_in = gr.Textbox(label="Text to Speak", placeholder="Type here...", scale=3)
tts_lang_in = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1)
tts_test_btn = gr.Button("β–Ά Generate Speech", 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('<div class="sec-title">Live Complaint Statistics</div>')
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("""<div class="info-box" style="margin-top:16px">
<b>Map Engine:</b> Plotly Scattermapbox + OpenStreetMap β€” renders in all Gradio versions<br>
<b>Coverage:</b> 7 Provinces β€” Punjab, Sindh, KPK, Balochistan, Islamabad, AJK, Gilgit-Baltistan<br>
<b>Voice:</b> Send Voice button β€” status label shows transcript / error in real time<br>
<b>TTS:</b> gTTS (Google Text-to-Speech online β€” Urdu, Punjabi, Sindhi, English)<br>
<b>PDF:</b> ReportLab with embedded complaint photo + full legal framework<br>
<b>AI:</b> YOLO detection + Gemini vision verification + LLaMA 3.3 legal advice (RAG)
</div>""")
return demo
# ══════════════════════════════════════════════════════════════
# LAUNCH
# ══════════════════════════════════════════════════════════════
if __name__ == "__main__":
print("Rahbar v9.1 β€” Map fix (Plotly) + Voice fix")
print(f"RAG Engine: {'ready' if rag_engine._initialized else 'initializing...'}")
provinces = len(PAKISTAN_GEO)
cities = sum(len(v) for v in PAKISTAN_GEO.values())
areas = sum(len(a) for prov in PAKISTAN_GEO.values() for a in prov.values())
print(f"Location DB: {provinces} provinces, {cities} cities, {areas} areas")
demo = build_ui()
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
theme=gr.themes.Base(
primary_hue=gr.themes.colors.green,
secondary_hue=gr.themes.colors.yellow,
),
css=CSS,
)