Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
╔══════════════════════════════════════════════════════════════╗
|
| 3 |
+
║ Rahbar
|
| 4 |
+
║ yeh sirf complaint nahi, poora civic guide hai
|
| 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"],
|
| 31 |
+
"Islamabad": ["F-7", "F-8", "F-10", "G-9", "G-10", "G-11", "Blue Area"],
|
| 32 |
+
"Rawalpindi": ["Saddar", "Bahria Town", "Chaklala", "Satellite Town", "Murree Road"],
|
| 33 |
+
"Faisalabad": ["Jinnah Colony", "Madina Town", "Peoples Colony", "Ghulam Muhammad Abad", "Susan Road"],
|
| 34 |
+
"Multan": ["Shah Rukn-e-Alam", "Cantt", "Gulgasht Colony", "New Multan", "Bosan Road"],
|
| 35 |
+
"Peshawar": ["Hayatabad", "University Town", "Cantt", "Saddar", "Gulbahar"],
|
| 36 |
+
"Quetta": ["Satellite Town", "Jinnah Town", "Cantt", "Sariab Road", "Brewery Road"],
|
| 37 |
+
}
|
| 38 |
+
|
| 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": ["Punjab Waste Management Act 2014", "Pakistan Environmental Protection Act 1997"],
|
| 46 |
+
"fine": "Rs. 500 – 50,000",
|
| 47 |
+
"authority": "Local Government Department",
|
| 48 |
+
"hotline": "1139",
|
| 49 |
+
"response": "48 hours",
|
| 50 |
+
},
|
| 51 |
+
"Pot Hole": {
|
| 52 |
+
"laws": ["National Highways Safety Ordinance 2000", "Punjab Local Government Act 2022"],
|
| 53 |
+
"fine": "Dept. liable for damages",
|
| 54 |
+
"authority": "National Highway Authority (NHA)",
|
| 55 |
+
"hotline": "051-9032800",
|
| 56 |
+
"response": "72 hours",
|
| 57 |
+
},
|
| 58 |
+
"Pipe Leakage": {
|
| 59 |
+
"laws": ["WASA Act", "Punjab Water Act 2019"],
|
| 60 |
+
"fine": "Compensatory damages applicable",
|
| 61 |
+
"authority": "WASA",
|
| 62 |
+
"hotline": "042-99200300",
|
| 63 |
+
"response": "24 hours",
|
| 64 |
+
},
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
# ─── LOCALIZED TEXT ───────────────────────────────────────────
|
| 68 |
+
LOCALIZED = {
|
| 69 |
+
"Garbage": {
|
| 70 |
+
"English": "Dumping garbage is a criminal offence. Fine: Rs. 500–50,000.",
|
| 71 |
+
"اردو (Urdu)": "کچرا پھینکنا جرم ہے، جرمانہ 500 سے 50,000 روپے",
|
| 72 |
+
"پنجابی (Punjabi)": "ਕੂੜਾ ਸੁੱਟਣਾ ਜੁਰਮ ਹੈ, ਜੁਰਮਾਨਾ 500 ਤੋਂ 50,000 ਰੁਪਏ",
|
| 73 |
+
"سندھی (Sindhi)": "ڪچرو اڇلائڻ جرم آهي، ڏنڊ 500 کان 50,000 رپيا",
|
| 74 |
+
},
|
| 75 |
+
"Pot Hole": {
|
| 76 |
+
"English": "Road repair is the government's responsibility within 72 hours.",
|
| 77 |
+
"اردو (Urdu)": "سڑک کی مرمت حکومت کی ذمہ داری ہے، 72 گھنٹے",
|
| 78 |
+
"پنجابی (Punjabi)": "ਸੜਕ ਦੀ ਮੁਰੰਮਤ ਸਰਕਾਰ ਦੀ ਜ਼ਿੰਮੇਵਾਰੀ ਹੈ",
|
| 79 |
+
"سندھی (Sindhi)": "سڙڪ جي مرمت حڪومت جي ذميواري آهي",
|
| 80 |
+
},
|
| 81 |
+
"Pipe Leakage": {
|
| 82 |
+
"English": "Pipe leakage must be repaired within 24 hours by WASA.",
|
| 83 |
+
"اردو (Urdu)": "پائپ لیکیج کی مرمت 24 گھنٹے میں لازمی ہے",
|
| 84 |
+
"پنجابی (Punjabi)": "ਪਾਈਪ ਲੀਕੇਜ ਦੀ ਮੁਰੰਮਤ 24 ਘੰਟਿਆਂ ਵਿੱਚ ਲਾਜ਼ਮੀ ਹੈ",
|
| 85 |
+
"سندھی (Sindhi)": "پائپ ليڪيج جي مرمت 24 ڪلاڪن ۾ لازمي آهي",
|
| 86 |
+
},
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
# Waste-related COCO class IDs
|
| 90 |
+
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}
|
| 91 |
+
|
| 92 |
+
# ─── YOLO DETECTION ────────────────────────────────���──────────
|
| 93 |
+
def detect_with_yolo(image_pil, issue_type):
|
| 94 |
+
try:
|
| 95 |
+
from ultralytics import YOLO
|
| 96 |
+
import numpy as np
|
| 97 |
+
model_path = "yolo26n.pt"
|
| 98 |
+
model = YOLO(model_path)
|
| 99 |
+
img_np = np.array(image_pil)
|
| 100 |
+
results = model(img_np, verbose=False)
|
| 101 |
+
result = results[0]
|
| 102 |
+
names = model.names
|
| 103 |
+
detected = []
|
| 104 |
+
severity = 1
|
| 105 |
+
clean_issue = issue_type.split(" ", 1)[-1]
|
| 106 |
+
for box in result.boxes:
|
| 107 |
+
cls_id = int(box.cls[0])
|
| 108 |
+
conf = float(box.conf[0])
|
| 109 |
+
label = names.get(cls_id, f"class_{cls_id}")
|
| 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 = Image.fromarray(annotated_np)
|
| 117 |
+
summary = f"Detected {len(detected)} object(s): {', '.join(detected[:5])}" if detected else "No specific objects detected by YOLO."
|
| 118 |
+
severity = max(severity, 3)
|
| 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 ANALYSIS ──────────────────────────────────────────
|
| 126 |
+
def analyze_with_gemini(image_pil, issue, location, city, yolo_summary):
|
| 127 |
+
if not GOOGLE_API_KEY:
|
| 128 |
+
return "⚠️ GOOGLE_API_KEY not set. Gemini analysis skipped."
|
| 129 |
+
try:
|
| 130 |
+
import google.generativeai as genai
|
| 131 |
+
genai.configure(api_key=GOOGLE_API_KEY)
|
| 132 |
+
model = genai.GenerativeModel("gemini-3-flash-preview")
|
| 133 |
+
buf = io.BytesIO()
|
| 134 |
+
image_pil.save(buf, format="JPEG")
|
| 135 |
+
image_bytes = buf.getvalue()
|
| 136 |
+
prompt = f"""
|
| 137 |
+
You are a STRICT Pakistani Civic Issue Inspector AI. Verify if the submitted image shows a real civic problem.
|
| 138 |
+
|
| 139 |
+
REPORTED ISSUE: '{issue}'
|
| 140 |
+
CITY: {city}
|
| 141 |
+
LOCATION: {location}
|
| 142 |
+
YOLO DETECTION SUMMARY: {yolo_summary}
|
| 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 in English]
|
| 158 |
+
REASON_URDU: [2-3 sentences in Urdu]
|
| 159 |
+
SEVERITY: [single integer 1-10]
|
| 160 |
+
CONFIDENCE: [percentage like 87%]
|
| 161 |
+
RECOMMENDED_ACTION: [What authorities should do in one sentence]
|
| 162 |
+
"""
|
| 163 |
+
image_part = {"mime_type": "image/jpeg", "data": base64.b64encode(image_bytes).decode()}
|
| 164 |
+
response = model.generate_content([prompt, image_part])
|
| 165 |
+
return response.text.strip()
|
| 166 |
+
except Exception as e:
|
| 167 |
+
return f"⚠️ Gemini error: {e}"
|
| 168 |
+
|
| 169 |
+
def parse_gemini_response(gemini_text):
|
| 170 |
+
result = {"status": "UNKNOWN", "reason": "Gemini analysis could not be parsed.", "reason_urdu": "", "severity": 5, "confidence": "0%", "action": ""}
|
| 171 |
+
if not gemini_text:
|
| 172 |
+
return result
|
| 173 |
+
status_match = re.search(r"STATUS:\s*(APPROVED|REJECTED)", gemini_text, re.IGNORECASE)
|
| 174 |
+
if status_match:
|
| 175 |
+
result["status"] = status_match.group(1).upper()
|
| 176 |
+
reason_match = re.search(r"REASON:\s*(.+?)(?=REASON_URDU:|$)", gemini_text, re.DOTALL | re.IGNORECASE)
|
| 177 |
+
if reason_match:
|
| 178 |
+
result["reason"] = reason_match.group(1).strip()
|
| 179 |
+
reason_urdu_match = re.search(r"REASON_URDU:\s*(.+?)(?=SEVERITY:|$)", gemini_text, re.DOTALL | re.IGNORECASE)
|
| 180 |
+
if reason_urdu_match:
|
| 181 |
+
result["reason_urdu"] = reason_urdu_match.group(1).strip()
|
| 182 |
+
severity_match = re.search(r"SEVERITY:\s*(\d+)", gemini_text)
|
| 183 |
+
if severity_match:
|
| 184 |
+
result["severity"] = int(severity_match.group(1))
|
| 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 |
+
clean = issue.split(" ", 1)[-1]
|
| 197 |
+
kb = LEGAL_KB.get(clean, {})
|
| 198 |
+
return (f"📚 Applicable Laws: {', '.join(kb.get('laws', []))}\n💰 Fine/Penalty: {kb.get('fine', 'N/A')}\n📞 Authority Hotline: {kb.get('hotline', 'N/A')}\n⏱️ Expected Response: {kb.get('response', 'N/A')}\n\n(Tip: Set GROQ_API_KEY for AI-generated legal advice)")
|
| 199 |
+
try:
|
| 200 |
+
from groq import Groq
|
| 201 |
+
client = Groq(api_key=GROQ_API_KEY)
|
| 202 |
+
clean = issue.split(" ", 1)[-1]
|
| 203 |
+
kb = LEGAL_KB.get(clean, {})
|
| 204 |
+
prompt = f"""You are a Pakistani civic law expert for the Rahbar platform.
|
| 205 |
+
|
| 206 |
+
Complaint: {issue} in {location}, {city}
|
| 207 |
+
Severity: {severity}/10
|
| 208 |
+
Detection: {yolo_summary}
|
| 209 |
+
Applicable Laws: {', '.join(kb.get('laws', []))}
|
| 210 |
+
|
| 211 |
+
Provide:
|
| 212 |
+
1. Citizen's legal rights in this situation
|
| 213 |
+
2. Exact legal sections that apply
|
| 214 |
+
3. How to escalate if the authority doesn't respond within {kb.get('response', '72 hours')}
|
| 215 |
+
4. Official complaint process (step by step)
|
| 216 |
+
5. Compensation/fine the authority may face
|
| 217 |
+
|
| 218 |
+
Be concise and practical for a Pakistani citizen."""
|
| 219 |
+
chat = client.chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "user", "content": prompt}], max_tokens=600)
|
| 220 |
+
return chat.choices[0].message.content.strip()
|
| 221 |
+
except Exception as e:
|
| 222 |
+
return f"⚠️ Llama 3 error: {e}"
|
| 223 |
+
|
| 224 |
+
# ─── TEXT-TO-SPEECH ───────────────────────────────────────────
|
| 225 |
+
LANG_CODES = {"English": "en", "اردو (Urdu)": "ur", "پنجابی (Punjabi)": "pa", "سندھی (Sindhi)": "sd"}
|
| 226 |
+
def make_tts(text, language):
|
| 227 |
+
try:
|
| 228 |
+
from gtts import gTTS
|
| 229 |
+
lang_code = LANG_CODES.get(language, "en")
|
| 230 |
+
tts = gTTS(text=text[:500], lang=lang_code, slow=False)
|
| 231 |
+
path = f"/tmp/tts_{uuid.uuid4().hex[:8]}.mp3"
|
| 232 |
+
tts.save(path)
|
| 233 |
+
return path
|
| 234 |
+
except Exception as e:
|
| 235 |
+
print(f"TTS error: {e}")
|
| 236 |
+
return None
|
| 237 |
+
|
| 238 |
+
# ─── SPEECH-TO-TEXT ───────────────────────────────────────────
|
| 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"⚠️ STT error: {e}"
|
| 251 |
+
|
| 252 |
+
# ─── LEGAL INFO DISPLAY ───────────────────────────────────────
|
| 253 |
+
def law_info(issue, language):
|
| 254 |
+
clean = issue.split(" ", 1)[-1]
|
| 255 |
+
kb = LEGAL_KB.get(clean, {})
|
| 256 |
+
local = LOCALIZED.get(clean, {}).get(language, "")
|
| 257 |
+
out = f"## ⚖️ Legal Reference: {issue}\n\n**📜 Applicable Laws:**\n"
|
| 258 |
+
for law in kb.get("laws", []):
|
| 259 |
+
out += f" • {law}\n"
|
| 260 |
+
out += f"\n**💰 Fine / Penalty:** {kb.get('fine', 'N/A')}\n**🏛️ Authority:** {kb.get('authority', 'N/A')}\n**📞 Hotline:** {kb.get('hotline', 'N/A')}\n**⏱️ Response Time:** {kb.get('response', 'N/A')}\n"
|
| 261 |
+
if local:
|
| 262 |
+
out += f"\n---\n**Localized Message:**\n> {local}\n"
|
| 263 |
+
return out
|
| 264 |
+
|
| 265 |
+
# ─── WHATSAPP SHARE ───────────────────────────────────────────
|
| 266 |
+
def make_whatsapp_link(text):
|
| 267 |
+
encoded = urllib.parse.quote(text[:1000])
|
| 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 = {"Garbage": 0, "Pot Hole": 0, "Pipe Leakage": 0}
|
| 276 |
+
cities = {}
|
| 277 |
+
severities = []
|
| 278 |
+
for c in complaint_log:
|
| 279 |
+
issue = c.get("issue", "").split(" ", 1)[-1]
|
| 280 |
+
counts[issue] = counts.get(issue, 0) + 1
|
| 281 |
+
city = c.get("city", "Unknown")
|
| 282 |
+
cities[city] = cities.get(city, 0) + 1
|
| 283 |
+
severities.append(c.get("severity", 5))
|
| 284 |
+
avg_sev = sum(severities) / len(severities) if severities else 0
|
| 285 |
+
top_city = max(cities, key=cities.get) if cities else "N/A"
|
| 286 |
+
stats_md = f"""## 🛡️ Admin Dashboard
|
| 287 |
+
|
| 288 |
+
| Metric | Value |
|
| 289 |
+
|--------|-------|
|
| 290 |
+
| Total Complaints | **{total}** |
|
| 291 |
+
| Avg Severity | **{avg_sev:.1f}/10** |
|
| 292 |
+
| Top City | **{top_city}** |
|
| 293 |
+
|
| 294 |
+
### By Issue Type
|
| 295 |
+
| Issue | Count |
|
| 296 |
+
|-------|-------|
|
| 297 |
+
| 🗑️ Garbage | {counts['Garbage']} |
|
| 298 |
+
| 🕳️ Pot Hole | {counts['Pot Hole']} |
|
| 299 |
+
| 💧 Pipe Leakage | {counts['Pipe Leakage']} |
|
| 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 = "## 📋 Recent Complaints\n\n"
|
| 306 |
+
for c in reversed(complaint_log[-10:]):
|
| 307 |
+
log_md += f"**{c['id']}** | {c['timestamp']} | {c['city']}, {c['location']} | {c['issue']} | Severity {c['severity']}/10 | Citizen: {c.get('name', 'N/A')} (CNIC: {c.get('cnic', 'N/A')})\n\n"
|
| 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, description, language, enable_tts):
|
| 318 |
+
if image is None:
|
| 319 |
+
return (None, "⚠️ Please upload an image.", "", "", None, "")
|
| 320 |
+
if not location.strip():
|
| 321 |
+
return (None, "⚠️ Please enter a location.", "", "", None, "")
|
| 322 |
+
if not name.strip():
|
| 323 |
+
return (None, "⚠️ Please enter your full name.", "", "", None, "")
|
| 324 |
+
if not cnic.strip():
|
| 325 |
+
return (None, "⚠️ Please enter your CNIC (e.g., 1234567890123).", "", "", None, "")
|
| 326 |
+
|
| 327 |
+
complaint_id = f"RB-{uuid.uuid4().hex[:8].upper()}"
|
| 328 |
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 329 |
+
clean_issue = issue_type.split(" ", 1)[-1]
|
| 330 |
+
|
| 331 |
+
annotated_img, yolo_summary, yolo_severity = detect_with_yolo(image, issue_type)
|
| 332 |
+
gemini_raw = analyze_with_gemini(image, issue_type, location, city, yolo_summary)
|
| 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 |
+
**Reason:** {gemini_reason}
|
| 342 |
+
**Confidence:** {gemini_parsed.get('confidence', 'N/A')}
|
| 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 = "⚠️ Gemini API key missing – verification skipped. Complaint accepted based on YOLO."
|
| 350 |
+
gemini_status = "APPROVED_WITH_WARNING"
|
| 351 |
+
|
| 352 |
+
final_severity = gemini_severity if gemini_status == "APPROVED" else yolo_severity
|
| 353 |
+
llama_advice = analyze_with_llama(issue_type, location, city, yolo_summary, final_severity)
|
| 354 |
+
kb = LEGAL_KB.get(clean_issue, {})
|
| 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 |
+
report = f"""━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 365 |
+
Rahbar
|
| 366 |
+
water/sanitation Q&A + report routing
|
| 367 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 368 |
+
|
| 369 |
+
🆔 Complaint ID : {complaint_id}
|
| 370 |
+
🕐 Timestamp : {timestamp}
|
| 371 |
+
👤 Citizen : {name} (CNIC: {cnic})
|
| 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 by {name}. Issue: {issue_type} at {location} in {city}. Severity: {final_severity} out of 10. {local}"
|
| 419 |
+
tts_path = make_tts(tts_text, language)
|
| 420 |
+
|
| 421 |
+
return annotated_img, report, wa_md, llama_advice, tts_path, complaint_id
|
| 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 { --green-900: #0d2b1e; --green-800: #14432e; --green-700: #1a5c3f; --green-600: #1f7a52; --green-500: #25a06b; --green-400: #2ec97f; --amber-500: #f5a623; --amber-400: #f7bc57; --amber-300: #fbd07a; --red-500: #e85353; --white: #fafdf8; --gray-100: #f0f4ef; --gray-200: #dce6d8; --gray-400: #8fa88a; --shadow-sm: 0 2px 8px rgba(13,43,30,.15); --shadow-md: 0 4px 20px rgba(13,43,30,.2); --shadow-lg: 0 8px 40px rgba(13,43,30,.25); --radius: 12px; --radius-lg: 20px; }
|
| 427 |
+
*, *::before, *::after { box-sizing: border-box; }
|
| 428 |
+
body, .gradio-container { font-family: 'DM Sans', sans-serif !important; background: var(--white) !important; color: var(--green-900) !important; min-height: 100vh; }
|
| 429 |
+
.saaf-header { background: linear-gradient(135deg, var(--green-800) 0%, var(--green-900) 60%, #0a1f14 100%); border-bottom: 2px solid var(--green-600); padding: 24px 20px 20px; text-align: center; position: relative; overflow: hidden; }
|
| 430 |
+
.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; }
|
| 431 |
+
.saaf-header h1 { font-family: 'Playfair Display', serif !important; font-size: clamp(1.8rem, 5vw, 3rem) !important; font-weight: 900 !important; color: var(--white) !important; margin: 0 0 4px !important; letter-spacing: -0.02em; line-height: 1.1; }
|
| 432 |
+
.saaf-header .urdu-title { font-family: 'Noto Nastaliq Urdu', serif; font-size: clamp(1rem, 3vw, 1.6rem); color: var(--amber-400); direction: rtl; margin: 4px 0 8px; }
|
| 433 |
+
.saaf-header .tagline { font-size: clamp(.75rem, 2vw, .95rem); color: var(--green-400); letter-spacing: .08em; text-transform: uppercase; }
|
| 434 |
+
.badge-strip { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; padding: 10px 16px; background: var(--white); border-bottom: 1px solid var(--green-700); }
|
| 435 |
+
.badge { font-size: .72rem; font-weight: 600; letter-spacing: .06em; padding: 4px 12px; border-radius: 20px; text-transform: uppercase; }
|
| 436 |
+
.badge-ai { background: var(--white); color: var(--green-400); border: 1px solid var(--green-600); }
|
| 437 |
+
.badge-pk { background: var(--white); color: var(--amber-400); border: 1px solid rgba(245,166,35,.4); }
|
| 438 |
+
.badge-live { background: var(--white); color: #ff8080; border: 1px solid rgba(232,83,83,.4); }
|
| 439 |
+
.tabs { background: var(--white) !important; }
|
| 440 |
+
.tab-nav { background: var(--green-800) !important; border-bottom: 2px solid var(--green-700) !important; overflow-x: auto; white-space: nowrap; }
|
| 441 |
+
.tab-nav button { font-family: 'DM Sans', sans-serif !important; font-weight: 500 !important; font-size: .85rem !important; color: var(--gray-400) !important; padding: 12px 18px !important; border-radius: 0 !important; transition: all .2s !important; }
|
| 442 |
+
.tab-nav button.selected, .tab-nav button[aria-selected="true"] { color: var(--amber-400) !important; border-bottom: 3px solid var(--amber-500) !important; background: transparent !important; }
|
| 443 |
+
.card { background: var(--white); border: 1px solid var(--green-700); border-radius: var(--radius-lg); padding: 20px; margin-bottom: 16px; box-shadow: var(--shadow-sm); }
|
| 444 |
+
.card-title { font-size: .7rem; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--green-400); margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--green-700); }
|
| 445 |
+
.gradio-container input, .gradio-container textarea, .gradio-container select, label, .gradio-container .label-wrap span { color: var(--green-900) !important; }
|
| 446 |
+
.gradio-container input, .gradio-container textarea { background: var(--white) !important; border: 1px solid var(--green-600) !important; border-radius: var(--radius) !important; color: var(--green-900) !important; font-family: 'DM Sans', sans-serif !important; }
|
| 447 |
+
.gradio-container input:focus, .gradio-container textarea:focus { border-color: var(--amber-500) !important; outline: none !important; box-shadow: 0 0 0 3px rgba(245,166,35,.15) !important; }
|
| 448 |
+
.gradio-container .wrap, .gradio-container .svelte-1gfkn6j { background: var(--white) !important; border-color: var(--green-600) !important; }
|
| 449 |
+
.gradio-container .block { background: var(--white) !important; }
|
| 450 |
+
.gradio-container button.primary, .gradio-container .btn-primary { background: linear-gradient(135deg, var(--green-600), var(--green-500)) !important; color: var(--white) !important; border: none !important; border-radius: var(--radius) !important; font-family: 'DM Sans', sans-serif !important; font-weight: 600 !important; font-size: .9rem !important; padding: 12px 24px !important; cursor: pointer !important; transition: all .2s !important; box-shadow: var(--shadow-sm) !important; }
|
| 451 |
+
.gradio-container button.primary:hover { background: linear-gradient(135deg, var(--green-500), var(--green-400)) !important; transform: translateY(-1px) !important; box-shadow: var(--shadow-md) !important; }
|
| 452 |
+
.gradio-container button.secondary { background: var(--white) !important; border: 1px solid var(--green-600) !important; color: var(--green-400) !important; }
|
| 453 |
+
.gradio-container .upload-container, .gradio-container [data-testid="image"] { border: 2px dashed var(--green-600) !important; border-radius: var(--radius-lg) !important; background: var(--white) !important; }
|
| 454 |
+
.gradio-container .output-text, .gradio-container .output-markdown, .gradio-container textarea { background: var(--white) !important; color: var(--green-900) !important; border-radius: var(--radius) !important; }
|
| 455 |
+
.gradio-container .prose, .gradio-container .markdown-body { color: var(--green-900) !important; }
|
| 456 |
+
.gradio-container .prose h2, .gradio-container .prose h3 { color: var(--amber-400) !important; font-family: 'Playfair Display', serif !important; }
|
| 457 |
+
.gradio-container .prose a { color: var(--green-400) !important; }
|
| 458 |
+
.gradio-container .prose a[href*="wa.me"] { display: inline-block; var(--white); color: green !important; padding: 8px 18px; border-radius: 24px; text-decoration: none !important; font-weight: 600; margin-top: 8px; }
|
| 459 |
+
.gradio-container audio { width: 100% !important; border-radius: var(--radius) !important; background: var(--white) !important; }
|
| 460 |
+
.info-box { background: var(--white); border: 1px solid var(--green-600); border-left: 4px solid var(--green-500); border-radius: var(--radius); padding: 12px 16px; font-size: .88rem; line-height: 1.6; }
|
| 461 |
+
.warn-box { background: var(--white); border: 1px solid rgba(245,166,35,.4); border-left: 4px solid var(--amber-500); border-radius: var(--radius); padding: 12px 16px; font-size: .88rem; }
|
| 462 |
+
@media (max-width: 640px) { .saaf-header { padding: 16px 12px; } .tab-nav button { padding: 10px 12px !important; font-size: .78rem !important; } .gradio-container .block { padding: 8px !important; } .card { padding: 14px; } .badge { font-size: .65rem; padding: 3px 9px; } }
|
| 463 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 464 |
+
::-webkit-scrollbar-track { background: var(--white); }
|
| 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>🏙️ Rahbar</h1>
|
| 472 |
+
<div class="urdu-title">رہبر — ہمارا حق</div>
|
| 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">🤖 Gemini 2.0</span>
|
| 477 |
+
<span class="badge badge-ai">🔍 YOLOv8</span>
|
| 478 |
+
<span class="badge badge-ai">🦙 Llama 3</span>
|
| 479 |
+
<span class="badge badge-pk">🇵🇰 4 Languages</span>
|
| 480 |
+
<span class="badge badge-pk">⚖️ Live Laws</span>
|
| 481 |
+
<span class="badge badge-live">🔴 Live</span>
|
| 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 GRADIO UI ──────────────────────────────────────────
|
| 490 |
+
def build_ui():
|
| 491 |
+
with gr.Blocks(title="Rahbar | رہبر", theme=gr.themes.Base(primary_hue=gr.themes.colors.green, secondary_hue=gr.themes.colors.yellow)) as demo:
|
| 492 |
+
gr.HTML(HEADER_HTML)
|
| 493 |
+
with gr.Tabs():
|
| 494 |
+
# TAB 1 – REPORT ISSUE
|
| 495 |
+
with gr.Tab("📸 Report Issue"):
|
| 496 |
+
with gr.Row(equal_height=False):
|
| 497 |
+
with gr.Column(scale=1, min_width=280):
|
| 498 |
+
gr.HTML('<div class="card-title">👤 Citizen Details</div>')
|
| 499 |
+
name_tb = gr.Textbox(label="Full Name", placeholder="e.g., Ali Raza", lines=1)
|
| 500 |
+
cnic_tb = gr.Textbox(label="CNIC (without dashes)", placeholder="1234567890123", lines=1)
|
| 501 |
+
phone_tb = gr.Textbox(label="Phone Number (optional)", placeholder="03xxxxxxxxx", lines=1)
|
| 502 |
+
gr.HTML('<div class="card-title" style="margin-top:12px">📍 Complaint Details</div>')
|
| 503 |
+
image_input = gr.Image(type="pil", label="📷 Upload Photo", height=220)
|
| 504 |
+
issue_type = gr.Radio(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="🔖 Issue Type")
|
| 505 |
+
city_dd = gr.Dropdown(choices=list(CITIES_AREAS.keys()), value="Lahore", label="🏙️ City")
|
| 506 |
+
area_dd = gr.Dropdown(choices=CITIES_AREAS["Lahore"], value="Model Town", label="📌 Area")
|
| 507 |
+
location_tb = gr.Textbox(label="🗺️ Street / Landmark", placeholder="e.g., Main Boulevard near McDonald's", lines=1)
|
| 508 |
+
desc_tb = gr.Textbox(label="📝 Description (optional)", placeholder="Describe the issue...", lines=3)
|
| 509 |
+
language_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="🌐 Language / زبان")
|
| 510 |
+
tts_cb = gr.Checkbox(label="🔊 Read report aloud (TTS)", value=False)
|
| 511 |
+
submit_btn = gr.Button("🚀 Submit Complaint", variant="primary", size="lg")
|
| 512 |
+
with gr.Column(scale=2, min_width=300):
|
| 513 |
+
gr.HTML('<div class="card-title">📊 Analysis Results</div>')
|
| 514 |
+
annotated_out = gr.Image(label="🔍 AI Detection Result", height=250)
|
| 515 |
+
complaint_id_out = gr.Textbox(label="🆔 Complaint ID", interactive=False)
|
| 516 |
+
report_out = gr.Textbox(label="📋 Full Report", lines=18, interactive=False)
|
| 517 |
+
wa_out = gr.Markdown(label="📲 Share")
|
| 518 |
+
legal_advice_out = gr.Textbox(label="⚖️ Legal Advice (Llama 3)", lines=6, interactive=False)
|
| 519 |
+
tts_out = gr.Audio(label="🔊 Voice Report", autoplay=False)
|
| 520 |
+
city_dd.change(fn=update_areas, inputs=[city_dd], outputs=[area_dd])
|
| 521 |
+
submit_btn.click(fn=make_report, inputs=[image_input, issue_type, city_dd, area_dd, name_tb, cnic_tb, phone_tb, desc_tb, language_dd, tts_cb], outputs=[annotated_out, report_out, wa_out, legal_advice_out, tts_out, complaint_id_out])
|
| 522 |
+
|
| 523 |
+
# TAB 2 – LAW REFERENCE
|
| 524 |
+
with gr.Tab("⚖️ Law Reference"):
|
| 525 |
+
gr.HTML('<div class="card-title">📜 Pakistani Civic Laws Database</div>')
|
| 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 = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1)
|
| 529 |
+
law_out = gr.Markdown()
|
| 530 |
+
gr.Button("📖 Show Law", variant="primary").click(fn=law_info, inputs=[law_issue_dd, law_lang_dd], outputs=[law_out])
|
| 531 |
+
gr.HTML('<div class="info-box"><strong>📌 Quick Hotlines:</strong><br>🗑️ Garbage — <span class="hotline-pill">1139</span> | 🕳️ Roads — <span class="hotline-pill">051-9032800</span> | 💧 WASA — <span class="hotline-pill">042-99200300</span></div>')
|
| 532 |
+
|
| 533 |
+
# TAB 3 – VOICE INPUT
|
| 534 |
+
with gr.Tab("🎤 Voice Input"):
|
| 535 |
+
gr.HTML('<div class="card-title">🎙️ Speech-to-Text (STT)</div>')
|
| 536 |
+
gr.HTML('<div class="warn-box">Speak your complaint in any language. The system will transcribe it. You can then copy the transcription into the Report Issue tab.</div>')
|
| 537 |
+
audio_in = gr.Audio(type="filepath", label="🎤 Record or Upload Audio", sources=["microphone", "upload"])
|
| 538 |
+
stt_btn = gr.Button("📝 Transcribe", variant="primary")
|
| 539 |
+
stt_out = gr.Textbox(label="📄 Transcription", lines=5, interactive=True)
|
| 540 |
+
stt_btn.click(fn=stt, inputs=[audio_in], outputs=[stt_out])
|
| 541 |
+
gr.HTML('<div class="card-title" style="margin-top:20px">🔊 Text-to-Speech (TTS) Test</div>')
|
| 542 |
+
with gr.Row():
|
| 543 |
+
tts_text_in = gr.Textbox(label="Enter text", placeholder="Type something to hear...", scale=3)
|
| 544 |
+
tts_lang_in = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1)
|
| 545 |
+
tts_test_btn = gr.Button("▶️ Play", variant="secondary")
|
| 546 |
+
tts_test_out = gr.Audio(label="🔊 Output", autoplay=True)
|
| 547 |
+
tts_test_btn.click(fn=make_tts, inputs=[tts_text_in, tts_lang_in], outputs=[tts_test_out])
|
| 548 |
+
|
| 549 |
+
# TAB 4 – ADMIN DASHBOARD
|
| 550 |
+
with gr.Tab("🛡️ Admin Dashboard"):
|
| 551 |
+
gr.HTML('<div class="card-title">📊 Complaint Statistics</div>')
|
| 552 |
+
refresh_btn = gr.Button("🔄 Refresh Stats", variant="primary")
|
| 553 |
+
with gr.Row():
|
| 554 |
+
stats_out = gr.Markdown()
|
| 555 |
+
log_out = gr.Markdown()
|
| 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(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Base(primary_hue=gr.themes.colors.green, secondary_hue=gr.themes.colors.yellow), css=CSS, share=True, show_error=True)
|