Ayesha Mehar commited on
Commit
e9fa4bf
·
verified ·
1 Parent(s): cba07ee

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +396 -149
app.py CHANGED
@@ -3,9 +3,11 @@
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
@@ -292,9 +294,6 @@ Keep it concise and practical for a common Pakistani citizen."""
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():
@@ -348,7 +347,6 @@ Rules:
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"]})
@@ -472,36 +470,42 @@ def severity_icon(score):
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}
@@ -510,63 +514,57 @@ def build_professional_report(complaint_id, timestamp, name, cnic, phone, city,
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 : ___________________________
@@ -574,7 +572,6 @@ def build_professional_report(complaint_id, timestamp, name, cnic, phone, city,
574
 
575
  Generated by: Rahbar - Pakistan's AI Civic Redressal Platform
576
  Timestamp: {timestamp}
577
- ===========================================================================
578
  """
579
  return report
580
 
@@ -618,7 +615,6 @@ def make_report(image, issue_type, city, location, name, cnic, phone,
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(
@@ -641,7 +637,6 @@ def make_report(image, issue_type, city, location, name, cnic, phone,
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. "
@@ -649,53 +644,267 @@ def make_report(image, issue_type, city, location, name, cnic, phone,
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 = """
@@ -714,33 +923,30 @@ HEADER_HTML = """
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
 
@@ -753,40 +959,94 @@ MAP_HTML = """
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
 
@@ -794,26 +1054,27 @@ MAP_HTML = """
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');
@@ -840,7 +1101,7 @@ def update_areas(city):
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():
@@ -868,7 +1129,7 @@ def build_ui():
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)
@@ -932,13 +1193,7 @@ def build_ui():
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",
@@ -957,7 +1212,6 @@ def build_ui():
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?"],
@@ -975,14 +1229,9 @@ def build_ui():
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>')
@@ -1010,8 +1259,6 @@ if __name__ == "__main__":
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,
 
3
  ║ Rahbar | رہبر ║
4
  ║ Pakistan's AI-Powered Civic Complaint System ║
5
  ╚══════════════════════════════════════════════════════════════╝
6
+ Changes:
7
+ • GPS location button now fills location field via reverse geocoding
8
+ • Light/dark mode adaptive color scheme
9
+ • Report output: bold headings, no === lines around them
10
  • Chatbot uses {"role","content"} dict format (Gradio 6+)
 
11
  • Report TTS in user-selected language
12
  • Voice input (STT) working
13
  • پنجابی language added
 
294
 
295
  # ─── CHATBOT — Gradio 6+ dict format ─────────────────────────
296
  def legal_chatbot(user_message, history, language):
 
 
 
297
  if history is None:
298
  history = []
299
  if not user_message.strip():
 
347
  try:
348
  from groq import Groq
349
  client = Groq(api_key=GROQ_API_KEY)
 
350
  api_messages = [{"role": "system", "content": system_content}]
351
  for msg in history[-20:]:
352
  api_messages.append({"role": msg["role"], "content": msg["content"]})
 
470
  if score <= 8: return "ORANGE"
471
  return "RED"
472
 
473
+ # ─── PROFESSIONAL REPORT — clean bold headings, no === lines ──
474
  def build_professional_report(complaint_id, timestamp, name, cnic, phone, city, location,
475
  issue_type, language, severity, sev_icon,
476
  yolo_summary, gemini_status, gemini_reason,
477
  gemini_confidence, gemini_action, kb, description, local_msg):
478
  date_str = datetime.datetime.now().strftime("%d %B %Y")
479
  time_str = datetime.datetime.now().strftime("%I:%M %p")
480
+
481
+ laws_text = ""
482
+ for law in kb.get("laws", []):
483
+ laws_text += f"\n - {law}"
484
+
485
+ rights_text = ""
486
+ for right in kb.get("citizen_rights", []):
487
+ rights_text += f"\n - {right}"
488
+
489
+ report = f""" GOVERNMENT OF PAKISTAN — CIVIC COMPLAINT REPORT
490
+ Rahbar Digital Civic Redressal System
491
 
492
  Complaint Reference No. : {complaint_id}
493
  Date of Filing : {date_str} Time: {time_str}
494
  Filing Method : Rahbar Digital Platform (AI-Verified)
495
  Report Language : {language}
496
 
497
+
498
+ SECTION A — COMPLAINANT INFORMATION
499
+
500
  Full Name : {name}
501
  CNIC : {cnic}
502
  Contact : {phone if phone else "Not Provided"}
503
  City : {city}
504
  Area : {location}
505
 
506
+
507
+ SECTION B — COMPLAINT DETAILS
508
+
509
  Nature : {issue_type}
510
  Location : {location}, {city}
511
  Date & Time : {date_str}, {time_str}
 
514
  Description:
515
  {description.strip() if description.strip() else "[No additional description provided]"}
516
 
517
+
518
+ SECTION C — AI VERIFICATION RESULTS
519
+
520
  Status : {gemini_status}
521
  Confidence : {gemini_confidence}
522
  Finding : {gemini_reason}
523
  Action : {gemini_action if gemini_action else "Immediate field inspection required."}
524
  YOLO : {yolo_summary}
525
 
526
+
527
+ SECTION D — LEGAL FRAMEWORK
528
+
529
+ Applicable Legislation:{laws_text}
 
 
 
530
 
531
  Responsible Authority : {kb.get('authority','N/A')}
532
  Official Helpline : {kb.get('hotline','N/A')}
533
  Statutory Response Time: {kb.get('response','N/A')}
534
  Fine / Penalty : {kb.get('fine','N/A')}
535
 
536
+
537
+ SECTION E — CITIZEN'S LEGAL RIGHTS
538
+ {rights_text}
 
 
 
539
 
540
  Escalation : {kb.get('escalation','CM Portal: 0800-02345')}
541
 
542
+
543
+ SECTION F — LOCALIZED NOTICE ({language})
544
+
545
  {local_msg}
546
 
547
+
548
+ SECTION G — ACTION DIRECTIVE
549
+
550
  MANDATORY ACTION REQUIRED WITHIN: {kb.get('response','72 hours').upper()}
551
 
552
  Authority: {kb.get('authority','Local Authority')}
553
  Hotline : {kb.get('hotline','N/A')}
554
  Portal : citizenportal.gov.pk | CM: 0800-02345
555
 
556
+
557
  DECLARATION
558
+
559
  I, {name} (CNIC: {cnic}), declare that the information provided is
560
  true and correct to the best of my knowledge.
561
 
562
  Signature: ________________________ (Digital) Date: {date_str}
563
  Reference: {complaint_id}
564
 
565
+
566
  FOR OFFICIAL USE ONLY
567
+
568
  Received By : ___________________________
569
  Date of Receipt : ___________________________
570
  Action Taken : ___________________________
 
572
 
573
  Generated by: Rahbar - Pakistan's AI Civic Redressal Platform
574
  Timestamp: {timestamp}
 
575
  """
576
  return report
577
 
 
615
  local = LOCALIZED.get(clean_issue, {}).get(language, "")
616
  sev_icon = severity_icon(final_severity)
617
 
 
618
  llama_advice = analyze_with_llama(issue_type, location, city, yolo_summary, final_severity, language)
619
 
620
  report = build_professional_report(
 
637
  f"Filed: {timestamp}")
638
  wa_md = f"[Share on WhatsApp]({make_whatsapp_link(wa_text)})"
639
 
 
640
  report_tts_path = None
641
  if enable_tts:
642
  tts_text = (f"Complaint {complaint_id} filed. "
 
644
  f"Severity: {final_severity} out of 10. {local}")
645
  report_tts_path = make_tts(tts_text, language)
646
 
 
647
  advice_tts_path = make_tts(llama_advice[:600], language)
648
 
649
  return annotated_img, report, wa_md, llama_advice, report_tts_path, complaint_id, advice_tts_path
650
 
651
+ # ─── CSS — Light/Dark mode adaptive ──────────────────────────
652
  CSS = """
653
  @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');
654
+
655
+ /* ── Light mode palette ── */
656
+ :root {
657
+ --g9:#0d2b1e; --g8:#14432e; --g7:#1a5c3f; --g6:#1f7a52;
658
+ --g5:#25a06b; --g4:#2ec97f;
659
+ --a5:#f5a623; --a4:#d4870e; --a3:#b8730c;
660
+ --surface:#ffffff; --surface2:#f4f8f5; --surface3:#e8f3ed;
661
+ --text-primary:#0d2b1e; --text-secondary:#2d5a3e; --text-muted:#5a8a6e;
662
+ --border:#b8d9c5; --border-strong:#1f7a52;
663
+ --shadow:0 2px 8px rgba(13,43,30,.10);
664
+ --radius:12px; --radius-lg:20px;
665
+ --header-bg:linear-gradient(135deg,#14432e 0%,#0d2b1e 60%,#0a1f14 100%);
666
+ --badge-bg:#ffffff; --badge-border:#1a5c3f;
667
+ --warn-bg:#fffbf0; --warn-border:rgba(245,166,35,.5);
668
+ --info-bg:#f0faf4; --info-border:#1f7a52;
669
+ }
670
+
671
+ /* ── Dark mode palette ── */
672
+ @media (prefers-color-scheme: dark) {
673
+ :root {
674
+ --g9:#d0f0df; --g8:#1e6644; --g7:#28885a; --g6:#30aa72;
675
+ --g5:#3dcf8a; --g4:#5de3a3;
676
+ --a5:#f7bc57; --a4:#f5a623; --a3:#f0921a;
677
+ --surface:#0f1f16; --surface2:#162b1e; --surface3:#1c3828;
678
+ --text-primary:#d0f0df; --text-secondary:#8fcfae; --text-muted:#5a9a78;
679
+ --border:#2a5c3e; --border-strong:#30aa72;
680
+ --shadow:0 2px 12px rgba(0,0,0,.4);
681
+ --header-bg:linear-gradient(135deg,#0a1f14 0%,#071510 60%,#050f0a 100%);
682
+ --badge-bg:#0f1f16; --badge-border:#28885a;
683
+ --warn-bg:#1e1800; --warn-border:rgba(247,188,87,.4);
684
+ --info-bg:#0e1f16; --info-border:#30aa72;
685
+ }
686
+ }
687
+
688
+ *, *::before, *::after { box-sizing: border-box; }
689
+
690
+ body, .gradio-container {
691
+ font-family: 'DM Sans', sans-serif !important;
692
+ background: var(--surface) !important;
693
+ color: var(--text-primary) !important;
694
+ }
695
+
696
+ /* ── Header ── */
697
+ .saaf-header {
698
+ background: var(--header-bg);
699
+ border-bottom: 2px solid var(--g6);
700
+ padding: 24px 20px 20px;
701
+ text-align: center;
702
+ position: relative;
703
+ overflow: hidden;
704
+ }
705
+ .saaf-header::before {
706
+ content: '';
707
+ position: absolute; inset: 0;
708
+ background: radial-gradient(ellipse 60% 60% at 50% 0%, rgba(37,160,107,.18), transparent);
709
+ pointer-events: none;
710
+ }
711
+ .saaf-header h1 {
712
+ font-family: 'Playfair Display', serif !important;
713
+ font-size: clamp(1.8rem, 5vw, 3rem) !important;
714
+ font-weight: 900 !important;
715
+ color: #fafdf8 !important;
716
+ margin: 0 0 4px !important;
717
+ line-height: 1.1;
718
+ }
719
+ .saaf-header .urdu-title {
720
+ font-family: 'Noto Nastaliq Urdu', serif;
721
+ font-size: clamp(1rem, 3vw, 1.6rem);
722
+ color: #f7bc57;
723
+ direction: rtl;
724
+ margin: 4px 0 8px;
725
+ }
726
+ .saaf-header .tagline {
727
+ font-size: clamp(.75rem, 2vw, .95rem);
728
+ color: #5de3a3;
729
+ letter-spacing: .08em;
730
+ text-transform: uppercase;
731
+ }
732
+
733
+ /* ── Badge strip ── */
734
+ .badge-strip {
735
+ display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
736
+ padding: 10px 16px;
737
+ background: var(--surface2);
738
+ border-bottom: 1px solid var(--border);
739
+ }
740
+ .badge {
741
+ font-size: .72rem; font-weight: 600; letter-spacing: .06em;
742
+ padding: 4px 12px; border-radius: 20px; text-transform: uppercase;
743
+ }
744
+ .badge-ai { background: var(--badge-bg); color: var(--g4); border: 1px solid var(--badge-border); }
745
+ .badge-pk { background: var(--badge-bg); color: var(--a4); border: 1px solid var(--warn-border); }
746
+ .badge-live{ background: var(--badge-bg); color: #ff8080; border: 1px solid rgba(232,83,83,.4); }
747
+
748
+ /* ── Tabs ── */
749
+ .tab-nav { background: var(--surface2) !important; border-bottom: 2px solid var(--border) !important; }
750
+ .tab-nav button {
751
+ font-family: 'DM Sans', sans-serif !important;
752
+ font-weight: 500 !important; font-size: .85rem !important;
753
+ color: var(--text-muted) !important;
754
+ padding: 12px 18px !important; border-radius: 0 !important;
755
+ transition: all .2s !important;
756
+ }
757
+ .tab-nav button.selected,
758
+ .tab-nav button[aria-selected="true"] {
759
+ color: var(--a4) !important;
760
+ border-bottom: 3px solid var(--a5) !important;
761
+ background: transparent !important;
762
+ }
763
+
764
+ /* ── Card title ── */
765
+ .card-title {
766
+ font-size: .7rem; font-weight: 600; letter-spacing: .1em;
767
+ text-transform: uppercase; color: var(--g4);
768
+ margin-bottom: 12px; padding-bottom: 8px;
769
+ border-bottom: 1px solid var(--border);
770
+ }
771
+
772
+ /* ── Labels ── */
773
+ label, .gradio-container .label-wrap span {
774
+ color: var(--text-primary) !important;
775
+ }
776
+
777
+ /* ── Inputs ── */
778
+ .gradio-container input,
779
+ .gradio-container textarea {
780
+ background: var(--surface) !important;
781
+ border: 1px solid var(--border-strong) !important;
782
+ border-radius: var(--radius) !important;
783
+ color: var(--text-primary) !important;
784
+ font-family: 'DM Sans', sans-serif !important;
785
+ }
786
+ .gradio-container input:focus,
787
+ .gradio-container textarea:focus {
788
+ border-color: var(--a5) !important;
789
+ outline: none !important;
790
+ box-shadow: 0 0 0 3px rgba(245,166,35,.15) !important;
791
+ }
792
+ .gradio-container .wrap { background: var(--surface) !important; border-color: var(--border-strong) !important; }
793
+ .gradio-container .block { background: var(--surface) !important; }
794
+
795
+ /* ── Buttons ── */
796
+ .gradio-container button.primary {
797
+ background: linear-gradient(135deg, var(--g6), var(--g5)) !important;
798
+ color: #fafdf8 !important;
799
+ border: none !important; border-radius: var(--radius) !important;
800
+ font-weight: 600 !important; font-size: .9rem !important;
801
+ padding: 12px 24px !important; cursor: pointer !important;
802
+ box-shadow: var(--shadow) !important; transition: all .2s !important;
803
+ }
804
+ .gradio-container button.primary:hover {
805
+ background: linear-gradient(135deg, var(--g5), var(--g4)) !important;
806
+ transform: translateY(-1px) !important;
807
+ }
808
+ .gradio-container button.secondary {
809
+ background: var(--surface) !important;
810
+ border: 1px solid var(--border-strong) !important;
811
+ color: var(--g4) !important;
812
+ }
813
+
814
+ /* ── Image upload ── */
815
+ .gradio-container [data-testid="image"] {
816
+ border: 2px dashed var(--border-strong) !important;
817
+ border-radius: var(--radius-lg) !important;
818
+ background: var(--surface2) !important;
819
+ }
820
+
821
+ /* ── Markdown headings ── */
822
+ .gradio-container .prose h2,
823
+ .gradio-container .prose h3 { color: var(--a4) !important; }
824
+
825
+ /* ── Audio ── */
826
+ .gradio-container audio { width: 100% !important; border-radius: var(--radius) !important; }
827
+
828
+ /* ── Info / warn boxes ── */
829
+ .info-box {
830
+ background: var(--info-bg);
831
+ border: 1px solid var(--info-border);
832
+ border-left: 4px solid var(--g5);
833
+ border-radius: var(--radius);
834
+ padding: 12px 16px;
835
+ font-size: .88rem; line-height: 1.6;
836
+ margin-bottom: 8px;
837
+ color: var(--text-secondary);
838
+ }
839
+ .warn-box {
840
+ background: var(--warn-bg);
841
+ border: 1px solid var(--warn-border);
842
+ border-left: 4px solid var(--a5);
843
+ border-radius: var(--radius);
844
+ padding: 12px 16px;
845
+ font-size: .88rem;
846
+ margin-bottom: 8px;
847
+ color: var(--text-secondary);
848
+ }
849
+
850
+ /* ── Hotline pill ── */
851
+ .hotline-pill {
852
+ display: inline-block;
853
+ background: var(--surface2); color: var(--a4);
854
+ border: 1px solid var(--warn-border);
855
+ border-radius: 20px; padding: 2px 12px;
856
+ font-size: .8rem; font-weight: 600;
857
+ }
858
+
859
+ /* ── Map div ── */
860
+ #rahbarMapDiv {
861
+ height: 220px; width: 100%;
862
+ border-radius: 12px;
863
+ border: 2px solid var(--border-strong);
864
+ margin-bottom: 8px;
865
+ background: var(--surface3);
866
+ position: relative;
867
+ }
868
+
869
+ /* ── GPS button inside HTML block ── */
870
+ .gps-btn {
871
+ background: linear-gradient(135deg, var(--g6), var(--g5)) !important;
872
+ color: #fafdf8 !important;
873
+ border: none !important; border-radius: 10px !important;
874
+ padding: 10px 20px !important; font-size: .9rem !important;
875
+ font-weight: 600 !important; cursor: pointer !important;
876
+ width: 100% !important; margin-bottom: 8px !important;
877
+ display: block !important;
878
+ }
879
+
880
+ /* ── Report textbox — bold section headings ── */
881
+ .gradio-container textarea {
882
+ font-family: 'DM Mono', 'Courier New', monospace !important;
883
+ font-size: .82rem !important;
884
+ line-height: 1.7 !important;
885
+ }
886
+
887
+ /* ── Dropdown ── */
888
+ .gradio-container select,
889
+ .gradio-container [data-testid="dropdown"] {
890
+ background: var(--surface) !important;
891
+ color: var(--text-primary) !important;
892
+ border-color: var(--border-strong) !important;
893
+ }
894
+
895
+ /* ── Chatbot ── */
896
+ .gradio-container .message.user { background: var(--surface3) !important; color: var(--text-primary) !important; }
897
+ .gradio-container .message.bot { background: var(--surface2) !important; color: var(--text-primary) !important; }
898
+
899
+ /* ── Scrollbar ── */
900
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
901
+ ::-webkit-scrollbar-track { background: var(--surface2); }
902
+ ::-webkit-scrollbar-thumb { background: var(--g6); border-radius: 3px; }
903
+
904
+ @media (max-width: 640px) {
905
+ .saaf-header { padding: 16px 12px; }
906
+ .tab-nav button { padding: 10px 12px !important; font-size: .78rem !important; }
907
+ }
908
  """
909
 
910
  HEADER_HTML = """
 
923
  </div>
924
  """
925
 
926
+ # ─── MAP HTML — GPS fills location + reverse geocodes address ─
927
  MAP_HTML = """
928
  <div style="margin:14px 0 4px">
929
  <div class="card-title">GPS / Map Location Picker</div>
930
  <div class="warn-box">
931
+ <strong>Please allow location access</strong> when your browser asks.
932
+ Tap the button below — your address will auto-fill the location field below.
933
+ <br><strong>براہ کرم Location Allow کریں۔</strong> آپ کا پتہ خودبخود بھر جائے گا۔
 
934
  </div>
935
 
936
+ <button class="gps-btn" onclick="window.rahbarGetGPS()">
937
+ 📍 Use My Current GPS Location / اپنی Location استعمال کریں
 
 
 
 
938
  </button>
939
 
940
  <div id="gpsStatus"
941
+ style="font-size:.82rem;min-height:20px;margin-bottom:6px;
942
+ font-weight:500;padding:4px 2px;color:var(--g4,#25a06b)">
943
+ Map ready. Tap GPS button to auto-fill your location or tap the map to drop a pin.
944
  </div>
945
 
946
  <div id="rahbarMapDiv"></div>
947
 
948
+ <p style="font-size:.72rem;color:var(--text-muted,#5a8a6e);margin:4px 0 10px">
949
+ Tap map to drop pin &middot; Drag pin to adjust &middot; GPS fills address automatically
950
  </p>
951
  </div>
952
 
 
959
 
960
  function setStatus(msg, color){
961
  var el = document.getElementById('gpsStatus');
962
+ if(!el) return;
963
+ el.textContent = msg;
964
+ el.style.color = color || 'var(--g4, #25a06b)';
965
+ }
966
+
967
+ /* ── Reverse geocode using Nominatim (free, no key needed) ── */
968
+ function reverseGeocode(lat, lng, callback){
969
+ var url = 'https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat='
970
+ + lat + '&lon=' + lng + '&zoom=17&addressdetails=1';
971
+ fetch(url, { headers: { 'Accept-Language': 'en' } })
972
+ .then(function(r){ return r.json(); })
973
+ .then(function(data){
974
+ var a = data.address || {};
975
+ var parts = [];
976
+ // Build a readable address from parts
977
+ if(a.road || a.pedestrian || a.footway)
978
+ parts.push(a.road || a.pedestrian || a.footway);
979
+ if(a.suburb || a.neighbourhood || a.quarter)
980
+ parts.push(a.suburb || a.neighbourhood || a.quarter);
981
+ if(a.city_district || a.county)
982
+ parts.push(a.city_district || a.county);
983
+ if(parts.length === 0 && data.display_name)
984
+ parts.push(data.display_name.split(',').slice(0,3).join(',').trim());
985
+ var address = parts.join(', ') || ('GPS: ' + lat.toFixed(5) + ', ' + lng.toFixed(5));
986
+ callback(address);
987
+ })
988
+ .catch(function(){
989
+ callback('GPS: ' + lat.toFixed(5) + ', ' + lng.toFixed(5));
990
+ });
991
  }
992
 
993
+ /* ── Fill Gradio location textbox ── */
994
+ function fillLocationField(text){
995
+ // Target the textarea/input whose label contains Street/Landmark/GPS
996
+ var filled = false;
997
  document.querySelectorAll('textarea, input[type=text]').forEach(function(el){
998
+ if(filled) return;
999
+ var ph = (el.placeholder || '').toLowerCase();
1000
  var lbl = '';
1001
  var blk = el.closest('.block');
1002
  if(blk){ var l = blk.querySelector('label'); if(l) lbl = l.textContent.toLowerCase(); }
1003
+ if(ph.indexOf('gps') > -1 || ph.indexOf('street') > -1
1004
+ || ph.indexOf('landmark') > -1 || lbl.indexOf('street') > -1
1005
+ || lbl.indexOf('landmark') > -1 || lbl.indexOf('gps') > -1){
1006
+ var proto = el.tagName === 'TEXTAREA'
1007
+ ? HTMLTextAreaElement.prototype
1008
+ : HTMLInputElement.prototype;
1009
+ var desc = Object.getOwnPropertyDescriptor(proto, 'value');
1010
+ if(desc && desc.set) desc.set.call(el, text); else el.value = text;
1011
+ el.dispatchEvent(new Event('input', { bubbles: true }));
1012
+ el.dispatchEvent(new Event('change', { bubbles: true }));
1013
+ filled = true;
1014
  }
1015
  });
1016
  }
1017
 
1018
+ function placePin(lat, lng){
1019
+ if(!rbMap) return;
1020
+ rbMark.setLatLng([lat, lng]);
1021
+ rbMap.setView([lat, lng], 17);
1022
+ }
1023
+
1024
+ function handleCoords(lat, lng, source){
1025
+ placePin(lat, lng);
1026
+ setStatus('Getting address…', 'var(--a4, #f5a623)');
1027
+ reverseGeocode(lat, lng, function(address){
1028
+ fillLocationField(address);
1029
+ setStatus('✅ Location filled: ' + address);
1030
+ });
1031
+ }
1032
+
1033
  function initMap(lat, lng){
1034
  if(!leafletReady){ setTimeout(function(){ initMap(lat, lng); }, 400); return; }
1035
  var el = document.getElementById('rahbarMapDiv');
1036
  if(!el) return;
1037
  if(rbMap){ rbMap.remove(); rbMap = null; }
1038
+ rbMap = L.map('rahbarMapDiv').setView([lat, lng], 13);
1039
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
1040
+ { attribution: '© OpenStreetMap', maxZoom: 19 }).addTo(rbMap);
1041
+ rbMark = L.marker([lat, lng], { draggable: true }).addTo(rbMap)
1042
+ .bindPopup('Drag or tap to set your location').openPopup();
1043
+ rbMark.on('dragend', function(e){
1044
+ var ll = e.target.getLatLng();
1045
+ handleCoords(ll.lat, ll.lng, 'drag');
1046
+ });
1047
+ rbMap.on('click', function(e){
1048
+ handleCoords(e.latlng.lat, e.latlng.lng, 'click');
1049
+ });
1050
  setTimeout(function(){ if(rbMap) rbMap.invalidateSize(); }, 350);
1051
  }
1052
 
 
1054
  if(!navigator.geolocation){
1055
  setStatus('GPS not supported by this browser.', '#e85353'); return;
1056
  }
1057
+ setStatus('Requesting location… please ALLOW when browser asks.', 'var(--a5, #f5a623)');
1058
  navigator.geolocation.getCurrentPosition(
1059
  function(pos){
1060
+ var lat = pos.coords.latitude, lng = pos.coords.longitude;
1061
+ setStatus('GPS found! Getting your address…', 'var(--g4, #2ec97f)');
1062
+ if(!rbMap){ initMap(lat, lng); }
1063
+ handleCoords(lat, lng, 'gps');
1064
  },
1065
  function(err){
1066
  var msgs = {
1067
+ 1: '❌ DENIED — Allow location in browser settings, then retry.',
1068
+ 2: '⚠️ Location unavailable. Check GPS/network and retry.',
1069
+ 3: '⏱️ Timeout — please try again.'
1070
  };
1071
  setStatus(msgs[err.code] || 'GPS error: ' + err.message, '#e85353');
1072
  },
1073
+ { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
1074
  );
1075
  };
1076
 
1077
+ /* Load Leaflet JS */
1078
  function loadLeaflet(){
1079
  if(typeof L !== 'undefined'){ leafletReady = true; initMap(DEFAULT_LAT, DEFAULT_LNG); return; }
1080
  var s = document.createElement('script');
 
1101
 
1102
  # ─── BUILD UI ─────────────────────────────────────────────────
1103
  def build_ui():
1104
+ with gr.Blocks(title="Rahbar | رہبر", css=CSS) as demo:
1105
  gr.HTML(HEADER_HTML)
1106
 
1107
  with gr.Tabs():
 
1129
 
1130
  location_tb = gr.Textbox(
1131
  label="Street / Landmark / GPS Coordinates",
1132
+ placeholder="GPS address auto-fills here — or type street/landmark",
1133
  lines=1
1134
  )
1135
  desc_tb = gr.Textbox(label="Description (optional)", placeholder="Describe the issue...", lines=3)
 
1193
  or your legal rights as a Pakistani citizen.
1194
  </div>""")
1195
  chat_lang_dd = gr.Dropdown(choices=LANGUAGES, value="English", label="Response Language")
1196
+ chatbot = gr.Chatbot(label="Rahbar Legal Assistant", height=600, value=[])
 
 
 
 
 
 
1197
  with gr.Row():
1198
  chat_input = gr.Textbox(
1199
  label="Your Question",
 
1212
  inputs=[chat_input, chatbot, chat_lang_dd],
1213
  outputs=[chatbot, chat_input]
1214
  )
 
1215
  gr.Examples(
1216
  examples=[
1217
  ["WASA did not fix the leakage for 3 days. What are my rights?"],
 
1229
  gr.HTML('<div class="card-title">Speech-to-Text (STT)</div>')
1230
  gr.HTML('<div class="warn-box">Record your complaint. Copy the transcription to the Report Issue tab.</div>')
1231
  gr.HTML('<div class="info-box">Allow microphone access when your browser asks. Works best in Chrome/Edge.<br>مائیکروفون کی اجازت دیں جب browser پوچھے۔</div>')
1232
+ audio_in = gr.Audio(type="filepath", label="Record or Upload Audio", sources=["microphone", "upload"])
1233
+ stt_btn = gr.Button("Transcribe Audio", variant="primary")
1234
+ stt_out = gr.Textbox(label="Transcription (editable — copy to Report tab)", lines=5, interactive=True)
 
 
 
 
 
1235
  stt_btn.click(fn=stt, inputs=[audio_in], outputs=[stt_out])
1236
 
1237
  gr.HTML('<div class="card-title" style="margin-top:20px">Text-to-Speech Test</div>')
 
1259
  demo.launch(
1260
  server_name="0.0.0.0",
1261
  server_port=7860,
 
 
1262
  share=True,
1263
  theme=gr.themes.Base(
1264
  primary_hue=gr.themes.colors.green,