Noshal commited on
Commit
8f62bbc
·
verified ·
1 Parent(s): e56ab2c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +463 -378
app.py CHANGED
@@ -1,11 +1,16 @@
1
  """
2
  ╔══════════════════════════════════════════════════════════════╗
3
  ║ Rahbar | رہبر ║
4
- ║ Pakistan's AI-Powered Civic Complaint System v4.0 ║
5
  ╚══════════════════════════════════════════════════════════════╝
6
- Gradio 6 compatible — Professional English UI
7
- Fixes: GPS via gr.Button js=, camera via native gr.Image,
8
- RAG chatbot, Groq Whisper STT, clean report headings
 
 
 
 
 
9
  """
10
 
11
  import os, io, re, uuid, base64, datetime, urllib.parse
@@ -195,12 +200,12 @@ Always include GPS coordinates and photos with your complaint.""",
195
  ]
196
 
197
 
198
- # ─── RAG ENGINE — TF-IDF + Cosine Similarity ──────────────────
199
  class RAGEngine:
200
  def __init__(self):
201
- self.documents = RAG_DOCUMENTS
202
- self.vectorizer = None
203
- self.doc_matrix = None
204
  self._initialized = False
205
 
206
  def initialize(self):
@@ -218,7 +223,7 @@ class RAGEngine:
218
  analyzer='char_wb', ngram_range=(2, 5),
219
  max_features=8000, sublinear_tf=True, min_df=1,
220
  )
221
- self.doc_matrix = self.vectorizer.fit_transform(corpus)
222
  self._initialized = True
223
  return True
224
  except Exception as e:
@@ -248,9 +253,9 @@ class RAGEngine:
248
  def _keyword_fallback(self, query, top_k=3):
249
  q = query.lower()
250
  keywords = {
251
- "Garbage": ["garbage", "waste", "sanitation", "trash", "1139"],
252
- "Pot Hole": ["pothole", "pot hole", "road", "nha"],
253
- "Pipe Leakage": ["water", "wasa", "pipe", "leakage", "contaminated"],
254
  }
255
  found_cat = None
256
  for cat, kws in keywords.items():
@@ -357,28 +362,33 @@ LEGAL_KB = {
357
  LOCALIZED = {
358
  "Garbage": {
359
  "English": "Dumping garbage is a criminal offence under Punjab Waste Management Act 2014. Fine: Rs. 500-50,000. Helpline: 1139",
360
- "اردو (Urdu)": "کچرا پھینکنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 کے تحت جرم ہے۔ جرمانہ: 500 سے 50,000 روپے۔",
361
- "پنجابی (Punjabi)": "کچرا سُٹنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 دے تحت جرم اے۔",
362
- "سندھی (Sindhi)": "ڪچرو اڇلائڻ پنجاب ويسٽ مئنيجمينٽ ايڪٽ 2014 تحت جرم آهي.",
363
  },
364
  "Pot Hole": {
365
  "English": "Road repair is the government's legal obligation within 72 hours under Punjab LGA 2022. NHA: 051-9032800",
366
- "اردو (Urdu)": "سڑک کی مرمت پنجاب مقامی حکومت ایکٹ 2022 کے تحت 72 گھنٹوں میں حکومت کی ذمہ داری ہے۔",
367
- "پنجابی (Punjabi)": "سڑک دی مرمت پنجاب LGA 2022 دے تحت 72 گھنٹیاں وچ سرکار دی ذمہ واری اے۔",
368
- "سندھی (Sindhi)": "سڙڪ جي مرمت پنجاب LGA 2022 تحت 72 ڪلاڪن ۾ حڪومت جي ذميواري آهي.",
369
  },
370
  "Pipe Leakage": {
371
  "English": "Pipe leakage must be repaired within 24 hours by WASA under Punjab Water Act 2019. WASA: 042-99200300",
372
- "اردو (Urdu)": "پائپ لیکیج کی مرمت پنجاب واٹر ایکٹ 2019 کے تحت WASA کی 24 گھنٹوں میں ذمہ داری ہے۔",
373
- "پنجابی (Punjabi)": "پائپ لیکیج دی مرمت پنجاب واٹر ایکٹ 2019 دے تحت WASA دی 24 گھنٹیاں وچ ذمہ واری اے۔",
374
- "سندھی (Sindhi)": "پائپ ليڪيج جي مرمت پنجاب واٽر ايڪٽ 2019 تحت WASA جي 24 ڪلاڪن ۾ ذميواري آهي.",
375
  },
376
  }
377
 
378
  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}
 
 
 
379
  LANG_CODES = {
380
- "English": "en", "اردو (Urdu)": "ur",
381
- "پنجابی (Punjabi)": "pa", "سندھی (Sindhi)": "sd",
 
 
382
  }
383
 
384
 
@@ -451,9 +461,9 @@ def parse_gemini_response(text):
451
  m = re.search(pat, text, re.IGNORECASE)
452
  if m:
453
  val = m.group(1)
454
- if key == "status": r[key] = val.upper()
455
  elif key == "severity": r[key] = int(val)
456
- else: r[key] = val
457
  for pat, key in [
458
  (r"REASON:\s*(.+?)(?=REASON_URDU:|SEVERITY:|$)", "reason"),
459
  (r"REASON_URDU:\s*(.+?)(?=SEVERITY:|$)", "reason_urdu"),
@@ -469,9 +479,9 @@ def analyze_with_llama(issue, location, city, yolo_summary, severity, language="
469
  clean = issue.split(" ", 1)[-1]
470
  kb = LEGAL_KB.get(clean, {})
471
  lang_map = {
472
- "اردو (Urdu)": "Respond entirely in Urdu.",
473
- "پنجابی (Punjabi)": "Respond in Punjabi (Shahmukhi script).",
474
- "سندھی (Sindhi)": "Respond in Sindhi.",
475
  }
476
  lang_instruction = lang_map.get(language, "Respond in clear professional English.")
477
  if not GROQ_API_KEY:
@@ -520,9 +530,9 @@ def legal_chatbot_rag(user_message, history, language):
520
  retrieved_docs = rag_engine.retrieve(user_message, top_k=3)
521
  rag_context = rag_engine.format_context(retrieved_docs)
522
  lang_map = {
523
- "اردو (Urdu)": "Respond entirely in Urdu.",
524
- "پنجابی (Punjabi)": "Respond in Punjabi (Shahmukhi script).",
525
- "سندھی (Sindhi)": "Respond in Sindhi.",
526
  }
527
  lang_instruction = lang_map.get(language, "Respond in clear professional English.")
528
  system_content = f"""You are Rahbar Legal Assistant — a civic rights advisor for Pakistani citizens.
@@ -561,7 +571,7 @@ RAG Context (from Knowledge Base):
561
  answer = resp.choices[0].message.content.strip()
562
  if retrieved_docs:
563
  refs = [f"[{d['title'][:35]}...]" for d in retrieved_docs[:2]]
564
- answer += f"\n\n_Sources: {' | '.join(refs)}_"
565
  except Exception as e:
566
  answer = f"Error: {e}"
567
  return history + [
@@ -570,7 +580,28 @@ RAG Context (from Knowledge Base):
570
  ], ""
571
 
572
 
573
- # ─── TTS ───────────────────────────────────────────────────��──
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  def make_tts(text, language):
575
  try:
576
  from gtts import gTTS
@@ -580,14 +611,23 @@ def make_tts(text, language):
580
  tts.save(path)
581
  return path
582
  except Exception as e:
583
- print(f"TTS error: {e}")
584
- return None
 
 
 
 
 
 
 
 
 
585
 
586
 
587
  # ─── STT — Groq Whisper primary, SpeechRecognition fallback ──
588
  def stt(audio_file):
589
  if audio_file is None:
590
- return "No audio provided. Please record or upload audio first."
591
 
592
  def ensure_wav(path):
593
  if path.lower().endswith(".wav"):
@@ -610,11 +650,11 @@ def stt(audio_file):
610
  model="whisper-large-v3", file=f, response_format="text"
611
  )
612
  text = result if isinstance(result, str) else result.text
613
- return text.strip() or "No speech detected in audio."
614
  except Exception as e:
615
  groq_err = str(e)
616
  else:
617
- groq_err = "GROQ_API_KEY not set"
618
 
619
  try:
620
  import speech_recognition as sr
@@ -625,10 +665,10 @@ def stt(audio_file):
625
  audio_data = recognizer.record(src)
626
  return recognizer.recognize_google(audio_data)
627
  except Exception as e2:
628
- return (f"Transcription failed.\n"
629
- f"Groq Whisper error: {groq_err}\n"
630
- f"Fallback error: {e2}\n"
631
- f"Tip: Set GROQ_API_KEY for reliable Whisper transcription.")
632
 
633
 
634
  # ─── LAW REFERENCE ────────────────────────────────────────────
@@ -637,18 +677,18 @@ def law_info(issue, language):
637
  kb = LEGAL_KB.get(clean, {})
638
  local = LOCALIZED.get(clean, {}).get(language, "")
639
  rights = "\n".join(f" * {r}" for r in kb.get("citizen_rights", []))
640
- out = f"## Legal Reference: {issue}\n\n### Applicable Laws\n"
641
  for law in kb.get("laws", []):
642
  out += f" * {law}\n"
643
- out += (f"\n### Fine / Penalty\n{kb.get('fine','N/A')}\n"
644
- f"\n### Responsible Authority\n{kb.get('authority','N/A')}\n"
645
- f"\n### Helpline\n**{kb.get('hotline','N/A')}**\n"
646
- f"\n### Mandated Response Time\n{kb.get('response','N/A')}\n"
647
- f"\n### Your Rights as a Citizen\n{rights}\n"
648
- f"\n### Escalation Path\n{kb.get('escalation','N/A')}\n")
649
  if local:
650
- out += f"\n---\n### Localized Message ({language})\n> {local}\n"
651
- out += f"\n---\n*Source: {kb.get('dataset_ref','Pakistani civic law databases')}*"
652
  return out
653
 
654
 
@@ -656,7 +696,7 @@ def law_info(issue, language):
656
  def get_admin_stats():
657
  total = len(complaint_log)
658
  if total == 0:
659
- return "No complaints filed yet.", ""
660
  counts = {"Garbage": 0, "Pot Hole": 0, "Pipe Leakage": 0}
661
  cities, severities = {}, []
662
  for c in complaint_log:
@@ -667,29 +707,29 @@ def get_admin_stats():
667
  severities.append(c.get("severity", 5))
668
  avg_sev = sum(severities) / len(severities) if severities else 0
669
  top_city = max(cities, key=cities.get) if cities else "N/A"
