Siddharth Ravikumar commited on
Commit
aef3662
·
1 Parent(s): 55e8306

Pull Spaces decorators up

Browse files
Files changed (1) hide show
  1. app.py +253 -253
app.py CHANGED
@@ -434,6 +434,259 @@ def get_rules_json():
434
  return json.dumps(rule_loader.get_all_rules())
435
 
436
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  # ── Build Gradio App ──────────────────────────────────────────────────
438
 
439
  CUSTOM_CSS = """
@@ -548,90 +801,7 @@ with gr.Blocks(
548
  # State for context
549
  chat_system_ctx = gr.State(value="You are TraceScene AI assistant. You help insurers and investigating officers analyze accident cases, traffic rules, and insurance clauses. Answer concisely and accurately based on the context provided.")
550
 
551
- def load_chat_context(case_id):
552
- if not case_id:
553
- default_ctx = "You are TraceScene AI assistant. You help insurers and investigating officers analyze accident cases, traffic rules, and insurance clauses. Answer concisely and accurately.\n\n"
554
- # Load traffic rules as general context
555
- ensure_init()
556
- rules_data = rule_loader.get_all_rules()
557
- rules_text = ""
558
- for cat in rules_data.get("categories", []):
559
- rules_text += f"\nCategory: {cat.get('name', '')}\n"
560
- for r in cat.get("rules", []):
561
- rules_text += f" - {r.get('id', '')}: {r.get('title', '')} (Severity: {r.get('severity', '')})\n"
562
- ctx = default_ctx + "TRAFFIC RULES:\n" + rules_text
563
- return ctx, "*General mode: traffic rules loaded. Ask any question!*"
564
-
565
- ensure_init()
566
- case = run_async(db.get_case(int(case_id)))
567
- if not case:
568
- return "", f"❌ Case {int(case_id)} not found."
569
-
570
- analyses = run_async(db.get_analyses_by_case(int(case_id)))
571
- parties = run_async(db.get_parties_by_case(int(case_id)))
572
- violations = run_async(db.get_violations_by_case(int(case_id)))
573
- fault = run_async(db.get_fault_analysis(int(case_id)))
574
- rules_data = rule_loader.get_all_rules()
575
-
576
- ctx = f"""You are TraceScene AI assistant analyzing Case #{case.get('case_number', '')}.
577
- Location: {case.get('location', 'Unknown')}
578
- Date: {case.get('incident_date', 'Unknown')}
579
- Officer: {case.get('officer_name', 'Unknown')}
580
- Status: {case.get('status', 'Unknown')}
581
-
582
- SCENE ANALYSES:\n"""
583
- for a in analyses:
584
- ctx += f"\n--- Photo Analysis ---\n{a.get('raw_analysis', '')}\n"
585
-
586
- if parties:
587
- ctx += "\nPARTIES IDENTIFIED:\n"
588
- for p in parties:
589
- ctx += f" - {p.get('label', '')}: {p.get('vehicle_type', '')} {p.get('vehicle_color', '')} — {p.get('vehicle_description', '')}\n"
590
-
591
- if violations:
592
- ctx += "\nVIOLATIONS FOUND:\n"
593
- for v in violations:
594
- ctx += f" - {v.get('rule_title', '')} (Severity: {v.get('severity', '')}, Confidence: {v.get('confidence', 0):.0%})\n"
595
-
596
- if fault:
597
- ctx += f"\nFAULT ANALYSIS:\n Primary Fault: {fault.get('primary_fault_party', 'N/A')}\n Confidence: {fault.get('overall_confidence', 0):.0%}\n Summary: {fault.get('analysis_summary', '')}\n"
598
-
599
- # Append traffic rules
600
- rules_text = ""
601
- for cat in rules_data.get("categories", []):
602
- rules_text += f"\nCategory: {cat.get('name', '')}\n"
603
- for r in cat.get("rules", []):
604
- rules_text += f" - {r.get('id', '')}: {r.get('title', '')} (Severity: {r.get('severity', '')})\n"
605
- ctx += "\nTRAFFIC RULES:\n" + rules_text
606
-
607
- return ctx, f"✅ Case **{case.get('case_number', '')}** loaded with {len(analyses)} analyses, {len(violations)} violations."
608
-
609
- @spaces.GPU(duration=120)
610
- def chat_respond(user_message, history, system_ctx):
611
- if not user_message or not user_message.strip():
612
- return history, "", system_ctx
613
- ensure_init()
614
- if not inference_engine.is_loaded:
615
- inference_engine.load_model()
616
- try:
617
- # Use the vision model's text generation capability for chat
618
- chat_prompt = f"""You are TraceScene AI assistant helping with accident analysis.
619
 
