Update app.py
Browse files
app.py
CHANGED
|
@@ -1,30 +1,25 @@
|
|
| 1 |
"""
|
| 2 |
╔══════════════════════════════════════════════════════════════╗
|
| 3 |
-
║ Rahbar
|
| 4 |
-
║
|
| 5 |
-
║ (water/sanitation Q&A + report routing)
|
| 6 |
-
║ ║
|
| 7 |
╚══════════════════════════════════════════════════════════════╝
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"""
|
| 9 |
|
| 10 |
-
import os
|
| 11 |
-
import io
|
| 12 |
-
import re
|
| 13 |
-
import uuid
|
| 14 |
-
import base64
|
| 15 |
-
import datetime
|
| 16 |
-
import urllib.parse
|
| 17 |
from PIL import Image
|
| 18 |
import gradio as gr
|
| 19 |
|
| 20 |
-
# ─── API KEYS FROM ENVIRONMENT ────────────────────────────────
|
| 21 |
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "")
|
| 22 |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
|
| 23 |
|
| 24 |
-
# ─── COMPLAINT LOG (IN-MEMORY) ────────────────────────────────
|
| 25 |
complaint_log = []
|
| 26 |
|
| 27 |
-
# ─── CITIES & AREAS ───────────────────────────────────────────
|
| 28 |
CITIES_AREAS = {
|
| 29 |
"Lahore": ["Model Town", "DHA", "Gulberg", "Johar Town", "Bahria Town", "Township", "Cantonment"],
|
| 30 |
"Karachi": ["Clifton", "DHA", "Gulshan-e-Iqbal", "PECHS", "Korangi", "Saddar", "North Nazimabad"],
|
|
@@ -39,63 +34,139 @@ CITIES_AREAS = {
|
|
| 39 |
ISSUE_TYPES = ["🗑️ Garbage", "🕳️ Pot Hole", "💧 Pipe Leakage"]
|
| 40 |
LANGUAGES = ["English", "اردو (Urdu)", "پنجابی (Punjabi)", "سندھی (Sindhi)"]
|
| 41 |
|
| 42 |
-
# ─── RAG KNOWLEDGE BASE ───────────────────────────────────────
|
| 43 |
LEGAL_KB = {
|
| 44 |
"Garbage": {
|
| 45 |
-
"laws": [
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
},
|
| 51 |
"Pot Hole": {
|
| 52 |
-
"laws": [
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
},
|
| 58 |
"Pipe Leakage": {
|
| 59 |
-
"laws": [
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
},
|
| 65 |
}
|
| 66 |
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
LOCALIZED = {
|
| 69 |
"Garbage": {
|
| 70 |
-
"English":
|
| 71 |
-
"اردو (Urdu)":
|
| 72 |
-
"پنجابی (Punjabi)": "
|
| 73 |
-
"سندھی (Sindhi)":
|
| 74 |
},
|
| 75 |
"Pot Hole": {
|
| 76 |
-
"English":
|
| 77 |
-
"اردو (Urdu)":
|
| 78 |
-
"پنجابی (Punjabi)": "
|
| 79 |
-
"سندھی (Sindhi)":
|
| 80 |
},
|
| 81 |
"Pipe Leakage": {
|
| 82 |
-
"English":
|
| 83 |
-
"اردو (Urdu)":
|
| 84 |
-
"پنجابی (Punjabi)": "
|
| 85 |
-
"سندھی (Sindhi)":
|
| 86 |
},
|
| 87 |
}
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
-
# ─── YOLO
|
| 93 |
def detect_with_yolo(image_pil, issue_type):
|
| 94 |
try:
|
| 95 |
from ultralytics import YOLO
|
| 96 |
import numpy as np
|
| 97 |
-
|
| 98 |
-
model = YOLO(model_path)
|
| 99 |
img_np = np.array(image_pil)
|
| 100 |
results = model(img_np, verbose=False)
|
| 101 |
result = results[0]
|
|
@@ -105,27 +176,27 @@ def detect_with_yolo(image_pil, issue_type):
|
|
| 105 |
clean_issue = issue_type.split(" ", 1)[-1]
|
| 106 |
for box in result.boxes:
|
| 107 |
cls_id = int(box.cls[0])
|
| 108 |
-
conf
|
| 109 |
-
label
|
| 110 |
detected.append(f"{label} ({conf:.0%})")
|
| 111 |
if clean_issue == "Garbage" and cls_id in WASTE_CLASS_IDS:
|
| 112 |
severity = min(10, severity + 2)
|
| 113 |
elif clean_issue in ("Pot Hole", "Pipe Leakage"):
|
| 114 |
severity = min(10, severity + 1)
|
| 115 |
annotated_np = result.plot()
|
| 116 |
-
annotated
|
| 117 |
-
summary = f"Detected {len(detected)} object(s): {', '.join(detected[:5])}"
|
| 118 |
-
|
| 119 |
-
return annotated, summary, severity
|
| 120 |
except ImportError:
|
| 121 |
return image_pil, "YOLO not installed – skipping object detection.", 5
|
| 122 |
except Exception as e:
|
| 123 |
return image_pil, f"YOLO error: {e}", 5
|
| 124 |
|
| 125 |
-
# ─── GEMINI
|
| 126 |
def analyze_with_gemini(image_pil, issue, location, city, yolo_summary):
|
| 127 |
if not GOOGLE_API_KEY:
|
| 128 |
-
return "
|
| 129 |
try:
|
| 130 |
import google.generativeai as genai
|
| 131 |
genai.configure(api_key=GOOGLE_API_KEY)
|
|
@@ -133,101 +204,176 @@ def analyze_with_gemini(image_pil, issue, location, city, yolo_summary):
|
|
| 133 |
buf = io.BytesIO()
|
| 134 |
image_pil.save(buf, format="JPEG")
|
| 135 |
image_bytes = buf.getvalue()
|
| 136 |
-
prompt = f"""
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
YOUR TASK - Carefully examine the image and determine:
|
| 145 |
-
- Garbage: Must show actual waste/litter/garbage heap/trash bags
|
| 146 |
-
- Pot Hole: Must show a road with a clearly visible hole or severe damage
|
| 147 |
-
- Pipe Leakage: Must show water leaking from pipe or water flooding from pipe burst
|
| 148 |
-
|
| 149 |
-
STRICT RULES:
|
| 150 |
-
- Clean area, indoor scene, person alone, vegetation only → REJECT
|
| 151 |
-
- Single leaf or minor dirt is NOT garbage → REJECT
|
| 152 |
-
- Rough road surface without actual hole → REJECT
|
| 153 |
-
- Be strict. Real civic problems are obvious and undeniable.
|
| 154 |
-
|
| 155 |
-
Respond ONLY in this EXACT format (no extra text):
|
| 156 |
STATUS: [APPROVED or REJECTED]
|
| 157 |
-
REASON: [2-3 sentences
|
| 158 |
-
REASON_URDU: [2-3 sentences
|
| 159 |
-
SEVERITY: [
|
| 160 |
-
CONFIDENCE: [
|
| 161 |
-
RECOMMENDED_ACTION: [
|
| 162 |
-
"""
|
| 163 |
image_part = {"mime_type": "image/jpeg", "data": base64.b64encode(image_bytes).decode()}
|
| 164 |
-
response
|
| 165 |
return response.text.strip()
|
| 166 |
except Exception as e:
|
| 167 |
-
return f"
|
| 168 |
|
| 169 |
def parse_gemini_response(gemini_text):
|
| 170 |
-
|
| 171 |
-
if not gemini_text:
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
if
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
if
|
| 184 |
-
|
| 185 |
-
conf_match = re.search(r"CONFIDENCE:\s*(\d+)%", gemini_text)
|
| 186 |
-
if conf_match:
|
| 187 |
-
result["confidence"] = conf_match.group(1) + "%"
|
| 188 |
-
action_match = re.search(r"RECOMMENDED_ACTION:\s*(.+?)(?=$)", gemini_text, re.DOTALL)
|
| 189 |
-
if action_match:
|
| 190 |
-
result["action"] = action_match.group(1).strip()
|
| 191 |
-
return result
|
| 192 |
|
| 193 |
# ─── LLAMA 3 LEGAL ADVICE ─────────────────────────────────────
|
| 194 |
-
def analyze_with_llama(issue, location, city, yolo_summary, severity):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
if not GROQ_API_KEY:
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
try:
|
| 200 |
from groq import Groq
|
| 201 |
client = Groq(api_key=GROQ_API_KEY)
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
| 220 |
return chat.choices[0].message.content.strip()
|
| 221 |
except Exception as e:
|
| 222 |
-
return f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
def make_tts(text, language):
|
| 227 |
try:
|
| 228 |
from gtts import gTTS
|
| 229 |
lang_code = LANG_CODES.get(language, "en")
|
| 230 |
-
tts
|
| 231 |
path = f"/tmp/tts_{uuid.uuid4().hex[:8]}.mp3"
|
| 232 |
tts.save(path)
|
| 233 |
return path
|
|
@@ -235,56 +381,67 @@ def make_tts(text, language):
|
|
| 235 |
print(f"TTS error: {e}")
|
| 236 |
return None
|
| 237 |
|
| 238 |
-
# ───
|
| 239 |
def stt(audio_file):
|
| 240 |
if audio_file is None:
|
| 241 |
-
return "No audio provided."
|
| 242 |
try:
|
| 243 |
import speech_recognition as sr
|
| 244 |
recognizer = sr.Recognizer()
|
| 245 |
with sr.AudioFile(audio_file) as src:
|
|
|
|
| 246 |
audio_data = recognizer.record(src)
|
| 247 |
text = recognizer.recognize_google(audio_data)
|
| 248 |
return text
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
except Exception as e:
|
| 250 |
-
return f"
|
| 251 |
|
| 252 |
-
# ───
|
| 253 |
def law_info(issue, language):
|
| 254 |
-
clean
|
| 255 |
-
kb
|
| 256 |
-
local
|
| 257 |
-
|
|
|
|
|
|
|
| 258 |
for law in kb.get("laws", []):
|
| 259 |
-
out += f"
|
| 260 |
-
out += f"\n
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
if local:
|
| 262 |
-
out += f"\n---\n
|
|
|
|
| 263 |
return out
|
| 264 |
|
| 265 |
-
# ─── WHATSAPP
|
| 266 |
def make_whatsapp_link(text):
|
| 267 |
-
|
| 268 |
-
return f"https://wa.me/?text={encoded}"
|
| 269 |
|
| 270 |
# ─── ADMIN STATS ──────────────────────────────────────────────
|
| 271 |
def get_admin_stats():
|
| 272 |
total = len(complaint_log)
|
| 273 |
if total == 0:
|
| 274 |
return "No complaints filed yet.", ""
|
| 275 |
-
counts
|
| 276 |
-
cities
|
| 277 |
severities = []
|
| 278 |
for c in complaint_log:
|
| 279 |
-
issue = c.get("issue",
|
| 280 |
counts[issue] = counts.get(issue, 0) + 1
|
| 281 |
-
city = c.get("city",
|
| 282 |
cities[city] = cities.get(city, 0) + 1
|
| 283 |
severities.append(c.get("severity", 5))
|
| 284 |
-
avg_sev
|
| 285 |
top_city = max(cities, key=cities.get) if cities else "N/A"
|
| 286 |
-
stats_md = f"""##
|
| 287 |
-
|
| 288 |
| Metric | Value |
|
| 289 |
|--------|-------|
|
| 290 |
| Total Complaints | **{total}** |
|
|
@@ -294,269 +451,570 @@ def get_admin_stats():
|
|
| 294 |
### By Issue Type
|
| 295 |
| Issue | Count |
|
| 296 |
|-------|-------|
|
| 297 |
-
|
|
| 298 |
-
|
|
| 299 |
-
|
|
| 300 |
|
| 301 |
### By City
|
| 302 |
"""
|
| 303 |
for city, cnt in sorted(cities.items(), key=lambda x: -x[1]):
|
| 304 |
stats_md += f"| {city} | {cnt} |\n"
|
| 305 |
-
log_md = "##
|
| 306 |
for c in reversed(complaint_log[-10:]):
|
| 307 |
-
log_md += f"**{c['id']}** | {c['timestamp']} | {c['city']}, {c['location']} |
|
|
|
|
|
|
|
| 308 |
return stats_md, log_md
|
| 309 |
|
| 310 |
def severity_icon(score):
|
| 311 |
-
if score <= 3: return "
|
| 312 |
-
if score <= 6: return "
|
| 313 |
-
if score <= 8: return "
|
| 314 |
-
return "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
# ─── MAIN REPORT FUNCTION ─────────────────────────────────────
|
| 317 |
-
def make_report(image, issue_type, city, location, name, cnic, phone,
|
|
|
|
| 318 |
if image is None:
|
| 319 |
-
return (None, "
|
| 320 |
if not location.strip():
|
| 321 |
-
return (None, "
|
| 322 |
if not name.strip():
|
| 323 |
-
return (None, "
|
| 324 |
if not cnic.strip():
|
| 325 |
-
return (None, "
|
| 326 |
-
|
| 327 |
complaint_id = f"RB-{uuid.uuid4().hex[:8].upper()}"
|
| 328 |
-
timestamp
|
| 329 |
-
clean_issue
|
| 330 |
-
|
| 331 |
annotated_img, yolo_summary, yolo_severity = detect_with_yolo(image, issue_type)
|
| 332 |
-
gemini_raw
|
| 333 |
gemini_parsed = parse_gemini_response(gemini_raw)
|
| 334 |
gemini_status = gemini_parsed["status"]
|
| 335 |
gemini_reason = gemini_parsed["reason"]
|
| 336 |
gemini_severity = gemini_parsed["severity"]
|
| 337 |
-
|
| 338 |
-
if gemini_status == "REJECTED":
|
| 339 |
-
error_msg = f"""❌ **COMPLAINT REJECTED BY AI VERIFICATION**
|
| 340 |
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
-
Please upload a clear image that clearly shows the reported issue ({issue_type}).
|
| 345 |
-
Your complaint has **NOT** been logged."""
|
| 346 |
-
return (annotated_img, error_msg, "", "", None, complaint_id)
|
| 347 |
-
|
| 348 |
if gemini_status == "UNKNOWN" and "GOOGLE_API_KEY not set" in gemini_raw:
|
| 349 |
-
gemini_reason = "
|
| 350 |
gemini_status = "APPROVED_WITH_WARNING"
|
| 351 |
-
|
| 352 |
final_severity = gemini_severity if gemini_status == "APPROVED" else yolo_severity
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
local = LOCALIZED.get(clean_issue, {}).get(language, "")
|
| 356 |
sev_icon = severity_icon(final_severity)
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
complaint_log.append({
|
| 359 |
"id": complaint_id, "timestamp": timestamp, "city": city, "location": location,
|
| 360 |
"issue": issue_type, "severity": final_severity, "language": language,
|
| 361 |
"name": name, "cnic": cnic, "phone": phone,
|
| 362 |
})
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
📞 Phone : {phone if phone else "(not provided)"}
|
| 373 |
-
📍 Location : {location}, {city}
|
| 374 |
-
🔖 Issue Type : {issue_type}
|
| 375 |
-
🌐 Language : {language}
|
| 376 |
-
|
| 377 |
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 378 |
-
{sev_icon} SEVERITY SCORE: {final_severity}/10
|
| 379 |
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 380 |
-
|
| 381 |
-
📷 YOLO DETECTION SUMMARY:
|
| 382 |
-
{yolo_summary}
|
| 383 |
-
|
| 384 |
-
🤖 AI VERIFICATION (Gemini):
|
| 385 |
-
Status: {gemini_status}
|
| 386 |
-
Reason: {gemini_reason}
|
| 387 |
-
Confidence: {gemini_parsed.get('confidence', 'N/A')}
|
| 388 |
-
|
| 389 |
-
⚖️ LEGAL INFORMATION:
|
| 390 |
-
• Laws : {", ".join(kb.get("laws", []))}
|
| 391 |
-
• Fine : {kb.get("fine", "N/A")}
|
| 392 |
-
• Authority: {kb.get("authority", "N/A")}
|
| 393 |
-
• Hotline : {kb.get("hotline", "N/A")}
|
| 394 |
-
• Response: {kb.get("response", "N/A")}
|
| 395 |
-
|
| 396 |
-
📜 LEGAL ADVICE (Llama 3):
|
| 397 |
-
{llama_advice}
|
| 398 |
-
|
| 399 |
-
💬 LOCALIZED MESSAGE ({language}):
|
| 400 |
-
{local}
|
| 401 |
-
|
| 402 |
-
📝 CITIZEN DESCRIPTION:
|
| 403 |
-
{description if description.strip() else "(none provided)"}
|
| 404 |
-
|
| 405 |
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 406 |
-
✅ ACTION REQUIRED within {kb.get("response", "72 hours")}
|
| 407 |
-
Contact: {kb.get("authority", "Local Authority")}
|
| 408 |
-
Hotline: {kb.get("hotline", "N/A")}
|
| 409 |
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 410 |
-
"""
|
| 411 |
-
|
| 412 |
-
wa_text = f"🏙️ Rahbar Complaint\nID: {complaint_id}\nIssue: {issue_type}\nLocation: {location}, {city}\nSeverity: {final_severity}/10\nHotline: {kb.get('hotline', 'N/A')}\nFiled: {timestamp}"
|
| 413 |
-
wa_link = make_whatsapp_link(wa_text)
|
| 414 |
-
wa_md = f"[📲 Share on WhatsApp]({wa_link})"
|
| 415 |
-
|
| 416 |
-
tts_path = None
|
| 417 |
if enable_tts:
|
| 418 |
-
tts_text = f"Complaint {complaint_id} filed
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
# ─── CSS ──────────────────────────────────────────────────────
|
| 424 |
CSS = """
|
| 425 |
@import url('https://fonts.googleapis.com/css2?family=Noto+Nastaliq+Urdu&family=Playfair+Display:wght@700;900&family=DM+Sans:wght@300;400;500;600&display=swap');
|
| 426 |
-
:root
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
.
|
| 433 |
-
.saaf-header
|
| 434 |
-
.
|
| 435 |
-
.
|
| 436 |
-
.
|
| 437 |
-
.
|
| 438 |
-
.badge-
|
| 439 |
-
.
|
| 440 |
-
.
|
| 441 |
-
.
|
| 442 |
-
.
|
| 443 |
-
.
|
| 444 |
-
.
|
| 445 |
-
.
|
| 446 |
-
.
|
| 447 |
-
.gradio-container
|
| 448 |
-
.gradio-container
|
| 449 |
-
.gradio-container .
|
| 450 |
-
.gradio-container
|
| 451 |
-
.gradio-container
|
| 452 |
-
.gradio-container button.
|
| 453 |
-
.gradio-container .
|
| 454 |
-
.gradio-container .
|
| 455 |
-
.gradio-container
|
| 456 |
-
.gradio-container .prose h2,
|
| 457 |
-
.gradio-container
|
| 458 |
-
.
|
| 459 |
-
.
|
| 460 |
-
.
|
| 461 |
-
|
| 462 |
-
@media
|
| 463 |
-
::-webkit-scrollbar
|
| 464 |
-
::-webkit-scrollbar-
|
| 465 |
-
::-webkit-scrollbar-thumb { background: var(--green-600); border-radius: 3px; }
|
| 466 |
-
.hotline-pill { display: inline-block; background: var(--white); color: var(--amber-400); border: 1px solid rgba(245,166,35,.35); border-radius: 20px; padding: 2px 12px; font-size: .8rem; font-weight: 600; }
|
| 467 |
"""
|
| 468 |
|
| 469 |
HEADER_HTML = """
|
| 470 |
<div class="saaf-header">
|
| 471 |
-
<h1>
|
| 472 |
-
<div class="urdu-title">رہبر
|
| 473 |
<div class="tagline">Pakistan's AI-Powered Civic Complaint System</div>
|
| 474 |
</div>
|
| 475 |
<div class="badge-strip">
|
| 476 |
-
<span class="badge badge-ai">
|
| 477 |
-
<span class="badge badge-ai">
|
| 478 |
-
<span class="badge badge-ai">
|
| 479 |
-
<span class="badge badge-pk">
|
| 480 |
-
<span class="badge badge-pk">
|
| 481 |
-
<span class="badge badge-live">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
"""
|
| 484 |
|
| 485 |
def update_areas(city):
|
| 486 |
areas = CITIES_AREAS.get(city, ["Enter area manually"])
|
| 487 |
return gr.Dropdown(choices=areas, value=areas[0])
|
| 488 |
|
| 489 |
-
# ─── BUILD
|
| 490 |
def build_ui():
|
| 491 |
-
with gr.Blocks(title="Rahbar | رہبر"
|
| 492 |
gr.HTML(HEADER_HTML)
|
|
|
|
| 493 |
with gr.Tabs():
|
| 494 |
-
|
| 495 |
-
|
|
|
|
| 496 |
with gr.Row(equal_height=False):
|
|
|
|
| 497 |
with gr.Column(scale=1, min_width=280):
|
| 498 |
-
gr.HTML('<div class="card-title">
|
| 499 |
-
name_tb
|
| 500 |
-
cnic_tb
|
| 501 |
-
phone_tb = gr.Textbox(label="Phone Number (optional)", placeholder="03xxxxxxxxx",
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
with gr.Column(scale=2, min_width=300):
|
| 513 |
-
gr.HTML('<div class="card-title">
|
| 514 |
-
annotated_out
|
| 515 |
-
complaint_id_out = gr.Textbox(label="
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
city_dd.change(fn=update_areas, inputs=[city_dd], outputs=[area_dd])
|
| 521 |
-
submit_btn.click(
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
with gr.Row():
|
| 527 |
law_issue_dd = gr.Dropdown(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="Select Issue", scale=1)
|
| 528 |
-
law_lang_dd
|
| 529 |
law_out = gr.Markdown()
|
| 530 |
-
gr.Button("
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
stt_btn.click(fn=stt, inputs=[audio_in], outputs=[stt_out])
|
| 541 |
-
|
|
|
|
| 542 |
with gr.Row():
|
| 543 |
-
tts_text_in = gr.Textbox(label="Enter text", placeholder="Type something
|
| 544 |
tts_lang_in = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1)
|
| 545 |
-
tts_test_btn = gr.Button("
|
| 546 |
-
tts_test_out = gr.Audio(label="
|
| 547 |
tts_test_btn.click(fn=make_tts, inputs=[tts_text_in, tts_lang_in], outputs=[tts_test_out])
|
| 548 |
-
|
| 549 |
-
# TAB 4
|
| 550 |
-
with gr.Tab("
|
| 551 |
-
gr.HTML('<div class="card-title">
|
| 552 |
-
refresh_btn = gr.Button("
|
| 553 |
with gr.Row():
|
| 554 |
stats_out = gr.Markdown()
|
| 555 |
-
log_out
|
| 556 |
refresh_btn.click(fn=get_admin_stats, outputs=[stats_out, log_out])
|
| 557 |
-
|
| 558 |
return demo
|
| 559 |
|
|
|
|
| 560 |
if __name__ == "__main__":
|
| 561 |
demo = build_ui()
|
| 562 |
-
demo.launch(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
╔══════════════════════════════════════════════════════════════╗
|
| 3 |
+
║ Rahbar | رہبر ║
|
| 4 |
+
║ Pakistan's AI-Powered Civic Complaint System ║
|
|
|
|
|
|
|
| 5 |
╚══════════════════════════════════════════════════════════════╝
|
| 6 |
+
Fixes:
|
| 7 |
+
• Chatbot uses {"role","content"} dict format (Gradio 6+)
|
| 8 |
+
• GPS button works with proper permission flow
|
| 9 |
+
• Report TTS in user-selected language
|
| 10 |
+
• Voice input (STT) working
|
| 11 |
+
• پنجابی language added
|
| 12 |
"""
|
| 13 |
|
| 14 |
+
import os, io, re, uuid, base64, datetime, urllib.parse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
from PIL import Image
|
| 16 |
import gradio as gr
|
| 17 |
|
|
|
|
| 18 |
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "")
|
| 19 |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
|
| 20 |
|
|
|
|
| 21 |
complaint_log = []
|
| 22 |
|
|
|
|
| 23 |
CITIES_AREAS = {
|
| 24 |
"Lahore": ["Model Town", "DHA", "Gulberg", "Johar Town", "Bahria Town", "Township", "Cantonment"],
|
| 25 |
"Karachi": ["Clifton", "DHA", "Gulshan-e-Iqbal", "PECHS", "Korangi", "Saddar", "North Nazimabad"],
|
|
|
|
| 34 |
ISSUE_TYPES = ["🗑️ Garbage", "🕳️ Pot Hole", "💧 Pipe Leakage"]
|
| 35 |
LANGUAGES = ["English", "اردو (Urdu)", "پنجابی (Punjabi)", "سندھی (Sindhi)"]
|
| 36 |
|
|
|
|
| 37 |
LEGAL_KB = {
|
| 38 |
"Garbage": {
|
| 39 |
+
"laws": [
|
| 40 |
+
"Punjab Waste Management Act 2014",
|
| 41 |
+
"Pakistan Environmental Protection Act 1997 (Section 11)",
|
| 42 |
+
"Punjab Local Government Act 2022 (Schedule II – Sanitation Duties)",
|
| 43 |
+
"Pakistan Penal Code Section 268 – Public Nuisance",
|
| 44 |
+
],
|
| 45 |
+
"fine": "Rs. 500 – 50,000 (per offence)",
|
| 46 |
+
"authority": "Local Government / Solid Waste Management Board",
|
| 47 |
+
"hotline": "1139",
|
| 48 |
+
"response": "48 hours",
|
| 49 |
+
"citizen_rights": [
|
| 50 |
+
"Right to clean environment (Article 9 & 14, Constitution of Pakistan)",
|
| 51 |
+
"Right to file FIR under PPC Section 268 if authority fails to act",
|
| 52 |
+
"Right to compensation for health damage under EPA 1997",
|
| 53 |
+
],
|
| 54 |
+
"escalation": "Chief Minister Complaints Cell: 0800-02345 | citizenportal.gov.pk",
|
| 55 |
+
"dataset_ref": "Punjab Solid Waste Management Board Open Dataset (data.punjab.gov.pk)",
|
| 56 |
},
|
| 57 |
"Pot Hole": {
|
| 58 |
+
"laws": [
|
| 59 |
+
"National Highways Safety Ordinance 2000",
|
| 60 |
+
"Punjab Local Government Act 2022 (Section 54 – Road Maintenance Obligation)",
|
| 61 |
+
"Motor Vehicles Ordinance 1965 (Liability of road authority)",
|
| 62 |
+
"Tort law – Negligence (Pakistani courts)",
|
| 63 |
+
],
|
| 64 |
+
"fine": "Authority liable for vehicle damage & personal injury claims",
|
| 65 |
+
"authority": "National Highway Authority (NHA) / C&W Department / LDA",
|
| 66 |
+
"hotline": "051-9032800",
|
| 67 |
+
"response": "72 hours",
|
| 68 |
+
"citizen_rights": [
|
| 69 |
+
"Right to claim compensation for vehicle damage or personal injury",
|
| 70 |
+
"Right to lodge complaint with Federal Ombudsman (Mohtasib)",
|
| 71 |
+
"Right to file writ petition in High Court for dereliction of duty",
|
| 72 |
+
],
|
| 73 |
+
"escalation": "Federal Ombudsman: 051-9204551 | nha.gov.pk",
|
| 74 |
+
"dataset_ref": "NHA Road Quality Inspection Reports (nha.gov.pk/publications)",
|
| 75 |
},
|
| 76 |
"Pipe Leakage": {
|
| 77 |
+
"laws": [
|
| 78 |
+
"Punjab Water Act 2019 (Section 23 – Supply Obligation)",
|
| 79 |
+
"WASA Act – Water & Sanitation Agency Bylaws",
|
| 80 |
+
"Pakistan Environmental Protection Act 1997 (Section 13 – Water Pollution)",
|
| 81 |
+
"Punjab Local Government Act 2022 (Water & Sewerage Schedules)",
|
| 82 |
+
"Constitution of Pakistan Article 9 – Right to Life (includes safe water)",
|
| 83 |
+
],
|
| 84 |
+
"fine": "Compensatory damages + Rs. 10,000 – 5,00,000 under PWA 2019",
|
| 85 |
+
"authority": "WASA / Pakistan Water Authority",
|
| 86 |
+
"hotline": "042-99200300",
|
| 87 |
+
"response": "24 hours",
|
| 88 |
+
"citizen_rights": [
|
| 89 |
+
"Right to safe and clean drinking water (SC ruling 2018 – PLD 2018 SC 1)",
|
| 90 |
+
"Right to compensation for property damage from water leakage",
|
| 91 |
+
"Right to disconnect billing if water supply is contaminated",
|
| 92 |
+
"Right to file complaint with Pakistan Water Authority (PWA)",
|
| 93 |
+
],
|
| 94 |
+
"escalation": "Pakistan Water Authority: 051-9246150 | CM Portal: 0800-02345",
|
| 95 |
+
"dataset_ref": "WASA Annual Service Quality Reports | Pakistan Water Authority Datasets",
|
| 96 |
},
|
| 97 |
}
|
| 98 |
|
| 99 |
+
WATER_SANITATION_KB = """
|
| 100 |
+
WATER & SANITATION CIVIC KNOWLEDGE BASE FOR PAKISTAN
|
| 101 |
+
====================================================
|
| 102 |
+
LAWS & REGULATIONS:
|
| 103 |
+
- Punjab Water Act 2019: WASA must respond to leakages within 24 hours.
|
| 104 |
+
- Pakistan Environmental Protection Act 1997: Prohibits water pollution (Section 13). Fine up to Rs. 1,000,000.
|
| 105 |
+
- National Drinking Water Policy 2009: Sets quality standards aligned with WHO guidelines.
|
| 106 |
+
- Constitution Article 9: Right to life = access to clean water (SC 2018 ruling).
|
| 107 |
+
- Punjab Local Government Act 2022: Mandates water/sanitation services as core LG duty.
|
| 108 |
+
|
| 109 |
+
CITIZEN RIGHTS:
|
| 110 |
+
- Right to receive written response within 3 working days.
|
| 111 |
+
- Can claim compensation if contaminated water causes illness (EPA 1997).
|
| 112 |
+
- If WASA fails within 24 hours, escalate to Deputy Commissioner office.
|
| 113 |
+
- Can file writ petition in High Court citing Article 9.
|
| 114 |
+
|
| 115 |
+
ESCALATION LADDER:
|
| 116 |
+
1. WASA / Local Authority Helpline (first 24-48 hours)
|
| 117 |
+
2. Deputy Commissioner / District Administration
|
| 118 |
+
3. Chief Minister Complaints Cell: 0800-02345 (toll-free)
|
| 119 |
+
4. Pakistan Citizen Portal: citizenportal.gov.pk
|
| 120 |
+
5. Federal Ombudsman (Wafaqi Mohtasib): 051-9204551
|
| 121 |
+
6. High Court Writ Petition (if fundamental right violated)
|
| 122 |
+
|
| 123 |
+
IMPORTANT HOTLINES:
|
| 124 |
+
- WASA Lahore: 042-99200300
|
| 125 |
+
- WASA Karachi (KWSB): 021-99231677
|
| 126 |
+
- WASA Rawalpindi: 051-5594244
|
| 127 |
+
- CM Punjab Complaints: 0800-02345
|
| 128 |
+
- Federal Ombudsman: 051-9204551
|
| 129 |
+
- Pakistan Citizen Portal: citizenportal.gov.pk
|
| 130 |
+
- Garbage/Sanitation: 1139
|
| 131 |
+
- Roads/NHA: 051-9032800
|
| 132 |
+
"""
|
| 133 |
+
|
| 134 |
LOCALIZED = {
|
| 135 |
"Garbage": {
|
| 136 |
+
"English": "Dumping garbage is a criminal offence under Punjab Waste Management Act 2014. Fine: Rs. 500-50,000.",
|
| 137 |
+
"اردو (Urdu)": "کچرا پھینکنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 کے تحت جرم ہے۔ جرمانہ: 500 سے 50,000 روپے۔",
|
| 138 |
+
"پنجابی (Punjabi)": "کچرا سُٹنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 دے تحت جرم اے۔ جرمانہ: 500 توں 50,000 روپے۔",
|
| 139 |
+
"سندھی (Sindhi)": "ڪچرو اڇلائڻ پنجاب ويسٽ مئنيجمينٽ ايڪٽ 2014 تحت جرم آهي. ڏنڊ: 500 کان 50,000 رپيا.",
|
| 140 |
},
|
| 141 |
"Pot Hole": {
|
| 142 |
+
"English": "Road repair is the government's legal obligation within 72 hours under Punjab LGA 2022.",
|
| 143 |
+
"اردو (Urdu)": "سڑک کی مرمت پنجاب مقامی حکومت ایکٹ 2022 کے تحت حکومت کی قانونی ذمہ داری ہے۔ وقت: 72 گھنٹے۔",
|
| 144 |
+
"پنجابی (Punjabi)": "سڑک دی مرمت پنجاب LGA 2022 دے تحت 72 گھنٹیاں وچ سرکار دی ذمہ واری اے۔",
|
| 145 |
+
"سندھی (Sindhi)": "سڙڪ جي مرمت پنجاب LGA 2022 تحت 72 ڪلاڪن ۾ حڪومت جي ذميواري آهي.",
|
| 146 |
},
|
| 147 |
"Pipe Leakage": {
|
| 148 |
+
"English": "Pipe leakage must be repaired within 24 hours by WASA under Punjab Water Act 2019.",
|
| 149 |
+
"اردو (Urdu)": "پائپ لیکیج کی مرمت پنجاب واٹر ایکٹ 2019 کے تحت WASA کی ذمہ داری ہے۔ وقت: 24 گھنٹے۔",
|
| 150 |
+
"پنجابی (Punjabi)": "پائپ لیکیج دی مرمت پنجاب واٹر ایکٹ 2019 دے تحت WASA دی 24 گھنٹیاں وچ ذمہ واری اے۔",
|
| 151 |
+
"سندھی (Sindhi)": "پائپ ليڪيج جي مرمت پنجاب واٽر ايڪٽ 2019 تحت WASA جي 24 ڪلاڪن ۾ ذميواري آهي.",
|
| 152 |
},
|
| 153 |
}
|
| 154 |
|
| 155 |
+
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}
|
| 156 |
+
|
| 157 |
+
LANG_CODES = {
|
| 158 |
+
"English": "en",
|
| 159 |
+
"اردو (Urdu)": "ur",
|
| 160 |
+
"پنجابی (Punjabi)": "pa",
|
| 161 |
+
"سندھی (Sindhi)": "sd",
|
| 162 |
+
}
|
| 163 |
|
| 164 |
+
# ─── YOLO ─────────────────────────────────────────────────────
|
| 165 |
def detect_with_yolo(image_pil, issue_type):
|
| 166 |
try:
|
| 167 |
from ultralytics import YOLO
|
| 168 |
import numpy as np
|
| 169 |
+
model = YOLO("yolo26n.pt")
|
|
|
|
| 170 |
img_np = np.array(image_pil)
|
| 171 |
results = model(img_np, verbose=False)
|
| 172 |
result = results[0]
|
|
|
|
| 176 |
clean_issue = issue_type.split(" ", 1)[-1]
|
| 177 |
for box in result.boxes:
|
| 178 |
cls_id = int(box.cls[0])
|
| 179 |
+
conf = float(box.conf[0])
|
| 180 |
+
label = names.get(cls_id, f"class_{cls_id}")
|
| 181 |
detected.append(f"{label} ({conf:.0%})")
|
| 182 |
if clean_issue == "Garbage" and cls_id in WASTE_CLASS_IDS:
|
| 183 |
severity = min(10, severity + 2)
|
| 184 |
elif clean_issue in ("Pot Hole", "Pipe Leakage"):
|
| 185 |
severity = min(10, severity + 1)
|
| 186 |
annotated_np = result.plot()
|
| 187 |
+
annotated = Image.fromarray(annotated_np)
|
| 188 |
+
summary = (f"Detected {len(detected)} object(s): {', '.join(detected[:5])}"
|
| 189 |
+
if detected else "No specific objects detected by YOLO.")
|
| 190 |
+
return annotated, summary, max(severity, 3)
|
| 191 |
except ImportError:
|
| 192 |
return image_pil, "YOLO not installed – skipping object detection.", 5
|
| 193 |
except Exception as e:
|
| 194 |
return image_pil, f"YOLO error: {e}", 5
|
| 195 |
|
| 196 |
+
# ─── GEMINI ───────────────────────────────────────────────────
|
| 197 |
def analyze_with_gemini(image_pil, issue, location, city, yolo_summary):
|
| 198 |
if not GOOGLE_API_KEY:
|
| 199 |
+
return "WARNING GOOGLE_API_KEY not set. Gemini analysis skipped."
|
| 200 |
try:
|
| 201 |
import google.generativeai as genai
|
| 202 |
genai.configure(api_key=GOOGLE_API_KEY)
|
|
|
|
| 204 |
buf = io.BytesIO()
|
| 205 |
image_pil.save(buf, format="JPEG")
|
| 206 |
image_bytes = buf.getvalue()
|
| 207 |
+
prompt = f"""You are a STRICT Pakistani Civic Issue Inspector AI.
|
| 208 |
+
REPORTED ISSUE: '{issue}' | CITY: {city} | LOCATION: {location}
|
| 209 |
+
YOLO: {yolo_summary}
|
| 210 |
+
|
| 211 |
+
Check if image shows: Garbage=actual waste/litter, Pot Hole=visible road hole, Pipe Leakage=water from pipe.
|
| 212 |
+
Clean/indoor/person-only/vegetation REJECT. Be strict.
|
| 213 |
+
|
| 214 |
+
Respond ONLY in this format:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
STATUS: [APPROVED or REJECTED]
|
| 216 |
+
REASON: [2-3 sentences English]
|
| 217 |
+
REASON_URDU: [2-3 sentences Urdu]
|
| 218 |
+
SEVERITY: [1-10]
|
| 219 |
+
CONFIDENCE: [XX%]
|
| 220 |
+
RECOMMENDED_ACTION: [one sentence]"""
|
|
|
|
| 221 |
image_part = {"mime_type": "image/jpeg", "data": base64.b64encode(image_bytes).decode()}
|
| 222 |
+
response = model.generate_content([prompt, image_part])
|
| 223 |
return response.text.strip()
|
| 224 |
except Exception as e:
|
| 225 |
+
return f"WARNING Gemini error: {e}"
|
| 226 |
|
| 227 |
def parse_gemini_response(gemini_text):
|
| 228 |
+
r = {"status":"UNKNOWN","reason":"Could not parse.","reason_urdu":"","severity":5,"confidence":"0%","action":""}
|
| 229 |
+
if not gemini_text: return r
|
| 230 |
+
m = re.search(r"STATUS:\s*(APPROVED|REJECTED)", gemini_text, re.IGNORECASE)
|
| 231 |
+
if m: r["status"] = m.group(1).upper()
|
| 232 |
+
m = re.search(r"SEVERITY:\s*(\d+)", gemini_text)
|
| 233 |
+
if m: r["severity"] = int(m.group(1))
|
| 234 |
+
m = re.search(r"CONFIDENCE:\s*(\d+%)", gemini_text, re.IGNORECASE)
|
| 235 |
+
if m: r["confidence"] = m.group(1)
|
| 236 |
+
m = re.search(r"REASON:\s*(.+?)(?=REASON_URDU:|SEVERITY:|$)", gemini_text, re.DOTALL|re.IGNORECASE)
|
| 237 |
+
if m: r["reason"] = m.group(1).strip()
|
| 238 |
+
m = re.search(r"REASON_URDU:\s*(.+?)(?=SEVERITY:|$)", gemini_text, re.DOTALL|re.IGNORECASE)
|
| 239 |
+
if m: r["reason_urdu"] = m.group(1).strip()
|
| 240 |
+
m = re.search(r"RECOMMENDED_ACTION:\s*(.+?)(?=$)", gemini_text, re.DOTALL)
|
| 241 |
+
if m: r["action"] = m.group(1).strip()
|
| 242 |
+
return r
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
# ─── LLAMA 3 LEGAL ADVICE ─────────────────────────────────────
|
| 245 |
+
def analyze_with_llama(issue, location, city, yolo_summary, severity, language="English"):
|
| 246 |
+
clean = issue.split(" ", 1)[-1]
|
| 247 |
+
kb = LEGAL_KB.get(clean, {})
|
| 248 |
+
lang_map = {
|
| 249 |
+
"اردو (Urdu)": "Respond entirely in Urdu.",
|
| 250 |
+
"پنجابی (Punjabi)": "Respond in Punjabi (Shahmukhi script).",
|
| 251 |
+
"سندھی (Sindhi)": "Respond in Sindhi.",
|
| 252 |
+
}
|
| 253 |
+
lang_instruction = lang_map.get(language, "Respond in clear professional English.")
|
| 254 |
+
|
| 255 |
if not GROQ_API_KEY:
|
| 256 |
+
rights = "\n".join(f" * {r}" for r in kb.get("citizen_rights", []))
|
| 257 |
+
return (
|
| 258 |
+
"Applicable Laws:\n" + "\n".join(f" * {l}" for l in kb.get("laws",[])) +
|
| 259 |
+
f"\n\nCitizen Rights:\n{rights}" +
|
| 260 |
+
f"\n\nFine/Penalty: {kb.get('fine','N/A')}" +
|
| 261 |
+
f"\nAuthority Hotline: {kb.get('hotline','N/A')}" +
|
| 262 |
+
f"\nExpected Response: {kb.get('response','N/A')}" +
|
| 263 |
+
f"\n\nEscalation: {kb.get('escalation','N/A')}" +
|
| 264 |
+
"\n\n(Set GROQ_API_KEY for AI-generated detailed legal advice)"
|
| 265 |
+
)
|
| 266 |
try:
|
| 267 |
from groq import Groq
|
| 268 |
client = Groq(api_key=GROQ_API_KEY)
|
| 269 |
+
prompt = f"""You are a Pakistani civic law expert for the Rahbar citizen platform.
|
| 270 |
+
{lang_instruction}
|
| 271 |
+
|
| 272 |
+
Complaint: {issue} in {location}, {city} | Severity: {severity}/10
|
| 273 |
+
Applicable Laws: {', '.join(kb.get('laws',[]))}
|
| 274 |
+
Response Time Requirement: {kb.get('response','72 hours')}
|
| 275 |
+
|
| 276 |
+
Provide structured legal advice:
|
| 277 |
+
1. Citizen's specific legal rights (cite law names/sections)
|
| 278 |
+
2. Exact steps to file a formal complaint (numbered)
|
| 279 |
+
3. What to do if authority does not respond in time
|
| 280 |
+
4. Possible compensation/action available
|
| 281 |
+
5. Relevant hotlines and escalation contacts
|
| 282 |
+
|
| 283 |
+
Keep it concise and practical for a common Pakistani citizen."""
|
| 284 |
+
chat = client.chat.completions.create(
|
| 285 |
+
model="llama-3.3-70b-versatile",
|
| 286 |
+
messages=[{"role": "user", "content": prompt}],
|
| 287 |
+
max_tokens=700
|
| 288 |
+
)
|
| 289 |
return chat.choices[0].message.content.strip()
|
| 290 |
except Exception as e:
|
| 291 |
+
return f"Llama 3 error: {e}"
|
| 292 |
+
|
| 293 |
+
# ─── CHATBOT — Gradio 6+ dict format ─────────────────────────
|
| 294 |
+
def legal_chatbot(user_message, history, language):
|
| 295 |
+
"""
|
| 296 |
+
Gradio 6 with type='messages': history = list of {"role":..., "content":...} dicts
|
| 297 |
+
"""
|
| 298 |
+
if history is None:
|
| 299 |
+
history = []
|
| 300 |
+
if not user_message.strip():
|
| 301 |
+
return history, ""
|
| 302 |
+
|
| 303 |
+
lang_map = {
|
| 304 |
+
"English": "Respond in clear professional English.",
|
| 305 |
+
"اردو (Urdu)": "اردو میں جواب دیں۔",
|
| 306 |
+
"پنجابی (Punjabi)": "پنجابی (شاہ مکھی) وچ جواب دیو۔",
|
| 307 |
+
"سندھی (Sindhi)": "سنڌي ۾ جواب ڏيو.",
|
| 308 |
+
}
|
| 309 |
+
lang_instruction = lang_map.get(language, "Respond in English.")
|
| 310 |
+
|
| 311 |
+
system_content = f"""You are Rahbar Legal Assistant — a civic rights advisor for Pakistani citizens.
|
| 312 |
+
{lang_instruction}
|
| 313 |
+
|
| 314 |
+
Specialise ONLY in: water supply (WASA, pipe leakage, contamination), sanitation/garbage, pot holes, road maintenance, Pakistani civic law, complaint filing and escalation.
|
| 315 |
+
|
| 316 |
+
Knowledge base:
|
| 317 |
+
{WATER_SANITATION_KB}
|
| 318 |
+
|
| 319 |
+
Rules:
|
| 320 |
+
- Cite specific Pakistani laws/sections
|
| 321 |
+
- Give hotline numbers when relevant
|
| 322 |
+
- Be empathetic; stay on topic
|
| 323 |
+
- Max 250 words unless complex legal explanation needed"""
|
| 324 |
+
|
| 325 |
+
if not GROQ_API_KEY:
|
| 326 |
+
q = user_message.lower()
|
| 327 |
+
if any(w in q for w in ["wasa","pipe","water","leakage","leak"]):
|
| 328 |
+
kb = LEGAL_KB["Pipe Leakage"]
|
| 329 |
+
elif any(w in q for w in ["garbage","waste","trash","kachra","sanitation"]):
|
| 330 |
+
kb = LEGAL_KB["Garbage"]
|
| 331 |
+
elif any(w in q for w in ["pothole","pot hole","road","sadak"]):
|
| 332 |
+
kb = LEGAL_KB["Pot Hole"]
|
| 333 |
+
else:
|
| 334 |
+
kb = None
|
| 335 |
+
if kb:
|
| 336 |
+
answer = (f"Laws: {', '.join(kb['laws'][:2])}\n"
|
| 337 |
+
f"Hotline: {kb['hotline']}\n"
|
| 338 |
+
f"Response Time: {kb['response']}\n"
|
| 339 |
+
f"Escalation: {kb.get('escalation','CM Portal: 0800-02345')}\n\n"
|
| 340 |
+
"(Set GROQ_API_KEY for full AI answers)")
|
| 341 |
+
else:
|
| 342 |
+
answer = "I can help with water, sanitation, garbage, and road issues in Pakistan. Please ask about a specific civic issue."
|
| 343 |
+
return history + [
|
| 344 |
+
{"role": "user", "content": user_message},
|
| 345 |
+
{"role": "assistant", "content": answer},
|
| 346 |
+
], ""
|
| 347 |
|
| 348 |
+
try:
|
| 349 |
+
from groq import Groq
|
| 350 |
+
client = Groq(api_key=GROQ_API_KEY)
|
| 351 |
+
# Build Groq API messages
|
| 352 |
+
api_messages = [{"role": "system", "content": system_content}]
|
| 353 |
+
for msg in history[-20:]:
|
| 354 |
+
api_messages.append({"role": msg["role"], "content": msg["content"]})
|
| 355 |
+
api_messages.append({"role": "user", "content": user_message})
|
| 356 |
+
|
| 357 |
+
resp = client.chat.completions.create(
|
| 358 |
+
model="llama-3.3-70b-versatile",
|
| 359 |
+
messages=api_messages,
|
| 360 |
+
max_tokens=400
|
| 361 |
+
)
|
| 362 |
+
answer = resp.choices[0].message.content.strip()
|
| 363 |
+
except Exception as e:
|
| 364 |
+
answer = f"Error: {e}"
|
| 365 |
+
|
| 366 |
+
return history + [
|
| 367 |
+
{"role": "user", "content": user_message},
|
| 368 |
+
{"role": "assistant", "content": answer},
|
| 369 |
+
], ""
|
| 370 |
+
|
| 371 |
+
# ─── TTS ──────────────────────────────────────────────────────
|
| 372 |
def make_tts(text, language):
|
| 373 |
try:
|
| 374 |
from gtts import gTTS
|
| 375 |
lang_code = LANG_CODES.get(language, "en")
|
| 376 |
+
tts = gTTS(text=str(text)[:600], lang=lang_code, slow=False)
|
| 377 |
path = f"/tmp/tts_{uuid.uuid4().hex[:8]}.mp3"
|
| 378 |
tts.save(path)
|
| 379 |
return path
|
|
|
|
| 381 |
print(f"TTS error: {e}")
|
| 382 |
return None
|
| 383 |
|
| 384 |
+
# ─── STT ──────────────────────────────────────────────────────
|
| 385 |
def stt(audio_file):
|
| 386 |
if audio_file is None:
|
| 387 |
+
return "No audio provided. Please record or upload audio first."
|
| 388 |
try:
|
| 389 |
import speech_recognition as sr
|
| 390 |
recognizer = sr.Recognizer()
|
| 391 |
with sr.AudioFile(audio_file) as src:
|
| 392 |
+
recognizer.adjust_for_ambient_noise(src, duration=0.5)
|
| 393 |
audio_data = recognizer.record(src)
|
| 394 |
text = recognizer.recognize_google(audio_data)
|
| 395 |
return text
|
| 396 |
+
except sr.UnknownValueError:
|
| 397 |
+
return "Could not understand audio. Please speak clearly and try again."
|
| 398 |
+
except sr.RequestError as e:
|
| 399 |
+
return f"Speech recognition service error: {e}"
|
| 400 |
except Exception as e:
|
| 401 |
+
return f"STT error: {e}"
|
| 402 |
|
| 403 |
+
# ─── LAW REFERENCE ────────────────────────────────────────────
|
| 404 |
def law_info(issue, language):
|
| 405 |
+
clean = issue.split(" ", 1)[-1]
|
| 406 |
+
kb = LEGAL_KB.get(clean, {})
|
| 407 |
+
local = LOCALIZED.get(clean, {}).get(language, "")
|
| 408 |
+
rights = "\n".join(f" * {r}" for r in kb.get("citizen_rights", []))
|
| 409 |
+
out = f"## Legal Reference: {issue}\n\n"
|
| 410 |
+
out += "### Applicable Laws\n"
|
| 411 |
for law in kb.get("laws", []):
|
| 412 |
+
out += f" * {law}\n"
|
| 413 |
+
out += (f"\n### Fine / Penalty\n{kb.get('fine','N/A')}\n"
|
| 414 |
+
f"\n### Responsible Authority\n{kb.get('authority','N/A')}\n"
|
| 415 |
+
f"\n### Helpline\n**{kb.get('hotline','N/A')}**\n"
|
| 416 |
+
f"\n### Mandated Response Time\n{kb.get('response','N/A')}\n"
|
| 417 |
+
f"\n### Your Rights as a Citizen\n{rights}\n"
|
| 418 |
+
f"\n### Escalation Path\n{kb.get('escalation','N/A')}\n")
|
| 419 |
if local:
|
| 420 |
+
out += f"\n---\n### Localized Message ({language})\n> {local}\n"
|
| 421 |
+
out += f"\n---\n*Source: {kb.get('dataset_ref','Pakistani civic law databases')}*"
|
| 422 |
return out
|
| 423 |
|
| 424 |
+
# ─── WHATSAPP ─────────────────────────────────────────────────
|
| 425 |
def make_whatsapp_link(text):
|
| 426 |
+
return f"https://wa.me/?text={urllib.parse.quote(text[:1000])}"
|
|
|
|
| 427 |
|
| 428 |
# ─── ADMIN STATS ──────────────────────────────────────────────
|
| 429 |
def get_admin_stats():
|
| 430 |
total = len(complaint_log)
|
| 431 |
if total == 0:
|
| 432 |
return "No complaints filed yet.", ""
|
| 433 |
+
counts = {"Garbage": 0, "Pot Hole": 0, "Pipe Leakage": 0}
|
| 434 |
+
cities = {}
|
| 435 |
severities = []
|
| 436 |
for c in complaint_log:
|
| 437 |
+
issue = c.get("issue","").split(" ",1)[-1]
|
| 438 |
counts[issue] = counts.get(issue, 0) + 1
|
| 439 |
+
city = c.get("city","Unknown")
|
| 440 |
cities[city] = cities.get(city, 0) + 1
|
| 441 |
severities.append(c.get("severity", 5))
|
| 442 |
+
avg_sev = sum(severities)/len(severities) if severities else 0
|
| 443 |
top_city = max(cities, key=cities.get) if cities else "N/A"
|
| 444 |
+
stats_md = f"""## Admin Dashboard
|
|
|
|
| 445 |
| Metric | Value |
|
| 446 |
|--------|-------|
|
| 447 |
| Total Complaints | **{total}** |
|
|
|
|
| 451 |
### By Issue Type
|
| 452 |
| Issue | Count |
|
| 453 |
|-------|-------|
|
| 454 |
+
| Garbage | {counts['Garbage']} |
|
| 455 |
+
| Pot Hole | {counts['Pot Hole']} |
|
| 456 |
+
| Pipe Leakage | {counts['Pipe Leakage']} |
|
| 457 |
|
| 458 |
### By City
|
| 459 |
"""
|
| 460 |
for city, cnt in sorted(cities.items(), key=lambda x: -x[1]):
|
| 461 |
stats_md += f"| {city} | {cnt} |\n"
|
| 462 |
+
log_md = "## Recent Complaints\n\n"
|
| 463 |
for c in reversed(complaint_log[-10:]):
|
| 464 |
+
log_md += (f"**{c['id']}** | {c['timestamp']} | {c['city']}, {c['location']} | "
|
| 465 |
+
f"{c['issue']} | Severity {c['severity']}/10 | "
|
| 466 |
+
f"Citizen: {c.get('name','N/A')} (CNIC: {c.get('cnic','N/A')})\n\n")
|
| 467 |
return stats_md, log_md
|
| 468 |
|
| 469 |
def severity_icon(score):
|
| 470 |
+
if score <= 3: return "GREEN"
|
| 471 |
+
if score <= 6: return "YELLOW"
|
| 472 |
+
if score <= 8: return "ORANGE"
|
| 473 |
+
return "RED"
|
| 474 |
+
|
| 475 |
+
# ─── PROFESSIONAL REPORT ──────────────────────────────────────
|
| 476 |
+
def build_professional_report(complaint_id, timestamp, name, cnic, phone, city, location,
|
| 477 |
+
issue_type, language, severity, sev_icon,
|
| 478 |
+
yolo_summary, gemini_status, gemini_reason,
|
| 479 |
+
gemini_confidence, gemini_action, kb, description, local_msg):
|
| 480 |
+
date_str = datetime.datetime.now().strftime("%d %B %Y")
|
| 481 |
+
time_str = datetime.datetime.now().strftime("%I:%M %p")
|
| 482 |
+
report = f"""
|
| 483 |
+
===========================================================================
|
| 484 |
+
GOVERNMENT OF PAKISTAN - CIVIC COMPLAINT REPORT
|
| 485 |
+
Rahbar Digital Civic Redressal System
|
| 486 |
+
===========================================================================
|
| 487 |
+
|
| 488 |
+
Complaint Reference No. : {complaint_id}
|
| 489 |
+
Date of Filing : {date_str} Time: {time_str}
|
| 490 |
+
Filing Method : Rahbar Digital Platform (AI-Verified)
|
| 491 |
+
Report Language : {language}
|
| 492 |
+
|
| 493 |
+
===========================================================================
|
| 494 |
+
SECTION A - COMPLAINANT INFORMATION
|
| 495 |
+
===========================================================================
|
| 496 |
+
Full Name : {name}
|
| 497 |
+
CNIC : {cnic}
|
| 498 |
+
Contact : {phone if phone else "Not Provided"}
|
| 499 |
+
City : {city}
|
| 500 |
+
Area : {location}
|
| 501 |
+
|
| 502 |
+
===========================================================================
|
| 503 |
+
SECTION B - COMPLAINT DETAILS
|
| 504 |
+
===========================================================================
|
| 505 |
+
Nature : {issue_type}
|
| 506 |
+
Location : {location}, {city}
|
| 507 |
+
Date & Time : {date_str}, {time_str}
|
| 508 |
+
Severity : [{sev_icon}] {severity} / 10
|
| 509 |
+
|
| 510 |
+
Description:
|
| 511 |
+
{description.strip() if description.strip() else "[No additional description provided]"}
|
| 512 |
+
|
| 513 |
+
===========================================================================
|
| 514 |
+
SECTION C - AI VERIFICATION RESULTS
|
| 515 |
+
===========================================================================
|
| 516 |
+
Status : {gemini_status}
|
| 517 |
+
Confidence : {gemini_confidence}
|
| 518 |
+
Finding : {gemini_reason}
|
| 519 |
+
Action : {gemini_action if gemini_action else "Immediate field inspection required."}
|
| 520 |
+
YOLO : {yolo_summary}
|
| 521 |
+
|
| 522 |
+
===========================================================================
|
| 523 |
+
SECTION D - LEGAL FRAMEWORK
|
| 524 |
+
===========================================================================
|
| 525 |
+
Applicable Legislation:"""
|
| 526 |
+
for law in kb.get("laws", []):
|
| 527 |
+
report += f"\n - {law}"
|
| 528 |
+
report += f"""
|
| 529 |
+
|
| 530 |
+
Responsible Authority : {kb.get('authority','N/A')}
|
| 531 |
+
Official Helpline : {kb.get('hotline','N/A')}
|
| 532 |
+
Statutory Response Time: {kb.get('response','N/A')}
|
| 533 |
+
Fine / Penalty : {kb.get('fine','N/A')}
|
| 534 |
+
|
| 535 |
+
===========================================================================
|
| 536 |
+
SECTION E - CITIZEN'S LEGAL RIGHTS
|
| 537 |
+
==========================================================================="""
|
| 538 |
+
for right in kb.get("citizen_rights", []):
|
| 539 |
+
report += f"\n - {right}"
|
| 540 |
+
report += f"""
|
| 541 |
+
|
| 542 |
+
Escalation : {kb.get('escalation','CM Portal: 0800-02345')}
|
| 543 |
+
|
| 544 |
+
===========================================================================
|
| 545 |
+
SECTION F - LOCALIZED NOTICE ({language})
|
| 546 |
+
===========================================================================
|
| 547 |
+
{local_msg}
|
| 548 |
+
|
| 549 |
+
===========================================================================
|
| 550 |
+
SECTION G - ACTION DIRECTIVE
|
| 551 |
+
===========================================================================
|
| 552 |
+
MANDATORY ACTION REQUIRED WITHIN: {kb.get('response','72 hours').upper()}
|
| 553 |
+
|
| 554 |
+
Authority: {kb.get('authority','Local Authority')}
|
| 555 |
+
Hotline : {kb.get('hotline','N/A')}
|
| 556 |
+
Portal : citizenportal.gov.pk | CM: 0800-02345
|
| 557 |
+
|
| 558 |
+
===========================================================================
|
| 559 |
+
DECLARATION
|
| 560 |
+
===========================================================================
|
| 561 |
+
I, {name} (CNIC: {cnic}), declare that the information provided is
|
| 562 |
+
true and correct to the best of my knowledge.
|
| 563 |
+
|
| 564 |
+
Signature: ________________________ (Digital) Date: {date_str}
|
| 565 |
+
Reference: {complaint_id}
|
| 566 |
+
|
| 567 |
+
===========================================================================
|
| 568 |
+
FOR OFFICIAL USE ONLY
|
| 569 |
+
===========================================================================
|
| 570 |
+
Received By : ___________________________
|
| 571 |
+
Date of Receipt : ___________________________
|
| 572 |
+
Action Taken : ___________________________
|
| 573 |
+
Resolved On : ___________________________
|
| 574 |
+
|
| 575 |
+
Generated by: Rahbar - Pakistan's AI Civic Redressal Platform
|
| 576 |
+
Timestamp: {timestamp}
|
| 577 |
+
===========================================================================
|
| 578 |
+
"""
|
| 579 |
+
return report
|
| 580 |
|
| 581 |
# ─── MAIN REPORT FUNCTION ─────────────────────────────────────
|
| 582 |
+
def make_report(image, issue_type, city, location, name, cnic, phone,
|
| 583 |
+
description, language, enable_tts):
|
| 584 |
if image is None:
|
| 585 |
+
return (None, "Please upload an image.", "", "", None, "", None)
|
| 586 |
if not location.strip():
|
| 587 |
+
return (None, "Please enter a location.", "", "", None, "", None)
|
| 588 |
if not name.strip():
|
| 589 |
+
return (None, "Please enter your full name.", "", "", None, "", None)
|
| 590 |
if not cnic.strip():
|
| 591 |
+
return (None, "Please enter your CNIC.", "", "", None, "", None)
|
| 592 |
+
|
| 593 |
complaint_id = f"RB-{uuid.uuid4().hex[:8].upper()}"
|
| 594 |
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 595 |
+
clean_issue = issue_type.split(" ", 1)[-1]
|
| 596 |
+
|
| 597 |
annotated_img, yolo_summary, yolo_severity = detect_with_yolo(image, issue_type)
|
| 598 |
+
gemini_raw = analyze_with_gemini(image, issue_type, location, city, yolo_summary)
|
| 599 |
gemini_parsed = parse_gemini_response(gemini_raw)
|
| 600 |
gemini_status = gemini_parsed["status"]
|
| 601 |
gemini_reason = gemini_parsed["reason"]
|
| 602 |
gemini_severity = gemini_parsed["severity"]
|
|
|
|
|
|
|
|
|
|
| 603 |
|
| 604 |
+
if gemini_status == "REJECTED":
|
| 605 |
+
return (annotated_img,
|
| 606 |
+
f"COMPLAINT REJECTED BY AI VERIFICATION\n\nReason: {gemini_reason}\n"
|
| 607 |
+
f"Confidence: {gemini_parsed.get('confidence','N/A')}\n\n"
|
| 608 |
+
f"Please upload a clear image showing the reported issue ({issue_type}).\n"
|
| 609 |
+
"Your complaint has NOT been logged.",
|
| 610 |
+
"", "", None, complaint_id, None)
|
| 611 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
if gemini_status == "UNKNOWN" and "GOOGLE_API_KEY not set" in gemini_raw:
|
| 613 |
+
gemini_reason = "Gemini API key missing - verification skipped."
|
| 614 |
gemini_status = "APPROVED_WITH_WARNING"
|
| 615 |
+
|
| 616 |
final_severity = gemini_severity if gemini_status == "APPROVED" else yolo_severity
|
| 617 |
+
kb = LEGAL_KB.get(clean_issue, {})
|
| 618 |
+
local = LOCALIZED.get(clean_issue, {}).get(language, "")
|
|
|
|
| 619 |
sev_icon = severity_icon(final_severity)
|
| 620 |
+
|
| 621 |
+
# Legal advice in user's selected language
|
| 622 |
+
llama_advice = analyze_with_llama(issue_type, location, city, yolo_summary, final_severity, language)
|
| 623 |
+
|
| 624 |
+
report = build_professional_report(
|
| 625 |
+
complaint_id, timestamp, name, cnic, phone, city, location,
|
| 626 |
+
issue_type, language, final_severity, sev_icon,
|
| 627 |
+
yolo_summary, gemini_status, gemini_reason,
|
| 628 |
+
gemini_parsed.get("confidence","N/A"), gemini_parsed.get("action",""),
|
| 629 |
+
kb, description, local
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
complaint_log.append({
|
| 633 |
"id": complaint_id, "timestamp": timestamp, "city": city, "location": location,
|
| 634 |
"issue": issue_type, "severity": final_severity, "language": language,
|
| 635 |
"name": name, "cnic": cnic, "phone": phone,
|
| 636 |
})
|
| 637 |
+
|
| 638 |
+
wa_text = (f"Rahbar Complaint\nRef: {complaint_id}\nIssue: {issue_type}\n"
|
| 639 |
+
f"Location: {location}, {city}\nSeverity: {final_severity}/10\n"
|
| 640 |
+
f"Authority: {kb.get('authority','N/A')}\nHotline: {kb.get('hotline','N/A')}\n"
|
| 641 |
+
f"Filed: {timestamp}")
|
| 642 |
+
wa_md = f"[Share on WhatsApp]({make_whatsapp_link(wa_text)})"
|
| 643 |
+
|
| 644 |
+
# Report TTS in selected language, includes localized law notice
|
| 645 |
+
report_tts_path = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
if enable_tts:
|
| 647 |
+
tts_text = (f"Complaint {complaint_id} filed. "
|
| 648 |
+
f"Issue: {issue_type} at {location} in {city}. "
|
| 649 |
+
f"Severity: {final_severity} out of 10. {local}")
|
| 650 |
+
report_tts_path = make_tts(tts_text, language)
|
| 651 |
+
|
| 652 |
+
# Legal advice TTS always generated in selected language
|
| 653 |
+
advice_tts_path = make_tts(llama_advice[:600], language)
|
| 654 |
+
|
| 655 |
+
return annotated_img, report, wa_md, llama_advice, report_tts_path, complaint_id, advice_tts_path
|
| 656 |
|
| 657 |
# ─── CSS ──────────────────────────────────────────────────────
|
| 658 |
CSS = """
|
| 659 |
@import url('https://fonts.googleapis.com/css2?family=Noto+Nastaliq+Urdu&family=Playfair+Display:wght@700;900&family=DM+Sans:wght@300;400;500;600&display=swap');
|
| 660 |
+
:root{
|
| 661 |
+
--g9:#0d2b1e;--g8:#14432e;--g7:#1a5c3f;--g6:#1f7a52;--g5:#25a06b;--g4:#2ec97f;
|
| 662 |
+
--a5:#f5a623;--a4:#f7bc57;--w:#fafdf8;--gr:#8fa88a;
|
| 663 |
+
--sh:0 2px 8px rgba(13,43,30,.15);--r:12px;--rl:20px;
|
| 664 |
+
}
|
| 665 |
+
*,*::before,*::after{box-sizing:border-box}
|
| 666 |
+
body,.gradio-container{font-family:'DM Sans',sans-serif!important;background:var(--w)!important;color:var(--g9)!important}
|
| 667 |
+
.saaf-header{background:linear-gradient(135deg,var(--g8) 0%,var(--g9) 60%,#0a1f14 100%);border-bottom:2px solid var(--g6);padding:24px 20px 20px;text-align:center;position:relative;overflow:hidden}
|
| 668 |
+
.saaf-header::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 60% 60% at 50% 0%,rgba(37,160,107,.18),transparent);pointer-events:none}
|
| 669 |
+
.saaf-header h1{font-family:'Playfair Display',serif!important;font-size:clamp(1.8rem,5vw,3rem)!important;font-weight:900!important;color:var(--w)!important;margin:0 0 4px!important;line-height:1.1}
|
| 670 |
+
.saaf-header .urdu-title{font-family:'Noto Nastaliq Urdu',serif;font-size:clamp(1rem,3vw,1.6rem);color:var(--a4);direction:rtl;margin:4px 0 8px}
|
| 671 |
+
.saaf-header .tagline{font-size:clamp(.75rem,2vw,.95rem);color:var(--g4);letter-spacing:.08em;text-transform:uppercase}
|
| 672 |
+
.badge-strip{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;padding:10px 16px;background:var(--w);border-bottom:1px solid var(--g7)}
|
| 673 |
+
.badge{font-size:.72rem;font-weight:600;letter-spacing:.06em;padding:4px 12px;border-radius:20px;text-transform:uppercase}
|
| 674 |
+
.badge-ai{background:var(--w);color:var(--g4);border:1px solid var(--g6)}
|
| 675 |
+
.badge-pk{background:var(--w);color:var(--a4);border:1px solid rgba(245,166,35,.4)}
|
| 676 |
+
.badge-live{background:var(--w);color:#ff8080;border:1px solid rgba(232,83,83,.4)}
|
| 677 |
+
.tab-nav{background:var(--g8)!important;border-bottom:2px solid var(--g7)!important}
|
| 678 |
+
.tab-nav button{font-family:'DM Sans',sans-serif!important;font-weight:500!important;font-size:.85rem!important;color:var(--gr)!important;padding:12px 18px!important;border-radius:0!important;transition:all .2s!important}
|
| 679 |
+
.tab-nav button.selected,.tab-nav button[aria-selected="true"]{color:var(--a4)!important;border-bottom:3px solid var(--a5)!important;background:transparent!important}
|
| 680 |
+
.card-title{font-size:.7rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--g4);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--g7)}
|
| 681 |
+
label,.gradio-container .label-wrap span{color:var(--g9)!important}
|
| 682 |
+
.gradio-container input,.gradio-container textarea{background:var(--w)!important;border:1px solid var(--g6)!important;border-radius:var(--r)!important;color:var(--g9)!important;font-family:'DM Sans',sans-serif!important}
|
| 683 |
+
.gradio-container input:focus,.gradio-container textarea:focus{border-color:var(--a5)!important;outline:none!important;box-shadow:0 0 0 3px rgba(245,166,35,.15)!important}
|
| 684 |
+
.gradio-container .wrap{background:var(--w)!important;border-color:var(--g6)!important}
|
| 685 |
+
.gradio-container .block{background:var(--w)!important}
|
| 686 |
+
.gradio-container button.primary{background:linear-gradient(135deg,var(--g6),var(--g5))!important;color:var(--w)!important;border:none!important;border-radius:var(--r)!important;font-weight:600!important;font-size:.9rem!important;padding:12px 24px!important;cursor:pointer!important;box-shadow:var(--sh)!important;transition:all .2s!important}
|
| 687 |
+
.gradio-container button.primary:hover{background:linear-gradient(135deg,var(--g5),var(--g4))!important;transform:translateY(-1px)!important}
|
| 688 |
+
.gradio-container button.secondary{background:var(--w)!important;border:1px solid var(--g6)!important;color:var(--g4)!important}
|
| 689 |
+
.gradio-container [data-testid="image"]{border:2px dashed var(--g6)!important;border-radius:var(--rl)!important}
|
| 690 |
+
.gradio-container .prose h2,.gradio-container .prose h3{color:var(--a4)!important}
|
| 691 |
+
.gradio-container audio{width:100%!important;border-radius:var(--r)!important}
|
| 692 |
+
.info-box{background:var(--w);border:1px solid var(--g6);border-left:4px solid var(--g5);border-radius:var(--r);padding:12px 16px;font-size:.88rem;line-height:1.6;margin-bottom:8px}
|
| 693 |
+
.warn-box{background:var(--w);border:1px solid rgba(245,166,35,.4);border-left:4px solid var(--a5);border-radius:var(--r);padding:12px 16px;font-size:.88rem;margin-bottom:8px}
|
| 694 |
+
.hotline-pill{display:inline-block;background:var(--w);color:var(--a4);border:1px solid rgba(245,166,35,.35);border-radius:20px;padding:2px 12px;font-size:.8rem;font-weight:600}
|
| 695 |
+
#rahbarMapDiv{height:220px;width:100%;border-radius:12px;border:2px solid #1f7a52;margin-bottom:8px;background:#e8f5e9;position:relative}
|
| 696 |
+
@media(max-width:640px){.saaf-header{padding:16px 12px}.tab-nav button{padding:10px 12px!important;font-size:.78rem!important}}
|
| 697 |
+
::-webkit-scrollbar{width:6px;height:6px}
|
| 698 |
+
::-webkit-scrollbar-thumb{background:var(--g6);border-radius:3px}
|
|
|
|
|
|
|
| 699 |
"""
|
| 700 |
|
| 701 |
HEADER_HTML = """
|
| 702 |
<div class="saaf-header">
|
| 703 |
+
<h1>Rahbar</h1>
|
| 704 |
+
<div class="urdu-title">رہبر - ہمارا حق</div>
|
| 705 |
<div class="tagline">Pakistan's AI-Powered Civic Complaint System</div>
|
| 706 |
</div>
|
| 707 |
<div class="badge-strip">
|
| 708 |
+
<span class="badge badge-ai">Gemini 2.0</span>
|
| 709 |
+
<span class="badge badge-ai">YOLOv8</span>
|
| 710 |
+
<span class="badge badge-ai">Llama 3</span>
|
| 711 |
+
<span class="badge badge-pk">4 Languages</span>
|
| 712 |
+
<span class="badge badge-pk">Live Laws</span>
|
| 713 |
+
<span class="badge badge-live">LIVE</span>
|
| 714 |
+
</div>
|
| 715 |
+
"""
|
| 716 |
+
|
| 717 |
+
MAP_HTML = """
|
| 718 |
+
<div style="margin:14px 0 4px">
|
| 719 |
+
<div class="card-title">GPS / Map Location Picker</div>
|
| 720 |
+
<div class="warn-box">
|
| 721 |
+
<strong>Please allow location access</strong> when your browser asks permission.
|
| 722 |
+
Tap the button below, then accept the location prompt.
|
| 723 |
+
Coordinates will auto-fill the location field.
|
| 724 |
+
<br><strong>براہ کرم Location Allow کریں</strong> جب browser پوچھے۔
|
| 725 |
+
</div>
|
| 726 |
+
|
| 727 |
+
<button
|
| 728 |
+
style="background:linear-gradient(135deg,#1f7a52,#25a06b);color:#fafdf8;border:none;
|
| 729 |
+
border-radius:10px;padding:10px 20px;font-size:.9rem;font-weight:600;
|
| 730 |
+
cursor:pointer;width:100%;margin-bottom:8px;display:block"
|
| 731 |
+
onclick="window.rahbarGetGPS()">
|
| 732 |
+
Use My Current GPS Location / اپنی Location استعمال کریں
|
| 733 |
+
</button>
|
| 734 |
+
|
| 735 |
+
<div id="gpsStatus"
|
| 736 |
+
style="font-size:.8rem;min-height:20px;margin-bottom:6px;color:#25a06b;font-weight:500;padding:4px 0">
|
| 737 |
+
Map ready. Tap GPS button to get your location, or tap map to drop a pin.
|
| 738 |
+
</div>
|
| 739 |
+
|
| 740 |
+
<div id="rahbarMapDiv"></div>
|
| 741 |
+
|
| 742 |
+
<p style="font-size:.72rem;color:#8fa88a;margin:4px 0 10px">
|
| 743 |
+
Tap map to drop pin · Drag pin to adjust · GPS button for auto-location
|
| 744 |
+
</p>
|
| 745 |
</div>
|
| 746 |
+
|
| 747 |
+
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
| 748 |
+
|
| 749 |
+
<script>
|
| 750 |
+
(function(){
|
| 751 |
+
var DEFAULT_LAT = 31.5204, DEFAULT_LNG = 74.3587;
|
| 752 |
+
var rbMap = null, rbMark = null, leafletReady = false;
|
| 753 |
+
|
| 754 |
+
function setStatus(msg, color){
|
| 755 |
+
var el = document.getElementById('gpsStatus');
|
| 756 |
+
if(el){ el.textContent = msg; el.style.color = color || '#25a06b'; }
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
function fillField(lat, lng){
|
| 760 |
+
var txt = 'GPS: ' + lat.toFixed(6) + ', ' + lng.toFixed(6);
|
| 761 |
+
setStatus('Pinned: ' + txt);
|
| 762 |
+
document.querySelectorAll('textarea, input[type=text]').forEach(function(el){
|
| 763 |
+
var ph = (el.placeholder || '').toLowerCase();
|
| 764 |
+
var lbl = '';
|
| 765 |
+
var blk = el.closest('.block');
|
| 766 |
+
if(blk){ var l = blk.querySelector('label'); if(l) lbl = l.textContent.toLowerCase(); }
|
| 767 |
+
if(ph.indexOf('gps') > -1 || ph.indexOf('street') > -1 || ph.indexOf('landmark') > -1
|
| 768 |
+
|| lbl.indexOf('street') > -1 || lbl.indexOf('landmark') > -1 || lbl.indexOf('gps') > -1){
|
| 769 |
+
var proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
| 770 |
+
var desc = Object.getOwnPropertyDescriptor(proto, 'value');
|
| 771 |
+
if(desc && desc.set) desc.set.call(el, txt); else el.value = txt;
|
| 772 |
+
el.dispatchEvent(new Event('input', {bubbles:true}));
|
| 773 |
+
el.dispatchEvent(new Event('change', {bubbles:true}));
|
| 774 |
+
}
|
| 775 |
+
});
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
function initMap(lat, lng){
|
| 779 |
+
if(!leafletReady){ setTimeout(function(){ initMap(lat, lng); }, 400); return; }
|
| 780 |
+
var el = document.getElementById('rahbarMapDiv');
|
| 781 |
+
if(!el) return;
|
| 782 |
+
if(rbMap){ rbMap.remove(); rbMap = null; }
|
| 783 |
+
rbMap = L.map('rahbarMapDiv').setView([lat, lng], 15);
|
| 784 |
+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
| 785 |
+
{attribution:'© OpenStreetMap', maxZoom:19}).addTo(rbMap);
|
| 786 |
+
rbMark = L.marker([lat, lng], {draggable:true}).addTo(rbMap)
|
| 787 |
+
.bindPopup('Drag or tap map to move pin').openPopup();
|
| 788 |
+
rbMark.on('dragend', function(e){ var ll=e.target.getLatLng(); fillField(ll.lat, ll.lng); });
|
| 789 |
+
rbMap.on('click', function(e){ rbMark.setLatLng(e.latlng); fillField(e.latlng.lat, e.latlng.lng); });
|
| 790 |
+
setTimeout(function(){ if(rbMap) rbMap.invalidateSize(); }, 350);
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
window.rahbarGetGPS = function(){
|
| 794 |
+
if(!navigator.geolocation){
|
| 795 |
+
setStatus('GPS not supported by this browser.', '#e85353'); return;
|
| 796 |
+
}
|
| 797 |
+
setStatus('Requesting location... please ALLOW when browser asks.', '#f7bc57');
|
| 798 |
+
navigator.geolocation.getCurrentPosition(
|
| 799 |
+
function(pos){
|
| 800 |
+
setStatus('Location found! Placing pin on map...');
|
| 801 |
+
initMap(pos.coords.latitude, pos.coords.longitude);
|
| 802 |
+
fillField(pos.coords.latitude, pos.coords.longitude);
|
| 803 |
+
},
|
| 804 |
+
function(err){
|
| 805 |
+
var msgs = {
|
| 806 |
+
1: 'DENIED - Please allow location in browser settings, then try again.',
|
| 807 |
+
2: 'Location unavailable. Check your GPS/network and retry.',
|
| 808 |
+
3: 'Timeout - please try again.'
|
| 809 |
+
};
|
| 810 |
+
setStatus(msgs[err.code] || 'GPS error: ' + err.message, '#e85353');
|
| 811 |
+
},
|
| 812 |
+
{enableHighAccuracy:true, timeout:15000, maximumAge:0}
|
| 813 |
+
);
|
| 814 |
+
};
|
| 815 |
+
|
| 816 |
+
/* Load Leaflet JS dynamically */
|
| 817 |
+
function loadLeaflet(){
|
| 818 |
+
if(typeof L !== 'undefined'){ leafletReady = true; initMap(DEFAULT_LAT, DEFAULT_LNG); return; }
|
| 819 |
+
var s = document.createElement('script');
|
| 820 |
+
s.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
|
| 821 |
+
s.onload = function(){ leafletReady = true; initMap(DEFAULT_LAT, DEFAULT_LNG); };
|
| 822 |
+
s.onerror = function(){ setStatus('Map failed to load. GPS button still works.', '#f7bc57'); };
|
| 823 |
+
document.head.appendChild(s);
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
function tryStart(){
|
| 827 |
+
if(document.getElementById('rahbarMapDiv')) loadLeaflet();
|
| 828 |
+
else setTimeout(tryStart, 500);
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', tryStart);
|
| 832 |
+
else setTimeout(tryStart, 300);
|
| 833 |
+
})();
|
| 834 |
+
</script>
|
| 835 |
"""
|
| 836 |
|
| 837 |
def update_areas(city):
|
| 838 |
areas = CITIES_AREAS.get(city, ["Enter area manually"])
|
| 839 |
return gr.Dropdown(choices=areas, value=areas[0])
|
| 840 |
|
| 841 |
+
# ─── BUILD UI ─────────────────────────────────────────────────
|
| 842 |
def build_ui():
|
| 843 |
+
with gr.Blocks(title="Rahbar | رہبر") as demo:
|
| 844 |
gr.HTML(HEADER_HTML)
|
| 845 |
+
|
| 846 |
with gr.Tabs():
|
| 847 |
+
|
| 848 |
+
# ── TAB 1: REPORT ISSUE ───────────────────────────
|
| 849 |
+
with gr.Tab("Report Issue"):
|
| 850 |
with gr.Row(equal_height=False):
|
| 851 |
+
|
| 852 |
with gr.Column(scale=1, min_width=280):
|
| 853 |
+
gr.HTML('<div class="card-title">Citizen Details</div>')
|
| 854 |
+
name_tb = gr.Textbox(label="Full Name", placeholder="e.g., Ali Raza", lines=1)
|
| 855 |
+
cnic_tb = gr.Textbox(label="CNIC (without dashes)", placeholder="1234567890123", lines=1)
|
| 856 |
+
phone_tb = gr.Textbox(label="Phone Number (optional)", placeholder="03xxxxxxxxx", lines=1)
|
| 857 |
+
|
| 858 |
+
gr.HTML('<div class="card-title" style="margin-top:12px">Issue Photo</div>')
|
| 859 |
+
gr.HTML('<div class="info-box">Upload a clear photo of the civic issue. On mobile you can use camera directly.</div>')
|
| 860 |
+
image_input = gr.Image(type="pil", label="Upload / Take Photo", height=200)
|
| 861 |
+
|
| 862 |
+
gr.HTML('<div class="card-title" style="margin-top:12px">Complaint Details</div>')
|
| 863 |
+
issue_type = gr.Radio(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="Issue Type")
|
| 864 |
+
city_dd = gr.Dropdown(choices=list(CITIES_AREAS.keys()), value="Lahore", label="City")
|
| 865 |
+
area_dd = gr.Dropdown(choices=CITIES_AREAS["Lahore"], value="Model Town", label="Area")
|
| 866 |
+
|
| 867 |
+
gr.HTML(MAP_HTML)
|
| 868 |
+
|
| 869 |
+
location_tb = gr.Textbox(
|
| 870 |
+
label="Street / Landmark / GPS Coordinates",
|
| 871 |
+
placeholder="GPS coordinates auto-fill here — or type street/landmark",
|
| 872 |
+
lines=1
|
| 873 |
+
)
|
| 874 |
+
desc_tb = gr.Textbox(label="Description (optional)", placeholder="Describe the issue...", lines=3)
|
| 875 |
+
language_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Report & Voice Language")
|
| 876 |
+
tts_cb = gr.Checkbox(label="Read report aloud (TTS)", value=False)
|
| 877 |
+
submit_btn = gr.Button("Submit Complaint", variant="primary", size="lg")
|
| 878 |
+
|
| 879 |
with gr.Column(scale=2, min_width=300):
|
| 880 |
+
gr.HTML('<div class="card-title">Analysis Results</div>')
|
| 881 |
+
annotated_out = gr.Image(label="AI Detection Result", height=230)
|
| 882 |
+
complaint_id_out = gr.Textbox(label="Complaint ID", interactive=False)
|
| 883 |
+
|
| 884 |
+
gr.HTML('<div class="card-title" style="margin-top:12px">Official Government Report</div>')
|
| 885 |
+
report_out = gr.Textbox(
|
| 886 |
+
label="Full Complaint Report (Ready for Submission)",
|
| 887 |
+
lines=20, interactive=False,
|
| 888 |
+
placeholder="Report will appear here after submission..."
|
| 889 |
+
)
|
| 890 |
+
wa_out = gr.Markdown()
|
| 891 |
+
report_tts_out = gr.Audio(label="Report Voice (in selected language)", autoplay=False)
|
| 892 |
+
|
| 893 |
+
gr.HTML('<div class="card-title" style="margin-top:16px">Legal Advice</div>')
|
| 894 |
+
gr.HTML('<div class="warn-box">AI-generated legal guidance in your selected language, based on Pakistani civic law.</div>')
|
| 895 |
+
legal_advice_out = gr.Textbox(
|
| 896 |
+
label="Legal Rights & Action Steps (in selected language)",
|
| 897 |
+
lines=10, interactive=False,
|
| 898 |
+
placeholder="Legal advice will appear here in your selected language..."
|
| 899 |
+
)
|
| 900 |
+
advice_tts_out = gr.Audio(label="Legal Advice Voice (in selected language)", autoplay=False)
|
| 901 |
+
|
| 902 |
city_dd.change(fn=update_areas, inputs=[city_dd], outputs=[area_dd])
|
| 903 |
+
submit_btn.click(
|
| 904 |
+
fn=make_report,
|
| 905 |
+
inputs=[image_input, issue_type, city_dd, area_dd, name_tb, cnic_tb,
|
| 906 |
+
phone_tb, desc_tb, language_dd, tts_cb],
|
| 907 |
+
outputs=[annotated_out, report_out, wa_out, legal_advice_out,
|
| 908 |
+
report_tts_out, complaint_id_out, advice_tts_out]
|
| 909 |
+
)
|
| 910 |
+
|
| 911 |
+
# ── TAB 2: LAW REFERENCE + CHATBOT ───────────────
|
| 912 |
+
with gr.Tab("Law Reference"):
|
| 913 |
+
gr.HTML('<div class="card-title">Pakistani Civic Laws Database</div>')
|
| 914 |
with gr.Row():
|
| 915 |
law_issue_dd = gr.Dropdown(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="Select Issue", scale=1)
|
| 916 |
+
law_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1)
|
| 917 |
law_out = gr.Markdown()
|
| 918 |
+
gr.Button("Show Law Details", variant="primary").click(
|
| 919 |
+
fn=law_info, inputs=[law_issue_dd, law_lang_dd], outputs=[law_out]
|
| 920 |
+
)
|
| 921 |
+
gr.HTML("""<div class="info-box">
|
| 922 |
+
<strong>Quick Hotlines:</strong><br>
|
| 923 |
+
Garbage: <span class="hotline-pill">1139</span> |
|
| 924 |
+
Roads: <span class="hotline-pill">051-9032800</span> |
|
| 925 |
+
WASA: <span class="hotline-pill">042-99200300</span> |
|
| 926 |
+
CM Portal: <span class="hotline-pill">0800-02345</span>
|
| 927 |
+
</div>""")
|
| 928 |
+
|
| 929 |
+
gr.HTML('<div class="card-title" style="margin-top:24px">Legal Guidance Chatbot</div>')
|
| 930 |
+
gr.HTML("""<div class="info-box">
|
| 931 |
+
Ask anything about water supply, pipe leakage, garbage, WASA, sewage, road issues
|
| 932 |
+
or your legal rights as a Pakistani citizen.
|
| 933 |
+
</div>""")
|
| 934 |
+
chat_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Response Language")
|
| 935 |
+
|
| 936 |
+
# KEY FIX: type="messages" for Gradio 6+
|
| 937 |
+
chatbot = gr.Chatbot(
|
| 938 |
+
label="Rahbar Legal Assistant",
|
| 939 |
+
height=600,
|
| 940 |
+
value=[],
|
| 941 |
+
)
|
| 942 |
+
with gr.Row():
|
| 943 |
+
chat_input = gr.Textbox(
|
| 944 |
+
label="Your Question",
|
| 945 |
+
placeholder="e.g., WASA ne pipe repair nahi ki, mujhe kya karna chahiye?",
|
| 946 |
+
lines=2, scale=4
|
| 947 |
+
)
|
| 948 |
+
chat_send_btn = gr.Button("Send", variant="primary", scale=1)
|
| 949 |
+
|
| 950 |
+
chat_send_btn.click(
|
| 951 |
+
fn=legal_chatbot,
|
| 952 |
+
inputs=[chat_input, chatbot, chat_lang_dd],
|
| 953 |
+
outputs=[chatbot, chat_input]
|
| 954 |
+
)
|
| 955 |
+
chat_input.submit(
|
| 956 |
+
fn=legal_chatbot,
|
| 957 |
+
inputs=[chat_input, chatbot, chat_lang_dd],
|
| 958 |
+
outputs=[chatbot, chat_input]
|
| 959 |
+
)
|
| 960 |
+
|
| 961 |
+
gr.Examples(
|
| 962 |
+
examples=[
|
| 963 |
+
["WASA did not fix the leakage for 3 days. What are my rights?"],
|
| 964 |
+
["میرے علاقے میں پانی آلودہ ہے۔ میں کہاں شکایت کروں؟"],
|
| 965 |
+
["Garbage has not been collected for a week. What law applies?"],
|
| 966 |
+
["How do I escalate if the authority ignores my complaint?"],
|
| 967 |
+
["پانی کا بل غلط آیا ہے، کہاں شکایت کروں؟"],
|
| 968 |
+
],
|
| 969 |
+
inputs=chat_input,
|
| 970 |
+
label="Try These Questions"
|
| 971 |
+
)
|
| 972 |
+
|
| 973 |
+
# ── TAB 3: VOICE INPUT ────────────────────────────
|
| 974 |
+
with gr.Tab("Voice Input"):
|
| 975 |
+
gr.HTML('<div class="card-title">Speech-to-Text (STT)</div>')
|
| 976 |
+
gr.HTML('<div class="warn-box">Record your complaint. Copy the transcription to the Report Issue tab.</div>')
|
| 977 |
+
gr.HTML('<div class="info-box">Allow microphone access when your browser asks. Works best in Chrome/Edge.<br>مائیکروفون کی اجازت دیں جب browser پوچھے۔</div>')
|
| 978 |
+
|
| 979 |
+
audio_in = gr.Audio(
|
| 980 |
+
type="filepath",
|
| 981 |
+
label="Record or Upload Audio",
|
| 982 |
+
sources=["microphone", "upload"]
|
| 983 |
+
)
|
| 984 |
+
stt_btn = gr.Button("Transcribe Audio", variant="primary")
|
| 985 |
+
stt_out = gr.Textbox(label="Transcription (editable — copy to Report tab)", lines=5, interactive=True)
|
| 986 |
stt_btn.click(fn=stt, inputs=[audio_in], outputs=[stt_out])
|
| 987 |
+
|
| 988 |
+
gr.HTML('<div class="card-title" style="margin-top:20px">Text-to-Speech Test</div>')
|
| 989 |
with gr.Row():
|
| 990 |
+
tts_text_in = gr.Textbox(label="Enter text to hear", placeholder="Type something...", scale=3)
|
| 991 |
tts_lang_in = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1)
|
| 992 |
+
tts_test_btn = gr.Button("Play", variant="secondary")
|
| 993 |
+
tts_test_out = gr.Audio(label="Output", autoplay=True)
|
| 994 |
tts_test_btn.click(fn=make_tts, inputs=[tts_text_in, tts_lang_in], outputs=[tts_test_out])
|
| 995 |
+
|
| 996 |
+
# ── TAB 4: ADMIN DASHBOARD ────────────────────────
|
| 997 |
+
with gr.Tab("Admin Dashboard"):
|
| 998 |
+
gr.HTML('<div class="card-title">Complaint Statistics</div>')
|
| 999 |
+
refresh_btn = gr.Button("Refresh Stats", variant="primary")
|
| 1000 |
with gr.Row():
|
| 1001 |
stats_out = gr.Markdown()
|
| 1002 |
+
log_out = gr.Markdown()
|
| 1003 |
refresh_btn.click(fn=get_admin_stats, outputs=[stats_out, log_out])
|
| 1004 |
+
|
| 1005 |
return demo
|
| 1006 |
|
| 1007 |
+
|
| 1008 |
if __name__ == "__main__":
|
| 1009 |
demo = build_ui()
|
| 1010 |
+
demo.launch(
|
| 1011 |
+
server_name="0.0.0.0",
|
| 1012 |
+
server_port=7860,
|
| 1013 |
+
|
| 1014 |
+
css=CSS,
|
| 1015 |
+
share=True,
|
| 1016 |
+
theme=gr.themes.Base(
|
| 1017 |
+
primary_hue=gr.themes.colors.green,
|
| 1018 |
+
secondary_hue=gr.themes.colors.yellow
|
| 1019 |
+
),
|
| 1020 |
+
)
|