670
- stats_md = f"""## Admin Dashboard
671
- | Metric | Value |
672
  |--------|-------|
673
- | Total Complaints | **{total}** |
674
- | Average Severity | **{avg_sev:.1f}/10** |
675
- | Top City | **{top_city}** |
676
 
677
- ### By Issue Type
678
- | Issue | Count |
679
  |-------|-------|
680
- | Garbage | {counts['Garbage']} |
681
- | Pot Hole | {counts['Pot Hole']} |
682
- | Pipe Leakage | {counts['Pipe Leakage']} |
683
 
684
- ### By City
685
  """
686
  for city, cnt in sorted(cities.items(), key=lambda x: -x[1]):
687
  stats_md += f"| {city} | {cnt} |\n"
688
- log_md = "## Recent Complaints\n\n"
689
  for c in reversed(complaint_log[-10:]):
690
  log_md += (f"**{c['id']}** | {c['timestamp']} | {c['city']}, {c['location']} | "
691
- f"{c['issue']} | Severity {c['severity']}/10 | "
692
- f"Citizen: {c.get('name','N/A')} (CNIC: {c.get('cnic','N/A')})\n\n")
693
  return stats_md, log_md
694
 
695
 
@@ -700,7 +740,6 @@ def severity_icon(score):
700
  return "RED"
701
 
702
 
703
- # ─── Unicode bold for plain-text headings in gr.Textbox ───────
704
  def _ubold(text):
705
  result = []
706
  for ch in str(text):
