hug_mage.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. """Mage-VL demo — codec-native image / video understanding + proactive streaming.
  2. Preprocessing mirrors the reference `inference.py` (model repo) and
  3. `inference_streaming.py` (github.com/microsoft/Mage/tree/main/mage_vl):
  4. * image -> processor(text=..., images=[PIL])
  5. * video / frames -> processor(text=..., videos=[path], num_frames=N)
  6. * video / codec (HEVC) -> processor(..., video_backend="codec",
  7. codec_config={"engine": "hevc",
  8. "target_canvas": N,
  9. "patch": 16},
  10. max_pixels=150000)
  11. * streaming -> ffmpeg segment cuts -> codec inputs per segment ->
  12. model.streammind_gate_forward_segments(...) ->
  13. generate() only on segments above the gate threshold
  14. """
  15. import os
  16. os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
  17. os.environ.setdefault("ONLINE_CODEC_CACHE_DIR", "/tmp/mage_vl_codec_cache")
  18. os.environ.setdefault("STREAMMIND_CLIP_CACHE", "/tmp/mage_vl_clips")
  19. os.makedirs(os.environ["ONLINE_CODEC_CACHE_DIR"], exist_ok=True)
  20. os.makedirs(os.environ["STREAMMIND_CLIP_CACHE"], exist_ok=True)
  21. import importlib
  22. import subprocess
  23. import time
  24. import traceback
  25. import uuid
  26. import warnings
  27. from pathlib import Path
  28. import spaces # noqa: F401 (must precede torch)
  29. import torch
  30. import gradio as gr
  31. from PIL import Image
  32. from transformers import AutoModelForCausalLM, AutoProcessor
  33. MODEL_ID = "microsoft/Mage-VL"
  34. FRAMES_LABEL = "Uniform frame sampling"
  35. CODEC_LABEL = "Codec-native (HEVC)"
  36. # Reference defaults (inference.py)
  37. CODEC_MAX_PIXELS = 150_000 # pixel budget per codec canvas
  38. FRAME_MAX_PIXELS = 589_824 # 768x768 cap per sampled frame (guards 4K uploads)
  39. MAX_VIDEO_SECONDS = 150.0 # longer uploads are trimmed to keep GPU time sane
  40. STREAM_PROMPT = (
  41. "Please describe the video content in detail based on the provided information."
  42. )
  43. # --------------------------------------------------------------------------- model
  44. processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
  45. model = AutoModelForCausalLM.from_pretrained(
  46. MODEL_ID,
  47. trust_remote_code=True,
  48. dtype=torch.bfloat16,
  49. attn_implementation="sdpa",
  50. ).to("cuda").eval()
  51. # The trust_remote_code modules live in a package next to the processor class.
  52. _REMOTE_PKG = type(processor).__module__.rsplit(".", 1)[0]
  53. codec_mod = importlib.import_module(_REMOTE_PKG + ".codec_video_processing_mage_vl")
  54. video_mod = importlib.import_module(_REMOTE_PKG + ".video_processing_mage_vl")
  55. CodecConfig = codec_mod.CodecConfig
  56. MageVLVideoProcessor = video_mod.MageVLVideoProcessor
  57. ProcessorCls = type(processor)
  58. print(f"[init] model loaded, remote package = {_REMOTE_PKG}", flush=True)
  59. # ----------------------------------------------------- StreamMind cognition gate
  60. # `streammind_gate.py` in the model repo imports
  61. # `mamba_ssm.models.mixer_seq_simple.create_block`; the real mamba-ssm ships CUDA
  62. # extensions with no wheel for this runtime, so `./mamba_ssm/` in this Space is a
  63. # pure-PyTorch stand-in with identical parameter names/shapes (see its docstring).
  64. # With that in place the released gate code + checkpoint load unmodified.
  65. GATE_ERROR = ""
  66. try:
  67. _t0 = time.perf_counter()
  68. model.model._load_streammind_gate()
  69. print(f"[gate] loaded in {time.perf_counter() - _t0:.1f}s", flush=True)
  70. except Exception as exc: # surfaced in the streaming tab, never kills the boot
  71. GATE_ERROR = f"{type(exc).__name__}: {exc}"
  72. print("[gate] FAILED:\n" + traceback.format_exc(), flush=True)
  73. # --------------------------------------------------------------------------- utils
  74. def _ffprobe_duration(video_path):
  75. try:
  76. out = subprocess.run(
  77. ["ffprobe", "-v", "error", "-show_entries", "format=duration",
  78. "-of", "default=nw=1:nk=1", str(video_path)],
  79. check=True, capture_output=True, timeout=60, text=True,
  80. )
  81. return float(out.stdout.strip())
  82. except Exception:
  83. return 0.0
  84. def _probe(video_path):
  85. """(duration_seconds, fps, frame_count) via OpenCV, ffprobe as fallback."""
  86. import cv2
  87. cap = cv2.VideoCapture(video_path)
  88. try:
  89. fps = float(cap.get(cv2.CAP_PROP_FPS) or 0.0)
  90. frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
  91. finally:
  92. cap.release()
  93. if fps <= 0:
  94. fps = 30.0
  95. duration = frames / fps if frames > 0 else 0.0
  96. if duration <= 0:
  97. duration = _ffprobe_duration(video_path)
  98. frames = int(duration * fps)
  99. return duration, fps, frames
  100. def _trim_if_long(video_path, limit=MAX_VIDEO_SECONDS):
  101. """Keep the first `limit` seconds so a long upload can't blow the GPU budget."""
  102. duration, _, _ = _probe(video_path)
  103. if duration <= limit + 1.0:
  104. return video_path, duration, ""
  105. out = Path("/tmp/mage_vl_clips") / f"trim_{uuid.uuid4().hex}.mp4"
  106. subprocess.run(
  107. ["ffmpeg", "-y", "-loglevel", "error", "-t", f"{limit:.3f}",
  108. "-i", video_path, "-c", "copy", "-avoid_negative_ts", "make_zero", str(out)],
  109. check=True, capture_output=True, timeout=300,
  110. )
  111. note = (f"\n\n> Input was {duration:.0f}s long — only the first {limit:.0f}s "
  112. f"were used.")
  113. return str(out), min(duration, limit), note
  114. def _codec_cfg(target_canvas, max_pixels=CODEC_MAX_PIXELS):
  115. """Rebuild exactly the CodecConfig MageVLProcessor would build, so
  116. `process_codec_video` and the processor share one disk cache entry."""
  117. override = {"engine": "hevc", "target_canvas": int(target_canvas), "patch": 16}
  118. kwargs = dict(processor._codec_config_defaults)
  119. merged_dcvc = dict(kwargs.get("dcvc") or {})
  120. merged_dcvc.update(override.get("dcvc") or {})
  121. kwargs.update(override)
  122. if merged_dcvc:
  123. kwargs["dcvc"] = merged_dcvc
  124. kwargs["max_pixels"] = int(max_pixels)
  125. return CodecConfig(**kwargs), override
  126. def _frames_processor(max_pixels):
  127. """Processor clone whose video processor honours a per-frame pixel budget."""
  128. ip = processor.image_processor
  129. vp = MageVLVideoProcessor(
  130. image_processor=ip,
  131. min_pixels=getattr(ip, "min_pixels", 3136),
  132. max_pixels=int(max_pixels),
  133. patch_size=getattr(ip, "patch_size", 16),
  134. spatial_merge_size=getattr(ip, "merge_size", 2),
  135. )
  136. return ProcessorCls(
  137. image_processor=ip,
  138. tokenizer=processor.tokenizer,
  139. video_processor=vp,
  140. chat_template=processor.chat_template,
  141. codec_config=processor._codec_config_defaults,
  142. )
  143. def _prompt(kind, question):
  144. messages = [{"role": "user", "content": [{"type": kind}, {"type": "text", "text": question}]}]
  145. return processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
  146. def _to_cuda(inputs):
  147. out = {}
  148. for key, value in inputs.items():
  149. if not hasattr(value, "to"):
  150. continue
  151. out[key] = value.to("cuda")
  152. if key == "pixel_values":
  153. out[key] = out[key].to(model.dtype)
  154. return out
  155. def _visual_tokens(inputs):
  156. grid = inputs["image_grid_thw"]
  157. return int((grid[:, 0] * grid[:, 1] * grid[:, 2]).sum().item()) // 4
  158. @torch.inference_mode()
  159. def _generate(inputs, max_new_tokens):
  160. out = model.generate(**inputs, max_new_tokens=int(max_new_tokens), do_sample=False)
  161. new = out[0, inputs["input_ids"].shape[1]:]
  162. return processor.tokenizer.decode(new, skip_special_tokens=True).strip()
  163. # ------------------------------------------------------------------------- handlers
  164. def _image_duration(image=None, question=None, max_new_tokens=384, *args, **kwargs):
  165. # measured on ZeroGPU: ~13 s for a 2 MP image at 1024 new tokens
  166. return int(16 + float(max_new_tokens) * 0.013)
  167. @spaces.GPU(duration=_image_duration)
  168. def run_image(image, question, max_new_tokens=384):
  169. """Answer a question about a single image with Mage-VL.
  170. Args:
  171. image: the input image.
  172. question: what to ask about it.
  173. max_new_tokens: generation length cap.
  174. """
  175. if image is None:
  176. raise gr.Error("Please provide an image.")
  177. question = (question or "Describe this image in detail.").strip()
  178. if isinstance(image, str):
  179. image = Image.open(image)
  180. image = image.convert("RGB")
  181. inputs = processor(text=[_prompt("image", question)], images=[image],
  182. return_tensors="pt")
  183. tokens = _visual_tokens(inputs)
  184. started = time.perf_counter()
  185. answer = _generate(_to_cuda(inputs), max_new_tokens)
  186. elapsed = time.perf_counter() - started
  187. stats = (f"**{image.width}×{image.height}** · **{tokens:,}** visual tokens · "
  188. f"**{elapsed:.1f}s** on ZeroGPU")
  189. return answer, stats
  190. def _video_duration(video=None, question=None, backend=CODEC_LABEL, num_frames=32,
  191. max_new_tokens=384, *args, **kwargs):
  192. # measured on ZeroGPU (30 s clip, 64 canvases / 64 frames, 1024 new tokens):
  193. # codec 3.2 s prep + 13.3 s generate, frames 4.5 s prep + 16.0 s generate.
  194. # The base covers codec analysis on longer uploads (trimmed to 150 s).
  195. return int(30 + float(max_new_tokens) * 0.020)
  196. @spaces.GPU(duration=_video_duration)
  197. def run_video(video, question, backend=CODEC_LABEL, num_frames=32, max_new_tokens=384):
  198. """Answer a question about a video, with codec-native or frame-sampled tokens.
  199. Args:
  200. video: the input video file.
  201. question: what to ask about it.
  202. backend: "Codec-native (HEVC)" or "Uniform frame sampling".
  203. num_frames: codec canvases (codec backend) or sampled frames (frames backend).
  204. max_new_tokens: generation length cap.
  205. """
  206. if not video:
  207. raise gr.Error("Please provide a video.")
  208. question = (question or "Describe this video.").strip()
  209. num_frames = max(4, int(num_frames) // 4 * 4) # target_canvas % images_per_group == 0
  210. yield "", [], "Preparing video…"
  211. path, duration, note = _trim_if_long(video)
  212. _, fps, frame_count = _probe(path)
  213. text = _prompt("video", question)
  214. prep_start = time.perf_counter()
  215. if str(backend).startswith("Codec"):
  216. yield "", [], "Running codec analysis (cv-preinfer)…"
  217. cfg, override = _codec_cfg(num_frames)
  218. try:
  219. with warnings.catch_warnings():
  220. warnings.simplefilter("ignore")
  221. # process_codec_video is disk-cached, so running it here for the
  222. # canvas gallery costs nothing extra: the processor call below
  223. # hits the same cache entry.
  224. payload = codec_mod.process_codec_video(path, cfg)
  225. canvases, _, _ = codec_mod.drop_padding_canvases(
  226. payload["images"], payload["src_positions"]
  227. )
  228. inputs = processor(
  229. text=[text], videos=[path], video_backend="codec",
  230. max_pixels=CODEC_MAX_PIXELS, codec_config=override,
  231. return_tensors="pt", padding=True,
  232. )
  233. except Exception as exc:
  234. print("[codec] " + traceback.format_exc(), flush=True)
  235. raise gr.Error(
  236. "Codec analysis failed for this file "
  237. f"({type(exc).__name__}: {exc}). The codec path needs a video the "
  238. "HEVC/H.264 bit-cost reader can parse — try a standard .mp4, or "
  239. 'switch the tokenizer to "Uniform frame sampling".'
  240. ) from exc
  241. gallery = [(im, f"canvas {i + 1}") for i, im in enumerate(canvases)]
  242. sampled = min(cfg.num_sampled_frames(), max(1, frame_count))
  243. detail = (f"**codec-native (HEVC)** · {sampled} frames analysed → "
  244. f"**{len(canvases)} canvases** · {CODEC_MAX_PIXELS:,} px/canvas budget")
  245. else:
  246. yield "", [], "Sampling frames…"
  247. proc = _frames_processor(FRAME_MAX_PIXELS)
  248. inputs = proc(text=[text], videos=[path], num_frames=int(num_frames),
  249. return_tensors="pt", padding=True)
  250. frames, _, _ = video_mod.extract_video_frames_to_pil(
  251. video_path=path, max_frames=384, patch_size=16, min_pixels=3136,
  252. max_pixels=FRAME_MAX_PIXELS, resize_frames=True,
  253. fixed_num_frames=int(num_frames), target_fps=None,
  254. )
  255. gallery = [(im, f"frame {i + 1}") for i, im in enumerate(frames)]
  256. detail = (f"**uniform frame sampling** · **{len(frames)} frames** · "
  257. f"{FRAME_MAX_PIXELS:,} px/frame budget")
  258. prep = time.perf_counter() - prep_start
  259. tokens = _visual_tokens(inputs)
  260. prompt_tokens = int(inputs["input_ids"].shape[1])
  261. yield "", gallery, f"{detail} · **{tokens:,}** visual tokens — generating…"
  262. gen_start = time.perf_counter()
  263. answer = _generate(_to_cuda(inputs), max_new_tokens)
  264. gen = time.perf_counter() - gen_start
  265. stats = (f"{detail}\n\n"
  266. f"**{tokens:,}** visual tokens ({prompt_tokens:,} prompt tokens) · "
  267. f"video {duration:.1f}s @ {fps:.1f} fps · "
  268. f"preprocess **{prep:.1f}s** · generate **{gen:.1f}s**{note}")
  269. yield answer, gallery, stats
  270. def _extract_subclip(src, start, duration, out_path):
  271. if out_path.exists() and out_path.stat().st_size > 0:
  272. return out_path
  273. tmp = out_path.with_name(f".tmp_{out_path.name}")
  274. subprocess.run(
  275. ["ffmpeg", "-y", "-loglevel", "error", "-ss", f"{max(0.0, start):.3f}",
  276. "-t", f"{duration:.3f}", "-i", str(src), "-c", "copy",
  277. "-avoid_negative_ts", "make_zero", str(tmp)],
  278. check=True, capture_output=True, timeout=180,
  279. )
  280. tmp.replace(out_path)
  281. return out_path
  282. def _stream_duration(video=None, segment_sec=8.0, gate_threshold=0.5, max_segments=4,
  283. max_new_tokens=96, *args, **kwargs):
  284. # measured on ZeroGPU: 54 s for 8 segments all speaking at 256 new tokens
  285. # (~1.3 s codec analysis per segment + ~5 s generation per spoken segment).
  286. return int(14 + float(max_segments) * (2.5 + float(max_new_tokens) * 0.021))
  287. @spaces.GPU(duration=_stream_duration)
  288. def run_streaming(video, segment_sec=8.0, gate_threshold=0.5, max_segments=4,
  289. max_new_tokens=96):
  290. """Event-gated streaming commentary: the model only speaks on salient segments.
  291. Args:
  292. video: the input video file.
  293. segment_sec: length of each rolling codec segment, in seconds.
  294. gate_threshold: speak when the cognition gate's p(response) is above this.
  295. max_segments: how many leading segments to process.
  296. max_new_tokens: generation length cap per spoken segment.
  297. """
  298. if not video:
  299. raise gr.Error("Please provide a video.")
  300. if GATE_ERROR:
  301. raise gr.Error(f"The StreamMind gate could not be loaded: {GATE_ERROR}")
  302. segment_sec = float(segment_sec)
  303. max_segments = int(max_segments)
  304. yield "Cutting segments…"
  305. path, duration, note = _trim_if_long(video)
  306. clip_dir = Path("/tmp/mage_vl_clips")
  307. clip_dir.mkdir(parents=True, exist_ok=True)
  308. stem = Path(path).stem
  309. text = _prompt("video", STREAM_PROMPT)
  310. cfg, override = _codec_cfg(32)
  311. segments, spans = [], []
  312. start = 0.0
  313. index = 0
  314. while start < duration - 0.2 and index < max_segments:
  315. end = min(duration, start + segment_sec)
  316. clip = clip_dir / f"{stem}__t{int(start * 100):08d}__d{int((end - start) * 100):08d}.mp4"
  317. yield (f"Codec analysis for segment {index + 1} "
  318. f"(t={start:.1f}–{end:.1f}s)…")
  319. try:
  320. _extract_subclip(path, start, end - start, clip)
  321. with warnings.catch_warnings():
  322. warnings.simplefilter("ignore")
  323. inputs = processor(
  324. text=[text], videos=[str(clip)], video_backend="codec",
  325. max_pixels=CODEC_MAX_PIXELS, codec_config=override,
  326. return_tensors="pt", padding=False,
  327. )
  328. except Exception as exc:
  329. print(f"[stream] segment {start:.1f}-{end:.1f} unusable: {exc}", flush=True)
  330. start, index = end, index + 1
  331. continue
  332. segments.append(_to_cuda(inputs))
  333. spans.append((start, end))
  334. start, index = end, index + 1
  335. if not segments:
  336. yield "No usable segments — try a longer clip or a smaller segment length."
  337. return
  338. yield f"Scoring {len(segments)} segments with the cognition gate…"
  339. visual = [{k: seg[k] for k in ("pixel_values", "image_grid_thw", "patch_positions")
  340. if k in seg} for seg in segments]
  341. with torch.inference_mode():
  342. logits = model.model.streammind_gate_forward_segments(visual)[0]
  343. lengths = [int(seg["image_grid_thw"][:, 0].sum()) for seg in visual]
  344. boundaries = torch.tensor(lengths, device=logits.device).cumsum(0) - 1
  345. probs = torch.softmax(logits[boundaries].float(), dim=-1)[:, 1].tolist()
  346. lines = []
  347. for (span, inputs, prob) in zip(spans, segments, probs):
  348. head = f"**t = {span[0]:.1f}–{span[1]:.1f}s** · p(respond) = `{prob:.2f}`"
  349. if prob < float(gate_threshold):
  350. lines.append(f"{head} → 🔇 *silence*")
  351. yield "\n\n".join(lines)
  352. continue
  353. lines.append(f"{head} → 🗣️ *generating…*")
  354. yield "\n\n".join(lines)
  355. try:
  356. said = _generate(inputs, max_new_tokens)
  357. except Exception as exc:
  358. said = f"(generation failed: {type(exc).__name__}: {exc})"
  359. lines[-1] = f"{head} → 🗣️ {said}"
  360. yield "\n\n".join(lines)
  361. yield "\n\n".join(lines) + note
  362. # ------------------------------------------------------------------------------ UI
  363. CSS = """
  364. #col-container { max-width: 1180px; margin: 0 auto; }
  365. .dark .gradio-container { color: var(--body-text-color); }
  366. """
  367. INTRO = """# Mage-VL — codec-native streaming multimodal model
  368. [`microsoft/Mage-VL`](https://huggingface.co/microsoft/Mage-VL) is a 4B VLM whose
  369. visual encoder is trained **from scratch** to tokenize video the way a codec
  370. compresses it: keep every anchor (I) frame patch, keep only the predicted (P)
  371. frame patches where the codec spends bits, pack the survivors into **canvases**.
  372. That cuts visual tokens by >75% versus uniform frame sampling. A lightweight
  373. cognition gate on top turns the same weights into a proactive streaming model.
  374. [Project page](https://microsoft.github.io/Mage) · [GitHub](https://github.com/microsoft/Mage) · [Tech report](https://github.com/microsoft/Mage/blob/main/assets/mage_vl_tech_report.pdf)
  375. """
  376. with gr.Blocks(title="Mage-VL") as demo:
  377. with gr.Column(elem_id="col-container"):
  378. gr.Markdown(INTRO)
  379. with gr.Tabs():
  380. # ------------------------------------------------------------- image
  381. with gr.Tab("Image"):
  382. with gr.Row():
  383. with gr.Column():
  384. img_in = gr.Image(label="Image", type="pil", height=360)
  385. img_q = gr.Textbox(
  386. label="Question", lines=2,
  387. value="Describe this image in detail.",
  388. )
  389. img_btn = gr.Button("Ask", variant="primary")
  390. with gr.Column():
  391. img_out = gr.Textbox(label="Answer", lines=16, buttons=["copy"])
  392. img_stats = gr.Markdown()
  393. with gr.Accordion("Advanced", open=False):
  394. img_tokens = gr.Slider(64, 1024, value=384, step=32,
  395. label="Max new tokens")
  396. gr.Examples(
  397. examples=[
  398. ["examples/dog.jpg", "Describe this image in detail."],
  399. ["examples/dog.jpg", "Describe the rug behind the dog and the patterns woven into it."],
  400. ],
  401. inputs=[img_in, img_q],
  402. outputs=[img_out, img_stats],
  403. fn=run_image,
  404. cache_examples=True,
  405. cache_mode="lazy",
  406. )
  407. img_btn.click(run_image, inputs=[img_in, img_q, img_tokens],
  408. outputs=[img_out, img_stats], api_name="ask_image")
  409. # ------------------------------------------------------------- video
  410. with gr.Tab("Video"):
  411. gr.Markdown(
  412. "Switch **Tokenizer** to compare the codec-native path against "
  413. "uniform frame sampling — the gallery shows exactly what the "
  414. "vision encoder sees, and the stats line reports the visual-token "
  415. "count for each path."
  416. )
  417. with gr.Row():
  418. with gr.Column():
  419. vid_in = gr.Video(label="Video", height=320)
  420. vid_q = gr.Textbox(label="Question", lines=2,
  421. value="Describe this video.")
  422. vid_backend = gr.Radio(
  423. [CODEC_LABEL, FRAMES_LABEL], value=CODEC_LABEL,
  424. label="Tokenizer",
  425. )
  426. vid_btn = gr.Button("Ask", variant="primary")
  427. with gr.Column():
  428. vid_out = gr.Textbox(label="Answer", lines=14, buttons=["copy"])
  429. vid_stats = gr.Markdown()
  430. vid_gallery = gr.Gallery(
  431. label="What the vision encoder sees (codec canvases / sampled frames)",
  432. columns=8, height=260, object_fit="contain", show_label=True,
  433. )
  434. with gr.Accordion("Advanced", open=False):
  435. vid_frames = gr.Slider(
  436. 8, 64, value=32, step=4,
  437. label="Codec canvases (codec) / sampled frames (frames)",
  438. )
  439. vid_tokens = gr.Slider(64, 1024, value=384, step=32,
  440. label="Max new tokens")
  441. gr.Examples(
  442. examples=[
  443. ["examples/soccer-broadcast.mp4", "Describe this video.", CODEC_LABEL],
  444. ["examples/soccer-broadcast.mp4", "What is the score shown on the scoreboard, and which teams are playing?", CODEC_LABEL],
  445. ["examples/soccer-broadcast.mp4", "Describe this video.", FRAMES_LABEL],
  446. ],
  447. inputs=[vid_in, vid_q, vid_backend],
  448. outputs=[vid_out, vid_gallery, vid_stats],
  449. fn=run_video,
  450. cache_examples=True,
  451. cache_mode="lazy",
  452. )
  453. vid_btn.click(
  454. run_video,
  455. inputs=[vid_in, vid_q, vid_backend, vid_frames, vid_tokens],
  456. outputs=[vid_out, vid_gallery, vid_stats],
  457. api_name="ask_video",
  458. )
  459. # --------------------------------------------------------- streaming
  460. with gr.Tab("Proactive streaming"):
  461. gr.Markdown(
  462. "The video is split into non-overlapping codec segments. The "
  463. "**cognition gate** (System 1) scores every segment and stays "
  464. "silent on routine content; the full VLM (System 2) only "
  465. "generates commentary where `p(respond)` crosses the threshold."
  466. )
  467. with gr.Row():
  468. with gr.Column(scale=1):
  469. st_in = gr.Video(label="Video", height=300)
  470. st_btn = gr.Button("Run streaming", variant="primary")
  471. with gr.Accordion("Advanced", open=False):
  472. st_seg = gr.Slider(4.0, 12.0, value=8.0, step=1.0,
  473. label="Segment length (s)")
  474. st_thr = gr.Slider(0.1, 0.9, value=0.5, step=0.05,
  475. label="Gate threshold")
  476. st_max = gr.Slider(2, 8, value=4, step=1,
  477. label="Max segments")
  478. st_tokens = gr.Slider(32, 256, value=96, step=16,
  479. label="Max new tokens per response")
  480. with gr.Column(scale=2):
  481. st_out = gr.Markdown(
  482. value="_Timeline will appear here._",
  483. label="Gated timeline",
  484. )
  485. gr.Examples(
  486. examples=[["examples/soccer-broadcast.mp4"]],
  487. inputs=[st_in],
  488. outputs=[st_out],
  489. fn=run_streaming,
  490. cache_examples=True,
  491. cache_mode="lazy",
  492. )
  493. st_btn.click(
  494. run_streaming,
  495. inputs=[st_in, st_seg, st_thr, st_max, st_tokens],
  496. outputs=[st_out],
  497. api_name="run_streaming",
  498. )
  499. gr.Markdown(
  500. "Preprocessing follows the reference `inference.py` and "
  501. "`inference_streaming.py` from the model repo / "
  502. "[Mage GitHub](https://github.com/microsoft/Mage/tree/main/mage_vl). "
  503. "The codec path shells out to `cv-preinfer` (`codec-video-prep`) for "
  504. "HEVC bit-cost readiness; the DCVC-RT neural-codec engine is not "
  505. "wired up in this Space."
  506. )
  507. if __name__ == "__main__":
  508. demo.queue().launch(theme=gr.themes.Citrus(), css=CSS, show_error=True)