minicpmo_proactive_colab.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. """
  2. MiniCPM-o 4.5 主动式流媒体视频分析 — Google Colab 可执行脚本
  3. 用法:复制到 Colab 笔记本,依次运行各段代码
  4. """
  5. # ================================================================
  6. # 单元格 1:安装依赖
  7. # ================================================================
  8. import subprocess, sys, os, time, shutil
  9. import numpy as np
  10. import torch
  11. for pkg in [
  12. "transformers==4.51.0",
  13. "accelerate",
  14. "torch>=2.3.0,<=2.8.0",
  15. "torchaudio<=2.8.0",
  16. "minicpmo-utils[all]>=1.0.5",
  17. "librosa", "soundfile", "opencv-python",
  18. ]:
  19. mod = pkg.split("==")[0].split(">=")[0].split("<=")[0].split("[")[0].replace("-", "_")
  20. try:
  21. __import__(mod)
  22. except ImportError:
  23. print(f"[install] {pkg}")
  24. subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", pkg])
  25. print("[✓] 依赖就绪")
  26. # ================================================================
  27. # 单元格 2:上传视频或提供 URL
  28. # ================================================================
  29. from google.colab import files, drive
  30. VIDEO_PATH = None
  31. try:
  32. drive.mount('/content/drive', timeout_ms=5000)
  33. _dp = '/content/drive/MyDrive/video.mp4'
  34. if os.path.exists(_dp):
  35. VIDEO_PATH = _dp
  36. except Exception:
  37. pass
  38. if VIDEO_PATH is None:
  39. print("请上传 mp4 文件:")
  40. uploaded = files.upload()
  41. if uploaded:
  42. VIDEO_PATH = list(uploaded.keys())[0]
  43. print(f"[✓] 已上传: {VIDEO_PATH}")
  44. if VIDEO_PATH is None:
  45. URL = input("或粘贴视频 URL: ").strip().strip('"').strip("'")
  46. if URL:
  47. VIDEO_PATH = "/content/uploaded.mp4"
  48. subprocess.run(["wget", "-q", "-O", VIDEO_PATH, URL], check=True)
  49. print(f"[✓] 已下载: {VIDEO_PATH}")
  50. assert VIDEO_PATH and os.path.exists(VIDEO_PATH), "未找到视频文件,请重试"
  51. print(f"[video] {VIDEO_PATH} ({os.path.getsize(VIDEO_PATH)/1e6:.1f} MB)")
  52. # ================================================================
  53. # 单元格 3:加载 MiniCPM-o 4.5 模型
  54. # ================================================================
  55. print("[model] 加载中 (首次约 2-3 分钟) ...")
  56. t0 = time.time()
  57. from transformers import AutoModel
  58. MODEL_ID = "openbmb/MiniCPM-o-4_5"
  59. model = AutoModel.from_pretrained(
  60. MODEL_ID,
  61. trust_remote_code=True,
  62. attn_implementation="sdpa",
  63. torch_dtype=torch.bfloat16,
  64. init_vision=True,
  65. init_audio=True,
  66. init_tts=True,
  67. )
  68. model.eval().cuda()
  69. model.init_tts()
  70. print(f"[✓] 模型加载完成 ({time.time()-t0:.1f}s)")
  71. # ================================================================
  72. # 单元格 4:配置场景 Prompt
  73. # ================================================================
  74. SYSTEM_PROMPT = """\
  75. 你是一位专业的衣物护理顾问,正在观看一段洗衣机投放衣物的视频。
  76. 你的职责:
  77. 1. 仔细观察画面,识别每次被投放的衣物类型(颜色、材质、款式)
  78. 2. 每当检测到新衣物被投入时,立即主动输出一条简短的洗涤/护理建议
  79. 3. 每条建议以 [MM:SS] 开头标注时间戳
  80. 4. 如果没有新衣物出现,请保持沉默,不要输出任何内容
  81. 5. 推荐要具体实用,例如:"这件白色棉质T恤建议冷水轻柔洗涤,避免漂白"
  82. 请用中文回答。"""
  83. # 其他场景示例(取消注释替换即可):
  84. # SPORTS = """观看体育比赛直播,在每次进球或关键动作时主动给出解说,以 [MM:SS] 开头。"""
  85. # SECURITY = """观看监控视频,发现异常行为(人员闯入、物品移动)时主动告警,以 [MM:SS] 开头。"""
  86. # COOKING = """观看烹饪视频,每完成一个步骤时主动播报进度和下一步建议,以 [MM:SS] 开头。"""
  87. SEGMENT_SEC = 8.0 # 每段时长(秒)
  88. GENERATE_AUDIO = False # True 启用 TTS 语音输出
  89. REF_AUDIO = None # 可选:参考音频路径(用于声音克隆)
  90. print(f"[config] segment={SEGMENT_SEC}s audio={'ON' if GENERATE_AUDIO else 'OFF'}")
  91. # ================================================================
  92. # 单元格 5:视频分段 + 流式推理
  93. # ================================================================
  94. print("[infer] 开始推理 ...")
  95. from minicpmo.utils import get_video_frame_audio_segments
  96. import cv2
  97. def get_duration(path: str) -> float:
  98. cap = cv2.VideoCapture(path)
  99. fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
  100. n = cap.get(cv2.CAP_PROP_FRAME_COUNT)
  101. cap.release()
  102. return n / fps
  103. def split_segments(path: str, sec: float) -> list[dict]:
  104. dur = get_duration(path)
  105. segs, t, i = [], 0.0, 0
  106. while t < dur - 0.05:
  107. end = min(dur, t + sec)
  108. segs.append({"start": t, "end": end, "idx": i})
  109. t, i = end, i + 1
  110. return segs
  111. segments = split_segments(VIDEO_PATH, SEGMENT_SEC)
  112. dur = get_duration(VIDEO_PATH)
  113. print(f" 视频时长 {dur:.1f}s → {len(segments)} 段 (每段 {SEGMENT_SEC}s)")
  114. # 声音克隆(可选)
  115. if REF_AUDIO and os.path.exists(REF_AUDIO):
  116. ref_audio, _ = librosa.load(REF_AUDIO, sr=16000, mono=True)
  117. model.init_token2wav_cache(ref_audio)
  118. print(f" 声音克隆: {REF_AUDIO}")
  119. sys_msg = {"role": "system", "content": [SYSTEM_PROMPT]}
  120. session_id = "proactive_session"
  121. model.reset_session()
  122. results = []
  123. t_total = time.time()
  124. for seg in segments:
  125. # 提取子片段(避免重复解码)
  126. seg_path = f"/content/_seg_{seg['idx']:04d}.mp4"
  127. if not os.path.exists(seg_path):
  128. subprocess.run(
  129. ["ffmpeg", "-y", "-loglevel", "error",
  130. "-ss", f"{seg['start']}", "-t", f"{SEGMENT_SEC}",
  131. "-i", VIDEO_PATH, "-c", "copy",
  132. "-avoid_negative_ts", "make_zero", seg_path],
  133. capture_output=True,
  134. )
  135. print(f"\n[{seg['idx']:02d}] [{seg['start']:.1f}-{seg['end']:.1f}s] ...", end="", flush=True)
  136. t_seg = time.time()
  137. try:
  138. video_frames, audio_segments, stacked_frames = get_video_frame_audio_segments(
  139. seg_path, stack_frames=1, use_ffmpeg=True, adjust_audio_length=True
  140. )
  141. except Exception as e:
  142. print(f" ✗ 帧提取失败: {e}"); continue
  143. if not video_frames:
  144. print(" (无帧)"); continue
  145. # Prefill 系统提示
  146. model.streaming_prefill(session_id=session_id, msgs=[sys_msg])
  147. # Prefill 视觉+音频内容
  148. omni = []
  149. for fi, frame in enumerate(video_frames):
  150. omni.append(frame)
  151. if fi < len(audio_segments):
  152. omni.append(audio_segments[fi])
  153. if (stacked_frames is not None and fi < len(stacked_frames)
  154. and stacked_frames[fi] is not None):
  155. omni.append(stacked_frames[fi])
  156. omni.append("请按照系统指令,持续观察视频并主动给出建议。")
  157. last_idx = len(omni) - 1
  158. for idx, content in enumerate(omni):
  159. model.streaming_prefill(
  160. session_id=session_id,
  161. msgs=[{"role": "user", "content": [content]}],
  162. omni_mode=True,
  163. is_last_chunk=(idx == last_idx),
  164. )
  165. # 生成回复
  166. text_parts, audio_chunks = [], []
  167. try:
  168. gen = model.streaming_generate(
  169. session_id=session_id,
  170. generate_audio=GENERATE_AUDIO,
  171. use_tts_template=True,
  172. enable_thinking=False,
  173. do_sample=True,
  174. temperature=0.7,
  175. max_new_tokens=128,
  176. )
  177. for item in gen:
  178. if GENERATE_AUDIO and isinstance(item, tuple) and len(item) == 2:
  179. wav_chunk, text_chunk = item
  180. audio_chunks.append(wav_chunk)
  181. text_parts.append(text_chunk)
  182. else:
  183. text_chunk, _done = item
  184. text_parts.append(text_chunk)
  185. except Exception as e:
  186. print(f" ✗ 生成错误: {e}"); continue
  187. text = "".join(text_parts).strip()
  188. dt = time.time() - t_seg
  189. if text:
  190. print(f" ✓ [{dt:.1f}s] {text[:120]}")
  191. else:
  192. print(f" ○ 沉默 [{dt:.1f}s]")
  193. results.append({"seg_idx": seg["idx"], "start": seg["start"],
  194. "end": seg["end"], "text": text, "dt": dt})
  195. # ================================================================
  196. # 单元格 6:输出汇总
  197. # ================================================================
  198. print("\n" + "=" * 60)
  199. print(" 主动式分析结果")
  200. print("=" * 60)
  201. key = [r for r in results if r["text"] and r["text"].strip()]
  202. if not key:
  203. print(" (模型全程未检测到需主动报告的内容)")
  204. print(" 建议:调整 SYSTEM_PROMPT 或增大 SEGMENT_SEC")
  205. for r in key:
  206. print(f" [{r['start']:.1f}s] {r['text']}")
  207. total = sum(r["dt"] for r in results)
  208. print(f"\n总计: {total:.1f}s | 有输出节点: {len(key)}/{len(results)}")