@@ -791,13 +830,13 @@ def build_professional_report(complaint_id, timestamp, name, cnic, phone, city,
791
  def make_report(image, issue_type, city, location, name, cnic, phone,
792
  description, language, enable_tts):
793
  if image is None:
794
- return (None, "Please upload an image.", "", "", None, "", None)
795
  if not location.strip():
796
- return (None, "Please enter a location.", "", "", None, "", None)
797
  if not name.strip():
798
- return (None, "Please enter your full name.", "", "", None, "", None)
799
  if not cnic.strip():
800
- return (None, "Please enter your CNIC number.", "", "", None, "", None)
801
 
802
  complaint_id = f"RB-{uuid.uuid4().hex[:8].upper()}"
803
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -811,10 +850,10 @@ def make_report(image, issue_type, city, location, name, cnic, phone,
811
 
812
  if gemini_status == "REJECTED":
813
  return (annotated_img,
814
- f"COMPLAINT REJECTED BY AI VERIFICATION\n\nReason: {gemini_reason}\n"
815
- f"Confidence: {gemini_parsed.get('confidence','N/A')}\n\n"
816
- f"Please upload a clear image showing the reported issue ({issue_type}).\n"
817
- "Your complaint has NOT been logged.",
818
  "", "", None, complaint_id, None)
819
 
820
  if gemini_status == "UNKNOWN" and "GOOGLE_API_KEY not set" in gemini_raw:
@@ -841,16 +880,16 @@ def make_report(image, issue_type, city, location, name, cnic, phone,
841
  "name": name, "cnic": cnic, "phone": phone,
842
  })
843
 
844
- wa_text = (f"Rahbar Complaint\nRef: {complaint_id}\nIssue: {issue_type}\n"
845
- f"Location: {location}, {city}\nSeverity: {final_severity}/10\n"
846
- f"Authority: {kb.get('authority','N/A')}\n"
847
- f"Helpline: {kb.get('hotline','N/A')}\nFiled: {timestamp}")
848
- wa_md = f"[Share on WhatsApp](https://wa.me/?text={urllib.parse.quote(wa_text[:1000])})"
849
 
850
  report_tts_path = None
851
  if enable_tts:
852
- tts_text = (f"Complaint {complaint_id} filed. Issue: {issue_type} "
853
- f"at {location}, {city}. Severity: {final_severity} out of 10. {local}")
854
  report_tts_path = make_tts(tts_text, language)
855
 
856
  advice_tts_path = make_tts(llama_advice[:600], language)
@@ -858,10 +897,39 @@ def make_report(image, issue_type, city, location, name, cnic, phone,
858
 
859
 
860
  def update_areas(city):
861
- areas = CITIES_AREAS.get(city, ["Enter area manually"])
862
  return gr.Dropdown(choices=areas, value=areas[0])
863
 
864
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
865
  # ─── CSS ──────────────────────────────────────────────────────
866
  CSS = """
867
  @import url('https://fonts.googleapis.com/css2?family=Noto+Nastaliq+Urdu:wght@400;700&family=Playfair+Display:wght@700;900&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap');
@@ -923,6 +991,9 @@ label,.gradio-container .label-wrap span{color:var(--text-primary)!important}
923
  .gradio-container select,.gradio-container [data-testid="dropdown"]{background:var(--surface)!important;color:var(--text-primary)!important;border-color:var(--border-strong)!important}
924
  .gradio-container .message.user{background:var(--surface3)!important;color:var(--text-primary)!important}
925
  .gradio-container .message.bot{background:var(--surface2)!important;color:var(--text-primary)!important}
 
 
 
926
  ::-webkit-scrollbar{width:6px;height:6px}
927
  ::-webkit-scrollbar-track{background:var(--surface2)}
928
  ::-webkit-scrollbar-thumb{background:var(--g6);border-radius:3px}
@@ -940,220 +1011,201 @@ HEADER_HTML = """
940
  <span class="badge badge-ai">YOLOv8</span>
941
  <span class="badge badge-ai">Llama 3.3</span>
942
  <span class="badge badge-ai">RAG Chatbot</span>
943
- <span class="badge badge-pk">4 Languages</span>
944
  <span class="badge badge-live">LIVE</span>
945
  </div>
946
  """
947
 
948
- # ─── MAP WIDGET (inside gr.HTML = sandboxed iframe) ───────────
949
- # Draws the Leaflet map. GPS logic runs from parent page via gr.Button(js=).
950
- # Map receives coordinates via window.postMessage and moves the pin.
 
951
  MAP_HTML = """
952
- <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
953
- <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
954
-
955
- <div style="margin:6px 0 4px">
956
- <div id="gpsStatusDiv" style="
957
- font-size:.82rem;min-height:26px;margin-bottom:8px;font-weight:500;
958
- padding:7px 12px;color:#25a06b;border-radius:8px;
959
- background:#f4f8f5;border:1px solid #b8d9c5;">
960
- Map ready. Use the GPS button above or click the map to place a pin.
961
- </div>
962
- <div id="rahbarMapDiv" style="
963
- height:240px;width:100%;border-radius:12px;
964
- border:2px solid #1f7a52;margin-bottom:6px;
965
- background:#e8f5e9;position:relative;overflow:hidden;">
966
  </div>
967
- <p style="font-size:.71rem;color:#5a8a6e;margin:2px 0 8px">
968
- Click map or drag pin to adjust location &middot; GPS button auto-fills your address
 
969
  </p>
970
  </div>
971
 
 
972
  <script>
973
- (function(){
974
- var DEFAULT_LAT = 31.5204, DEFAULT_LNG = 74.3587;
975
- var rbMap = null, rbMarker = null;
976
-
977
- function setStatus(msg, color) {
978
- var el = document.getElementById('gpsStatusDiv');
979
- if (el) { el.textContent = msg; el.style.color = color || '#25a06b'; }
 
980
  }
 
981
 
982
- function notifyParent(lat, lng) {
983
- // Ask parent page to reverse-geocode and fill the textbox
984
- try { window.parent.postMessage({ type: 'rahbar_coords', lat: lat, lng: lng }, '*'); }
985
- catch(e) {}
986
- setStatus('Getting address for: ' + lat.toFixed(5) + ', ' + lng.toFixed(5) + '...', '#f5a623');
987
- }
988
 
989
- function buildMap(lat, lng) {
990
- if (rbMap) { rbMap.remove(); rbMap = null; rbMarker = null; }
991
- rbMap = L.map('rahbarMapDiv').setView([lat, lng], 15);
992
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
993
- attribution: '&copy; OpenStreetMap', maxZoom: 19
994
- }).addTo(rbMap);
995
- rbMarker = L.marker([lat, lng], { draggable: true })
996
- .addTo(rbMap)
997
- .bindPopup('Drag pin or click map to adjust')
998
- .openPopup();
999
- rbMarker.on('dragend', function(e) {
1000
- var ll = e.target.getLatLng();
1001
- notifyParent(ll.lat, ll.lng);
1002
- });
1003
- rbMap.on('click', function(e) {
1004
- rbMarker.setLatLng(e.latlng);
1005
- notifyParent(e.latlng.lat, e.latlng.lng);
1006
- });
1007
- setTimeout(function(){ if (rbMap) rbMap.invalidateSize(); }, 350);
1008
- }
1009
 
1010
- // Listen for commands from parent page
1011
- window.addEventListener('message', function(ev) {
1012
- if (!ev.data) return;
1013
- if (ev.data.type === 'rahbar_move_pin') {
1014
- var lat = ev.data.lat, lng = ev.data.lng;
1015
- if (!rbMap) buildMap(lat, lng);
1016
- else { rbMarker.setLatLng([lat, lng]); rbMap.setView([lat, lng], 17); }
1017
- setTimeout(function(){ if (rbMap) rbMap.invalidateSize(); }, 350);
1018
- }
1019
- if (ev.data.type === 'rahbar_status') {
1020
- setStatus(ev.data.msg, ev.data.color || '#25a06b');
1021
- }
1022
- });
1023
 
1024
- function tryInit() {
1025
- if (document.getElementById('rahbarMapDiv') && typeof L !== 'undefined') {
1026
- buildMap(DEFAULT_LAT, DEFAULT_LNG);
1027
- } else {
1028
- setTimeout(tryInit, 200);
1029
- }
1030
- }
1031
 
1032
- if (document.readyState === 'loading')
1033
- document.addEventListener('DOMContentLoaded', tryInit);
1034
- else
1035
- setTimeout(tryInit, 100);
1036
- })();
1037
- </script>
1038
- """
1039
 
1040
- # ─── GPS PARENT-PAGE SCRIPT ───────────────────────────────────
1041
- # Injected into the Gradio page directly (NOT inside an iframe).
1042
- # Has full access to navigator.geolocation and the real page DOM.
1043
- # Wires the GPS gr.Button by elem_id and handles reverse-geocoding.
1044
- GPS_PARENT_SCRIPT = """
1045
- <script>
1046
- (function(){
1047
- // Reverse-geocode coordinates to a human-readable address
1048
- function reverseGeocode(lat, lng, cb) {
1049
- var url = 'https://nominatim.openstreetmap.org/reverse?format=jsonv2'
1050
- + '&lat=' + lat + '&lon=' + lng + '&zoom=17&addressdetails=1';
1051
- fetch(url, { headers: { 'Accept-Language': 'en' } })
1052
- .then(function(r) { return r.json(); })
1053
- .then(function(d) {
1054
- var a = d.address || {}, p = [];
1055
- if (a.road || a.pedestrian) p.push(a.road || a.pedestrian);
1056
- if (a.suburb || a.neighbourhood) p.push(a.suburb || a.neighbourhood);
1057
- if (a.city_district || a.county) p.push(a.city_district || a.county);
1058
- if (!p.length && d.display_name) p.push(d.display_name.split(',').slice(0,3).join(',').trim());
1059
- cb(p.join(', ') || ('GPS:' + lat.toFixed(5) + ',' + lng.toFixed(5)));
1060
- })
1061
- .catch(function() { cb('GPS:' + lat.toFixed(5) + ',' + lng.toFixed(5)); });
1062
- }
1063
 
1064
- // Write text into the Gradio location textbox (React/Svelte-safe)
1065
- function fillLocationTb(text) {
1066
- // Try elem_id wrapper first
1067
- var wrap = document.getElementById('rahbar_location_tb');
1068
- var el = wrap ? (wrap.querySelector('textarea') || wrap.querySelector('input')) : null;
1069
- // Fallback: scan all visible inputs for matching placeholder text
1070
- if (!el) {
1071
- var all = document.querySelectorAll('textarea, input[type="text"]');
1072
- for (var i = 0; i < all.length; i++) {
1073
- var ph = (all[i].placeholder || '').toLowerCase();
1074
- if (ph.indexOf('gps') > -1 || ph.indexOf('street') > -1 || ph.indexOf('landmark') > -1) {
1075
- el = all[i]; break;
1076
- }
1077
- }
1078
- }
1079
- if (!el) { console.warn('Rahbar GPS: location textbox not found'); return; }
1080
- // Svelte/React-compatible value setter
1081
- var proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
1082
- var setter = Object.getOwnPropertyDescriptor(proto, 'value');
1083
- if (setter && setter.set) setter.set.call(el, text); else el.value = text;
1084
- el.dispatchEvent(new Event('input', { bubbles: true }));
1085
- el.dispatchEvent(new Event('change', { bubbles: true }));
1086
- }
1087
 
1088
- // Send a message to all iframe contentWindows (reaches the map widget)
1089
- function msgIframes(data) {
1090
- document.querySelectorAll('iframe').forEach(function(f) {
1091
- try { f.contentWindow.postMessage(data, '*'); } catch(e) {}
1092
- });
1093
- }
 
 
 
1094
 
1095
- // Handle a resolved lat/lng pair: move map pin + reverse-geocode + fill textbox
1096
- function handleCoords(lat, lng) {
1097
- msgIframes({ type: 'rahbar_move_pin', lat: lat, lng: lng });
1098
- msgIframes({ type: 'rahbar_status', msg: 'Fetching address...', color: '#f5a623' });
1099
- reverseGeocode(lat, lng, function(addr) {
1100
- fillLocationTb(addr);
1101
- msgIframes({ type: 'rahbar_status', msg: '✅ Location filled: ' + addr, color: '#25a06b' });
 
 
 
 
 
 
 
 
 
 
 
 
1102
  });
1103
- }
1104
 
1105
- // GPS button click handler
1106
- function doGPS() {
1107
- if (!navigator.geolocation) {
1108
- alert('Geolocation / GPS is not supported by this browser.');
1109
- return;
 
 
 
 
 
 
 
1110
  }
1111
- msgIframes({ type: 'rahbar_status', msg: 'Requesting GPS — please ALLOW when your browser prompts...', color: '#f5a623' });
1112
- navigator.geolocation.getCurrentPosition(
1113
- function(pos) { handleCoords(pos.coords.latitude, pos.coords.longitude); },
1114
- function(err) {
1115
- var msgs = {
1116
- 1: '❌ Location access DENIED. Open browser settings, allow this site location access, then retry.',
1117
- 2: '⚠️ Location signal unavailable. Check GPS/WiFi and retry.',
1118
- 3: '⏱️ GPS request timed out. Please try again.'
1119
- };
1120
- var msg = msgs[err.code] || ('GPS error: ' + err.message);
1121
- msgIframes({ type: 'rahbar_status', msg: msg, color: '#e85353' });
1122
- alert(msg);
1123
- },
1124
- { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
1125
- );
1126
  }
1127
-
1128
- // Listen for map-pin-drag events bubbled up from iframes
1129
- window.addEventListener('message', function(ev) {
1130
- if (ev.data && ev.data.type === 'rahbar_coords') {
1131
- handleCoords(ev.data.lat, ev.data.lng);
1132
- }
 
1133
  });
 
1134
 
1135
- // Attach click listener to the GPS gr.Button (identified by elem_id wrapper)
1136
- function attachGpsButton() {
1137
- var wrap = document.getElementById('rahbar_gps_btn');
1138
- var btn = wrap ? wrap.querySelector('button') : null;
1139
- if (btn) {
1140
- // Clone to clear any stale Gradio listeners
1141
- var fresh = btn.cloneNode(true);
1142
- btn.parentNode.replaceChild(fresh, btn);
1143
- fresh.addEventListener('click', function(e) {
1144
- e.preventDefault(); e.stopPropagation(); doGPS();
1145
- });
1146
- } else {
1147
- // Button not yet in DOM — retry
1148
- setTimeout(attachGpsButton, 400);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1149
  }
1150
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1151
 
1152
- if (document.readyState === 'loading')
1153
- document.addEventListener('DOMContentLoaded', function() { setTimeout(attachGpsButton, 800); });
1154
- else
1155
- setTimeout(attachGpsButton, 800);
1156
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1157
  </script>
1158
  """
1159
 
@@ -1166,97 +1218,87 @@ def build_ui():
1166
  with gr.Tabs():
1167
 
1168
  # ══════════════════���═══════════════════════════════
1169
- # TAB 1 — REPORT ISSUE
1170
  # ══════════════════════════════════════════════════
1171
- with gr.Tab("📝 Report Issue"):
1172
  with gr.Row(equal_height=False):
1173
 
1174
- # ── LEFT: inputs ──────────────────────────
1175
  with gr.Column(scale=1, min_width=300):
1176
 
1177
- gr.HTML('<div class="card-title">Citizen Details</div>')
1178
- name_tb = gr.Textbox(label="Full Name", placeholder="e.g., Ali Raza", lines=1)
1179
- cnic_tb = gr.Textbox(label="CNIC (without dashes)", placeholder="1234567890123", lines=1)
1180
- phone_tb = gr.Textbox(label="Phone Number (optional)", placeholder="03xxxxxxxxx", lines=1)
1181
 
1182
- gr.HTML('<div class="card-title" style="margin-top:14px">Issue Photo</div>')
1183
- gr.HTML("""<div class="info-box">Upload a clear photo of the civic issue.
1184
- On mobile: tap "Webcam" to use your device camera (front or rear).</div>""")
1185
- # sources=["webcam","upload"] enables native camera on mobile/desktop
1186
  image_input = gr.Image(
1187
  type="pil",
1188
- label="Upload or Capture Photo",
1189
  sources=["webcam", "upload"],
1190
  height=220,
1191
  )
1192
 
1193
- gr.HTML('<div class="card-title" style="margin-top:14px">Complaint Details</div>')
1194
- issue_type = gr.Radio(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="Issue Type")
1195
- city_dd = gr.Dropdown(choices=list(CITIES_AREAS.keys()), value="Lahore", label="City")
1196
- area_dd = gr.Dropdown(choices=CITIES_AREAS["Lahore"], value="Model Town", label="Area")
1197
 
1198
- # ── GPS section ───────────────────────
1199
- gr.HTML('<div class="card-title" style="margin-top:14px">GPS Location</div>')
1200
  gr.HTML("""<div class="warn-box">
1201
- <strong>Click "Get My GPS Location"</strong> and allow location access when prompted.
1202
- Your street address will auto-fill below via OpenStreetMap reverse geocoding.
1203
- You can also click the map or drag the pin to set a custom location.
1204
  </div>""")
1205
 
1206
- # gr.Button with elem_id — GPS script attaches to this button
1207
- # using addEventListener in the PARENT page (not inside an iframe),
1208
- # so navigator.geolocation is fully accessible.
1209
  gps_btn = gr.Button(
1210
- "📍 Get My GPS Location",
1211
  variant="primary",
1212
  elem_id="rahbar_gps_btn",
1213
  )
1214
 
1215
- # Map widget (rendered inside a sandboxed iframe by Gradio)
1216
  gr.HTML(MAP_HTML)
1217
 
1218
- # Location textbox — elem_id used by GPS script to fill it
1219
  location_tb = gr.Textbox(
1220
- label="Street / Landmark / GPS Address",
1221
- placeholder="GPS address auto-fills here — or type manually",
1222
  lines=1,
1223
  elem_id="rahbar_location_tb",
1224
  )
1225
 
1226
- # GPS parent-page script — injected directly into Gradio page
1227
- gr.HTML(GPS_PARENT_SCRIPT)
1228
-
1229
- desc_tb = gr.Textbox(label="Additional Description (optional)", placeholder="Describe the issue...", lines=3)
1230
- language_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Report & Voice Language")
1231
- tts_cb = gr.Checkbox(label="Read report aloud (Text-to-Speech)", value=False)
1232
- submit_btn = gr.Button("Submit Complaint", variant="primary", size="lg")
1233
 
1234
- # ── RIGHT: outputs ────────────────────────
1235
  with gr.Column(scale=2, min_width=320):
1236
 
1237
- gr.HTML('<div class="card-title">AI Analysis Results</div>')
1238
- annotated_out = gr.Image(label="AI Detection Result", height=240)
1239
- complaint_id_out = gr.Textbox(label="Complaint ID", interactive=False)
1240
 
1241
- gr.HTML('<div class="card-title" style="margin-top:14px">Official Complaint Report</div>')
1242
  report_out = gr.Textbox(
1243
- label="Full Complaint Report (Ready for Submission)",
1244
  lines=22, interactive=False,
1245
- placeholder="Report will appear here after submission..."
1246
  )
1247
  wa_out = gr.Markdown()
1248
- report_tts_out = gr.Audio(label="Report Audio (selected language)", autoplay=False)
1249
 
1250
- gr.HTML('<div class="card-title" style="margin-top:16px">Legal Advice</div>')
1251
- gr.HTML('<div class="info-box">AI-generated legal guidance based on Pakistani civic law, in your selected language.</div>')
1252
  legal_advice_out = gr.Textbox(
1253
- label="Legal Rights & Action Steps",
1254
  lines=12, interactive=False,
1255
- placeholder="Legal advice will appear here..."
1256
  )
1257
- advice_tts_out = gr.Audio(label="Legal Advice Audio (selected language)", autoplay=False)
1258
 
1259
- # ── Event wiring ──────────────────────────────
1260
  city_dd.change(fn=update_areas, inputs=[city_dd], outputs=[area_dd])
1261
  submit_btn.click(
1262
  fn=make_report,
@@ -1267,43 +1309,78 @@ You can also click the map or drag the pin to set a custom location.
1267
  )
1268
 
1269
  # ══════════════════════════════════════════════════
1270
- # TAB 2 — LAW REFERENCE + RAG CHATBOT
1271
  # ══════════════════════════════════════════════════
1272
- with gr.Tab("⚖️ Law & Chatbot"):
1273
 
1274
- gr.HTML('<div class="card-title">Pakistani Civic Laws Database</div>')
1275
  with gr.Row():
1276
- law_issue_dd = gr.Dropdown(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="Select Issue", scale=1)
1277
- law_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1)
1278
  law_out = gr.Markdown()
1279
- gr.Button("Show Law Details", variant="primary").click(
1280
  fn=law_info, inputs=[law_issue_dd, law_lang_dd], outputs=[law_out]
1281
  )
1282
  gr.HTML("""<div class="info-box">
1283
- <strong>Quick Helplines:</strong>&nbsp;
1284
- Garbage: <span class="hotline-pill">1139</span>&nbsp;|&nbsp;
1285
- Roads/NHA: <span class="hotline-pill">051-9032800</span>&nbsp;|&nbsp;
1286
- WASA Lahore: <span class="hotline-pill">042-99200300</span>&nbsp;|&nbsp;
1287
- CM Portal: <span class="hotline-pill">0800-02345</span>&nbsp;|&nbsp;
1288
- Fed. Ombudsman: <span class="hotline-pill">051-9204551</span>
1289
  </div>""")
1290
 
1291
- gr.HTML('<div class="card-title" style="margin-top:24px">🤖 RAG Legal Chatbot</div>')
1292
  gr.HTML("""<div class="info-box">
1293
- This chatbot uses <strong>Retrieval-Augmented Generation (RAG)</strong> with:
1294
- Road Issues Detection Dataset &nbsp;|&nbsp; Urban Issues Dataset &nbsp;|&nbsp; Consumer Complaints Dataset.<br>
1295
- Ask about water supply, WASA, pipe leakage, garbage, road potholes, or your legal rights as a Pakistani citizen.
1296
  </div>""")
1297
- chat_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Response Language")
1298
- chatbot = gr.Chatbot(label="Rahbar RAG Legal Assistant", height=500, value=[])
 
 
 
1299
  with gr.Row():
1300
  chat_input = gr.Textbox(
1301
- label="Your Question",
1302
- placeholder="e.g., WASA did not fix the pipe for 3 days. What are my legal rights?",
1303
  lines=2, scale=4
1304
  )
1305
- chat_send_btn = gr.Button("Send", variant="primary", scale=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1306
 
 
1307
  chat_send_btn.click(
1308
  fn=legal_chatbot_rag,
1309
  inputs=[chat_input, chatbot, chat_lang_dd],
@@ -1314,75 +1391,83 @@ Ask about water supply, WASA, pipe leakage, garbage, road potholes, or your lega
1314
  inputs=[chat_input, chatbot, chat_lang_dd],
1315
  outputs=[chatbot, chat_input]
1316
  )
1317
- gr.Examples(
1318
- examples=[
1319
- ["WASA did not fix the leakage for 3 days. What are my rights?"],
1320
- ["The water in my area is contaminated. Where do I complain?"],
1321
- ["Garbage has not been collected for a week. What law applies?"],
1322
- ["How do I escalate if the authority ignores my complaint?"],
1323
- ["A pothole damaged my car. Can I claim compensation?"],
1324
- ["How do I file a complaint on the Pakistan Citizen Portal?"],
1325
- ],
1326
- inputs=chat_input,
1327
- label="Try These Questions"
 
 
 
 
 
 
 
 
1328
  )
1329
 
1330
  # ══════════════════════════════════════════════════
1331
- # TAB 3 — VOICE INPUT / STT
1332
  # ══════════════════════════════════════════════════
1333
- with gr.Tab("🎤 Voice Input"):
1334
 
1335
- gr.HTML('<div class="card-title">Speech-to-Text (STT)</div>')
1336
  gr.HTML("""<div class="info-box">
1337
- Record your complaint or description using the microphone below.
1338
- Transcription uses <strong>Groq Whisper large-v3</strong> (if GROQ_API_KEY is set)
1339
- or Google Speech Recognition (fallback).<br>
1340
- Supports Urdu, English, Punjabi, and Sindhi. Please allow microphone access when prompted.
1341
  </div>""")
1342
  gr.HTML("""<div class="warn-box">
1343
- <strong>Tip:</strong> Speak clearly with minimal background noise.
1344
- After transcription, copy the text and paste it into the Report Issue tab.
1345
  </div>""")
1346
  audio_in = gr.Audio(
1347
  type="filepath",
1348
- label="Record or Upload Audio",
1349
  sources=["microphone", "upload"],
1350
  )
1351
- stt_btn = gr.Button("Transcribe Audio", variant="primary")
1352
  stt_out = gr.Textbox(
1353
- label="Transcription (editable — copy to Report Issue tab)",
1354
  lines=6, interactive=True,
1355
- placeholder="Transcribed text will appear here..."
1356
  )
1357
  stt_btn.click(fn=stt, inputs=[audio_in], outputs=[stt_out])
1358
 
1359
- gr.HTML('<div class="card-title" style="margin-top:24px">Text-to-Speech Test</div>')
1360
- gr.HTML('<div class="info-box">Test the TTS engine in any supported language.</div>')
1361
  with gr.Row():
1362
- tts_text_in = gr.Textbox(label="Enter text to hear", placeholder="Type something...", scale=3)
1363
- tts_lang_in = gr.Dropdown(choices=LANGUAGES, value="English", label="Language", scale=1)
1364
- tts_test_btn = gr.Button("▶ Play", variant="secondary")
1365
- tts_test_out = gr.Audio(label="Audio Output", autoplay=True)
1366
  tts_test_btn.click(fn=make_tts, inputs=[tts_text_in, tts_lang_in], outputs=[tts_test_out])
1367
 
1368
  # ══════════════════════════════════════════════════
1369
- # TAB 4 — ADMIN DASHBOARD
1370
  # ══════════════════════════════════════════════════
1371
- with gr.Tab("📊 Admin Dashboard"):
1372
 
1373
- gr.HTML('<div class="card-title">Complaint Statistics</div>')
1374
- refresh_btn = gr.Button("Refresh Statistics", variant="primary")
1375
  with gr.Row():
1376
  stats_out = gr.Markdown()
1377
  log_out = gr.Markdown()
1378
  refresh_btn.click(fn=get_admin_stats, outputs=[stats_out, log_out])
1379
 
1380
  gr.HTML("""<div class="info-box" style="margin-top:16px">
1381
- <strong>RAG Knowledge Base:</strong>
1382
  Road Issues Detection Dataset &nbsp;|&nbsp; Urban Issues Dataset &nbsp;|&nbsp; Consumer Complaints Dataset<br>
1383
- <strong>AI Models:</strong>
1384
  Gemini 2.0 Flash &nbsp;|&nbsp; YOLOv8 &nbsp;|&nbsp; Llama 3.3 70B &nbsp;|&nbsp; Whisper large-v3 (Groq)<br>
1385
- <strong>Vector Search:</strong> TF-IDF Cosine Similarity (char n-gram, multilingual, no download required)
1386
  </div>""")
1387
 
1388
  return demo
@@ -1390,8 +1475,8 @@ Gemini 2.0 Flash &nbsp;|&nbsp; YOLOv8 &nbsp;|&nbsp; Llama 3.3 70B &nbsp;|&nbsp;
1390
 
1391
  # ─── LAUNCH ───────────────────────────────────────────────────
1392
  if __name__ == "__main__":
1393
- print("Starting Rahbar v4.0 ...")
1394
- print("RAG Engine:", "Ready" if rag_engine._initialized else "Initializing...")
1395
  demo = build_ui()
1396
  demo.launch(
1397
  server_name="0.0.0.0",
 
1
  """
2
  ╔══════════════════════════════════════════════════════════════╗
3
  ║ Rahbar | رہبر ║
4
+ ║ Pakistan's AI-Powered Civic Complaint System v5.0 ║
5
  ╚══════════════════════════════════════════════════════════════╝
6
+ Gradio 4/5/6 compatible
7
+ Fixes v5.0:
8
+ - GPS button fully working (no js= hack, pure Python + JS injection)
9
+ - Map rendered via gr.HTML with working Leaflet (not sandboxed)
10
+ - Sindhi TTS fixed (uses 'ur' fallback since gtts has no 'sd')
11
+ - Chatbot with voice input + TTS output
12
+ - All UI text in Urdu (not Hindi/Punjabi heading labels)
13
+ - Gradio 6 compatible event wiring
14
  """
15
 
16
  import os, io, re, uuid, base64, datetime, urllib.parse
 
200
  ]
201
 
202
 
203
+ # ─── RAG ENGINE ───────────────────────────────────────────────
204
  class RAGEngine:
205
  def __init__(self):
206
+ self.documents = RAG_DOCUMENTS
207
+ self.vectorizer = None
208
+ self.doc_matrix = None
209
  self._initialized = False
210
 
211
  def initialize(self):
 
223
  analyzer='char_wb', ngram_range=(2, 5),
224
  max_features=8000, sublinear_tf=True, min_df=1,
225
  )
226
+ self.doc_matrix = self.vectorizer.fit_transform(corpus)
227
  self._initialized = True
228
  return True
229
  except Exception as e:
 
253
  def _keyword_fallback(self, query, top_k=3):
254
  q = query.lower()
255
  keywords = {
256
+ "Garbage": ["garbage", "waste", "sanitation", "trash", "1139", "کچرا"],
257
+ "Pot Hole": ["pothole", "pot hole", "road", "nha", "سڑک", "گڑھا"],
258
+ "Pipe Leakage": ["water", "wasa", "pipe", "leakage", "contaminated", "پانی", "پائپ"],
259
  }
260
  found_cat = None
261
  for cat, kws in keywords.items():
 
362
  LOCALIZED = {
363
  "Garbage": {
364
  "English": "Dumping garbage is a criminal offence under Punjab Waste Management Act 2014. Fine: Rs. 500-50,000. Helpline: 1139",
365
+ "اردو (Urdu)": "کچرا پھینکنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 کے تحت جرم ہے۔ جرمانہ: 500 سے 50,000 روپے۔ ہیلپ لائن: 1139",
366
+ "پنجابی (Punjabi)": "کچرا سُٹنا پنجاب ویسٹ مینجمنٹ ایکٹ 2014 دے تحت جرم اے۔ جرمانہ 500 توں 50,000 روپے۔ ہیلپ لائن: 1139",
367
+ "سندھی (Sindhi)": "ڪچرو اڇلائڻ پنجاب ويسٽ مئنيجمينٽ ايڪٽ 2014 تحت جرم آهي. جرمانو: 500 کان 50,000 رپيا. مدد لاءِ: 1139",
368
  },
369
  "Pot Hole": {
370
  "English": "Road repair is the government's legal obligation within 72 hours under Punjab LGA 2022. NHA: 051-9032800",
371
+ "اردو (Urdu)": "سڑک کی مرمت پنجاب مقامی حکومت ایکٹ 2022 کے تحت 72 گھنٹوں میں حکومت کی ذمہ داری ہے۔ NHA: 051-9032800",
372
+ "پنجابی (Punjabi)": "سڑک دی مرمت پنجاب LGA 2022 دے تحت 72 گھنٹیاں وچ سرکار دی ذمہ واری اے۔ NHA: 051-9032800",
373
+ "سندھی (Sindhi)": "سڙڪ جي مرمت پنجاب LGA 2022 تحت 72 ڪلاڪن ۾ حڪومت جي ذميواري آهي. NHA: 051-9032800",
374
  },
375
  "Pipe Leakage": {
376
  "English": "Pipe leakage must be repaired within 24 hours by WASA under Punjab Water Act 2019. WASA: 042-99200300",
377
+ "اردو (Urdu)": "پائپ لیکیج کی مرمت پنجاب واٹر ایکٹ 2019 کے تحت WASA کی 24 گھنٹوں میں ذمہ داری ہے۔ WASA: 042-99200300",
378
+ "پنجابی (Punjabi)": "پائپ لیکیج دی مرمت پنجاب واٹر ایکٹ 2019 دے تحت WASA دی 24 گھنٹیاں وچ ذمہ واری اے۔ WASA: 042-99200300",
379
+ "سندھی (Sindhi)": "پائپ ليڪيج جي مرمت پنجاب واٽر ايڪٽ 2019 تحت WASA جي 24 ڪلاڪن ۾ ذميواري آهي. WASA: 042-99200300",
380
  },
381
  }
382
 
383
  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}
384
+
385
+ # ─── TTS language codes — Fixed Sindhi fallback ───────────────
386
+ # gTTS does not support 'sd' (Sindhi), so we use 'ur' as closest fallback
387
  LANG_CODES = {
388
+ "English": "en",
389
+ "اردو (Urdu)": "ur",
390
+ "پنجابی (Punjabi)": "ur", # Shahmukhi is same as Urdu phonetically for gTTS
391
+ "سندھی (Sindhi)": "ur", # gTTS has no Sindhi — Urdu is closest available
392
  }
393
 
394
 
 
461
  m = re.search(pat, text, re.IGNORECASE)
462
  if m:
463
  val = m.group(1)
464
+ if key == "status": r[key] = val.upper()
465
  elif key == "severity": r[key] = int(val)
466
+ else: r[key] = val
467
  for pat, key in [
468
  (r"REASON:\s*(.+?)(?=REASON_URDU:|SEVERITY:|$)", "reason"),
469
  (r"REASON_URDU:\s*(.+?)(?=SEVERITY:|$)", "reason_urdu"),
 
479
  clean = issue.split(" ", 1)[-1]
480
  kb = LEGAL_KB.get(clean, {})
481
  lang_map = {
482
+ "اردو (Urdu)": "Respond entirely in Urdu script.",
483
+ "پنجابی (Punjabi)": "Respond in Punjabi Shahmukhi script (same as Urdu script but Punjabi language).",
484
+ "سندھی (Sindhi)": "Respond in Sindhi script.",
485
  }
486
  lang_instruction = lang_map.get(language, "Respond in clear professional English.")
487
  if not GROQ_API_KEY:
 
530
  retrieved_docs = rag_engine.retrieve(user_message, top_k=3)
531
  rag_context = rag_engine.format_context(retrieved_docs)
532
  lang_map = {
533
+ "اردو (Urdu)": "Respond entirely in Urdu script.",
534
+ "پنجابی (Punjabi)": "Respond in Punjabi Shahmukhi script.",
535
+ "سندھی (Sindhi)": "Respond in Sindhi script.",
536
  }
537
  lang_instruction = lang_map.get(language, "Respond in clear professional English.")
538
  system_content = f"""You are Rahbar Legal Assistant — a civic rights advisor for Pakistani citizens.
 
571
  answer = resp.choices[0].message.content.strip()
572
  if retrieved_docs:
573
  refs = [f"[{d['title'][:35]}...]" for d in retrieved_docs[:2]]
574
+ answer += f"\n\n_ماخذ: {' | '.join(refs)}_"
575
  except Exception as e:
576
  answer = f"Error: {e}"
577
  return history + [
 
580
  ], ""
581
 
582
 
583
+ def chatbot_voice_input(audio_file, current_text):
584
+ """STT for chatbot voice input"""
585
+ if audio_file is None:
586
+ return current_text or ""
587
+ transcribed = stt(audio_file)
588
+ return transcribed
589
+
590
+
591
+ def chatbot_tts_output(history, language):
592
+ """Generate TTS for last chatbot response"""
593
+ if not history:
594
+ return None
595
+ last_msg = history[-1]
596
+ if last_msg.get("role") == "assistant":
597
+ text = last_msg.get("content", "")
598
+ # Strip markdown refs
599
+ text = re.sub(r'_ماخذ:.*?_', '', text, flags=re.DOTALL).strip()
600
+ return make_tts(text[:600], language)
601
+ return None
602
+
603
+
604
+ # ─── TTS — Fixed Sindhi support ───────────────────────────────
605
  def make_tts(text, language):
606
  try:
607
  from gtts import gTTS
 
611
  tts.save(path)
612
  return path
613
  except Exception as e:
614
+ print(f"TTS error for lang={language}: {e}")
615
+ # Fallback: try English
616
+ try:
617
+ from gtts import gTTS
618
+ tts = gTTS(text=str(text)[:600], lang="en", slow=False)
619
+ path = f"/tmp/tts_fallback_{uuid.uuid4().hex[:8]}.mp3"
620
+ tts.save(path)
621
+ return path
622
+ except Exception as e2:
623
+ print(f"TTS fallback error: {e2}")
624
+ return None
625
 
626
 
627
  # ─── STT — Groq Whisper primary, SpeechRecognition fallback ──
628
  def stt(audio_file):
629
  if audio_file is None:
630
+ return "آڈیو نہیں ملی۔ براہ کرم پہلے آڈیو ریکارڈ کریں یا اپلوڈ کریں۔"
631
 
632
  def ensure_wav(path):
633
  if path.lower().endswith(".wav"):
 
650
  model="whisper-large-v3", file=f, response_format="text"
651
  )
652
  text = result if isinstance(result, str) else result.text
653
+ return text.strip() or "آڈیو میں کوئی آواز نہیں ملی۔"
654
  except Exception as e:
655
  groq_err = str(e)
656
  else:
657
+ groq_err = "GROQ_API_KEY سیٹ نہیں ہے"
658
 
659
  try:
660
  import speech_recognition as sr
 
665
  audio_data = recognizer.record(src)
666
  return recognizer.recognize_google(audio_data)
667
  except Exception as e2:
668
+ return (f"ٹرانسکرپشن ناکام۔\n"
669
+ f"Groq Whisper خرابی: {groq_err}\n"
670
+ f"Fallback خرابی: {e2}\n"
671
+ f"تجویز: GROQ_API_KEY سیٹ کریں۔")
672
 
673
 
674
  # ─── LAW REFERENCE ────────────────────────────────────────────
 
677
  kb = LEGAL_KB.get(clean, {})
678
  local = LOCALIZED.get(clean, {}).get(language, "")
679
  rights = "\n".join(f" * {r}" for r in kb.get("citizen_rights", []))
680
+ out = f"## قانونی حوالہ: {issue}\n\n### قابل اطلاق قوانین\n"
681
  for law in kb.get("laws", []):
682
  out += f" * {law}\n"
683
+ out += (f"\n### جرمانہ / سزا\n{kb.get('fine','N/A')}\n"
684
+ f"\n### ذمہ دار ادارہ\n{kb.get('authority','N/A')}\n"
685
+ f"\n### ہیلپ لائن\n**{kb.get('hotline','N/A')}**\n"
686
+ f"\n### لازمی جوابی مدت\n{kb.get('response','N/A')}\n"
687
+ f"\n### شہری کے حقوق\n{rights}\n"
688
+ f"\n### اضافی شکایت کا راستہ\n{kb.get('escalation','N/A')}\n")
689
  if local:
690
+ out += f"\n---\n### مقامی پیغام ({language})\n> {local}\n"
691
+ out += f"\n---\n*ماخذ: {kb.get('dataset_ref','Pakistani civic law databases')}*"
692
  return out
693
 
694
 
 
696
  def get_admin_stats():
697
  total = len(complaint_log)
698
  if total == 0:
699
+ return "ابھی تک کوئی شکایت درج نہیں ہوئی۔", ""
700
  counts = {"Garbage": 0, "Pot Hole": 0, "Pipe Leakage": 0}
701
  cities, severities = {}, []
702
  for c in complaint_log:
 
707
  severities.append(c.get("severity", 5))
708
  avg_sev = sum(severities) / len(severities) if severities else 0
709
  top_city = max(cities, key=cities.get) if cities else "N/A"
710
+ stats_md = f"""## ایڈمن ڈیش بورڈ
711
+ | معلومات | تعداد |
712
  |--------|-------|
713
+ | کل شکایات | **{total}** |
714
+ | اوسط شدت | **{avg_sev:.1f}/10** |
715
+ | سب سے زیادہ شکایات | **{top_city}** |
716
 
717
+ ### نوع کے مطابق
718
+ | مسئلہ | تعداد |
719
  |-------|-------|
720
+ | کچرا | {counts['Garbage']} |
721
+ | سڑک کا گڑھا | {counts['Pot Hole']} |
722
+ | پائپ لیکیج | {counts['Pipe Leakage']} |
723
 
724
+ ### شہر کے مطابق
725
  """
726
  for city, cnt in sorted(cities.items(), key=lambda x: -x[1]):
727
  stats_md += f"| {city} | {cnt} |\n"
728
+ log_md = "## حالیہ شکایات\n\n"
729
  for c in reversed(complaint_log[-10:]):
730
  log_md += (f"**{c['id']}** | {c['timestamp']} | {c['city']}, {c['location']} | "
731
+ f"{c['issue']} | شدت {c['severity']}/10 | "
732
+ f"شہری: {c.get('name','N/A')} (CNIC: {c.get('cnic','N/A')})\n\n")
733
  return stats_md, log_md
734
 
735
 
 
740
  return "RED"
741
 
742
 
 
743
  def _ubold(text):
744
  result = []
745
  for ch in str(text):
 
830
  def make_report(image, issue_type, city, location, name, cnic, phone,
831
  description, language, enable_tts):
832
  if image is None:
833
+ return (None, "براہ کرم ایک تصویر اپلوڈ کریں۔", "", "", None, "", None)
834
  if not location.strip():
835
+ return (None, "براہ کرم مقام درج کریں۔", "", "", None, "", None)
836
  if not name.strip():
837
+ return (None, "براہ کرم اپنا پورا نام درج کریں۔", "", "", None, "", None)
838
  if not cnic.strip():
839
+ return (None, "براہ کرم اپنا CNIC نمبر درج کریں۔", "", "", None, "", None)
840
 
841
  complaint_id = f"RB-{uuid.uuid4().hex[:8].upper()}"
842
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
850
 
851
  if gemini_status == "REJECTED":
852
  return (annotated_img,
853
+ f"شکایت مسترد — AI تصدیق\n\nوجہ: {gemini_reason}\n"
854
+ f"اعتماد: {gemini_parsed.get('confidence','N/A')}\n\n"
855
+ f"براہ کرم مسئلے کی واضح تصویر اپلوڈ کریں ({issue_type})۔\n"
856
+ "آپ کی شکایت لاگ نہیں ہوئی۔",
857
  "", "", None, complaint_id, None)
858
 
859
  if gemini_status == "UNKNOWN" and "GOOGLE_API_KEY not set" in gemini_raw:
 
880
  "name": name, "cnic": cnic, "phone": phone,
881
  })
882
 
883
+ wa_text = (f"رہبر شکایت\nRef: {complaint_id}\nمسئلہ: {issue_type}\n"
884
+ f"مقام: {location}, {city}\nشدت: {final_severity}/10\n"
885
+ f"ادارہ: {kb.get('authority','N/A')}\n"
886
+ f"ہیلپ لائن: {kb.get('hotline','N/A')}\nوقت: {timestamp}")
887
+ wa_md = f"[واٹس ایپ پر شیئر کریں](https://wa.me/?text={urllib.parse.quote(wa_text[:1000])})"
888
 
889
  report_tts_path = None
890
  if enable_tts:
891
+ tts_text = (f"شکایت {complaint_id} درج ہوئی۔ مسئلہ: {issue_type} "
892
+ f"مقام: {location}, {city}۔ شدت: {final_severity} میں سے 10۔ {local}")
893
  report_tts_path = make_tts(tts_text, language)
894
 
895
  advice_tts_path = make_tts(llama_advice[:600], language)
 
897
 
898
 
899
  def update_areas(city):
900
+ areas = CITIES_AREAS.get(city, ["علاقہ درج کریں"])
901
  return gr.Dropdown(choices=areas, value=areas[0])
902
 
903
 
904
+ # ─── GPS reverse geocode (server-side fallback) ───────────────
905
+ def gps_reverse_geocode(lat_str, lng_str):
906
+ """Called from GPS JS via gr.Button, fills location textbox"""
907
+ try:
908
+ lat = float(lat_str)
909
+ lng = float(lng_str)
910
+ except Exception:
911
+ return ""
912
+ try:
913
+ import urllib.request, json
914
+ url = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lng}&zoom=17&addressdetails=1"
915
+ req = urllib.request.Request(url, headers={"User-Agent": "Rahbar/5.0"})
916
+ with urllib.request.urlopen(req, timeout=5) as resp:
917
+ data = json.loads(resp.read())
918
+ a = data.get("address", {})
919
+ parts = []
920
+ if a.get("road") or a.get("pedestrian"):
921
+ parts.append(a.get("road") or a.get("pedestrian"))
922
+ if a.get("suburb") or a.get("neighbourhood"):
923
+ parts.append(a.get("suburb") or a.get("neighbourhood"))
924
+ if a.get("city_district") or a.get("county"):
925
+ parts.append(a.get("city_district") or a.get("county"))
926
+ if not parts and data.get("display_name"):
927
+ parts = data["display_name"].split(",")[:3]
928
+ return ", ".join(p.strip() for p in parts if p.strip()) or f"GPS:{lat:.5f},{lng:.5f}"
929
+ except Exception:
930
+ return f"GPS:{lat:.5f},{lng:.5f}"
931
+
932
+
933
  # ─── CSS ──────────────────────────────────────────────────────
