Noshal commited on
Commit
0736f65
·
verified ·
1 Parent(s): 12d828b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +562 -0
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> &nbsp;|&nbsp; 🕳️ Roads — <span class="hotline-pill">051-9032800</span> &nbsp;|&nbsp; 💧 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)