samxkevin commited on
Commit
ed4d016
·
verified ·
1 Parent(s): 264dea1

Adding the pre-requisites

Browse files
Files changed (4) hide show
  1. README.md +19 -8
  2. app.py +203 -0
  3. requirements.txt +20 -0
  4. scenesolver_core.py +0 -0
README.md CHANGED
@@ -1,15 +1,26 @@
1
  ---
2
  title: SceneSolver
3
- emoji: 🦀
4
- colorFrom: red
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.26.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
11
  license: mit
12
- short_description: Multimodal Video Anomaly Detection & AI assistance
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: SceneSolver
3
+ author: samxkevin
 
 
4
  sdk: gradio
5
+ sdk_version: 5.49.1
 
6
  app_file: app.py
 
7
  license: mit
8
+ short_description: Multimodal surveillance analysis and evidence synthesis
9
  ---
10
 
11
+ # SceneSolver
12
+
13
+ SceneSolver is a multimodal surveillance-analysis system. The public demo exposes the inference pipeline while the trained model assets remain in the author's Hugging Face repositories.
14
+
15
+ ## Pipeline
16
+
17
+ AE anomaly signal → TimeSformer binary gate → TimeSformer multi-class classification → YOLO weapon detection → audio features → tracking/behavior extraction → fusion reasoning → RL3033 temporal reasoning → optional LLaVA contextual explanation.
18
+
19
+ ## Model sources
20
+
21
+ - `samxkevin/scenesolver-assets`
22
+ - `samxkevin/SceneDescriptionModel`
23
+
24
+ ## Notes
25
+
26
+ The system is an experimental/research project. Results are not definitive conclusions and require human review.
app.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import traceback
4
+ from functools import lru_cache
5
+
6
+ import gradio as gr
7
+ import spaces
8
+
9
+ from scenesolver_core import SceneSolverPipeline, frame_to_timestamp
10
+
11
+ APP_TITLE = "SceneSolver"
12
+ APP_SUBTITLE = "Multimodal surveillance analysis and evidence synthesis"
13
+
14
+ CSS = """
15
+ :root {
16
+ --bg: #000000;
17
+ --panel: #0b0b0b;
18
+ --line: #222222;
19
+ --muted: #8a8a8a;
20
+ --text: #f2f2f2;
21
+ --accent: #ff8c00;
22
+ }
23
+ body, .gradio-container { background: var(--bg) !important; color: var(--text) !important; }
24
+ .gradio-container { max-width: 1180px !important; }
25
+ .ss-header { padding: 28px 0 18px; border-bottom: 1px solid var(--line); margin-bottom: 24px; }
26
+ .ss-kicker { font: 700 12px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .16em; color: var(--accent); text-transform: uppercase; }
27
+ .ss-title { font: 400 42px/1.1 Georgia, 'Times New Roman', serif; margin-top: 8px; }
28
+ .ss-subtitle { color: var(--muted); font: 15px/1.6 Georgia, 'Times New Roman', serif; max-width: 780px; }
29
+ .ss-note { color: #777; font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; }
30
+ .ss-section { color: #ddd; font: 700 12px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .12em; text-transform: uppercase; margin: 18px 0 10px; }
31
+ #result pre { max-height: 520px; overflow: auto; }
32
+ footer { display: none !important; }
33
+ """
34
+
35
+ @lru_cache(maxsize=1)
36
+ def get_pipeline():
37
+ return SceneSolverPipeline()
38
+
39
+
40
+ def _jsonable(value):
41
+ if hasattr(value, "tolist"):
42
+ return value.tolist()
43
+ if isinstance(value, dict):
44
+ return {str(k): _jsonable(v) for k, v in value.items()}
45
+ if isinstance(value, (list, tuple)):
46
+ return [_jsonable(v) for v in value]
47
+ try:
48
+ json.dumps(value)
49
+ return value
50
+ except TypeError:
51
+ return str(value)
52
+
53
+
54
+ def _summary(results):
55
+ s0 = results.get("stage0") or {}
56
+ s1 = results.get("stage1") or {}
57
+ s2 = results.get("stage2") or {}
58
+ s3 = results.get("stage3") or {}
59
+ s4 = results.get("stage4") or {}
60
+ s5 = results.get("stage5") or {}
61
+ s6 = results.get("stage6") or {}
62
+ s7 = results.get("stage7") or {}
63
+ s8 = results.get("stage8") or {}
64
+ fps = float(s0.get("fps", 25.0))
65
+
66
+ def ts(idx):
67
+ return frame_to_timestamp(int(idx or 0), fps)
68
+
69
+ risk = float(s7.get("final_risk", s6.get("fused_risk", 0.0)))
70
+ data = {
71
+ "status": results.get("status", "UNKNOWN"),
72
+ "risk_level": results.get("risk_label_final", s6.get("risk_level", "LOW")),
73
+ "final_risk": risk,
74
+ "peak_timestamp": ts(s0.get("peak_frame_idx", 0)),
75
+ "anomaly_window": f"{ts(s0.get('anomaly_start_frame', 0))} -> {ts(s0.get('anomaly_end_frame', 0))}",
76
+ "timeformer_binary": {
77
+ "classification": s1.get("classification", "Unknown"),
78
+ "confidence": float(s1.get("confidence", 0.0)),
79
+ },
80
+ "anomaly_class": {
81
+ "class": s2.get("class", "Skipped"),
82
+ "confidence": float(s2.get("confidence", 0.0)),
83
+ "top3": s2.get("top3", []),
84
+ },
85
+ "weapon": {
86
+ "detected": bool(s3.get("weapon_flag", False)),
87
+ "dominant_class": s3.get("dominant_class", "None"),
88
+ "count": int(s3.get("weapon_count", 0)),
89
+ },
90
+ "audio": {
91
+ "status": s4.get("status", "N/A"),
92
+ "score": float(s4.get("audio_score", 0.0)),
93
+ },
94
+ "motion": {
95
+ "intensity": float(s5.get("motion_intensity", 0.0)),
96
+ "behaviors": s5.get("behaviors", []),
97
+ },
98
+ "fusion": {
99
+ "risk": float(s6.get("fused_risk", 0.0)),
100
+ "level": s6.get("risk_level", "LOW"),
101
+ "reasoning": s6.get("reasoning_trace", []),
102
+ },
103
+ "rl3033": {
104
+ "risk": float(s7.get("final_risk", risk)),
105
+ "status": s7.get("status", ""),
106
+ "interpretation": s7.get("interpretation", ""),
107
+ },
108
+ "llava": {
109
+ "narrative": s8.get("unified_narrative") or s8.get("description", "")
110
+ } if s8 else None,
111
+ }
112
+ return data
113
+
114
+
115
+ @spaces.GPU
116
+
117
+ def analyze_video(video_path, include_llava):
118
+ if not video_path:
119
+ raise gr.Error("Please upload a video first.")
120
+ try:
121
+ pipeline = get_pipeline()
122
+ results = pipeline.analyze(
123
+ video_path=video_path,
124
+ base_output_dir="/tmp/scenesolver_space",
125
+ enable_llava=bool(include_llava),
126
+ enable_rl=True,
127
+ enable_tracking=True,
128
+ )
129
+ summary = _summary(results)
130
+ top_frame = None
131
+ s0 = results.get("stage0") or {}
132
+ frames = s0.get("frames") or []
133
+ indices = s0.get("sampled_indices") or []
134
+ if frames and indices:
135
+ peak_idx = int(s0.get("peak_score_idx", 0))
136
+ frame_idx = indices[min(peak_idx, len(indices) - 1)]
137
+ frame_idx = min(int(frame_idx), len(frames) - 1)
138
+ top_frame = frames[frame_idx]
139
+
140
+ pdf_path = results.get("pdf_path_local") or results.get("pdf_path")
141
+ if not pdf_path or not os.path.exists(pdf_path):
142
+ pdf_path = None
143
+
144
+ return (
145
+ summary,
146
+ top_frame,
147
+ json.dumps(_jsonable(summary), indent=2),
148
+ pdf_path,
149
+ )
150
+ except Exception as exc:
151
+ traceback.print_exc()
152
+ raise gr.Error(f"Analysis failed: {exc}")
153
+
154
+
155
+ with gr.Blocks(title=APP_TITLE, css=CSS, theme=gr.themes.Base()) as demo:
156
+ gr.HTML(f"""
157
+ <div class='ss-header'>
158
+ <div class='ss-kicker'>Scene Solver v3</div>
159
+ <div class='ss-title'>{APP_TITLE}</div>
160
+ <div class='ss-subtitle'>{APP_SUBTITLE}. The pipeline combines anomaly detection,
161
+ temporal video understanding, weapon detection, audio and motion signals,
162
+ rule-based fusion, temporal reasoning, and optional multimodal explanation.</div>
163
+ <div class='ss-note'>This is a research/demo system. ML outputs are investigative aids and require human review.</div>
164
+ </div>
165
+ """)
166
+
167
+ with gr.Row():
168
+ with gr.Column(scale=1):
169
+ gr.HTML("<div class='ss-section'>Input</div>")
170
+ video = gr.Video(label="CCTV / surveillance video", sources=["upload"], height=280)
171
+ include_llava = gr.Checkbox(
172
+ value=False,
173
+ label="Enable LLaVA contextual explanation",
174
+ info="Adds the final multimodal narrative stage and may take substantially longer.",
175
+ )
176
+ analyse = gr.Button("Analyse Footage", variant="primary")
177
+ gr.Markdown(
178
+ "**Pipeline:** AE → TimeSformer Binary → TimeSformer Multi-class → YOLO → Audio → Tracking → Fusion → RL3033 → optional LLaVA",
179
+ elem_classes=["ss-note"],
180
+ )
181
+
182
+ with gr.Column(scale=1):
183
+ gr.HTML("<div class='ss-section'>Assessment</div>")
184
+ result = gr.JSON(label="Structured result", elem_id="result")
185
+ frame = gr.Image(label="Peak anomaly frame", interactive=False)
186
+ result_text = gr.Code(label="JSON export", language="json", interactive=False)
187
+ report = gr.File(label="Generated forensic PDF", interactive=False)
188
+
189
+ analyse.click(
190
+ fn=analyze_video,
191
+ inputs=[video, include_llava],
192
+ outputs=[result, frame, result_text, report],
193
+ api_name="analyze",
194
+ queue=True,
195
+ )
196
+
197
+ gr.Markdown(
198
+ "Public demo derived from the SceneSolver project. Model checkpoints are loaded from the author's public Hugging Face repositories.",
199
+ elem_classes=["ss-note"],
200
+ )
201
+
202
+ if __name__ == "__main__":
203
+ demo.queue(max_size=2).launch()
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=5.0,<6.0
2
+ spaces>=0.38.0
3
+ huggingface_hub>=0.34.0
4
+ numpy
5
+ opencv-python-headless
6
+ torch
7
+ torchvision
8
+ transformers>=4.45
9
+ accelerate
10
+ safetensors
11
+ Pillow
12
+ tqdm
13
+ matplotlib
14
+ reportlab
15
+ stable-baselines3
16
+ ultralytics
17
+ lap
18
+ librosa
19
+ openai-whisper
20
+ cohere>=5.0.0
scenesolver_core.py ADDED
The diff for this file is too large to render. See raw diff