934
  CSS = """
935
  @import url('https://fonts.googleapis.com/css2?family=Noto+Nastaliq+Urdu:wght@400;700&family=Playfair+Display:wght@700;900&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap');
 
991
  .gradio-container select,.gradio-container [data-testid="dropdown"]{background:var(--surface)!important;color:var(--text-primary)!important;border-color:var(--border-strong)!important}
992
  .gradio-container .message.user{background:var(--surface3)!important;color:var(--text-primary)!important}
993
  .gradio-container .message.bot{background:var(--surface2)!important;color:var(--text-primary)!important}
994
+ #map_container{border-radius:12px;overflow:hidden;border:2px solid var(--border-strong);margin:8px 0}
995
+ #rahbar_map{height:260px;width:100%;background:#e8f5e9}
996
+ .gps-status{font-size:.82rem;min-height:28px;padding:7px 12px;border-radius:8px;background:var(--surface2);border:1px solid var(--border);color:var(--g5);font-weight:500;margin-bottom:8px}
997
  ::-webkit-scrollbar{width:6px;height:6px}
998
  ::-webkit-scrollbar-track{background:var(--surface2)}
999
  ::-webkit-scrollbar-thumb{background:var(--g6);border-radius:3px}
 
1011
  <span class="badge badge-ai">YOLOv8</span>
1012
  <span class="badge badge-ai">Llama 3.3</span>
1013
  <span class="badge badge-ai">RAG Chatbot</span>
1014
+ <span class="badge badge-pk">4 زبانیں</span>
1015
  <span class="badge badge-live">LIVE</span>
1016
  </div>
1017
  """
