Noshal commited on
Commit
cba07ee
·
verified ·
1 Parent(s): 8f7d694

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +805 -347
app.py CHANGED
@@ -1,30 +1,25 @@
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"],
@@ -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": ["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]
@@ -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 = 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)
@@ -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
- 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
@@ -235,56 +381,67 @@ def make_tts(text, language):
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}** |
@@ -294,269 +451,570 @@ def get_admin_stats():
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)
 
 
 
 
 
 
 
 
 
 
 
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 &middot; Drag pin to adjust &middot; 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> &nbsp;|&nbsp;
924
+ Roads: <span class="hotline-pill">051-9032800</span> &nbsp;|&nbsp;
925
+ WASA: <span class="hotline-pill">042-99200300</span> &nbsp;|&nbsp;
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
+ )