| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996 |
- # ============================================================
- # Mage-VL 第五版
- # T4 16GB / 足球视频 / 4秒一段 / 真正逐段实时流式分析
- #
- # 核心流程:
- #
- # 视频
- # ↓
- # 当前 4 秒
- # ↓
- # 精确切片(重新编码)
- # ↓
- # Codec
- # ↓
- # Gate
- # ↓
- # ┌───────────────┐
- # │ p < threshold │ → 跳过
- # │ p >= threshold│ → Mage-VL
- # └───────────────┘
- # ↓
- # 输出
- # ↓
- # 清理 GPU
- # ↓
- # 下一 4 秒
- #
- # 不会提前 Codec 全部视频
- # ============================================================
- import os
- # ------------------------------------------------------------
- # 0. 环境变量
- # ------------------------------------------------------------
- os.environ.setdefault(
- "PYTORCH_CUDA_ALLOC_CONF",
- "expandable_segments:True,max_split_size_mb:128"
- )
- os.environ.setdefault(
- "ONLINE_CODEC_CACHE_DIR",
- "/tmp/mage_vl_codec_cache"
- )
- os.environ.setdefault(
- "STREAMMIND_CLIP_CACHE",
- "/tmp/mage_vl_clips"
- )
- os.makedirs(
- os.environ["ONLINE_CODEC_CACHE_DIR"],
- exist_ok=True
- )
- os.makedirs(
- os.environ["STREAMMIND_CLIP_CACHE"],
- exist_ok=True
- )
- # ------------------------------------------------------------
- # 1. Python imports
- # ------------------------------------------------------------
- import gc
- import time
- import warnings
- import subprocess
- import importlib
- from pathlib import Path
- import torch
- from transformers import (
- AutoModelForCausalLM,
- AutoProcessor,
- )
- # ------------------------------------------------------------
- # 2. 基本配置
- # ------------------------------------------------------------
- MODEL_ID = "microsoft/Mage-VL"
- VIDEO_FILE = "/content/soccer-broadcast.mp4"
- # 每次分析多少秒
- SEGMENT_SEC = 4.0
- # 最多分析多少段
- #
- # None = 整个视频
- #
- # 如果只是测试,可以设置成 8
- MAX_SEGMENTS = None
- # Gate 阈值
- GATE_THRESHOLD = 0.50
- # 最大生成 token
- MAX_NEW_TOKENS = 64
- # Codec 最大像素
- #
- # T4 16GB 推荐先保持 64000
- #
- # 如果显存仍然紧张:
- # 48000
- # 32000
- #
- CODEC_MAX_PIXELS = 64_000
- # Codec target canvas
- CODEC_TARGET_CANVAS = 24
- # 是否模拟真实时间
- #
- # True:
- # 如果处理速度 > 1x,则等待
- #
- # False:
- # 处理完马上进入下一段
- SIMULATE_REALTIME = True
- # 为了避免频繁 empty_cache
- # 可以每 N 个 segment 强制一次
- EMPTY_CACHE_EVERY = 1
- # ------------------------------------------------------------
- # 3. Prompt
- # ------------------------------------------------------------
- STREAM_PROMPT = """
- 请用中文分析这段足球视频。
- 重点描述:
- 1. 视频中有哪些人物或球员
- 2. 球员正在做什么
- 3. 当前比赛发生了什么
- 4. 是否有射门、传球、带球、抢断、犯规、角球、任意球、进球等动作
- 5. 如果可以判断,请说明球员的大致位置和动作
- 6. 判断当前是什么体育项目
- 只描述视频中能够观察到的内容,不要猜测无法确认的信息。
- 尽量简洁,适合实时视频分析。
- """.strip()
- # ------------------------------------------------------------
- # 4. CUDA / dtype
- # ------------------------------------------------------------
- if not torch.cuda.is_available():
- raise RuntimeError(
- "没有检测到 CUDA。请确认当前环境已经启用 NVIDIA GPU。"
- )
- DEVICE = "cuda"
- GPU_NAME = torch.cuda.get_device_name(0)
- print("=" * 70)
- print("Mage-VL 第五版")
- print("=" * 70)
- print(f"[GPU] {GPU_NAME}")
- print(f"[CUDA] {torch.version.cuda}")
- print(f"[PyTorch] {torch.__version__}")
- print("=" * 70)
- # ------------------------------------------------------------
- # 5. T4 使用 FP16
- # ------------------------------------------------------------
- #
- # T4 是 Turing 架构。
- #
- # 相比 BF16:
- # T4 上 FP16 通常更加合适。
- #
- # ------------------------------------------------------------
- MODEL_DTYPE = torch.float16
- # ------------------------------------------------------------
- # 6. 加载 Processor
- # ------------------------------------------------------------
- print()
- print("[init] 正在加载 Processor...")
- processor = AutoProcessor.from_pretrained(
- MODEL_ID,
- trust_remote_code=True,
- )
- print("[init] Processor 加载完成")
- # ------------------------------------------------------------
- # 7. 加载模型
- # ------------------------------------------------------------
- print("[init] 正在加载 Mage-VL...")
- model = AutoModelForCausalLM.from_pretrained(
- MODEL_ID,
- trust_remote_code=True,
- torch_dtype=MODEL_DTYPE,
- attn_implementation="sdpa",
- )
- model = model.to(DEVICE).eval()
- print("[init] Mage-VL 加载完成")
- # ------------------------------------------------------------
- # 8. 找到远程 codec 模块
- # ------------------------------------------------------------
- _REMOTE_PKG = type(processor).__module__.rsplit(".", 1)[0]
- codec_mod = importlib.import_module(
- _REMOTE_PKG + ".codec_video_processing_mage_vl"
- )
- CodecConfig = codec_mod.CodecConfig
- # ------------------------------------------------------------
- # 9. 加载 Cognition Gate
- # ------------------------------------------------------------
- try:
- model.model._load_streammind_gate()
- print("[gate] 认知门控加载成功")
- except Exception as exc:
- print(
- f"[gate] 认知门控加载失败: {type(exc).__name__}: {exc}"
- )
- # ------------------------------------------------------------
- # 10. 显存信息
- # ------------------------------------------------------------
- def print_gpu_memory(prefix="GPU"):
- if not torch.cuda.is_available():
- return
- allocated = torch.cuda.memory_allocated() / 1024**3
- reserved = torch.cuda.memory_reserved() / 1024**3
- total = torch.cuda.get_device_properties(0).total_memory
- total = total / 1024**3
- free = total - allocated
- print(
- f"[{prefix}] "
- f"allocated={allocated:.2f}GB "
- f"reserved={reserved:.2f}GB "
- f"free≈{free:.2f}GB "
- f"total={total:.2f}GB",
- flush=True,
- )
- # ------------------------------------------------------------
- # 11. FFprobe 获取视频时长
- # ------------------------------------------------------------
- def _ffprobe_duration(video_path):
- try:
- result = subprocess.run(
- [
- "ffprobe",
- "-v",
- "error",
- "-show_entries",
- "format=duration",
- "-of",
- "default=nw=1:nk=1",
- str(video_path),
- ],
- check=True,
- capture_output=True,
- timeout=60,
- text=True,
- )
- value = result.stdout.strip()
- if not value:
- return 0.0
- return float(value)
- except Exception as exc:
- print(
- f"[ffprobe] 获取视频时长失败: {exc}",
- flush=True,
- )
- return 0.0
- # ------------------------------------------------------------
- # 12. Codec 配置
- # ------------------------------------------------------------
- def _codec_cfg(
- target_canvas=CODEC_TARGET_CANVAS,
- max_pixels=CODEC_MAX_PIXELS,
- ):
- override = {
- "engine": "hevc",
- "target_canvas": int(target_canvas),
- "patch": 16,
- }
- kwargs = dict(
- processor._codec_config_defaults
- )
- merged_dcvc = dict(
- kwargs.get("dcvc") or {}
- )
- merged_dcvc.update(
- override.get("dcvc") or {}
- )
- kwargs.update(override)
- if merged_dcvc:
- kwargs["dcvc"] = merged_dcvc
- kwargs["max_pixels"] = int(max_pixels)
- return CodecConfig(**kwargs), override
- # ------------------------------------------------------------
- # 13. Prompt
- # ------------------------------------------------------------
- def _prompt(kind, question):
- messages = [
- {
- "role": "user",
- "content": [
- {
- "type": kind
- },
- {
- "type": "text",
- "text": question,
- },
- ],
- }
- ]
- return processor.apply_chat_template(
- messages,
- tokenize=False,
- add_generation_prompt=True,
- )
- # ------------------------------------------------------------
- # 14. CPU → GPU
- # ------------------------------------------------------------
- def _to_cuda(inputs):
- output = {}
- for key, value in inputs.items():
- if not hasattr(value, "to"):
- continue
- value = value.to(DEVICE)
- if key == "pixel_values":
- value = value.to(MODEL_DTYPE)
- output[key] = value
- return output
- # ------------------------------------------------------------
- # 15. 精确切片
- # ------------------------------------------------------------
- #
- # 重要:
- #
- # 原来的:
- #
- # -c copy
- #
- # 是直接复制码流。
- #
- # 这样 -ss 可能落在关键帧之外,
- # 导致实际得到的视频开始时间不够精确。
- #
- # 这里改成重新编码。
- #
- # 优点:
- # 时间窗口更加准确
- #
- # 缺点:
- # CPU 会多一点开销。
- #
- # 对“模拟实时分析”而言,
- # 这个方案更稳。
- #
- # ------------------------------------------------------------
- def _extract_subclip(
- src,
- start,
- duration,
- out_path,
- ):
- if duration <= 0:
- raise ValueError(
- f"duration 必须 > 0,当前={duration}"
- )
- tmp = out_path.with_name(
- f".tmp_{out_path.name}"
- )
- if tmp.exists():
- try:
- tmp.unlink()
- except Exception:
- pass
- command = [
- "ffmpeg",
- "-y",
- "-loglevel",
- "error",
- # 精确时间定位
- "-ss",
- f"{max(0.0, start):.3f}",
- "-i",
- str(src),
- "-t",
- f"{duration:.3f}",
- # 视频重新编码
- "-c:v",
- "libx264",
- # 很快的 CPU 编码
- "-preset",
- "ultrafast",
- # 保证足够好的视觉质量
- "-crf",
- "23",
- # 避免音频处理
- "-an",
- # 独立可解码
- "-pix_fmt",
- "yuv420p",
- str(tmp),
- ]
- subprocess.run(
- command,
- check=True,
- capture_output=True,
- timeout=180,
- )
- if not tmp.exists() or tmp.stat().st_size <= 0:
- raise RuntimeError(
- f"FFmpeg 没有生成有效文件: {tmp}"
- )
- if out_path.exists():
- try:
- out_path.unlink()
- except Exception:
- pass
- tmp.replace(out_path)
- return out_path
- # ------------------------------------------------------------
- # 16. 清理显存
- # ------------------------------------------------------------
- def _cleanup_cuda(*objects):
- for obj in objects:
- try:
- del obj
- except Exception:
- pass
- gc.collect()
- if torch.cuda.is_available():
- torch.cuda.empty_cache()
- try:
- torch.cuda.ipc_collect()
- except Exception:
- pass
- # ------------------------------------------------------------
- # 17. Gate
- # ------------------------------------------------------------
- @torch.inference_mode()
- def _run_gate(cpu_inputs):
- """
- 只把当前 segment 的 Gate 所需视觉特征送入 GPU。
- 不保存多个 segment。
- """
- cuda_visual_features = {}
- for key in (
- "pixel_values",
- "image_grid_thw",
- "patch_positions",
- ):
- if key not in cpu_inputs:
- continue
- value = cpu_inputs[key].to(DEVICE)
- if key == "pixel_values":
- value = value.to(MODEL_DTYPE)
- cuda_visual_features[key] = value
- try:
- logits = (
- model.model.streammind_gate_forward_segments(
- [cuda_visual_features]
- )[0]
- )
- # 当前只有一个 segment
- #
- # image_grid_thw 第一维是 segment/frame 相关结构,
- # 与你第四版保持同样的计算方式。
- length = int(
- cpu_inputs["image_grid_thw"][:, 0].sum()
- )
- boundary = length - 1
- probability = torch.softmax(
- logits[boundary].float(),
- dim=-1,
- )[1].item()
- return float(probability)
- finally:
- _cleanup_cuda(
- cuda_visual_features,
- locals().get("logits"),
- )
- # ------------------------------------------------------------
- # 18. Mage-VL 生成
- # ------------------------------------------------------------
- @torch.inference_mode()
- def _generate(
- inputs,
- max_new_tokens=MAX_NEW_TOKENS,
- ):
- output = model.generate(
- **inputs,
- max_new_tokens=int(
- max_new_tokens
- ),
- do_sample=False,
- use_cache=True,
- )
- prompt_length = inputs[
- "input_ids"
- ].shape[1]
- generated = output[
- 0,
- prompt_length:
- ]
- text = processor.tokenizer.decode(
- generated,
- skip_special_tokens=True,
- ).strip()
- del output
- del generated
- return text
- # ------------------------------------------------------------
- # 19. 计算实时倍率
- # ------------------------------------------------------------
- def _realtime_speed(
- video_duration,
- processing_time,
- ):
- if processing_time <= 0:
- return 0.0
- return (
- video_duration /
- processing_time
- )
- # ------------------------------------------------------------
- # 20. 单 segment 分析
- # ------------------------------------------------------------
- def _analyze_segment(
- clip,
- text,
- codec_override,
- ):
- # --------------------------------------------------------
- # Codec
- # --------------------------------------------------------
- with warnings.catch_warnings():
- warnings.simplefilter("ignore")
- inputs = processor(
- text=[text],
- videos=[
- str(clip)
- ],
- video_backend="codec",
- max_pixels=CODEC_MAX_PIXELS,
- codec_config=codec_override,
- return_tensors="pt",
- padding=False,
- )
- # --------------------------------------------------------
- # Gate
- # --------------------------------------------------------
- probability = _run_gate(
- inputs
- )
- return inputs, probability
- # ------------------------------------------------------------
- # 21. 真正流式分析
- # ------------------------------------------------------------
- def stream_video_analysis(
- video_path: str,
- segment_sec: float = SEGMENT_SEC,
- gate_threshold: float = GATE_THRESHOLD,
- max_segments=MAX_SEGMENTS,
- max_new_tokens: int = MAX_NEW_TOKENS,
- simulate_realtime: bool = SIMULATE_REALTIME,
- ):
- path = Path(video_path)
- if not path.exists():
- raise FileNotFoundError(
- f"找不到视频文件: {video_path}"
- )
- # --------------------------------------------------------
- # 获取时长
- # --------------------------------------------------------
- duration = _ffprobe_duration(path)
- if duration <= 0:
- raise RuntimeError(
- "无法获得视频时长。"
- )
- # --------------------------------------------------------
- # 临时目录
- # --------------------------------------------------------
- clip_dir = Path(
- os.environ["STREAMMIND_CLIP_CACHE"]
- )
- clip_dir.mkdir(
- parents=True,
- exist_ok=True,
- )
- # --------------------------------------------------------
- # Prompt
- # --------------------------------------------------------
- text = _prompt(
- "video",
- STREAM_PROMPT,
- )
- # --------------------------------------------------------
- # Codec 配置
- # --------------------------------------------------------
- _, codec_override = _codec_cfg(
- CODEC_TARGET_CANVAS
- )
- # --------------------------------------------------------
- # 状态
- # --------------------------------------------------------
- start = 0.0
- index = 0
- total_processed = 0.0
- all_lines = []
- yield (
- f"开始实时流式分析\n"
- f"视频总时长: {duration:.1f}s\n"
- f"分析窗口: {segment_sec:.1f}s\n"
- f"Gate threshold: {gate_threshold:.2f}\n"
- f"模拟实时: {simulate_realtime}"
- )
- # ========================================================
- # 核心循环
- # ========================================================
- while start < duration - 0.05:
- if (
- max_segments is not None
- and index >= max_segments
- ):
- break
- # ----------------------------------------------------
- # 当前 segment
- # ----------------------------------------------------
- end = min(
- duration,
- start + segment_sec,
- )
- current_duration = end - start
- index += 1
- segment_begin_time = time.time()
- clip = clip_dir / (
- f"{path.stem}"
- f"__segment_{index:06d}"
- f"__{start:.3f}_{end:.3f}.mp4"
- )
- # ----------------------------------------------------
- # 状态输出
- # ----------------------------------------------------
- yield (
- "\n".join(all_lines)
- + (
- "\n\n"
- if all_lines
- else ""
- )
- + f"⏱️ [{index}] "
- f"正在分析 "
- f"{start:.1f}–{end:.1f}s ..."
- )
- inputs = None
- cuda_inputs = None
- try:
- # =================================================
- # STEP 1
- # 精确切片
- # =================================================
- extract_begin = time.time()
- _extract_subclip(
- path,
- start,
- current_duration,
- clip,
- )
- extract_time = (
- time.time()
- - extract_begin
- )
- # =================================================
- # STEP 2
- # Codec + Gate
- # =================================================
- codec_begin = time.time()
- inputs, probability = (
- _analyze_segment(
- clip,
- text,
- codec_override,
- )
- )
- codec_gate_time = (
- time.time()
- - codec_begin
- )
- # =================================================
- # Gate 结果
- # =================================================
- head = (
- f"**t = {start:.1f}–{end:.1f}s** "
- f"· Gate = `{probability:.2f}`"
- )
- # -------------------------------------------------
- # Gate 不通过
- # -------------------------------------------------
- if probability < float(
- gate_threshold
- ):
- processing_time = (
- time.time()
- - segment_begin_time
- )
- speed = _realtime_speed(
- current_duration,
- processing_time,
- )
- line = (
- f"{head} → 🔇 "
- f"*跳过生成* "
- f"(processing={processing_time:.2f}s, "
- f"realtime={speed:.2f}×)"
- )
- all_lines.append(line)
- yield (
- "\n\n".join(all_lines)
- )
- # -------------------------------------------------
- # Gate 通过
- # -------------------------------------------------
- else:
- yield (
- "\n\n".join(all_lines)
- + (
- "\n\n"
- if all_lines
- else ""
- )
- + (
- f"{head} → 🧠 "
- f"*正在生成...*"
- )
- )
- # =============================================
- # STEP 3
- # 当前 segment 才进入 LLM
- # =============================================
- generate_begin = time.time()
- cuda_inputs = _to_cuda(
- inputs
- )
- said = _generate(
- cuda_inputs,
- max_new_tokens,
- )
- generate_time = (
- time.time()
- - generate_begin
- )
- processing_time = (
- time.time()
- - segment_begin_time
- )
- speed = _realtime_speed(
- current_duration,
- processing_time,
- )
- line = (
- f"{head} → 🗣️ {said}\n"
- f" Codec/Gate="
- f"{codec_gate_time:.2f}s"
- f" · Generate="
- f"{generate_time:.2f}s"
- f" · Total="
- f"{processing_time:.2f}s"
- f" · realtime="
- f"{speed:.2f}×"
- )
- all_lines.append(line)
- yield (
- "\n\n".join(all_lines)
- )
- # -------------------------------------------------
- # 统计
- # -------------------------------------------------
- processing_time = (
- time.time()
- - segment_begin_time
- )
- total_processed += (
- processing_time
- )
- # -------------------------------------------------
- # 模拟实时
- #
- # 如果:
- #
- # 4 秒视频
- # 实际只花 2 秒
- #
- # 等 2 秒再进入下一段。
- #
- # 如果实际用了 6 秒:
- #
- # 不等待。
- #
- # 这样不会越来越落后。
- # -------------------------------------------------
- if simulate_realtime:
- remaining = (
- current_duration
- - processing_time
- )
- if remaining > 0:
- yield (
- "\n\n".join(all_lines)
- + "\n\n"
- + (
- f"⏳ 当前处理速度 "
- f"{speed:.2f}×,"
- f"等待 {remaining:.2f}s "
- f"模拟实时..."
- )
- )
- time.sleep(
- remaining
- )
- except Exception as exc:
- processing_time = (
- time.time()
- - segment_begin_time
- )
- line = (
- f"**t = {start:.1f}–{end:.1f}s** "
- f"→ ❌ "
- f"{type(exc).__name__}: "
- f"{exc}"
- )
- all_lines.append(line)
- yield (
- "\n\n".join(all_lines)
- )
- finally:
- # =================================================
- # STEP 4
- # 清理当前 segment
- # =================================================
- _cleanup_cuda(
- cuda_inputs,
- inputs,
- )
- cuda_inputs = None
- inputs = None
- # ------------------------------------------------
- # 删除临时视频
- # ------------------------------------------------
- try:
- if clip.exists():
- clip.unlink()
- except Exception:
- pass
- # ------------------------------------------------
- # 定期清理 GPU
- # ------------------------------------------------
- if (
- index % EMPTY_CACHE_EVERY
- == 0
- ):
- gc.collect()
- torch.cuda.empty_cache()
- # ------------------------------------------------
- # 下一段
- # ------------------------------------------------
- start = end
- # ------------------------------------------------
- # 显存监控
- # ------------------------------------------------
- print_gpu_memory(
- prefix=f"segment {index}"
- )
- # ========================================================
- # 完成
- # ========================================================
- yield (
- "\n\n".join(all_lines)
- + "\n\n"
- + "=" * 60
- + "\n"
- + "✅ 实时流式分析完成"
- + "\n"
- + f"共处理 {index} 个 segment"
- + "\n"
- + f"视频时长 {min(start, duration):.1f}s"
- + "\n"
- + f"累计处理时间 {total_processed:.1f}s"
- + "\n"
- + "=" * 60
- )
- # ============================================================
- # 22. Main
- # ============================================================
- if __name__ == "__main__":
- print()
- print("=" * 70)
- print("开始测试 Mage-VL 实时流式视频分析")
- print("=" * 70)
- print()
- for chunk_output in stream_video_analysis(
- video_path=VIDEO_FILE,
- segment_sec=4.0,
- gate_threshold=0.50,
- max_segments=MAX_SEGMENTS,
- max_new_tokens=64,
- simulate_realtime=True,
- ):
- # 清屏
- print(
- "\033[H\033[J",
- end=""
- )
- print(
- "=== Mage-VL 实时流式分析 ==="
- )
- print()
- print(chunk_output)
- print()
- print("-" * 70)
|