mymage_test2.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996
  1. # ============================================================
  2. # Mage-VL 第五版
  3. # T4 16GB / 足球视频 / 4秒一段 / 真正逐段实时流式分析
  4. #
  5. # 核心流程:
  6. #
  7. # 视频
  8. # ↓
  9. # 当前 4 秒
  10. # ↓
  11. # 精确切片(重新编码)
  12. # ↓
  13. # Codec
  14. # ↓
  15. # Gate
  16. # ↓
  17. # ┌───────────────┐
  18. # │ p < threshold │ → 跳过
  19. # │ p >= threshold│ → Mage-VL
  20. # └───────────────┘
  21. # ↓
  22. # 输出
  23. # ↓
  24. # 清理 GPU
  25. # ↓
  26. # 下一 4 秒
  27. #
  28. # 不会提前 Codec 全部视频
  29. # ============================================================
  30. import os
  31. # ------------------------------------------------------------
  32. # 0. 环境变量
  33. # ------------------------------------------------------------
  34. os.environ.setdefault(
  35. "PYTORCH_CUDA_ALLOC_CONF",
  36. "expandable_segments:True,max_split_size_mb:128"
  37. )
  38. os.environ.setdefault(
  39. "ONLINE_CODEC_CACHE_DIR",
  40. "/tmp/mage_vl_codec_cache"
  41. )
  42. os.environ.setdefault(
  43. "STREAMMIND_CLIP_CACHE",
  44. "/tmp/mage_vl_clips"
  45. )
  46. os.makedirs(
  47. os.environ["ONLINE_CODEC_CACHE_DIR"],
  48. exist_ok=True
  49. )
  50. os.makedirs(
  51. os.environ["STREAMMIND_CLIP_CACHE"],
  52. exist_ok=True
  53. )
  54. # ------------------------------------------------------------
  55. # 1. Python imports
  56. # ------------------------------------------------------------
  57. import gc
  58. import time
  59. import warnings
  60. import subprocess
  61. import importlib
  62. from pathlib import Path
  63. import torch
  64. from transformers import (
  65. AutoModelForCausalLM,
  66. AutoProcessor,
  67. )
  68. # ------------------------------------------------------------
  69. # 2. 基本配置
  70. # ------------------------------------------------------------
  71. MODEL_ID = "microsoft/Mage-VL"
  72. VIDEO_FILE = "/content/soccer-broadcast.mp4"
  73. # 每次分析多少秒
  74. SEGMENT_SEC = 4.0
  75. # 最多分析多少段
  76. #
  77. # None = 整个视频
  78. #
  79. # 如果只是测试,可以设置成 8
  80. MAX_SEGMENTS = None
  81. # Gate 阈值
  82. GATE_THRESHOLD = 0.50
  83. # 最大生成 token
  84. MAX_NEW_TOKENS = 64
  85. # Codec 最大像素
  86. #
  87. # T4 16GB 推荐先保持 64000
  88. #
  89. # 如果显存仍然紧张:
  90. # 48000
  91. # 32000
  92. #
  93. CODEC_MAX_PIXELS = 64_000
  94. # Codec target canvas
  95. CODEC_TARGET_CANVAS = 24
  96. # 是否模拟真实时间
  97. #
  98. # True:
  99. # 如果处理速度 > 1x,则等待
  100. #
  101. # False:
  102. # 处理完马上进入下一段
  103. SIMULATE_REALTIME = True
  104. # 为了避免频繁 empty_cache
  105. # 可以每 N 个 segment 强制一次
  106. EMPTY_CACHE_EVERY = 1
  107. # ------------------------------------------------------------
  108. # 3. Prompt
  109. # ------------------------------------------------------------
  110. STREAM_PROMPT = """
  111. 请用中文分析这段足球视频。
  112. 重点描述:
  113. 1. 视频中有哪些人物或球员
  114. 2. 球员正在做什么
  115. 3. 当前比赛发生了什么
  116. 4. 是否有射门、传球、带球、抢断、犯规、角球、任意球、进球等动作
  117. 5. 如果可以判断,请说明球员的大致位置和动作
  118. 6. 判断当前是什么体育项目
  119. 只描述视频中能够观察到的内容,不要猜测无法确认的信息。
  120. 尽量简洁,适合实时视频分析。
  121. """.strip()
  122. # ------------------------------------------------------------
  123. # 4. CUDA / dtype
  124. # ------------------------------------------------------------
  125. if not torch.cuda.is_available():
  126. raise RuntimeError(
  127. "没有检测到 CUDA。请确认当前环境已经启用 NVIDIA GPU。"
  128. )
  129. DEVICE = "cuda"
  130. GPU_NAME = torch.cuda.get_device_name(0)
  131. print("=" * 70)
  132. print("Mage-VL 第五版")
  133. print("=" * 70)
  134. print(f"[GPU] {GPU_NAME}")
  135. print(f"[CUDA] {torch.version.cuda}")
  136. print(f"[PyTorch] {torch.__version__}")
  137. print("=" * 70)
  138. # ------------------------------------------------------------
  139. # 5. T4 使用 FP16
  140. # ------------------------------------------------------------
  141. #
  142. # T4 是 Turing 架构。
  143. #
  144. # 相比 BF16:
  145. # T4 上 FP16 通常更加合适。
  146. #
  147. # ------------------------------------------------------------
  148. MODEL_DTYPE = torch.float16
  149. # ------------------------------------------------------------
  150. # 6. 加载 Processor
  151. # ------------------------------------------------------------
  152. print()
  153. print("[init] 正在加载 Processor...")
  154. processor = AutoProcessor.from_pretrained(
  155. MODEL_ID,
  156. trust_remote_code=True,
  157. )
  158. print("[init] Processor 加载完成")
  159. # ------------------------------------------------------------
  160. # 7. 加载模型
  161. # ------------------------------------------------------------
  162. print("[init] 正在加载 Mage-VL...")
  163. model = AutoModelForCausalLM.from_pretrained(
  164. MODEL_ID,
  165. trust_remote_code=True,
  166. torch_dtype=MODEL_DTYPE,
  167. attn_implementation="sdpa",
  168. )
  169. model = model.to(DEVICE).eval()
  170. print("[init] Mage-VL 加载完成")
  171. # ------------------------------------------------------------
  172. # 8. 找到远程 codec 模块
  173. # ------------------------------------------------------------
  174. _REMOTE_PKG = type(processor).__module__.rsplit(".", 1)[0]
  175. codec_mod = importlib.import_module(
  176. _REMOTE_PKG + ".codec_video_processing_mage_vl"
  177. )
  178. CodecConfig = codec_mod.CodecConfig
  179. # ------------------------------------------------------------
  180. # 9. 加载 Cognition Gate
  181. # ------------------------------------------------------------
  182. try:
  183. model.model._load_streammind_gate()
  184. print("[gate] 认知门控加载成功")
  185. except Exception as exc:
  186. print(
  187. f"[gate] 认知门控加载失败: {type(exc).__name__}: {exc}"
  188. )
  189. # ------------------------------------------------------------
  190. # 10. 显存信息
  191. # ------------------------------------------------------------
  192. def print_gpu_memory(prefix="GPU"):
  193. if not torch.cuda.is_available():
  194. return
  195. allocated = torch.cuda.memory_allocated() / 1024**3
  196. reserved = torch.cuda.memory_reserved() / 1024**3
  197. total = torch.cuda.get_device_properties(0).total_memory
  198. total = total / 1024**3
  199. free = total - allocated
  200. print(
  201. f"[{prefix}] "
  202. f"allocated={allocated:.2f}GB "
  203. f"reserved={reserved:.2f}GB "
  204. f"free≈{free:.2f}GB "
  205. f"total={total:.2f}GB",
  206. flush=True,
  207. )
  208. # ------------------------------------------------------------
  209. # 11. FFprobe 获取视频时长
  210. # ------------------------------------------------------------
  211. def _ffprobe_duration(video_path):
  212. try:
  213. result = subprocess.run(
  214. [
  215. "ffprobe",
  216. "-v",
  217. "error",
  218. "-show_entries",
  219. "format=duration",
  220. "-of",
  221. "default=nw=1:nk=1",
  222. str(video_path),
  223. ],
  224. check=True,
  225. capture_output=True,
  226. timeout=60,
  227. text=True,
  228. )
  229. value = result.stdout.strip()
  230. if not value:
  231. return 0.0
  232. return float(value)
  233. except Exception as exc:
  234. print(
  235. f"[ffprobe] 获取视频时长失败: {exc}",
  236. flush=True,
  237. )
  238. return 0.0
  239. # ------------------------------------------------------------
  240. # 12. Codec 配置
  241. # ------------------------------------------------------------
  242. def _codec_cfg(
  243. target_canvas=CODEC_TARGET_CANVAS,
  244. max_pixels=CODEC_MAX_PIXELS,
  245. ):
  246. override = {
  247. "engine": "hevc",
  248. "target_canvas": int(target_canvas),
  249. "patch": 16,
  250. }
  251. kwargs = dict(
  252. processor._codec_config_defaults
  253. )
  254. merged_dcvc = dict(
  255. kwargs.get("dcvc") or {}
  256. )
  257. merged_dcvc.update(
  258. override.get("dcvc") or {}
  259. )
  260. kwargs.update(override)
  261. if merged_dcvc:
  262. kwargs["dcvc"] = merged_dcvc
  263. kwargs["max_pixels"] = int(max_pixels)
  264. return CodecConfig(**kwargs), override
  265. # ------------------------------------------------------------
  266. # 13. Prompt
  267. # ------------------------------------------------------------
  268. def _prompt(kind, question):
  269. messages = [
  270. {
  271. "role": "user",
  272. "content": [
  273. {
  274. "type": kind
  275. },
  276. {
  277. "type": "text",
  278. "text": question,
  279. },
  280. ],
  281. }
  282. ]
  283. return processor.apply_chat_template(
  284. messages,
  285. tokenize=False,
  286. add_generation_prompt=True,
  287. )
  288. # ------------------------------------------------------------
  289. # 14. CPU → GPU
  290. # ------------------------------------------------------------
  291. def _to_cuda(inputs):
  292. output = {}
  293. for key, value in inputs.items():
  294. if not hasattr(value, "to"):
  295. continue
  296. value = value.to(DEVICE)
  297. if key == "pixel_values":
  298. value = value.to(MODEL_DTYPE)
  299. output[key] = value
  300. return output
  301. # ------------------------------------------------------------
  302. # 15. 精确切片
  303. # ------------------------------------------------------------
  304. #
  305. # 重要:
  306. #
  307. # 原来的:
  308. #
  309. # -c copy
  310. #
  311. # 是直接复制码流。
  312. #
  313. # 这样 -ss 可能落在关键帧之外,
  314. # 导致实际得到的视频开始时间不够精确。
  315. #
  316. # 这里改成重新编码。
  317. #
  318. # 优点:
  319. # 时间窗口更加准确
  320. #
  321. # 缺点:
  322. # CPU 会多一点开销。
  323. #
  324. # 对“模拟实时分析”而言,
  325. # 这个方案更稳。
  326. #
  327. # ------------------------------------------------------------
  328. def _extract_subclip(
  329. src,
  330. start,
  331. duration,
  332. out_path,
  333. ):
  334. if duration <= 0:
  335. raise ValueError(
  336. f"duration 必须 > 0,当前={duration}"
  337. )
  338. tmp = out_path.with_name(
  339. f".tmp_{out_path.name}"
  340. )
  341. if tmp.exists():
  342. try:
  343. tmp.unlink()
  344. except Exception:
  345. pass
  346. command = [
  347. "ffmpeg",
  348. "-y",
  349. "-loglevel",
  350. "error",
  351. # 精确时间定位
  352. "-ss",
  353. f"{max(0.0, start):.3f}",
  354. "-i",
  355. str(src),
  356. "-t",
  357. f"{duration:.3f}",
  358. # 视频重新编码
  359. "-c:v",
  360. "libx264",
  361. # 很快的 CPU 编码
  362. "-preset",
  363. "ultrafast",
  364. # 保证足够好的视觉质量
  365. "-crf",
  366. "23",
  367. # 避免音频处理
  368. "-an",
  369. # 独立可解码
  370. "-pix_fmt",
  371. "yuv420p",
  372. str(tmp),
  373. ]
  374. subprocess.run(
  375. command,
  376. check=True,
  377. capture_output=True,
  378. timeout=180,
  379. )
  380. if not tmp.exists() or tmp.stat().st_size <= 0:
  381. raise RuntimeError(
  382. f"FFmpeg 没有生成有效文件: {tmp}"
  383. )
  384. if out_path.exists():
  385. try:
  386. out_path.unlink()
  387. except Exception:
  388. pass
  389. tmp.replace(out_path)
  390. return out_path
  391. # ------------------------------------------------------------
  392. # 16. 清理显存
  393. # ------------------------------------------------------------
  394. def _cleanup_cuda(*objects):
  395. for obj in objects:
  396. try:
  397. del obj
  398. except Exception:
  399. pass
  400. gc.collect()
  401. if torch.cuda.is_available():
  402. torch.cuda.empty_cache()
  403. try:
  404. torch.cuda.ipc_collect()
  405. except Exception:
  406. pass
  407. # ------------------------------------------------------------
  408. # 17. Gate
  409. # ------------------------------------------------------------
  410. @torch.inference_mode()
  411. def _run_gate(cpu_inputs):
  412. """
  413. 只把当前 segment 的 Gate 所需视觉特征送入 GPU。
  414. 不保存多个 segment。
  415. """
  416. cuda_visual_features = {}
  417. for key in (
  418. "pixel_values",
  419. "image_grid_thw",
  420. "patch_positions",
  421. ):
  422. if key not in cpu_inputs:
  423. continue
  424. value = cpu_inputs[key].to(DEVICE)
  425. if key == "pixel_values":
  426. value = value.to(MODEL_DTYPE)
  427. cuda_visual_features[key] = value
  428. try:
  429. logits = (
  430. model.model.streammind_gate_forward_segments(
  431. [cuda_visual_features]
  432. )[0]
  433. )
  434. # 当前只有一个 segment
  435. #
  436. # image_grid_thw 第一维是 segment/frame 相关结构,
  437. # 与你第四版保持同样的计算方式。
  438. length = int(
  439. cpu_inputs["image_grid_thw"][:, 0].sum()
  440. )
  441. boundary = length - 1
  442. probability = torch.softmax(
  443. logits[boundary].float(),
  444. dim=-1,
  445. )[1].item()
  446. return float(probability)
  447. finally:
  448. _cleanup_cuda(
  449. cuda_visual_features,
  450. locals().get("logits"),
  451. )
  452. # ------------------------------------------------------------
  453. # 18. Mage-VL 生成
  454. # ------------------------------------------------------------
  455. @torch.inference_mode()
  456. def _generate(
  457. inputs,
  458. max_new_tokens=MAX_NEW_TOKENS,
  459. ):
  460. output = model.generate(
  461. **inputs,
  462. max_new_tokens=int(
  463. max_new_tokens
  464. ),
  465. do_sample=False,
  466. use_cache=True,
  467. )
  468. prompt_length = inputs[
  469. "input_ids"
  470. ].shape[1]
  471. generated = output[
  472. 0,
  473. prompt_length:
  474. ]
  475. text = processor.tokenizer.decode(
  476. generated,
  477. skip_special_tokens=True,
  478. ).strip()
  479. del output
  480. del generated
  481. return text
  482. # ------------------------------------------------------------
  483. # 19. 计算实时倍率
  484. # ------------------------------------------------------------
  485. def _realtime_speed(
  486. video_duration,
  487. processing_time,
  488. ):
  489. if processing_time <= 0:
  490. return 0.0
  491. return (
  492. video_duration /
  493. processing_time
  494. )
  495. # ------------------------------------------------------------
  496. # 20. 单 segment 分析
  497. # ------------------------------------------------------------
  498. def _analyze_segment(
  499. clip,
  500. text,
  501. codec_override,
  502. ):
  503. # --------------------------------------------------------
  504. # Codec
  505. # --------------------------------------------------------
  506. with warnings.catch_warnings():
  507. warnings.simplefilter("ignore")
  508. inputs = processor(
  509. text=[text],
  510. videos=[
  511. str(clip)
  512. ],
  513. video_backend="codec",
  514. max_pixels=CODEC_MAX_PIXELS,
  515. codec_config=codec_override,
  516. return_tensors="pt",
  517. padding=False,
  518. )
  519. # --------------------------------------------------------
  520. # Gate
  521. # --------------------------------------------------------
  522. probability = _run_gate(
  523. inputs
  524. )
  525. return inputs, probability
  526. # ------------------------------------------------------------
  527. # 21. 真正流式分析
  528. # ------------------------------------------------------------
  529. def stream_video_analysis(
  530. video_path: str,
  531. segment_sec: float = SEGMENT_SEC,
  532. gate_threshold: float = GATE_THRESHOLD,
  533. max_segments=MAX_SEGMENTS,
  534. max_new_tokens: int = MAX_NEW_TOKENS,
  535. simulate_realtime: bool = SIMULATE_REALTIME,
  536. ):
  537. path = Path(video_path)
  538. if not path.exists():
  539. raise FileNotFoundError(
  540. f"找不到视频文件: {video_path}"
  541. )
  542. # --------------------------------------------------------
  543. # 获取时长
  544. # --------------------------------------------------------
  545. duration = _ffprobe_duration(path)
  546. if duration <= 0:
  547. raise RuntimeError(
  548. "无法获得视频时长。"
  549. )
  550. # --------------------------------------------------------
  551. # 临时目录
  552. # --------------------------------------------------------
  553. clip_dir = Path(
  554. os.environ["STREAMMIND_CLIP_CACHE"]
  555. )
  556. clip_dir.mkdir(
  557. parents=True,
  558. exist_ok=True,
  559. )
  560. # --------------------------------------------------------
  561. # Prompt
  562. # --------------------------------------------------------
  563. text = _prompt(
  564. "video",
  565. STREAM_PROMPT,
  566. )
  567. # --------------------------------------------------------
  568. # Codec 配置
  569. # --------------------------------------------------------
  570. _, codec_override = _codec_cfg(
  571. CODEC_TARGET_CANVAS
  572. )
  573. # --------------------------------------------------------
  574. # 状态
  575. # --------------------------------------------------------
  576. start = 0.0
  577. index = 0
  578. total_processed = 0.0
  579. all_lines = []
  580. yield (
  581. f"开始实时流式分析\n"
  582. f"视频总时长: {duration:.1f}s\n"
  583. f"分析窗口: {segment_sec:.1f}s\n"
  584. f"Gate threshold: {gate_threshold:.2f}\n"
  585. f"模拟实时: {simulate_realtime}"
  586. )
  587. # ========================================================
  588. # 核心循环
  589. # ========================================================
  590. while start < duration - 0.05:
  591. if (
  592. max_segments is not None
  593. and index >= max_segments
  594. ):
  595. break
  596. # ----------------------------------------------------
  597. # 当前 segment
  598. # ----------------------------------------------------
  599. end = min(
  600. duration,
  601. start + segment_sec,
  602. )
  603. current_duration = end - start
  604. index += 1
  605. segment_begin_time = time.time()
  606. clip = clip_dir / (
  607. f"{path.stem}"
  608. f"__segment_{index:06d}"
  609. f"__{start:.3f}_{end:.3f}.mp4"
  610. )
  611. # ----------------------------------------------------
  612. # 状态输出
  613. # ----------------------------------------------------
  614. yield (
  615. "\n".join(all_lines)
  616. + (
  617. "\n\n"
  618. if all_lines
  619. else ""
  620. )
  621. + f"⏱️ [{index}] "
  622. f"正在分析 "
  623. f"{start:.1f}–{end:.1f}s ..."
  624. )
  625. inputs = None
  626. cuda_inputs = None
  627. try:
  628. # =================================================
  629. # STEP 1
  630. # 精确切片
  631. # =================================================
  632. extract_begin = time.time()
  633. _extract_subclip(
  634. path,
  635. start,
  636. current_duration,
  637. clip,
  638. )
  639. extract_time = (
  640. time.time()
  641. - extract_begin
  642. )
  643. # =================================================
  644. # STEP 2
  645. # Codec + Gate
  646. # =================================================
  647. codec_begin = time.time()
  648. inputs, probability = (
  649. _analyze_segment(
  650. clip,
  651. text,
  652. codec_override,
  653. )
  654. )
  655. codec_gate_time = (
  656. time.time()
  657. - codec_begin
  658. )
  659. # =================================================
  660. # Gate 结果
  661. # =================================================
  662. head = (
  663. f"**t = {start:.1f}–{end:.1f}s** "
  664. f"· Gate = `{probability:.2f}`"
  665. )
  666. # -------------------------------------------------
  667. # Gate 不通过
  668. # -------------------------------------------------
  669. if probability < float(
  670. gate_threshold
  671. ):
  672. processing_time = (
  673. time.time()
  674. - segment_begin_time
  675. )
  676. speed = _realtime_speed(
  677. current_duration,
  678. processing_time,
  679. )
  680. line = (
  681. f"{head} → 🔇 "
  682. f"*跳过生成* "
  683. f"(processing={processing_time:.2f}s, "
  684. f"realtime={speed:.2f}×)"
  685. )
  686. all_lines.append(line)
  687. yield (
  688. "\n\n".join(all_lines)
  689. )
  690. # -------------------------------------------------
  691. # Gate 通过
  692. # -------------------------------------------------
  693. else:
  694. yield (
  695. "\n\n".join(all_lines)
  696. + (
  697. "\n\n"
  698. if all_lines
  699. else ""
  700. )
  701. + (
  702. f"{head} → 🧠 "
  703. f"*正在生成...*"
  704. )
  705. )
  706. # =============================================
  707. # STEP 3
  708. # 当前 segment 才进入 LLM
  709. # =============================================
  710. generate_begin = time.time()
  711. cuda_inputs = _to_cuda(
  712. inputs
  713. )
  714. said = _generate(
  715. cuda_inputs,
  716. max_new_tokens,
  717. )
  718. generate_time = (
  719. time.time()
  720. - generate_begin
  721. )
  722. processing_time = (
  723. time.time()
  724. - segment_begin_time
  725. )
  726. speed = _realtime_speed(
  727. current_duration,
  728. processing_time,
  729. )
  730. line = (
  731. f"{head} → 🗣️ {said}\n"
  732. f" Codec/Gate="
  733. f"{codec_gate_time:.2f}s"
  734. f" · Generate="
  735. f"{generate_time:.2f}s"
  736. f" · Total="
  737. f"{processing_time:.2f}s"
  738. f" · realtime="
  739. f"{speed:.2f}×"
  740. )
  741. all_lines.append(line)
  742. yield (
  743. "\n\n".join(all_lines)
  744. )
  745. # -------------------------------------------------
  746. # 统计
  747. # -------------------------------------------------
  748. processing_time = (
  749. time.time()
  750. - segment_begin_time
  751. )
  752. total_processed += (
  753. processing_time
  754. )
  755. # -------------------------------------------------
  756. # 模拟实时
  757. #
  758. # 如果:
  759. #
  760. # 4 秒视频
  761. # 实际只花 2 秒
  762. #
  763. # 等 2 秒再进入下一段。
  764. #
  765. # 如果实际用了 6 秒:
  766. #
  767. # 不等待。
  768. #
  769. # 这样不会越来越落后。
  770. # -------------------------------------------------
  771. if simulate_realtime:
  772. remaining = (
  773. current_duration
  774. - processing_time
  775. )
  776. if remaining > 0:
  777. yield (
  778. "\n\n".join(all_lines)
  779. + "\n\n"
  780. + (
  781. f"⏳ 当前处理速度 "
  782. f"{speed:.2f}×,"
  783. f"等待 {remaining:.2f}s "
  784. f"模拟实时..."
  785. )
  786. )
  787. time.sleep(
  788. remaining
  789. )
  790. except Exception as exc:
  791. processing_time = (
  792. time.time()
  793. - segment_begin_time
  794. )
  795. line = (
  796. f"**t = {start:.1f}–{end:.1f}s** "
  797. f"→ ❌ "
  798. f"{type(exc).__name__}: "
  799. f"{exc}"
  800. )
  801. all_lines.append(line)
  802. yield (
  803. "\n\n".join(all_lines)
  804. )
  805. finally:
  806. # =================================================
  807. # STEP 4
  808. # 清理当前 segment
  809. # =================================================
  810. _cleanup_cuda(
  811. cuda_inputs,
  812. inputs,
  813. )
  814. cuda_inputs = None
  815. inputs = None
  816. # ------------------------------------------------
  817. # 删除临时视频
  818. # ------------------------------------------------
  819. try:
  820. if clip.exists():
  821. clip.unlink()
  822. except Exception:
  823. pass
  824. # ------------------------------------------------
  825. # 定期清理 GPU
  826. # ------------------------------------------------
  827. if (
  828. index % EMPTY_CACHE_EVERY
  829. == 0
  830. ):
  831. gc.collect()
  832. torch.cuda.empty_cache()
  833. # ------------------------------------------------
  834. # 下一段
  835. # ------------------------------------------------
  836. start = end
  837. # ------------------------------------------------
  838. # 显存监控
  839. # ------------------------------------------------
  840. print_gpu_memory(
  841. prefix=f"segment {index}"
  842. )
  843. # ========================================================
  844. # 完成
  845. # ========================================================
  846. yield (
  847. "\n\n".join(all_lines)
  848. + "\n\n"
  849. + "=" * 60
  850. + "\n"
  851. + "✅ 实时流式分析完成"
  852. + "\n"
  853. + f"共处理 {index} 个 segment"
  854. + "\n"
  855. + f"视频时长 {min(start, duration):.1f}s"
  856. + "\n"
  857. + f"累计处理时间 {total_processed:.1f}s"
  858. + "\n"
  859. + "=" * 60
  860. )
  861. # ============================================================
  862. # 22. Main
  863. # ============================================================
  864. if __name__ == "__main__":
  865. print()
  866. print("=" * 70)
  867. print("开始测试 Mage-VL 实时流式视频分析")
  868. print("=" * 70)
  869. print()
  870. for chunk_output in stream_video_analysis(
  871. video_path=VIDEO_FILE,
  872. segment_sec=4.0,
  873. gate_threshold=0.50,
  874. max_segments=MAX_SEGMENTS,
  875. max_new_tokens=64,
  876. simulate_realtime=True,
  877. ):
  878. # 清屏
  879. print(
  880. "\033[H\033[J",
  881. end=""
  882. )
  883. print(
  884. "=== Mage-VL 实时流式分析 ==="
  885. )
  886. print()
  887. print(chunk_output)
  888. print()
  889. print("-" * 70)