mymage_window_test.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. import os, gc, time, subprocess, importlib, warnings
  2. from pathlib import Path
  3. os.environ.setdefault(
  4. "PYTORCH_CUDA_ALLOC_CONF",
  5. "expandable_segments:True,max_split_size_mb:128"
  6. )
  7. os.environ.setdefault("ONLINE_CODEC_CACHE_DIR", "/tmp/mage_vl_codec_cache")
  8. os.environ.setdefault("STREAMMIND_CLIP_CACHE", "/tmp/mage_vl_clips")
  9. os.makedirs(os.environ["ONLINE_CODEC_CACHE_DIR"], exist_ok=True)
  10. os.makedirs(os.environ["STREAMMIND_CLIP_CACHE"], exist_ok=True)
  11. import torch
  12. from transformers import AutoModelForCausalLM, AutoProcessor
  13. # ===================== 配置 =====================
  14. MODEL_ID = "microsoft/Mage-VL"
  15. VIDEO_FILE = "/content/soccer-broadcast.mp4"
  16. SEGMENT_SEC = 4.0
  17. # Gate 使用:
  18. # 前4秒 + 当前4秒 = 最多8秒视觉上下文
  19. CONTEXT_SEC = 4.0
  20. # LLM 保留最近几条文字事件
  21. HISTORY_SIZE = 3
  22. MAX_SEGMENTS = None # 测试可改成 8
  23. GATE_THRESHOLD = 0.50
  24. MAX_NEW_TOKENS = 64
  25. CODEC_MAX_PIXELS = 64_000
  26. CODEC_TARGET_CANVAS = 24
  27. SIMULATE_REALTIME = True
  28. # ===================== Prompt =====================
  29. GATE_PROMPT = """
  30. 这是足球比赛视频。
  31. 请判断视频末尾的当前时间窗口是否包含值得进一步分析的内容。
  32. 重点关注传球、带球、射门、抢断、犯规、角球、任意球、进球、
  33. 危险进攻、球员明显动作或比赛状态变化。
  34. """.strip()
  35. def make_prompt(start, end, history):
  36. history_text = "\n".join(
  37. f"- {x}" for x in history[-HISTORY_SIZE:]
  38. )
  39. if not history_text:
  40. history_text = "暂无历史事件。"
  41. return f"""
  42. 请用中文分析足球比赛视频。
  43. 当前重点时间:
  44. {start:.1f}–{end:.1f} 秒
  45. 视频包含:
  46. - 前面的视觉内容:用于理解动作上下文
  47. - 当前时间窗口:重点分析当前发生的事情
  48. 最近已经观察到的事件:
  49. {history_text}
  50. 请重点描述当前时间窗口发生了什么,并结合前面的视觉内容判断动作是否连续。
  51. 重点关注:
  52. 传球、带球、射门、抢断、犯规、角球、任意球、进球、
  53. 危险进攻、球员位置变化以及比赛状态变化。
  54. 如果当前只是普通画面,没有明显事件,也请简短说明。
  55. 不要猜测无法从视频确认的信息。
  56. 尽量简洁,适合实时视频分析。
  57. """.strip()
  58. # ===================== 模型 =====================
  59. if not torch.cuda.is_available():
  60. raise RuntimeError("未检测到 CUDA")
  61. DEVICE = "cuda"
  62. DTYPE = torch.float16
  63. print(f"[GPU] {torch.cuda.get_device_name(0)}")
  64. print(f"[CUDA] {torch.version.cuda}")
  65. print("[init] 加载 Processor...")
  66. processor = AutoProcessor.from_pretrained(
  67. MODEL_ID,
  68. trust_remote_code=True
  69. )
  70. print("[init] 加载 Mage-VL...")
  71. model = AutoModelForCausalLM.from_pretrained(
  72. MODEL_ID,
  73. trust_remote_code=True,
  74. torch_dtype=DTYPE,
  75. attn_implementation="sdpa",
  76. ).to(DEVICE).eval()
  77. print("[init] 模型加载完成")
  78. _REMOTE_PKG = type(processor).__module__.rsplit(".", 1)[0]
  79. codec_mod = importlib.import_module(
  80. _REMOTE_PKG + ".codec_video_processing_mage_vl"
  81. )
  82. CodecConfig = codec_mod.CodecConfig
  83. try:
  84. model.model._load_streammind_gate()
  85. print("[gate] Cognition Gate 加载成功")
  86. except Exception as e:
  87. print(f"[gate] 加载失败: {e}")
  88. # ===================== 工具 =====================
  89. def get_duration(path):
  90. try:
  91. r = subprocess.run(
  92. [
  93. "ffprobe", "-v", "error",
  94. "-show_entries", "format=duration",
  95. "-of", "default=nw=1:nk=1",
  96. str(path)
  97. ],
  98. capture_output=True,
  99. text=True,
  100. check=True,
  101. timeout=60
  102. )
  103. return float(r.stdout.strip())
  104. except Exception:
  105. return 0.0
  106. def codec_config():
  107. override = {
  108. "engine": "hevc",
  109. "target_canvas": CODEC_TARGET_CANVAS,
  110. "patch": 16,
  111. }
  112. kwargs = dict(processor._codec_config_defaults)
  113. dcvc = dict(kwargs.get("dcvc") or {})
  114. kwargs.update(override)
  115. if dcvc:
  116. kwargs["dcvc"] = dcvc
  117. kwargs["max_pixels"] = CODEC_MAX_PIXELS
  118. return CodecConfig(**kwargs), override
  119. def make_chat_prompt(text):
  120. messages = [{
  121. "role": "user",
  122. "content": [
  123. {"type": "video"},
  124. {"type": "text", "text": text},
  125. ],
  126. }]
  127. return processor.apply_chat_template(
  128. messages,
  129. tokenize=False,
  130. add_generation_prompt=True,
  131. )
  132. def extract_clip(src, start, length, out):
  133. tmp = out.with_name(".tmp_" + out.name)
  134. if tmp.exists():
  135. tmp.unlink()
  136. subprocess.run(
  137. [
  138. "ffmpeg", "-y", "-loglevel", "error",
  139. "-ss", f"{max(0, start):.3f}",
  140. "-i", str(src),
  141. "-t", f"{length:.3f}",
  142. "-c:v", "libx264",
  143. "-preset", "ultrafast",
  144. "-crf", "23",
  145. "-an",
  146. "-pix_fmt", "yuv420p",
  147. str(tmp)
  148. ],
  149. check=True,
  150. capture_output=True,
  151. timeout=180
  152. )
  153. tmp.replace(out)
  154. return out
  155. def to_cuda(inputs):
  156. result = {}
  157. for k, v in inputs.items():
  158. if not hasattr(v, "to"):
  159. continue
  160. v = v.to(DEVICE)
  161. if k == "pixel_values":
  162. v = v.to(DTYPE)
  163. result[k] = v
  164. return result
  165. def cleanup(*objs):
  166. for obj in objs:
  167. try:
  168. del obj
  169. except Exception:
  170. pass
  171. gc.collect()
  172. torch.cuda.empty_cache()
  173. def gpu_mem():
  174. a = torch.cuda.memory_allocated() / 1024**3
  175. r = torch.cuda.memory_reserved() / 1024**3
  176. return f"{a:.2f}GB / reserved {r:.2f}GB"
  177. # ===================== Gate =====================
  178. @torch.inference_mode()
  179. def run_gate(inputs):
  180. feat = {}
  181. for k in (
  182. "pixel_values",
  183. "image_grid_thw",
  184. "patch_positions",
  185. ):
  186. if k not in inputs:
  187. continue
  188. v = inputs[k].to(DEVICE)
  189. if k == "pixel_values":
  190. v = v.to(DTYPE)
  191. feat[k] = v
  192. logits = None
  193. try:
  194. logits = model.model.streammind_gate_forward_segments(
  195. [feat]
  196. )[0]
  197. # 这里取视觉序列最后位置。
  198. #
  199. # 因为输入是:
  200. # 前4秒 + 当前4秒
  201. #
  202. # 所以最后位置对应当前4秒末尾,
  203. # Gate 可以利用前面的视觉内容作为上下文。
  204. length = int(
  205. inputs["image_grid_thw"][:, 0].sum()
  206. )
  207. prob = torch.softmax(
  208. logits[length - 1].float(),
  209. dim=-1
  210. )[1].item()
  211. return float(prob)
  212. finally:
  213. cleanup(feat, logits)
  214. # ===================== Codec =====================
  215. def encode_video(
  216. clip,
  217. prompt,
  218. codec_override
  219. ):
  220. with warnings.catch_warnings():
  221. warnings.simplefilter("ignore")
  222. return processor(
  223. text=[prompt],
  224. videos=[str(clip)],
  225. video_backend="codec",
  226. max_pixels=CODEC_MAX_PIXELS,
  227. codec_config=codec_override,
  228. return_tensors="pt",
  229. padding=False,
  230. )
  231. # ===================== Mage-VL =====================
  232. @torch.inference_mode()
  233. def generate(inputs, max_new_tokens):
  234. out = model.generate(
  235. **inputs,
  236. max_new_tokens=max_new_tokens,
  237. do_sample=False,
  238. use_cache=True,
  239. )
  240. n = inputs["input_ids"].shape[1]
  241. text = processor.tokenizer.decode(
  242. out[0, n:],
  243. skip_special_tokens=True
  244. ).strip()
  245. del out
  246. return text
  247. # ===================== 主循环 =====================
  248. def stream_video_analysis(
  249. video_path,
  250. segment_sec=SEGMENT_SEC,
  251. context_sec=CONTEXT_SEC,
  252. gate_threshold=GATE_THRESHOLD,
  253. max_segments=MAX_SEGMENTS,
  254. max_new_tokens=MAX_NEW_TOKENS,
  255. simulate_realtime=SIMULATE_REALTIME,
  256. ):
  257. path = Path(video_path)
  258. if not path.exists():
  259. raise FileNotFoundError(video_path)
  260. total = get_duration(path)
  261. if total <= 0:
  262. raise RuntimeError("无法获取视频时长")
  263. clip_dir = Path(
  264. os.environ["STREAMMIND_CLIP_CACHE"]
  265. )
  266. clip_dir.mkdir(
  267. parents=True,
  268. exist_ok=True
  269. )
  270. _, codec_override = codec_config()
  271. start = 0.0
  272. index = 0
  273. # 最近几次文字事件
  274. history = []
  275. # 所有输出
  276. results = []
  277. yield (
  278. f"开始实时流式分析\n"
  279. f"视频={total:.1f}s | "
  280. f"当前窗口={segment_sec:.1f}s | "
  281. f"视觉上下文={context_sec:.1f}s | "
  282. f"历史={HISTORY_SIZE}条"
  283. )
  284. while start < total - 0.05:
  285. if (
  286. max_segments is not None
  287. and index >= max_segments
  288. ):
  289. break
  290. # 当前时间窗口
  291. end = min(
  292. total,
  293. start + segment_sec
  294. )
  295. current_length = end - start
  296. # --------------------------------------------------
  297. # 视觉上下文:
  298. #
  299. # 第1段:
  300. # 0-4
  301. #
  302. # 第2段:
  303. # 0-8
  304. #
  305. # 第3段:
  306. # 4-12
  307. #
  308. # 第4段:
  309. # 8-16
  310. # --------------------------------------------------
  311. context_start = max(
  312. 0.0,
  313. start - context_sec
  314. )
  315. context_end = end
  316. visual_length = (
  317. context_end - context_start
  318. )
  319. index += 1
  320. t0 = time.time()
  321. clip = clip_dir / (
  322. f"{path.stem}_"
  323. f"{index:06d}_"
  324. f"{context_start:.2f}_"
  325. f"{context_end:.2f}.mp4"
  326. )
  327. inputs = None
  328. cuda_inputs = None
  329. try:
  330. # ==============================================
  331. # 1. 当前滑动视觉窗口
  332. # ==============================================
  333. extract_clip(
  334. path,
  335. context_start,
  336. visual_length,
  337. clip
  338. )
  339. # ==============================================
  340. # 2. Gate
  341. #
  342. # Gate 看到:
  343. #
  344. # 前4秒 + 当前4秒
  345. # ==============================================
  346. gate_prompt = make_chat_prompt(
  347. GATE_PROMPT
  348. )
  349. inputs = encode_video(
  350. clip,
  351. gate_prompt,
  352. codec_override
  353. )
  354. prob = run_gate(inputs)
  355. head = (
  356. f"**{start:.1f}–{end:.1f}s** "
  357. f"Gate=`{prob:.2f}` "
  358. f"[context={context_start:.1f}–{end:.1f}s]"
  359. )
  360. # ==============================================
  361. # 3. Gate 不通过
  362. # ==============================================
  363. if prob < gate_threshold:
  364. elapsed = time.time() - t0
  365. speed = current_length / elapsed
  366. line = (
  367. f"{head} → 🔇 跳过 "
  368. f"({elapsed:.2f}s / "
  369. f"{speed:.2f}×)"
  370. )
  371. results.append(line)
  372. yield "\n\n".join(results)
  373. # ==============================================
  374. # 4. Gate 通过
  375. # ==============================================
  376. else:
  377. yield (
  378. "\n\n".join(results)
  379. + (
  380. "\n\n"
  381. if results else ""
  382. )
  383. + f"{head} → 🧠 生成中..."
  384. )
  385. # ------------------------------------------
  386. # LLM 使用:
  387. #
  388. # 视觉:
  389. # 前4秒 + 当前4秒
  390. #
  391. # 文本:
  392. # 最近3个事件
  393. # ------------------------------------------
  394. llm_prompt = make_prompt(
  395. start,
  396. end,
  397. history
  398. )
  399. # 重新生成带历史 prompt 的输入
  400. #
  401. # 这里重新 processor 是为了让:
  402. # Gate prompt 和 LLM prompt 分开。
  403. #
  404. # 视频 Codec 本身仍然是同一个滑动窗口。
  405. # ------------------------------------------
  406. cleanup(inputs)
  407. inputs = None
  408. inputs = encode_video(
  409. clip,
  410. make_chat_prompt(llm_prompt),
  411. codec_override
  412. )
  413. gt = time.time()
  414. cuda_inputs = to_cuda(
  415. inputs
  416. )
  417. said = generate(
  418. cuda_inputs,
  419. max_new_tokens
  420. )
  421. gen_time = time.time() - gt
  422. elapsed = time.time() - t0
  423. speed = current_length / elapsed
  424. # ------------------------------------------
  425. # 保存历史事件
  426. # ------------------------------------------
  427. history.append(
  428. f"{start:.1f}–{end:.1f}s:{said}"
  429. )
  430. if len(history) > HISTORY_SIZE:
  431. history.pop(0)
  432. line = (
  433. f"{head} → 🗣️ {said}\n"
  434. f" Generate={gen_time:.2f}s | "
  435. f"Total={elapsed:.2f}s | "
  436. f"Realtime={speed:.2f}×"
  437. )
  438. results.append(line)
  439. yield "\n\n".join(results)
  440. # ==============================================
  441. # 5. 模拟实时
  442. # ==============================================
  443. elapsed = time.time() - t0
  444. if (
  445. simulate_realtime
  446. and elapsed < current_length
  447. ):
  448. time.sleep(
  449. current_length - elapsed
  450. )
  451. except Exception as e:
  452. results.append(
  453. f"**{start:.1f}–{end:.1f}s** "
  454. f"→ ❌ "
  455. f"{type(e).__name__}: {e}"
  456. )
  457. yield "\n\n".join(results)
  458. finally:
  459. cleanup(
  460. cuda_inputs,
  461. inputs
  462. )
  463. if clip.exists():
  464. try:
  465. clip.unlink()
  466. except Exception:
  467. pass
  468. start = end
  469. print(
  470. f"[segment {index}] "
  471. f"{start:.1f}/{total:.1f}s | "
  472. f"GPU {gpu_mem()}",
  473. flush=True
  474. )
  475. yield (
  476. "\n\n".join(results)
  477. + f"\n\n✅ 完成,共 {index} 个片段"
  478. )
  479. # ===================== Main =====================
  480. if __name__ == "__main__":
  481. for output in stream_video_analysis(
  482. VIDEO_FILE,
  483. segment_sec=4.0,
  484. context_sec=4.0,
  485. gate_threshold=0.50,
  486. max_segments=MAX_SEGMENTS,
  487. max_new_tokens=64,
  488. simulate_realtime=True,
  489. ):
  490. print("\033[H\033[J", end="")
  491. print("=== Mage-VL 第六版:上下文实时分析 ===\n")
  492. print(output)
  493. print("\n" + "-" * 70)