620
- CONTEXT:
621
- {system_ctx}
622
-
623
- USER QUESTION: {user_message.strip()}
624
-
625
- Provide a concise, helpful answer based on the context above."""
626
- # Create a small blank image for the vision model
627
- from PIL import Image as PILImg
628
- blank = PILImg.new('RGB', (64, 64), color=(0, 0, 0))
629
- response = gpu_run_inference(blank, chat_prompt)
630
- except Exception as e:
631
- response = f"Error: {e}"
632
- history = history or []
633
- history.append((user_message.strip(), response))
634
- return history, "", system_ctx
635
 
636
  chat_load_btn.click(load_chat_context, inputs=[chat_case_id], outputs=[chat_system_ctx, chat_context_status])
637
  chat_send_btn.click(chat_respond, inputs=[chat_input, chatbot, chat_system_ctx], outputs=[chatbot, chat_input, chat_system_ctx], api_name="chat")
@@ -645,176 +815,6 @@ Provide a concise, helpful answer based on the context above."""
645
  anim_btn = gr.Button("Generate Animation", variant="primary")
646
  anim_output = gr.HTML(label="Animation View")
647
 
648
- def generate_animation_fn(case_id):
649
- if not case_id:
650
- return "<p style='color:red;'>Enter a Case ID.</p>"
651
- ensure_init()
652
- analyses = run_async(db.get_analyses_by_case(int(case_id)))
653
- if not analyses:
654
- return "<p style='color:red;'>No analyses found. Run analysis first.</p>"
655
-
656
- # Parse scene details from the first analysis
657
- raw = analyses[0].get("raw_analysis", "")
658
-
659
- def extract_field(text, field):
660
- import re
661
- pattern = rf"{re.escape(field)}:\s*(.+)"
662
- m = re.search(pattern, text, re.IGNORECASE)
663
- return m.group(1).strip() if m else "Unknown"
664
-
665
- road_type = extract_field(raw, "Road Type")
666
- num_vehicles = extract_field(raw, "Vehicles Involved")
667
- v1_pos = extract_field(raw, "Vehicle 1 Position")
668
- v1_tyre = extract_field(raw, "Vehicle 1 Tyre Direction")
669
- impact = extract_field(raw, "Area of Impact")
670
- category = extract_field(raw, "Accident Category")
671
- v1_make = extract_field(raw, "Vehicle 1 Make/Model")
672
-
673
- # Check for Vehicle 2
674
- v2_pos = extract_field(raw, "Vehicle 2 Position")
675
- v2_tyre = extract_field(raw, "Vehicle 2 Tyre Direction")
676
- v2_make = extract_field(raw, "Vehicle 2 Make/Model")
677
- has_v2 = v2_make != "Unknown"
678
-
679
- # Determine colors from extracted make
680
- import re as re_mod
681
- def extract_color(make_str):
682
- colors = ["Red", "Blue", "White", "Black", "Silver", "Grey", "Green", "Yellow", "Brown", "Orange"]
683
- for c in colors:
684
- if c.lower() in make_str.lower():
685
- return c.lower()
686
- return "#3b82f6"
687
-
688
- v1_color = extract_color(v1_make)
689
- v2_color = extract_color(v2_make) if has_v2 else "#ef4444"
690
-
691
- # Severity affects animation speed
692
- speed_map = {"mild": 1.5, "medium": 2.5, "critical": 4.0}
693
- anim_speed = speed_map.get(category.lower(), 2.5)
694
-
695
- # Road layout
696
- road_is_intersection = "intersection" in road_type.lower()
697
- road_is_highway = "highway" in road_type.lower()
698
-
699
- num_v = 1
700
- try:
701
- num_v = int(num_vehicles)
702
- except:
703
- pass
704
- # Unique ID to force Gradio to re-render on each click (enables replay)
705
- import random
706
- uid = random.randint(10000, 99999)
707
- # Determine animation duration based on severity
708
- dur = "3s" if category.lower() == "mild" else "2s" if category.lower() == "medium" else "1.5s"
709
- sev_color = "#22c55e" if category.lower() == "mild" else "#f59e0b" if category.lower() == "medium" else "#ef4444"
710
-
711
- # Build SVG road
712
- if road_is_intersection:
713
- road_svg = '''
714
- <rect x="0" y="160" width="700" height="100" fill="#555"/>
715
- <rect x="300" y="0" width="100" height="420" fill="#555"/>
716
- <line x1="0" y1="210" x2="300" y2="210" stroke="#fbbf24" stroke-width="2" stroke-dasharray="20,15"/>
717
- <line x1="400" y1="210" x2="700" y2="210" stroke="#fbbf24" stroke-width="2" stroke-dasharray="20,15"/>
718
- <line x1="350" y1="0" x2="350" y2="160" stroke="#fbbf24" stroke-width="2" stroke-dasharray="20,15"/>
719
- <line x1="350" y1="260" x2="350" y2="420" stroke="#fbbf24" stroke-width="2" stroke-dasharray="20,15"/>
720
- '''
721
- else:
722
- road_svg = '''
723
- <rect x="0" y="150" width="700" height="120" fill="#555" rx="2"/>
724
- <line x1="0" y1="210" x2="700" y2="210" stroke="#fbbf24" stroke-width="2" stroke-dasharray="20,15"/>
725
- <line x1="0" y1="150" x2="700" y2="150" stroke="white" stroke-width="2"/>
726
- <line x1="0" y1="270" x2="700" y2="270" stroke="white" stroke-width="2"/>
727
- '''
728
-
729
- # Vehicle 2 SVG (if present)
730
- v2_svg = ""
731
- if has_v2:
732
- if road_is_intersection:
733
- v2_svg = f'''<g>
734
- <animateTransform attributeName="transform" type="translate" from="0,0" to="0,135" dur="{dur}" fill="freeze"/>
735
- <rect x="325" y="60" width="50" height="26" rx="5" fill="{v2_color}" stroke="#fff" stroke-width="1"/>
736
- <text x="350" y="78" fill="white" font-size="10" font-weight="bold" text-anchor="middle">V2</text>
737
- </g>'''
738
- else:
739
- v2_svg = f'''<g>
740
- <animateTransform attributeName="transform" type="translate" from="0,0" to="-200,0" dur="{dur}" fill="freeze"/>
741
- <rect x="560" y="215" width="50" height="26" rx="5" fill="{v2_color}" stroke="#fff" stroke-width="1"/>
742
- <text x="585" y="233" fill="white" font-size="10" font-weight="bold" text-anchor="middle">V2</text>
743
- </g>'''
744
-
745
- html = f'''
746
- <div style="text-align:center; font-family: Inter, Arial, sans-serif;">
747
- <svg id="anim_{uid}" width="700" height="420" viewBox="0 0 700 420" xmlns="http://www.w3.org/2000/svg" style="border:1px solid #444; border-radius:10px; background:#1a1a2e;">
748
- <defs>
749
- <radialGradient id="glow_{uid}" cx="50%" cy="50%" r="50%">
750
- <stop offset="0%" stop-color="#fbbf24" stop-opacity="0.8"/>
751
- <stop offset="100%" stop-color="#fbbf24" stop-opacity="0"/>
752
- </radialGradient>
753
- </defs>
754
-
755
- {road_svg}
756
-
757
- <!-- Vehicle 1 -->
758
- <g>
759
- <animateTransform attributeName="transform" type="translate" from="0,0" to="200,0" dur="{dur}" fill="freeze"/>
760
- <rect x="80" y="190" width="50" height="26" rx="5" fill="{v1_color}" stroke="#fff" stroke-width="1"/>
761
- <text x="105" y="207" fill="white" font-size="10" font-weight="bold" text-anchor="middle">V1</text>
762
- </g>
763
-
764
- {v2_svg}
765
-
766
- <!-- Impact flash -->
767
- <circle cx="340" cy="210" r="0" fill="url(#glow_{uid})">
768
- <animate attributeName="r" values="0;0;0;0;0;0;0;45;55;0" dur="{dur}" fill="freeze"/>
769
- <animate attributeName="opacity" values="0;0;0;0;0;0;0;1;0.5;0" dur="{dur}" fill="freeze"/>
770
- </circle>
771
-
772
- <!-- Debris -->
773
- <circle cx="340" cy="210" r="3" fill="#fbbf24" opacity="0">
774
- <animate attributeName="opacity" values="0;0;0;0;0;0;0;1;0" dur="{dur}" fill="freeze"/>
775
- <animate attributeName="cx" values="340;340;340;340;340;340;340;310;290" dur="{dur}" fill="freeze"/>
776
- <animate attributeName="cy" values="210;210;210;210;210;210;210;185;170" dur="{dur}" fill="freeze"/>
777
- </circle>
778
- <circle cx="340" cy="210" r="2" fill="#ef4444" opacity="0">
779
- <animate attributeName="opacity" values="0;0;0;0;0;0;0;1;0" dur="{dur}" fill="freeze"/>
780
- <animate attributeName="cx" values="340;340;340;340;340;340;340;370;395" dur="{dur}" fill="freeze"/>
781
- <animate attributeName="cy" values="210;210;210;210;210;210;210;190;175" dur="{dur}" fill="freeze"/>
782
- </circle>
783
- <circle cx="340" cy="210" r="3" fill="#e2e8f0" opacity="0">
784
- <animate attributeName="opacity" values="0;0;0;0;0;0;0;1;0" dur="{dur}" fill="freeze"/>
785
- <animate attributeName="cx" values="340;340;340;340;340;340;340;320;305" dur="{dur}" fill="freeze"/>
786
- <animate attributeName="cy" values="210;210;210;210;210;210;210;235;255" dur="{dur}" fill="freeze"/>
787
- </circle>
788
- <circle cx="340" cy="210" r="2" fill="#f97316" opacity="0">
789
- <animate attributeName="opacity" values="0;0;0;0;0;0;0;1;0" dur="{dur}" fill="freeze"/>
790
- <animate attributeName="cx" values="340;340;340;340;340;340;340;365;385" dur="{dur}" fill="freeze"/>
791
- <animate attributeName="cy" values="210;210;210;210;210;210;210;230;250" dur="{dur}" fill="freeze"/>
792
- </circle>
793
-
794
- <!-- Collision label -->
795
- <text x="350" y="145" fill="#ef4444" font-size="18" font-weight="bold" text-anchor="middle" opacity="0" font-family="Inter, Arial, sans-serif">
796
- COLLISION
797
- <animate attributeName="opacity" values="0;0;0;0;0;0;0;1;1" dur="{dur}" fill="freeze"/>
798
- </text>
799
-
800
- <!-- HUD -->
801
- <rect x="10" y="350" width="680" height="60" rx="8" fill="rgba(0,0,0,0.6)"/>
802
- <text x="20" y="375" fill="#e2e8f0" font-size="12" font-family="Inter, Arial, sans-serif">{v1_make[:35]}</text>
803
- <text x="20" y="398" fill="#e2e8f0" font-size="12" font-family="Inter, Arial, sans-serif">{"" if not has_v2 else v2_make[:35]}{"Single vehicle accident" if not has_v2 else ""}</text>
804
- <text x="680" y="375" fill="{sev_color}" font-size="14" font-weight="bold" text-anchor="end" font-family="Inter, Arial, sans-serif">{category.upper()}</text>
805
- <text x="680" y="398" fill="#94a3b8" font-size="11" text-anchor="end" font-family="Inter, Arial, sans-serif">Impact: {impact} | Road: {road_type}</text>
806
- </svg>
807
- <div style="margin-top:8px; color:#94a3b8; font-size:12px;">
808
- Vehicles: {num_v} | Animation auto-plays on load
809
- </div>
810
- </div>
811
- '''
812
- return html
813
-
814
- anim_btn.click(generate_animation_fn, inputs=[anim_case_id], outputs=[anim_output])
815
-
816
-
817
-
818
  anim_btn.click(generate_animation_fn, inputs=[anim_case_id], outputs=[anim_output])