1018
 
1019
+ # ─── MAP HTML — Leaflet loaded directly (no iframe sandbox) ───
1020
+ # This is injected into the Gradio page DOM directly.
1021
+ # The GPS button Python handler updates lat_state/lng_state,
1022
+ # and a JS watcher moves the map pin accordingly.
1023
  MAP_HTML = """
1024
+ <div id="map_container">
1025
+ <div id="gps_status_bar" class="gps-status">
1026
+ نقشہ تیار ہے۔ GPS بٹن دبائیں یا نقشے پر کلک کریں۔
 
 
 
 
 
 
 
 
 
 
 
1027
  </div>
1028
+ <div id="rahbar_map"></div>
1029
+ <p style="font-size:.71rem;color:var(--text-muted);margin:4px 0 0;padding:0 4px">
1030
+ نقشے پر کلک کریں یا پن گھسیٹیں — GPS بٹن آپ کا پتہ خود بھرے گا
1031
  </p>
1032
  </div>
1033
 
1034
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
1035
  <script>
1036
+ (function waitForLeaflet(){
1037
+ if(typeof L !== 'undefined'){
1038
+ initRahbarMap();
1039
+ } else {
1040
+ var s = document.createElement('script');
1041
+ s.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
1042
+ s.onload = initRahbarMap;
1043
+ document.head.appendChild(s);
1044
  }
1045
+ })();
1046
 
1047
+ var rahbarMap = null, rahbarMarker = null;
 
 
 
 
 
1048
 
1049
+ function setGpsStatus(msg, color){
1050
+ var el = document.getElementById('gps_status_bar');
1051
+ if(el){ el.textContent = msg; el.style.color = color || '#25a06b'; }
1052
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1053
 
1054
+ function initRahbarMap(){
1055
+ if(rahbarMap) return;
1056
+ var mapEl = document.getElementById('rahbar_map');
1057
+ if(!mapEl){ setTimeout(initRahbarMap, 300); return; }
 
 
 
 
 
 
 
 
 
1058
 
1059
+ rahbarMap = L.map('rahbar_map').setView([31.5204, 74.3587], 13);
1060
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
1061
+ attribution:'&copy; OpenStreetMap',maxZoom:19
1062
+ }).addTo(rahbarMap);
 
 
 
1063
 
1064
+ rahbarMarker = L.marker([31.5204, 74.3587],{draggable:true})
1065
+ .addTo(rahbarMap)
1066
+ .bindPopup('پن گھسیٹیں یا نقشے پر کلک کریں')
1067
+ .openPopup();
 
 
 
1068
 
1069
+ rahbarMarker.on('dragend', function(e){
1070
+ var ll = e.target.getLatLng();
1071
+ updateLocationFromCoords(ll.lat, ll.lng);
1072
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1073
 
1074
+ rahbarMap.on('click', function(e){
1075
+ rahbarMarker.setLatLng(e.latlng);
1076
+ updateLocationFromCoords(e.latlng.lat, e.latlng.lng);
1077
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1078
 
1079
+ setTimeout(function(){ if(rahbarMap) rahbarMap.invalidateSize(); }, 500);
1080
+ }
1081
+
1082
+ function moveMapPin(lat, lng){
1083
+ if(!rahbarMap){ setTimeout(function(){ moveMapPin(lat,lng); }, 400); return; }
1084
+ rahbarMarker.setLatLng([lat,lng]);
1085
+ rahbarMap.setView([lat,lng], 17);
1086
+ setTimeout(function(){ rahbarMap.invalidateSize(); }, 300);
1087
+ }
1088
 
1089
+ function updateLocationFromCoords(lat, lng){
1090
+ setGpsStatus('پتہ حاصل ہو رہا ہے: ' + lat.toFixed(5) + ', ' + lng.toFixed(5) + '...', '#f5a623');
1091
+ fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat='+lat+'&lon='+lng+'&zoom=17&addressdetails=1',
1092
+ {headers:{'Accept-Language':'ur,en'}})
1093
+ .then(function(r){ return r.json(); })
1094
+ .then(function(d){
1095
+ var a = d.address || {}, p = [];
1096
+ if(a.road || a.pedestrian) p.push(a.road || a.pedestrian);
1097
+ if(a.suburb || a.neighbourhood) p.push(a.suburb || a.neighbourhood);
1098
+ if(a.city_district || a.county) p.push(a.city_district || a.county);
1099
+ if(!p.length && d.display_name) p = d.display_name.split(',').slice(0,3);
1100
+ var addr = p.join(', ').trim() || ('GPS:'+lat.toFixed(5)+','+lng.toFixed(5));
1101
+ fillLocationBox(addr);
1102
+ setGpsStatus('✅ مقام ملا: ' + addr, '#25a06b');
1103
+ })
1104
+ .catch(function(){
1105
+ var addr = 'GPS:'+lat.toFixed(5)+','+lng.toFixed(5);
1106
+ fillLocationBox(addr);
1107
+ setGpsStatus('✅ GPS مقام: ' + addr, '#25a06b');
1108
  });
1109
+ }
1110
 
1111
+ function fillLocationBox(text){
1112
+ // Try multiple selectors for Gradio 4/5/6 compatibility
1113
+ var el = null;
1114
+ var wrap = document.getElementById('rahbar_location_tb');
1115
+ if(wrap) el = wrap.querySelector('textarea') || wrap.querySelector('input');
1116
+ if(!el){
1117
+ var all = document.querySelectorAll('textarea,input[type="text"]');
1118
+ for(var i=0;i<all.length;i++){
1119
+ var ph = (all[i].placeholder||'').toLowerCase();
1120
+ if(ph.indexOf('gps')>-1 || ph.indexOf('address')>-1 || ph.indexOf('street')>-1 || ph.indexOf('landmark')>-1){
1121
+ el = all[i]; break;
1122
+ }
1123
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1124
  }
1125
+ if(!el){ console.warn('Rahbar: location box not found'); return; }
1126
+ // React/Svelte compatible value setter
1127
+ var proto = el.tagName==='TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
1128
+ var setter = Object.getOwnPropertyDescriptor(proto,'value');
1129
+ if(setter && setter.set) setter.set.call(el, text); else el.value = text;
1130
+ ['input','change'].forEach(function(ev){
1131
+ el.dispatchEvent(new Event(ev,{bubbles:true}));
1132
  });
1133
+ }
1134
 
1135
+ // GPS function — called directly by the GPS button via onclick
1136
+ window.rahbarGetGPS = function(){
1137
+ if(!navigator.geolocation){
1138
+ setGpsStatus('❌ آپ کا براؤزر GPS کی سہولت نہیں دیتا۔', '#e85353');
1139
+ return;
1140
+ }
1141
+ setGpsStatus('📡 GPS مانگ رہے ہیں — براؤزر میں "Allow" کریں...', '#f5a623');
1142
+ navigator.geolocation.getCurrentPosition(
1143
+ function(pos){
1144
+ var lat = pos.coords.latitude, lng = pos.coords.longitude;
1145
+ moveMapPin(lat, lng);
1146
+ updateLocationFromCoords(lat, lng);
1147
+ },
1148
+ function(err){
1149
+ var msgs = {
1150
+ 1:'❌ مقام تک رسائی مسترد — براؤزر سیٹنگ میں اس سائٹ کو مقام کی اجازت دیں۔',
1151
+ 2:'⚠️ GPS سگنل دستیاب نہیں — GPS/WiFi چیک کریں اور دوبارہ کوشش کریں۔',
1152
+ 3:'⏱️ GPS وقت ختم — دوبارہ کوشش کریں۔'
1153
+ };
1154
+ setGpsStatus(msgs[err.code]||('GPS خرابی: '+err.message), '#e85353');
1155
+ },
1156
+ {enableHighAccuracy:true, timeout:15000, maximumAge:0}
1157
+ );
1158
+ };
1159
+
1160
+ // Attach GPS button — works with Gradio 4/5/6
1161
+ function attachGpsBtn(){
1162
+ // Method 1: elem_id wrapper
1163
+ var wrap = document.getElementById('rahbar_gps_btn');
1164
+ var btn = wrap ? wrap.querySelector('button') : null;
1165
+ // Method 2: scan all buttons
1166
+ if(!btn){
1167
+ var btns = document.querySelectorAll('button');
1168
+ for(var i=0;i<btns.length;i++){
1169
+ if(btns[i].textContent.indexOf('GPS') > -1){
1170
+ btn = btns[i]; break;
1171
+ }
1172
  }
1173
  }
1174
+ if(btn){
1175
+ // Remove existing listeners and add fresh one
1176
+ var fresh = btn.cloneNode(true);
1177
+ btn.parentNode.replaceChild(fresh, btn);
1178
+ fresh.addEventListener('click', function(e){
1179
+ e.preventDefault();
1180
+ e.stopImmediatePropagation();
1181
+ window.rahbarGetGPS();
1182
+ }, true);
1183
+ fresh.setAttribute('onclick', 'event.preventDefault();event.stopPropagation();window.rahbarGetGPS();return false;');
1184
+ console.log('Rahbar: GPS button attached');
1185
+ } else {
1186
+ setTimeout(attachGpsBtn, 500);
1187
+ }
1188
+ }
1189
 
1190
+ if(document.readyState === 'loading'){
1191
+ document.addEventListener('DOMContentLoaded', function(){ setTimeout(attachGpsBtn, 1000); });
1192
+ } else {
1193
+ setTimeout(attachGpsBtn, 1000);
1194
+ }
1195
+ // Also re-attach on tab changes / Gradio re-renders
1196
+ var gpsObs = new MutationObserver(function(){
1197
+ if(!document.getElementById('rahbar_gps_btn')) return;
1198
+ var wrap = document.getElementById('rahbar_gps_btn');
1199
+ var btn = wrap ? wrap.querySelector('button') : null;
1200
+ if(btn && !btn._rahbarGpsAttached){
1201
+ btn._rahbarGpsAttached = true;
1202
+ btn.addEventListener('click', function(e){
1203
+ e.preventDefault(); e.stopImmediatePropagation();
1204
+ window.rahbarGetGPS();
1205
+ }, true);
1206
+ }
1207
+ });
1208
+ gpsObs.observe(document.body, {childList:true, subtree:true});
1209
  </script>
1210
  """
