multimodalart HF Staff commited on
Commit
65c1f46
·
verified ·
1 Parent(s): 52ae450

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. .gitattributes +2 -0
  2. README.md +82 -7
  3. app.py +377 -0
  4. case-01.mp4 +3 -0
  5. eating_spaghetti.mp4 +3 -0
  6. requirements.txt +7 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ case-01.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ eating_spaghetti.mp4 filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,88 @@
1
  ---
2
- title: Videojev
3
- emoji: 🌍
4
- colorFrom: indigo
5
- colorTo: pink
6
  sdk: gradio
7
  sdk_version: 6.30.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: VideoJev-4B
3
+ emoji: 🎬
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.30.0
 
8
  app_file: app.py
9
+ short_description: Temporal grounding + video QA in one forward pass
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 1h
12
  ---
13
 
14
+ # 🎬 VideoJev-4B — video decisions without generation
15
+
16
+ Interactive demo for **VideoJev-4B** ([ZhuYuhan/VideoJev-4B](https://huggingface.co/ZhuYuhan/VideoJev-4B), Apache-2.0),
17
+ the video *System One* model from
18
+ [**"VideoJev: System One Model for Video Understanding and Interaction"**](https://arxiv.org/abs/2610.18615).
19
+
20
+ Instead of decoding text token by token, VideoJev **scores candidate time intervals and answer options
21
+ directly against the video**: temporal grounding and multiple-choice video QA come out of a single forward
22
+ pass, orders of magnitude faster than generative answering on the same tasks. Ask several questions about
23
+ the same video and they are answered together, sharing one video encoding.
24
+
25
+ ## What you can do here
26
+
27
+ - **Temporal grounding** — "when does *X* happen?" → an event span `[start, end]` in seconds, with the
28
+ per-timestamp sigmoid probabilities it was cut from.
29
+ - **Multiple-choice video QA** — a question + options → the selected answer with softmax probabilities.
30
+
31
+ Both tasks run in one shared forward pass over your video; mix them freely in the question table.
32
+
33
+ ## Example videos
34
+
35
+ The two bundled example videos are the authors' own demo inputs:
36
+
37
+ - `eating_spaghetti.mp4` — the checkpoint's quick-start example
38
+ ([ZhuYuhan/VideoJev-Demo](https://huggingface.co/datasets/ZhuYuhan/VideoJev-Demo), itself mirrored from
39
+ [nielsr/video-demo](https://huggingface.co/datasets/nielsr/video-demo)).
40
+ - `case-01.mp4` — a Charades clip featured on the authors'
41
+ [project page](https://mcg-nju.github.io/VideoJev).
42
+
43
+ ## Running locally
44
+
45
+ ```python
46
+ from transformers import AutoModel, AutoProcessor
47
+ import torch, json
48
+
49
+ repo = "ZhuYuhan/VideoJev-4B"
50
+ processor = AutoProcessor.from_pretrained(repo, trust_remote_code=True)
51
+ model = AutoModel.from_pretrained(repo, trust_remote_code=True, dtype=torch.bfloat16,
52
+ attn_implementation="sdpa").to("cuda")
53
+ model.prepare_for_inference()
54
+
55
+ questions = [
56
+ {"id": "g1", "task": "grounding", "query": "The woman in green eats pasta."},
57
+ {"id": "q1", "task": "choice", "query": "What is the woman in green eating?",
58
+ "options": ["A. Pasta.", "B. Rice.", "C. Soup."]},
59
+ ]
60
+ print(json.dumps(model.predict_video(processor, "video.mp4", questions,
61
+ fps=2.0, max_frames=2048, context=8192), indent=2))
62
+ ```
63
+
64
+ This Space runs the checkpoint's own `trust_remote_code` modules (`modeling_videojev.py`,
65
+ `processing_videojev.py`) unchanged, with `attn_implementation="sdpa"` — on GPUs without
66
+ flash-linear-attention kernels, transformers' pure-torch chunked `gated_delta_rule` fallback
67
+ executes the hybrid linear-attention layers.
68
+
69
+ ## Links
70
+
71
+ - Paper: <https://arxiv.org/abs/2610.18615>
72
+ - Code (Apache-2.0): <https://github.com/MCG-NJU/VideoJev>
73
+ - Model: <https://huggingface.co/ZhuYuhan/VideoJev-4B> · larger MoE: <https://huggingface.co/ZhuYuhan/VideoJev-35B-A3B>
74
+ - Homepage: <https://mcg-nju.github.io/VideoJev>
75
+
76
+ ## Citation
77
+
78
+ ```bibtex
79
+ @misc{zhu2026videojevmodelvideounderstanding,
80
+ title={VideoJev: System One Model for Video Understanding and Interaction},
81
+ author={Yuhan Zhu and Xiangyu Zeng and Qingyi Si and Chenxu Yang and Yifei Li and Luozheng Qin and Junhao Zhou and Dingyu Yao and Jun Zhang and Chuanyu Qin and Zhiqiu Zhang and Yuandong Yang and Yuqi Xu and Tianyu Zhang and Hongcheng Guo and Jiaqi Wang and Nan Duan and Limin Wang},
82
+ year={2026},
83
+ eprint={2610.18615},
84
+ archivePrefix={arXiv},
85
+ primaryClass={cs.CV},
86
+ url={https://arxiv.org/abs/2610.18615},
87
+ }
88
+ ```
app.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """VideoJev-4B — temporal grounding + multiple-choice video QA in one forward pass.
2
+
3
+ Demo for https://huggingface.co/ZhuYuhan/VideoJev-4B (Apache-2.0), the video System One
4
+ model from "VideoJev: System One Model for Video Understanding and Interaction"
5
+ (arXiv:2610.18615). VideoJev scores time intervals and answer options directly against
6
+ the video — no autoregressive decoding — and answers a whole group of questions in a
7
+ single forward pass.
8
+
9
+ Inference follows the authors' reference path exactly (the `modeling_videojev.py` /
10
+ `processing_videojev.py` shipped inside the checkpoint, byte-identical to
11
+ MCG-NJU/VideoJev `videojev_hf/`): `prepare_video` → `encode_video` → `predict_features`,
12
+ which is precisely what `model.predict_video` does internally — split here only so the
13
+ demo can also show the per-timestamp probability timeline. The model runs with
14
+ `attn_implementation="sdpa"`: transformers 5.x falls back to a pure-torch chunked
15
+ `gated_delta_rule` for the hybrid linear-attention layers when flash-linear-attention
16
+ kernels are absent, which keeps the Space dependency-light.
17
+ """
18
+
19
+ import os
20
+
21
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
22
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
23
+ os.environ.setdefault("GRADIO_SSR_MODE", "false")
24
+ # Match the authors' env (scripts/env.sh) so native recurrence numerics stay put.
25
+ os.environ.setdefault("FLA_INTRACARD_CP", "0")
26
+
27
+ import spaces # noqa: E402 -- MUST come before torch / transformers / any CUDA-touching import
28
+
29
+ import html # noqa: E402
30
+ import string # noqa: E402
31
+ import sys # noqa: E402
32
+ import time # noqa: E402
33
+
34
+ import gradio as gr # noqa: E402
35
+ import torch # noqa: E402
36
+ from transformers import AutoModel, AutoProcessor # noqa: E402
37
+
38
+ MODEL_ID = "ZhuYuhan/VideoJev-4B"
39
+ LETTERS = string.ascii_uppercase
40
+
41
+ print("[videojev] loading processor + model (bf16, sdpa)…", flush=True)
42
+ processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
43
+ model = AutoModel.from_pretrained(
44
+ MODEL_ID,
45
+ trust_remote_code=True,
46
+ dtype=torch.bfloat16,
47
+ attn_implementation="sdpa",
48
+ ).to("cuda")
49
+ model.prepare_for_inference()
50
+
51
+ # Pull the checkpoint's own preprocessing module — the exact code revision the weights
52
+ # ship with — from the sys.modules entry trust_remote_code already created for it.
53
+ _processing = sys.modules[model.__class__.__module__]
54
+ prepare_video = _processing.prepare_video
55
+ print(f"[videojev] {MODEL_ID} ready — grounding threshold {model.config.decoder['threshold']}", flush=True)
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Input helpers
60
+ # ---------------------------------------------------------------------------
61
+
62
+ def _normalize_rows(rows) -> list:
63
+ """Accept whatever gr.Dataframe hands over (pandas / numpy / nested list)."""
64
+ if rows is None:
65
+ return []
66
+ if hasattr(rows, "values"): # pandas DataFrame
67
+ rows = rows.astype(object).where(rows.notna(), "").values.tolist()
68
+ elif hasattr(rows, "tolist"): # numpy array
69
+ rows = rows.tolist()
70
+ return [[("" if c is None else c) for c in list(r) + [""] * (3 - len(list(r)))] for r in rows]
71
+
72
+
73
+ def _parse_options(text) -> list:
74
+ """Split the options cell on ';' or newlines (commas are kept inside options)."""
75
+ if text is None or not str(text).strip():
76
+ return []
77
+ return [o.strip() for o in str(text).replace(";", "\n").split("\n") if o.strip()]
78
+
79
+
80
+ def _label_options(options: list) -> list:
81
+ """Keep author-format options ('A. …') untouched; add letter labels otherwise."""
82
+ out = []
83
+ for i, option in enumerate(options):
84
+ if i >= len(LETTERS):
85
+ raise gr.Error(f"At most {len(LETTERS)} options are supported.")
86
+ letter = LETTERS[i]
87
+ out.append(option if option.startswith(f"{letter}.") else f"{letter}. {option}")
88
+ return out
89
+
90
+
91
+ def _build_questions(rows) -> list:
92
+ """Turn the questions table into the authors' question-dict format."""
93
+ questions = []
94
+ for i, row in enumerate(_normalize_rows(rows)):
95
+ task, query, options_cell = row[0], row[1], row[2]
96
+ task = str(task).strip().lower()
97
+ query = str(query or "").strip()
98
+ if not query and not task:
99
+ continue
100
+ if task not in ("grounding", "choice"):
101
+ raise gr.Error(
102
+ f"Row {i + 1}: task must be 'grounding' or 'choice' (got '{row[0]}')."
103
+ )
104
+ if not query:
105
+ raise gr.Error(f"Row {i + 1}: the query text is required.")
106
+ if "<|" in query or "<|" in str(options_cell or ""):
107
+ raise gr.Error("Special control tokens (<|…|>) are not allowed in questions or options.")
108
+ options = _label_options(_parse_options(options_cell))
109
+ if task == "choice" and len(options) < 2:
110
+ raise gr.Error(f"Row {i + 1}: choice questions need at least two options, separated by ';' or new lines.")
111
+ question = {"id": f"q{i + 1}", "task": task, "query": query}
112
+ if task == "choice":
113
+ question["options"] = options
114
+ questions.append(question)
115
+ if not questions:
116
+ raise gr.Error("Add at least one question (a query text is required).")
117
+ return questions
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Rendering
122
+ # ---------------------------------------------------------------------------
123
+
124
+ _BAR = (
125
+ '<div style="display:flex;align-items:center;gap:8px;margin:2px 0">'
126
+ '<span style="flex:0 0 46px;text-align:right;font-size:12px;opacity:.65">{t:.1f}s</span>'
127
+ '<div style="flex:1;height:22px;background:var(--border-color-primary);border-radius:3px;position:relative;overflow:hidden">'
128
+ '<div style="width:{fill:.1f}%;position:absolute;inset:0;'
129
+ 'background:var(--color-accent);opacity:.25"></div>'
130
+ '<div style="position:absolute;left:{ls:.2f}%;width:{ws:.2f}%;top:3px;bottom:3px;'
131
+ 'background:var(--color-accent);border-radius:2px"></div>'
132
+ "</div>"
133
+ '<span style="flex:0 0 44px;font-size:12px;opacity:.75">{p:.2f}</span></div>'
134
+ )
135
+
136
+
137
+ def _grounding_html(item: dict, record: dict) -> str:
138
+ start, end = item["interval"]
139
+ duration = max(record["sampled_duration"], 1e-6)
140
+ left = 100.0 * start / duration
141
+ width = 100.0 * max(end - start, 0.0) / duration
142
+ bars = "".join(
143
+ _BAR.format(t=t, fill=100.0 * p, ls=left, ws=width, p=p)
144
+ for t, p in zip(record["score_times"], item["probabilities"])
145
+ )
146
+ return (
147
+ '<div style="padding:10px 0;border-bottom:1px solid var(--border-color-primary)">'
148
+ '<div style="font-size:12.5px;opacity:.7;margin-bottom:4px"><code>grounding</code> · '
149
+ f"{html.escape(item['_query'])}</div>"
150
+ '<div style="font-size:20px;font-weight:600;margin:2px 0">'
151
+ f"⏱ {start:.1f}s – {end:.1f}s"
152
+ '<span style="font-weight:400;font-size:12.5px;opacity:.65;margin-left:8px">'
153
+ f"highlighted over the {duration:.1f}s video</span></div>"
154
+ f'<div style="margin-top:6px">{bars}</div></div>'
155
+ )
156
+
157
+
158
+ def _choice_html(item: dict) -> str:
159
+ winner = item["answer_index"]
160
+ rows = "".join(
161
+ '<div style="display:flex;align-items:center;gap:8px;margin:2px 0">'
162
+ f'<span style="flex:1;font-size:12.5px;opacity:{1.0 if i == winner else 0.5};'
163
+ f'font-weight:{600 if i == winner else 400}">{html.escape(option)}</span>'
164
+ '<div style="flex:0 0 170px;background:var(--border-color-primary);height:12px;border-radius:3px">'
165
+ f'<div style="width:{100.0 * p:.1f}%;height:12px;border-radius:3px;background:var(--color-accent);'
166
+ f'opacity:{1.0 if i == winner else 0.4}"></div></div>'
167
+ f'<span style="flex:0 0 50px;font-size:12px;text-align:right;'
168
+ f'opacity:{1.0 if i == winner else 0.5}">{p * 100:.1f}%</span></div>'
169
+ for i, (option, p) in enumerate(zip(item["_options"], item["probabilities"]))
170
+ )
171
+ return (
172
+ '<div style="padding:10px 0;border-bottom:1px solid var(--border-color-primary)">'
173
+ '<div style="font-size:12.5px;opacity:.7;margin-bottom:4px"><code>choice</code> · '
174
+ f"{html.escape(item['_query'])}</div>"
175
+ '<div style="font-size:20px;font-weight:600;margin:2px 0">'
176
+ f"✔ {html.escape(item['answer'])}"
177
+ '<span style="font-weight:400;font-size:12.5px;opacity:.65;margin-left:8px">'
178
+ f"softmax P = {item['probabilities'][winner]:.3f}</span></div>"
179
+ f'<div style="margin-top:6px">{rows}</div></div>'
180
+ )
181
+
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # GPU handler
185
+ # ---------------------------------------------------------------------------
186
+
187
+ def _estimate_duration(video, rows, *args, **kwargs):
188
+ """Duration sizing: flat allowance (weight streaming + decode) + per-second cost."""
189
+ try:
190
+ import av
191
+
192
+ with av.open(str(video)) as container:
193
+ stream = container.streams.video[0]
194
+ if stream.duration is not None:
195
+ seconds = float(stream.duration * stream.time_base)
196
+ else:
197
+ seconds = container.duration / 1e6
198
+ except Exception:
199
+ seconds = 30.0
200
+ return min(240, 60 + int(seconds * 1.5))
201
+
202
+
203
+ @spaces.GPU(duration=_estimate_duration)
204
+ def analyze(
205
+ video,
206
+ rows,
207
+ fps=2.0,
208
+ max_frames=512,
209
+ context=32768,
210
+ threshold=0.25,
211
+ progress=gr.Progress(track_tqdm=True),
212
+ ):
213
+ """Answer a group of temporal-grounding and multiple-choice questions about one video in a single forward pass.
214
+
215
+ Args:
216
+ video: input video file (any container PyAV can decode).
217
+ rows: question table — one row per question with columns
218
+ [task ('grounding' | 'choice'), query, options]. Options apply to 'choice'
219
+ questions only and are separated by ';' or new lines; letters are added
220
+ automatically unless already present ("A. …").
221
+ fps: frame sampling rate (authors use 2 for QA, 4 for grounding).
222
+ max_frames: cap on sampled frames across the whole video.
223
+ context: token budget for video + queries; video resolution adapts to fit.
224
+ threshold: sigmoid threshold used to cut the grounding interval.
225
+ progress: Gradio progress bar.
226
+
227
+ Returns:
228
+ (results_html, stats_markdown, raw_json): per-question rendering, run stats,
229
+ and the raw model output (interval / answer / probabilities per question).
230
+ """
231
+ if not video:
232
+ raise gr.Error("Upload a video or pick one of the examples first.")
233
+ video = str(video)
234
+ questions = _build_questions(rows)
235
+ fps, max_frames, context, threshold = float(fps), int(max_frames), int(context), float(threshold)
236
+
237
+ t0 = time.perf_counter()
238
+ try:
239
+ with torch.inference_mode():
240
+ # The authors' exact predict_video pipeline, split open so the per-timestamp
241
+ # probability timeline can be shown in the UI.
242
+ pixels, record = prepare_video(processor, video, fps=fps, max_frames=max_frames, context=context)
243
+ features = model.encode_video(pixels, torch.tensor([record["grid_thw"]]))
244
+ results = model.predict_features(
245
+ processor, features, record, questions, context=context, threshold=threshold
246
+ )
247
+ except gr.Error:
248
+ raise
249
+ except Exception as e: # surface a readable message instead of a stack trace
250
+ raise gr.Error(f"VideoJev inference failed: {type(e).__name__}: {e}")
251
+ elapsed = time.perf_counter() - t0
252
+
253
+ parts = []
254
+ for item, question in zip(results, questions):
255
+ item = dict(item)
256
+ item["_query"] = question["query"]
257
+ if question["task"] == "choice":
258
+ item["_options"] = question["options"]
259
+ parts.append(_choice_html(item))
260
+ else:
261
+ parts.append(_grounding_html(item, record))
262
+
263
+ n_ground = sum(1 for q in questions if q["task"] == "grounding")
264
+ n_choice = len(questions) - n_ground
265
+ stats = (
266
+ f"**{len(questions)} question{'s' if len(questions) > 1 else ''}** "
267
+ f"({n_ground} grounding · {n_choice} choice) answered in "
268
+ f"**{elapsed:.1f}s** on GPU · {len(record['score_times'])} video timestamps · "
269
+ f"{record['tokens']:,} video tokens"
270
+ )
271
+ payload = [
272
+ {k: v for k, v in dict(item, query=q["query"]).items()}
273
+ for item, q in zip(results, questions)
274
+ ]
275
+ return "".join(parts), stats, payload
276
+
277
+
278
+ # ---------------------------------------------------------------------------
279
+ # UI
280
+ # ---------------------------------------------------------------------------
281
+
282
+ CSS = """
283
+ #col-container { max-width: 1200px; margin: 0 auto; }
284
+ .dark .gradio-container { color: var(--body-text-color); }
285
+ """
286
+
287
+ DEFAULT_ROWS = [
288
+ ["grounding", "The woman in green eats pasta.", ""],
289
+ ["choice", "What is the woman in green eating?", "A. Pasta.; B. Rice.; C. Soup."],
290
+ ]
291
+
292
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
293
+ gr.Markdown(
294
+ """
295
+ # 🎬 VideoJev-4B — video decisions without generation
296
+
297
+ **Temporal grounding + multiple-choice video QA in one forward pass.** VideoJev is a video *System One*
298
+ model: instead of decoding text, it scores candidate time intervals and answer options directly against
299
+ the video — orders of magnitude faster than generative answering on the same tasks. All questions below
300
+ are answered together, sharing one video encoding.
301
+
302
+ [Paper](https://arxiv.org/abs/2610.18615) · [Code](https://github.com/MCG-NJU/VideoJev) ·
303
+ [Model](https://huggingface.co/ZhuYuhan/VideoJev-4B) · [Homepage](https://mcg-nju.github.io/VideoJev)
304
+ """
305
+ )
306
+ with gr.Column(elem_id="col-container"):
307
+ with gr.Row():
308
+ video_in = gr.Video(label="Video", sources=["upload"])
309
+ with gr.Column(scale=7):
310
+ questions = gr.Dataframe(
311
+ headers=["task", "query", "options (choice only: ';' or newline separated)"],
312
+ datatype=["str", "str", "str"],
313
+ col_count=(3, "fixed"),
314
+ row_count=(1, "dynamic"),
315
+ value=DEFAULT_ROWS,
316
+ wrap=True,
317
+ label="Questions — all answered in ONE shared forward pass",
318
+ interactive=True,
319
+ )
320
+ run = gr.Button("Answer questions", variant="primary")
321
+
322
+ with gr.Accordion("Advanced settings", open=False):
323
+ with gr.Row():
324
+ fps = gr.Radio([2.0, 4.0], value=2.0, label="Sampling fps (2 = QA, 4 = grounding per authors)")
325
+ max_frames = gr.Slider(16, 2048, value=512, step=16, info="Caps sampling; covers 4+ min at 2 fps",
326
+ label="Max frames over the whole video")
327
+ context = gr.Slider(8192, 262144, value=32768, step=2048,
328
+ info="Video resolution adapts to fit this budget",
329
+ label="Context tokens")
330
+ threshold = gr.Slider(0.05, 0.95, value=0.25, step=0.05,
331
+ info="Sigmoid cut for grounding intervals",
332
+ label="Grounding threshold")
333
+
334
+ stats = gr.Markdown("")
335
+ results = gr.HTML(label="Results")
336
+ raw_json = gr.JSON(label="Raw model output")
337
+
338
+ with gr.Accordion("How to read the output", open=False):
339
+ gr.Markdown(
340
+ """
341
+ - **grounding** → an event span `[start, end]` in seconds. The bar chart shows the model's sigmoid
342
+ probability at each video timestamp (a translucent bar = below threshold, solid = inside the
343
+ returned span).
344
+ - **choice** → the selected option, with softmax probabilities over all options.
345
+ - The raw JSON is exactly what the checkpoint returns from `predict_features`: `interval`,
346
+ `answer`, `answer_index` and `probabilities`.
347
+ """
348
+ )
349
+
350
+ gr.Examples(
351
+ examples=[
352
+ ["eating_spaghetti.mp4", DEFAULT_ROWS],
353
+ ["case-01.mp4", [
354
+ ["grounding", "A man is sitting on a chair.", ""],
355
+ ["grounding", "The man gets up from the chair.", ""],
356
+ ["grounding", "The man runs toward the door.", ""],
357
+ ]],
358
+ ["case-01.mp4", [
359
+ ["choice", "What does the man do first?",
360
+ "A. He sits on a chair; B. He runs to the door; C. He eats pasta"],
361
+ ]],
362
+ ],
363
+ inputs=[video_in, questions],
364
+ outputs=[results, stats, raw_json],
365
+ fn=analyze,
366
+ cache_examples=True,
367
+ cache_mode="lazy",
368
+ )
369
+
370
+ run.click(
371
+ analyze,
372
+ inputs=[video_in, questions, fps, max_frames, context, threshold],
373
+ outputs=[results, stats, raw_json],
374
+ api_name="analyze",
375
+ )
376
+
377
+ demo.launch(mcp_server=True)
case-01.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f72a9fba1d3acdc1ef76f65b85ec00793628cf0fa9bb2ff25d6509000f9dfb5b
3
+ size 2020390
eating_spaghetti.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:252f63d13748f08acf56765c295506bfdb8bb73b822e93a33a57d73988814a71
3
+ size 1013655
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ transformers==5.14.1
2
+ torchvision
3
+ accelerate>=1.10
4
+ av==18.1.0
5
+ numpy>=1.26,<3
6
+ Pillow>=10
7
+ safetensors>=0.4