819
 
820
  # Hidden API-only endpoints (for @gradio/client from custom frontend)
 
434
  return json.dumps(rule_loader.get_all_rules())
435
 
436
 
437
+ def load_chat_context(case_id):
438
+ if not case_id:
439
+ default_ctx = "You are TraceScene AI assistant. You help insurers and investigating officers analyze accident cases, traffic rules, and insurance clauses. Answer concisely and accurately.\n\n"
440
+ # Load traffic rules as general context
441
+ ensure_init()
442
+ rules_data = rule_loader.get_all_rules()
443
+ rules_text = ""
444
+ for cat in rules_data.get("categories", []):
445
+ rules_text += f"\nCategory: {cat.get('name', '')}\n"
446
+ for r in cat.get("rules", []):
447
+ rules_text += f" - {r.get('id', '')}: {r.get('title', '')} (Severity: {r.get('severity', '')})\n"
448
+ ctx = default_ctx + "TRAFFIC RULES:\n" + rules_text
449
+ return ctx, "*General mode: traffic rules loaded. Ask any question!*"
450
+
451
+ ensure_init()
452
+ case = run_async(db.get_case(int(case_id)))
453
+ if not case:
454
+ return "", f"❌ Case {int(case_id)} not found."
455
+
456
+ analyses = run_async(db.get_analyses_by_case(int(case_id)))
457
+ parties = run_async(db.get_parties_by_case(int(case_id)))
458
+ violations = run_async(db.get_violations_by_case(int(case_id)))
459
+ fault = run_async(db.get_fault_analysis(int(case_id)))
460
+ rules_data = rule_loader.get_all_rules()
461
+
462
+ ctx = f"""You are TraceScene AI assistant analyzing Case #{case.get('case_number', '')}.
463
+ Location: {case.get('location', 'Unknown')}
464
+ Date: {case.get('incident_date', 'Unknown')}
465
+ Officer: {case.get('officer_name', 'Unknown')}
466
+ Status: {case.get('status', 'Unknown')}
467
+
468
+ SCENE ANALYSES:\n"""
469
+ for a in analyses:
470
+ ctx += f"\n--- Photo Analysis ---\n{a.get('raw_analysis', '')}\n"
471
+
472
+ if parties:
473
+ ctx += "\nPARTIES IDENTIFIED:\n"
474
+ for p in parties:
475
+ ctx += f" - {p.get('label', '')}: {p.get('vehicle_type', '')} {p.get('vehicle_color', '')} — {p.get('vehicle_description', '')}\n"
476
+
477
+ if violations:
478
+ ctx += "\nVIOLATIONS FOUND:\n"
479
+ for v in violations:
480
+ ctx += f" - {v.get('rule_title', '')} (Severity: {v.get('severity', '')}, Confidence: {v.get('confidence', 0):.0%})\n"
481
+
482
+ if fault:
483
+ ctx += f"\nFAULT ANALYSIS:\n Primary Fault: {fault.get('primary_fault_party', 'N/A')}\n Confidence: {fault.get('overall_confidence', 0):.0%}\n Summary: {fault.get('analysis_summary', '')}\n"
484
+
485
+ # Append traffic rules
486
+ rules_text = ""
487
+ for cat in rules_data.get("categories", []):
488
+ rules_text += f"\nCategory: {cat.get('name', '')}\n"
489
+ for r in cat.get("rules", []):
490
+ rules_text += f" - {r.get('id', '')}: {r.get('title', '')} (Severity: {r.get('severity', '')})\n"
491
+ ctx += "\nTRAFFIC RULES:\n" + rules_text
492
+
493
+ return ctx, f"✅ Case **{case.get('case_number', '')}** loaded with {len(analyses)} analyses, {len(violations)} violations."
494
+
495
+
496
+ @spaces.GPU(duration=120)
497
+ def chat_respond(user_message, history, system_ctx):
498
+ if not user_message or not user_message.strip():
499
+ return history, "", system_ctx
500
+ ensure_init()
501
+ if not inference_engine.is_loaded:
502
+ inference_engine.load_model()
503
+ try:
504
+ # Use the vision model's text generation capability for chat
505
+ chat_prompt = f"""You are TraceScene AI assistant helping with accident analysis.
506
+
507
+ CONTEXT:
508
+ {system_ctx}
509
+
510
+ USER QUESTION: {user_message.strip()}
511
+
512
+ Provide a concise, helpful answer based on the context above."""
513
+ # Create a small blank image for the vision model
514
+ from PIL import Image as PILImg
515
+ blank = PILImg.new('RGB', (64, 64), color=(0, 0, 0))
516
+ response = gpu_run_inference(blank, chat_prompt)
517
+ except Exception as e:
518
+ response = f"Error: {e}"
519
+ history = history or []
520
+ history.append((user_message.strip(), response))
521
+ return history, "", system_ctx
522
+
523
+
524
+ def generate_animation_fn(case_id):
525
+ if not case_id:
526
+ return "<p style='color:red;'>Enter a Case ID.</p>"
527
+ ensure_init()
528
+ analyses = run_async(db.get_analyses_by_case(int(case_id)))
529
+ if not analyses:
530
+ return "<p style='color:red;'>No analyses found. Run analysis first.</p>"
531
+
532
+ # Parse scene details from the first analysis
533
+ raw = analyses[0].get("raw_analysis", "")
534
+
535
+ def extract_field(text, field):
536
+ import re
537
+ pattern = rf"{re.escape(field)}:\s*(.+)"
538
+ m = re.search(pattern, text, re.IGNORECASE)
539
+ return m.group(1).strip() if m else "Unknown"
540
+
541
+ road_type = extract_field(raw, "Road Type")
542
+ num_vehicles = extract_field(raw, "Vehicles Involved")
543
+ v1_pos = extract_field(raw, "Vehicle 1 Position")
544
+ v1_tyre = extract_field(raw, "Vehicle 1 Tyre Direction")
545
+ impact = extract_field(raw, "Area of Impact")
546
+ category = extract_field(raw, "Accident Category")
547
+ v1_make = extract_field(raw, "Vehicle 1 Make/Model")
548
+
549
+ # Check for Vehicle 2
550
+ v2_pos = extract_field(raw, "Vehicle 2 Position")
551
+ v2_tyre = extract_field(raw, "Vehicle 2 Tyre Direction")
552
+ v2_make = extract_field(raw, "Vehicle 2 Make/Model")
553
+ has_v2 = v2_make != "Unknown"
554
+
555
+ # Determine colors from extracted make
556
+ import re as re_mod
557
+ def extract_color(make_str):
558
+ colors = ["Red", "Blue", "White", "Black", "Silver", "Grey", "Green", "Yellow", "Brown", "Orange"]
559
+ for c in colors:
560
+ if c.lower() in make_str.lower():
561
+ return c.lower()
562
+ return "#3b82f6"
563
+
564
+ v1_color = extract_color(v1_make)
565
+ v2_color = extract_color(v2_make) if has_v2 else "#ef4444"
566
+
567
+ # Severity affects animation speed
568
+ speed_map = {"mild": 1.5, "medium": 2.5, "critical": 4.0}
569
+ anim_speed = speed_map.get(category.lower(), 2.5)
570
+
571
+ # Road layout
572
+ road_is_intersection = "intersection" in road_type.lower()
573
+ road_is_highway = "highway" in road_type.lower()
574
+
575
+ num_v = 1
576
+ try:
577
+ num_v = int(num_vehicles)
578
+ except:
579
+ pass
580
+ # Unique ID to force Gradio to re-render on each click (enables replay)
581
+ import random
582
+ uid = random.randint(10000, 99999)
583
+ # Determine animation duration based on severity
584
+ dur = "3s" if category.lower() == "mild" else "2s" if category.lower() == "medium" else "1.5s"
585
+ sev_color = "#22c55e" if category.lower() == "mild" else "#f59e0b" if category.lower() == "medium" else "#ef4444"
586
+
587
+ # Build SVG road
588
+ if road_is_intersection:
589
+ road_svg = '''
590
+ <rect x="0" y="160" width="700" height="100" fill="#555"/>
591
+ <rect x="300" y="0" width="100" height="420" fill="#555"/>
592
+ <line x1="0" y1="210" x2="300" y2="210" stroke="#fbbf24" stroke-width="2" stroke-dasharray="20,15"/>
593
+ <line x1="400" y1="210" x2="700" y2="210" stroke="#fbbf24" stroke-width="2" stroke-dasharray="20,15"/>
594
+ <line x1="350" y1="0" x2="350" y2="160" stroke="#fbbf24" stroke-width="2" stroke-dasharray="20,15"/>
595
+ <line x1="350" y1="260" x2="350" y2="420" stroke="#fbbf24" stroke-width="2" stroke-dasharray="20,15"/>
596
+ '''
597
+ else:
598
+ road_svg = '''
599
+ <rect x="0" y="150" width="700" height="120" fill="#555" rx="2"/>
600
+ <line x1="0" y1="210" x2="700" y2="210" stroke="#fbbf24" stroke-width="2" stroke-dasharray="20,15"/>
601
+ <line x1="0" y1="150" x2="700" y2="150" stroke="white" stroke-width="2"/>
602
+ <line x1="0" y1="270" x2="700" y2="270" stroke="white" stroke-width="2"/>
603
+ '''
604
+
605
+ # Vehicle 2 SVG (if present)
606
+ v2_svg = ""
607
+ if has_v2:
608
+ if road_is_intersection:
609
+ v2_svg = f'''<g>
610
+ <animateTransform attributeName="transform" type="translate" from="0,0" to="0,135" dur="{dur}" fill="freeze"/>
611
+ <rect x="325" y="60" width="50" height="26" rx="5" fill="{v2_color}" stroke="#fff" stroke-width="1"/>
612
+ <text x="350" y="78" fill="white" font-size="10" font-weight="bold" text-anchor="middle">V2</text>
613
+ </g>'''
614
+ else:
615
+ v2_svg = f'''<g>
616
+ <animateTransform attributeName="transform" type="translate" from="0,0" to="-200,0" dur="{dur}" fill="freeze"/>
617
+ <rect x="560" y="215" width="50" height="26" rx="5" fill="{v2_color}" stroke="#fff" stroke-width="1"/>
618
+ <text x="585" y="233" fill="white" font-size="10" font-weight="bold" text-anchor="middle">V2</text>
619
+ </g>'''
620
+
621
+ html = f'''
622
+ <div style="text-align:center; font-family: Inter, Arial, sans-serif;">
623
+ <svg id="anim_{uid}" width="700" height="420" viewBox="0 0 700 420" xmlns="http://www.w3.org/2000/svg" style="border:1px solid #444; border-radius:10px; background:#1a1a2e;">
624
+ <defs>
625
+ <radialGradient id="glow_{uid}" cx="50%" cy="50%" r="50%">
626
+ <stop offset="0%" stop-color="#fbbf24" stop-opacity="0.8"/>
627
+ <stop offset="100%" stop-color="#fbbf24" stop-opacity="0"/>
628
+ </radialGradient>
629
+ </defs>
630
+
631
+ {road_svg}
632
+
633
+ <!-- Vehicle 1 -->
634
+ <g>
635
+ <animateTransform attributeName="transform" type="translate" from="0,0" to="200,0" dur="{dur}" fill="freeze"/>
636
+ <rect x="80" y="190" width="50" height="26" rx="5" fill="{v1_color}" stroke="#fff" stroke-width="1"/>
637
+ <text x="105" y="207" fill="white" font-size="10" font-weight="bold" text-anchor="middle">V1</text>
638
+ </g>
639
+
640
+ {v2_svg}
641
+
642
+ <!-- Impact flash -->
643
+ <circle cx="340" cy="210" r="0" fill="url(#glow_{uid})">
644
+ <animate attributeName="r" values="0;0;0;0;0;0;0;45;55;0" dur="{dur}" fill="freeze"/>
645
+ <animate attributeName="opacity" values="0;0;0;0;0;0;0;1;0.5;0" dur="{dur}" fill="freeze"/>
646
+ </circle>
647
+
648
+ <!-- Debris -->
649
+ <circle cx="340" cy="210" r="3" fill="#fbbf24" opacity="0">
650
+ <animate attributeName="opacity" values="0;0;0;0;0;0;0;1;0" dur="{dur}" fill="freeze"/>
651
+ <animate attributeName="cx" values="340;340;340;340;340;340;340;310;290" dur="{dur}" fill="freeze"/>
652
+ <animate attributeName="cy" values="210;210;210;210;210;210;210;185;170" dur="{dur}" fill="freeze"/>
653
+ </circle>
654
+ <circle cx="340" cy="210" r="2" fill="#ef4444" opacity="0">
655
+ <animate attributeName="opacity" values="0;0;0;0;0;0;0;1;0" dur="{dur}" fill="freeze"/>
656
+ <animate attributeName="cx" values="340;340;340;340;340;340;340;370;395" dur="{dur}" fill="freeze"/>
657
+ <animate attributeName="cy" values="210;210;210;210;210;210;210;190;175" dur="{dur}" fill="freeze"/>
658
+ </circle>
659
+ <circle cx="340" cy="210" r="3" fill="#e2e8f0" opacity="0">
660
+ <animate attributeName="opacity" values="0;0;0;0;0;0;0;1;0" dur="{dur}" fill="freeze"/>
661
+ <animate attributeName="cx" values="340;340;340;340;340;340;340;320;305" dur="{dur}" fill="freeze"/>
662
+ <animate attributeName="cy" values="210;210;210;210;210;210;210;235;255" dur="{dur}" fill="freeze"/>
663
+ </circle>
664
+ <circle cx="340" cy="210" r="2" fill="#f97316" opacity="0">
665
+ <animate attributeName="opacity" values="0;0;0;0;0;0;0;1;0" dur="{dur}" fill="freeze"/>
666
+ <animate attributeName="cx" values="340;340;340;340;340;340;340;365;385" dur="{dur}" fill="freeze"/>
667
+ <animate attributeName="cy" values="210;210;210;210;210;210;210;230;250" dur="{dur}" fill="freeze"/>
668
+ </circle>
669
+
670
+ <!-- Collision label -->
671
+ <text x="350" y="145" fill="#ef4444" font-size="18" font-weight="bold" text-anchor="middle" opacity="0" font-family="Inter, Arial, sans-serif">
672
+ COLLISION
673
+ <animate attributeName="opacity" values="0;0;0;0;0;0;0;1;1" dur="{dur}" fill="freeze"/>
674
+ </text>
675
+
676
+ <!-- HUD -->
677
+ <rect x="10" y="350" width="680" height="60" rx="8" fill="rgba(0,0,0,0.6)"/>
678
+ <text x="20" y="375" fill="#e2e8f0" font-size="12" font-family="Inter, Arial, sans-serif">{v1_make[:35]}</text>
679
+ <text x="20" y="398" fill="#e2e8f0" font-size="12" font-family="Inter, Arial, sans-serif">{"" if not has_v2 else v2_make[:35]}{"Single vehicle accident" if not has_v2 else ""}</text>
680
+ <text x="680" y="375" fill="{sev_color}" font-size="14" font-weight="bold" text-anchor="end" font-family="Inter, Arial, sans-serif">{category.upper()}</text>
681
+ <text x="680" y="398" fill="#94a3b8" font-size="11" text-anchor="end" font-family="Inter, Arial, sans-serif">Impact: {impact} | Road: {road_type}</text>
682
+ </svg>
683
+ <div style="margin-top:8px; color:#94a3b8; font-size:12px;">
684
+ Vehicles: {num_v} | Animation auto-plays on load
685
+ </div>
686
+ </div>
687
+ '''
688
+ return html
689
+
690
  # ── Build Gradio App ──────────────────────────────────────────────────
691
 
692
  CUSTOM_CSS = """
 
801
  # State for context
802
  chat_system_ctx = gr.State(value="You are TraceScene AI assistant. You help insurers and investigating officers analyze accident cases, traffic rules, and insurance clauses. Answer concisely and accurately based on the context provided.")
803
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
805
 
806
  chat_load_btn.click(load_chat_context, inputs=[chat_case_id], outputs=[chat_system_ctx, chat_context_status])
807
  chat_send_btn.click(chat_respond, inputs=[chat_input, chatbot, chat_system_ctx], outputs=[chatbot, chat_input, chat_system_ctx], api_name="chat")
 
815
  anim_btn = gr.Button("Generate Animation", variant="primary")
816
  anim_output = gr.HTML(label="Animation View")
817
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
818
  anim_btn.click(generate_animation_fn, inputs=[anim_case_id], outputs=[anim_output])
819
 
820
  # Hidden API-only endpoints (for @gradio/client from custom frontend)