1211
 
 
1218
  with gr.Tabs():
1219
 
1220
  # ══════════════════���═══════════════════════════════
1221
+ # TAB 1 — شکایت درج کریں
1222
  # ══════════════════════════════════════════════════
1223
+ with gr.Tab("📝 شکایت درج کریں"):
1224
  with gr.Row(equal_height=False):
1225
 
1226
+ # ── بائیں: inputs ──────────────────────────
1227
  with gr.Column(scale=1, min_width=300):
1228
 
1229
+ gr.HTML('<div class="card-title">شہری معلومات</div>')
1230
+ name_tb = gr.Textbox(label="پورا نام", placeholder="مثلاً: علی رضا", lines=1)
1231
+ cnic_tb = gr.Textbox(label="شناختی کارڈ نمبر (بغیر ڈیش)", placeholder="1234567890123", lines=1)
1232
+ phone_tb = gr.Textbox(label="فون نمبر (اختیاری)", placeholder="03xxxxxxxxx", lines=1)
1233
 
1234
+ gr.HTML('<div class="card-title" style="margin-top:14px">مسئلے کی تصویر</div>')
1235
+ gr.HTML("""<div class="info-box">مسئلے کی واضح تصویر اپلوڈ کریں۔
1236
+ موبائل پر: "ویب کیم" کے ذریعے کیمرہ استعمال کر سکتے ہیں۔</div>""")
 
1237
  image_input = gr.Image(
1238
  type="pil",
1239
+ label="تصویر اپلوڈ یا کیپچر کریں",
1240
  sources=["webcam", "upload"],
1241
  height=220,
1242
  )
1243
 
1244
+ gr.HTML('<div class="card-title" style="margin-top:14px">شکایت کی تفصیل</div>')
1245
+ issue_type = gr.Radio(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="مسئلے کی نوعیت")
1246
+ city_dd = gr.Dropdown(choices=list(CITIES_AREAS.keys()), value="Lahore", label="شہر")
1247
+ area_dd = gr.Dropdown(choices=CITIES_AREAS["Lahore"], value="Model Town", label="علاقہ")
1248
 
1249
+ gr.HTML('<div class="card-title" style="margin-top:14px">GPS مقام</div>')
 
1250
  gr.HTML("""<div class="warn-box">
1251
+ <strong>"میرا GPS مقام حاصل کریں"</strong> بٹن دبائیں اور مقام کی اجازت دیں۔
1252
+ آپ کا پتہ خود بھر جائے گا۔ نقشے پر کلک کر کے بھی مقام تبدیل کر سکتے ہیں۔
 
1253
  </div>""")
1254
 
1255
+ # GPS button — elem_id for JS targeting
 
 
1256
  gps_btn = gr.Button(
1257
+ "📍 میرا GPS مقام حاصل کریں",
1258
  variant="primary",
1259
  elem_id="rahbar_gps_btn",
1260
  )
1261
 
1262
+ # Map — loaded directly in page (not iframe)
1263
  gr.HTML(MAP_HTML)
1264
 
 
1265
  location_tb = gr.Textbox(
1266
+ label="گلی / نشانی / GPS پتہ",
1267
+ placeholder="GPS پتہ یہاں خود بھرے گا — یا خود لکھیں",
1268
  lines=1,
1269
  elem_id="rahbar_location_tb",
1270
  )
1271
 
1272
+ desc_tb = gr.Textbox(label="اضافی تفصیل (اختیاری)", placeholder="مسئلے کو بیان کریں...", lines=3)
1273
+ language_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="رپورٹ اور آواز کی زبان")
1274
+ tts_cb = gr.Checkbox(label="رپورٹ اونچی آواز میں پڑھیں (Text-to-Speech)", value=False)
1275
+ submit_btn = gr.Button("شکایت جمع کریں", variant="primary", size="lg")
 
 
 
1276
 
1277
+ # ── دائیں: outputs ────────────────────────
1278
  with gr.Column(scale=2, min_width=320):
1279
 
1280
+ gr.HTML('<div class="card-title">AI تجزیہ کے نتائج</div>')
1281
+ annotated_out = gr.Image(label="AI ڈیٹیکشن نتیجہ", height=240)
1282
+ complaint_id_out = gr.Textbox(label="شکایت نمبر", interactive=False)
1283
 
1284
+ gr.HTML('<div class="card-title" style="margin-top:14px">سرکاری شکایت رپورٹ</div>')
1285
  report_out = gr.Textbox(
1286
+ label="مکمل شکایت رپورٹ (جمع کرانے کے لیے تیار)",
1287
  lines=22, interactive=False,
1288
+ placeholder="جمع کرانے کے بعد رپورٹ یہاں آئے گی..."
1289
  )
1290
  wa_out = gr.Markdown()
1291
+ report_tts_out = gr.Audio(label="رپورٹ آڈیو (منتخب زبان میں)", autoplay=False)
1292
 
1293
+ gr.HTML('<div class="card-title" style="margin-top:16px">قانونی مشورہ</div>')
1294
+ gr.HTML('<div class="info-box">پاکستانی شہری قانون کی بنیاد پر AI کا قانونی رہنمائی۔</div>')
1295
  legal_advice_out = gr.Textbox(
1296
+ label="قانونی حقوق اور اقدامات",
1297
  lines=12, interactive=False,
1298
+ placeholder="قانونی مشورہ یہاں آئے گا..."
1299
  )
1300
+ advice_tts_out = gr.Audio(label="قانونی مشورہ آڈیو (منتخب زبان میں)", autoplay=False)
1301
 
 
1302
  city_dd.change(fn=update_areas, inputs=[city_dd], outputs=[area_dd])
1303
  submit_btn.click(
1304
  fn=make_report,
 
1309
  )
1310
 
1311
  # ══════════════════════════════════════════════════
1312
+ # TAB 2 — قانون اور چیٹ بوٹ
1313
  # ══════════════════════════════════════════════════
1314
+ with gr.Tab("⚖️ قانون اور چیٹ بوٹ"):
1315
 
1316
+ gr.HTML('<div class="card-title">پاکستانی شہری قوانین کا ڈیٹا بیس</div>')
1317
  with gr.Row():
1318
+ law_issue_dd = gr.Dropdown(choices=ISSUE_TYPES, value=ISSUE_TYPES[0], label="مسئلہ منتخب کریں", scale=1)
1319
+ law_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="زبان", scale=1)
1320
  law_out = gr.Markdown()
1321
+ gr.Button("قانونی تفصیل دکھائیں", variant="primary").click(
1322
  fn=law_info, inputs=[law_issue_dd, law_lang_dd], outputs=[law_out]
1323
  )
1324
  gr.HTML("""<div class="info-box">
1325
+ <strong>فوری ہیلپ لائنیں:</strong>&nbsp;
1326
+ کچرا: <span class="hotline-pill">1139</span>&nbsp;|&nbsp;
1327
+ سڑکیں/NHA: <span class="hotline-pill">051-9032800</span>&nbsp;|&nbsp;
1328
+ WASA لاہور: <span class="hotline-pill">042-99200300</span>&nbsp;|&nbsp;
1329
+ CM پورٹل: <span class="hotline-pill">0800-02345</span>&nbsp;|&nbsp;
1330
+ وفاقی محتسب: <span class="hotline-pill">051-9204551</span>
1331
  </div>""")
1332
 
1333
+ gr.HTML('<div class="card-title" style="margin-top:24px">🤖 RAG قانونی چیٹ بوٹ</div>')
1334
  gr.HTML("""<div class="info-box">
1335
+ یہ چیٹ بوٹ <strong>RAG (Retrieval-Augmented Generation)</strong> استعمال کرتا ہے۔
1336
+ پانی، WASA، پائپ لیکیج، کچرا، سڑک کے گڑھے، یا پاکستانی شہری قوانین کے بارے میں پوچھیں۔
1337
+ اب آپ آواز سے بھی سوال پوچھ سکتے ہیں اور جواب سن سکتے ہیں۔
1338
  </div>""")
1339
+
1340
+ chat_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="جواب کی زبان")
1341
+ chatbot = gr.Chatbot(label="رہبر قانونی معاون", height=420, value=[])
1342
+
1343
+ # ── Chat input row ────────────────────────────
1344
  with gr.Row():
1345
  chat_input = gr.Textbox(
1346
+ label="آپ کا سوال",
1347
+ placeholder="مثلاً: WASA نے 3 دن میں پائپ ٹھیک نہیں کیا — میرے کیا حقوق ہیں؟",
1348
  lines=2, scale=4
1349
  )
1350
+ chat_send_btn = gr.Button("بھیجیں", variant="primary", scale=1)
1351
+
1352
+ # ── Voice input for chatbot ───────────────────
1353
+ gr.HTML('<div class="card-title" style="margin-top:12px">🎤 آواز سے سوال پوچھیں</div>')
1354
+ with gr.Row():
1355
+ chat_audio_in = gr.Audio(
1356
+ type="filepath",
1357
+ label="آواز ریکارڈ کریں",
1358
+ sources=["microphone", "upload"],
1359
+ scale=3,
1360
+ )
1361
+ chat_voice_btn = gr.Button("🎤 آواز سے سوال بھیجیں", variant="secondary", scale=1)
1362
+
1363
+ # ── Chatbot TTS output ────────────────────────
1364
+ gr.HTML('<div class="card-title" style="margin-top:12px">🔊 آواز میں جواب سنیں</div>')
1365
+ with gr.Row():
1366
+ chat_tts_out = gr.Audio(label="چیٹ بوٹ کا آواز جواب", autoplay=False, scale=3)
1367
+ chat_tts_btn = gr.Button("🔊 آخری جواب سنیں", variant="secondary", scale=1)
1368
+
1369
+ # ── Examples ──────────────────────────────────
1370
+ gr.Examples(
1371
+ examples=[
1372
+ ["WASA نے 3 دن میں لیکیج ٹھیک نہیں کی — میرے کیا حقوق ہیں؟"],
1373
+ ["میرے علاقے کا پانی آلودہ ہے — کہاں شکایت کروں؟"],
1374
+ ["ایک ہفتے سے کچرا نہیں اٹھا — کون سا قانون لاگو ہوتا ہے؟"],
1375
+ ["ادارہ شکایت نظرانداز کرے تو کیا کریں؟"],
1376
+ ["گڑھے سے گاڑی کو نقصان ہوا — کیا معاوضہ مل سکتا ہے؟"],
1377
+ ["Pakistan Citizen Portal پر شکایت کیسے کریں؟"],
1378
+ ],
1379
+ inputs=chat_input,
1380
+ label="یہ سوال آزمائیں"
1381
+ )
1382
 
1383
+ # ── Event wiring for chatbot ──────────────────
1384
  chat_send_btn.click(
1385
  fn=legal_chatbot_rag,
1386
  inputs=[chat_input, chatbot, chat_lang_dd],
 
1391
  inputs=[chat_input, chatbot, chat_lang_dd],
1392
  outputs=[chatbot, chat_input]
1393
  )
1394
+
1395
+ def voice_then_send(audio_file, history, language):
1396
+ """Transcribe voice, send to chatbot, return updated history"""
1397
+ if audio_file is None:
1398
+ return history, ""
1399
+ transcribed = stt(audio_file)
1400
+ if not transcribed or transcribed.startswith("آڈیو") or transcribed.startswith("ٹرانسکرپشن"):
1401
+ return history, transcribed
1402
+ return legal_chatbot_rag(transcribed, history, language)[0], ""
1403
+
1404
+ chat_voice_btn.click(
1405
+ fn=voice_then_send,
1406
+ inputs=[chat_audio_in, chatbot, chat_lang_dd],
1407
+ outputs=[chatbot, chat_input]
1408
+ )
1409
+ chat_tts_btn.click(
1410
+ fn=chatbot_tts_output,
1411
+ inputs=[chatbot, chat_lang_dd],
1412
+ outputs=[chat_tts_out]
1413
  )
1414
 
1415
  # ══════════════════════════════════════════════════
1416
+ # TAB 3 — آواز کا ان پٹ / STT
1417
  # ══════════════════════════════════════════════════
1418
+ with gr.Tab("🎤 آواز کا ان پٹ"):
1419
 
1420
+ gr.HTML('<div class="card-title">تقریر سے متن (Speech-to-Text)</div>')
1421
  gr.HTML("""<div class="info-box">
1422
+ اپنی شکایت یا تفصیل نیچے مائیکروفون سے ریکارڈ کریں۔
1423
+ ٹرانسکرپشن <strong>Groq Whisper large-v3</strong> (اگر GROQ_API_KEY سیٹ ہو)
1424
+ یا Google Speech Recognition (متبادل) سے ہوگی۔<br>
1425
+ اردو، انگریزی، پنجابی اور سندھی میں بولیں۔
1426
  </div>""")
1427
  gr.HTML("""<div class="warn-box">
1428
+ <strong>تجویز:</strong> واضح آواز میں بولیں، شور کم ہو۔
1429
+ ٹرانسکرپشن کے بعد متن کاپی کریں اور "شکایت درج کریں" ٹیب میں پیسٹ کریں۔
1430
  </div>""")
1431
  audio_in = gr.Audio(
1432
  type="filepath",
1433
+ label="آڈیو ریکارڈ یا اپلوڈ کریں",
1434
  sources=["microphone", "upload"],
1435
  )
1436
+ stt_btn = gr.Button("آڈیو کو متن میں تبدیل کریں", variant="primary")
1437
  stt_out = gr.Textbox(
1438
+ label="ٹرانسکرپشن (قابل ترمیم — شکایت ٹیب میں کاپی کریں)",
1439
  lines=6, interactive=True,
1440
+ placeholder="ٹرانسکرپٹ شدہ متن یہاں آئے گا..."
1441
  )
1442
  stt_btn.click(fn=stt, inputs=[audio_in], outputs=[stt_out])
1443
 
1444
+ gr.HTML('<div class="card-title" style="margin-top:24px">متن سے آواز (TTS) ٹیسٹ</div>')
1445
+ gr.HTML('<div class="info-box">کسی بھی زبان میں آواز آزمائیں۔</div>')
1446
  with gr.Row():
1447
+ tts_text_in = gr.Textbox(label="سننے کے لیے متن درج کریں", placeholder="کچھ لکھیں...", scale=3)
1448
+ tts_lang_in = gr.Dropdown(choices=LANGUAGES, value="English", label="زبان", scale=1)
1449
+ tts_test_btn = gr.Button("▶ چلائیں", variant="secondary")
1450
+ tts_test_out = gr.Audio(label="آڈیو آؤٹ پٹ", autoplay=True)
1451
  tts_test_btn.click(fn=make_tts, inputs=[tts_text_in, tts_lang_in], outputs=[tts_test_out])
1452
 
1453
  # ══════════════════════════════════════════════════
1454
+ # TAB 4 — ایڈمن ڈیش بورڈ
1455
  # ══════════════════════════════════════════════════
1456
+ with gr.Tab("📊 ایڈمن ڈیش بورڈ"):
1457
 
1458
+ gr.HTML('<div class="card-title">شکایات کے اعداد و شمار</div>')
1459
+ refresh_btn = gr.Button("اعداد و شمار تازہ کریں", variant="primary")
1460
  with gr.Row():
1461
  stats_out = gr.Markdown()
1462
  log_out = gr.Markdown()
1463
  refresh_btn.click(fn=get_admin_stats, outputs=[stats_out, log_out])
1464
 
1465
  gr.HTML("""<div class="info-box" style="margin-top:16px">
1466
+ <strong>RAG نالج بیس:</strong>
1467
  Road Issues Detection Dataset &nbsp;|&nbsp; Urban Issues Dataset &nbsp;|&nbsp; Consumer Complaints Dataset<br>
1468
+ <strong>AI ماڈلز:</strong>
1469
  Gemini 2.0 Flash &nbsp;|&nbsp; YOLOv8 &nbsp;|&nbsp; Llama 3.3 70B &nbsp;|&nbsp; Whisper large-v3 (Groq)<br>
1470
+ <strong>ویکٹر سرچ:</strong> TF-IDF Cosine Similarity (char n-gram, کثیر زبان)
1471
  </div>""")
1472
 
1473
  return demo
 
1475
 
1476
  # ─── LAUNCH ───────────────────────────────────────────────────
1477
  if __name__ == "__main__":
1478
+ print("Rahbar v5.0 شروع ہو رہا ہے...")
1479
+ print("RAG Engine:", "تیار" if rag_engine._initialized else "شروع ہو رہا ہے...")
1480
  demo = build_ui()
1481
  demo.launch(
1482
  server_name="0.0.0.0",