已开启
feat: add camera-observation skill #1
feat: add camera-observation skill #1
已开启
Lilili创建于 8月14日
共 3 个文件变更+305-0
@@ -0,0 +1,128 @@
1+---
2+name: camera-observation
3+description: Use when the user wants to observe live camera footage — watching what happens outside/around a camera, checking the state of hardware (LED lights, LCD screens, PCBA boards) in view, or continuously describing/commenting on real-time camera scenes. Triggers include "watch the camera", "看下摄像头/外面发生了什么", "灯在闪吗/LCD 显示什么", live video commentary requests.
4+---
5+ 
6+# Camera Observation
7+ 
8+## Overview
9+ 
10+Observe a live camera and describe what is happening, using a two-process pipeline: a Windows-side capture script writes short video segments, and a WSL-side agent analyzes each new segment with a resident (loaded-once) vision-language model. Core principle: **load the model once, keep it resident, and analyze each new segment — never re-load weights per segment.**
11+ 
12+## Environment Requirements (先读这里,环境已就绪,不要从零安装)
13+ 
14+**两个 venv 已搭好,直接使用,禁止重新安装 torch/transformers。**
15+ 
16+| 角色 | 位置 | 已含依赖 |
17+| :--- | :--- | :--- |
18+| 捕获端(Windows) | `C:\Users\TL\Documents\Visio\.venv\Scripts\python.exe` | opencv-python 5.x(cv2) |
19+| 分析端(WSL) | `/root/mage/.venv/bin/python` | torch 2.10.0+cu128(CUDA 可用)、transformers 5.x、codec-video-prep、mamba-ssm、decord |
20+| 模型 | `/mnt/c/Users/TL/Documents/Visio/models/Mage-VL`(11GB,ModelScope 下载) | 完整 checkpoint |
21+| GPU | RTX 3080 20GB(Mage-VL BF16 需 8–10GB) | — |
22+| 推理代码 | `/mnt/c/Users/TL/Documents/Visio/Mage/mage_vl/` | inference_base.py 等 |
23+ 
24+**必须执行的检查顺序(本技能标准流程):**
25+ 
26+```bash
27+# 1. 分析端 venv 就绪(WSL)
28+wsl -e bash -c "/root/mage/.venv/bin/python -c 'import torch; print(torch.__version__, torch.cuda.is_available())'"
29+# 2. 模型存在(WSL)
30+wsl -e bash -c "ls /mnt/c/Users/TL/Documents/Visio/models/Mage-VL/config.json"
31+# 3. 捕获端 venv 就绪(Windows)
32+python -c "import cv2; print(cv2.__version__)" # 或用 .venv\Scripts\python.exe
33+```
34+ 
35+**If any check fails:**
36+- torch/transformers 缺失 → 检查是否用了 `/root/mage/.venv/bin/python`(不是系统 `python3`)。若 venv 损坏,按 `camera-observation` 之外的环境修复流程处理,**不要在系统 python 里 `pip install torch`**(PEP 668 会拦截,且会破坏已装环境)。
37+- 模型缺失 → 从 ModelScope 重新下载 `microsoft/Mage-VL` 到 `models/Mage-VL`。
38+- GPU 不可用 → 无法运行 Mage-VL,改用帧采样 + 描述性 fallback 或提示用户。
39+ 
40+**运行命令必须用 venv 解释器:**
41+- 捕获:`C:\Users\TL\Documents\Visio\.venv\Scripts\python.exe scripts/capture_live.py ...`
42+- 分析:`wsl -e bash -c "/root/mage/.venv/bin/python /mnt/c/Users/TL/.atomcode/skills/camera-observation/scripts/realtime_analyze.py --model /mnt/c/Users/TL/Documents/Visio/models/Mage-VL --live-dir /mnt/c/Users/TL/Documents/Visio/cam_live"`
43+ 
44+## When to Use
45+ 
46+- User wants to watch live camera footage and understand what is happening.
47+- User asks about hardware state visible to a camera (LED blinking/steady, LCD screen content, PCBA board condition).
48+- User wants continuous or periodic narration/commentary of a camera scene.
49+- Any request where the camera view must be fed to a vision-language model for real-time understanding.
50+ 
51+Do NOT use for: analyzing a saved video file (use the offline inference scripts instead), or when no camera is attached.
52+ 
53+## Core Pattern
54+ 
55+```
56+[Windows] capture_live.py → cam_live/seg_*.mp4 (every N seconds)
57+ ↓
58+[WSL] realtime_analyze.py → Mage-VL (loaded once) → live narration
59+```
60+ 
61+Two long-running processes. The Windows side captures; the WSL side polls for new segments, analyzes each with frame sampling, and prints the narration.
62+ 
63+## Model Hanging (常驻模型)
64+ 
65+The analyzer keeps the model resident so segments are analyzed in seconds instead of re-loading weights (≈1 min) each time.
66+ 
67+- **How it works**: `realtime_analyze.py` loads the model ONCE at startup (`AutoModelForCausalLM.from_pretrained(...).eval()`), then loops forever, watching `cam_live/` for new `seg_*.mp4` files.
68+- **Verify it is resident**: the log line `模型就绪 <device>,开始实时监听 <dir>/` appears after the ~1 min load; afterwards every segment is processed without re-loading.
69+- **Keep it alive**: run it with `nohup ... &` or in a dedicated terminal — do NOT start it per-segment.
70+- **Stop it**: `pkill -f realtime_analyze` (WSL). The capture side: stop the python process on Windows (`Get-Process python | Stop-Process` if needed).
71+- **Restart after edits**: edit the script, then restart the analyzer so the changes take effect.
72+- **Memory**: Mage-VL 4B BF16 ≈ 8–10 GB VRAM; ensure the GPU is free (RTX 3080 20GB works).
73+ 
74+## Using the Model (如何使用)
75+ 
76+Both scripts are parameterized — pass paths instead of editing them:
77+ 
78+```bash
79+# Terminal 1 (Windows)
80+python scripts/capture_live.py --segment 10 --out cam_live
81+ 
82+# Terminal 2 (WSL) — point MODEL/LIVE_DIR at your setup
83+HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 \
84+ python scripts/realtime_analyze.py \
85+ --model /mnt/c/Users/TL/Documents/Visio/models/Mage-VL \
86+ --live-dir /mnt/c/Users/TL/Documents/Visio/cam_live
87+```
88+ 
89+Parameters for `realtime_analyze.py`:
90+- `--model`: path to the Mage-VL checkpoint (default `microsoft/Mage-VL`; use local path + offline env vars to avoid network).
91+- `--live-dir`: directory the capture script writes segments into.
92+- `--num-frames`: frames sampled per segment (default 12 — enough to catch animations/LED blinks).
93+- `--max-new-tokens`: narration length (default 120).
94+- `--question`: the prompt used to analyze each segment (LED/LCD-aware by default).
95+ 
96+The question prompt defaults to asking about glowing LCD screens playing GIFs, LED blink vs steady, and scene content — keep it if your hardware watch targets LEDs/LCDs, or change it for general scene description.
97+ 
98+## Quick Reference
99+ 
100+| Concern | Command / Setting |
101+| :--- | :--- |
102+| Start capture (Windows) | `python scripts/capture_live.py --segment 10 --out cam_live` |
103+| Start analyzer (WSL) | `HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 python scripts/realtime_analyze.py --model <path> --live-dir <dir>` |
104+| Segment length | `--segment` (seconds; 10 is a good default) |
105+| Frames sampled per segment | `--num-frames` (default 12) |
106+| Live log | analyzer prints `[解说] ...` per segment |
107+| Camera not readable | Often "device in use" — kill stale python/capture processes or ask user to replug USB camera |
108+| Stop analyzer | `pkill -f realtime_analyze` (WSL) |
109+ 
110+## Implementation
111+ 
112+Both scripts live in `scripts/`:
113+ 
114+- **`scripts/capture_live.py`** (Windows, OpenCV): opens the camera (DSHOW preferred, falls back to default), writes a video segment every N seconds, retries failed frame grabs, and discards segments smaller than 50 KB (invalid).
115+- **`scripts/realtime_analyze.py`** (WSL, PyTorch + transformers): loads Mage-VL once with `attn_implementation="sdpa"`, then polls `--live-dir` for new segments and analyzes each unseen segment by frame-sampling `--num-frames` frames and asking the model to describe the scene (LED/LCD-aware question prompt).
116+ 
117+## Common Mistakes
118+ 
119+- **Re-loading the model per segment**: kills latency (each load ≈ 1 min). Keep the analyzer resident — this is the whole point of the two-process design.
120+- **Camera "device in use"**: a stale capture process or another app holds the camera. Kill leftover python/capture processes, or ask the user to unplug/replug the camera.
121+- **Empty/invalid segments**: OpenCV can write a truncated file if the camera glitches; the capture script already discards <50 KB segments — keep that guard.
122+- **Forgetting offline env vars**: run the analyzer with `HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1` so it never tries the network.
123+- **Hardcoded paths**: pass `--model` / `--live-dir`; do not edit the script per machine.
124+- **Starting the analyzer per segment**: wrong — one resident process serves all segments.
125+ 
126+## Real-World Impact
127+ 
128+Live observation successfully identified a round glowing LCD screen playing an animated GIF (a cartoon face changing expressions) plus a blinking green LED — details that single-frame analysis had repeatedly missed, because the 12-frame sampling captured the animation.
@@ -0,0 +1,65 @@
1+"""Windows 侧:持续抓取摄像头,每 N 秒保存一个片段供 WSL 模型实时分析。
2+ 
3+修复:强制 DSHOW 后端(避免 MSMF 抓帧失败)、抓帧失败自动重试、
4+片段过小视为无效丢弃。产出: cam_live/seg_<timestamp>.mp4
5+"""
6+import argparse
7+import cv2
8+import time
9+import os
atomgit-bot
atomgit-botatomgit-bot8月14日

🔵 Low Priority

变更行:capture_live.py:9 import os 在文件中从未被引用,属于死代码/未引用导入,lint 会告警且无任何用途。

建议:删除第 9 行未使用的 import os。

likedislike
不准确?
10+from pathlib import Path
11+ 
12+ 
13+def main():
14+ p = argparse.ArgumentParser()
15+ p.add_argument("--camera", type=int, default=0)
16+ p.add_argument("--segment", type=float, default=10.0, help="每段秒数")
17+ p.add_argument("--fps", type=int, default=10)
18+ p.add_argument("--out", default="cam_live")
19+ args = p.parse_args()
20+ 
21+ out = Path(args.out)
22+ out.mkdir(exist_ok=True)
23+ 
24+ # 只使用 DSHOW 后端,避免回退到 MSMF 导致抓帧失败
25+ cap = cv2.VideoCapture(args.camera, cv2.CAP_DSHOW)
26+ if not cap.isOpened():
27+ print("[错误] DSHOW 无法打开摄像头,尝试默认后端 ...", flush=True)
28+ cap = cv2.VideoCapture(args.camera)
29+ if not cap.isOpened():
30+ print("[错误] 无法打开摄像头", flush=True)
31+ return
32+ 
33+ w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 640)
34+ h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 480)
35+ print(f"[信息] 摄像头 {args.camera} 打开 ({w}x{h}),每 {args.segment}s 一段", flush=True)
36+ 
37+ seg = 0
38+ min_size = 50 * 1024 # 小于 50KB 视为无效片段
39+ while True:
40+ stamp = time.strftime("%H%M%S")
41+ path = out / f"seg_{seg:04d}_{stamp}.mp4"
42+ writer = cv2.VideoWriter(str(path), cv2.VideoWriter_fourcc(*"mp4v"), args.fps, (w, h))
atomgit-bot
atomgit-botatomgit-bot8月14日

🟠 High Priority

变更行:capture_live.py:42 直接用最终文件名 seg_XXXX.mp4 打开 cv2.VideoWriter 持续写入,直到第 53 行 writer.release() 才写入 mp4 的 moov 索引。 受影响行为/契约:realtime_analyze.py:96 用 glob("seg_*.mp4") 轮询并把新文件名立即加入 seen(第 97-99 行),没有任何"文件已完成"判断,默认假设目录里的 .mp4 都是完整片段。 失败模式:两个脚本并行运行时(这正是设计用途),capture_live 每 10 秒持续写一个文件,分析端每秒轮询,必然在文件还在写入时(moov 未写入、CAP_PROP_FRAME_COUNT 返回 0 或只读到前几帧)就把它抓走。此时 analyze_segment 返回"(无法读取片段)"或只分析到残缺帧;更关键的是该文件名已被加入 seen,等 10 秒后文件真正写完也不会再被分析。结果是几乎每一段画面都被永久跳过,整个实时分析流水线失效。

建议:先写入临时文件名(如 path.name + ".part",不会被 seg_*.mp4 匹配),在 writer.release() 后再 tmp_path.rename(path),确保分析端只会看到完整片段。

likedislike
不准确?
43+ t0 = time.time()
44+ written = 0
45+ while time.time() - t0 < args.segment:
46+ ok, frame = cap.read()
47+ if not ok:
48+ # 抓帧失败:短暂等待重试(摄像头可能被占用/信号抖动)
49+ time.sleep(0.2)
50+ continue
51+ writer.write(frame)
52+ written += 1
53+ writer.release()
54+ size = path.stat().st_size if path.exists() else 0
55+ if size < min_size or written < 5:
56+ # 无效片段:删除并重试
57+ path.unlink(missing_ok=True)
58+ print(f"[警告] {path.name} 无效({size}B/{written}帧),丢弃", flush=True)
59+ else:
60+ print(f"[抓取] {path.name} ({size//1024}KB, {written}帧)", flush=True)
61+ seg += 1
62+ 
63+ 
64+if __name__ == "__main__":
65+ main()
@@ -0,0 +1,112 @@
1+"""WSL 侧:模型常驻加载一次,循环分析摄像头片段并输出解说。
2+ 
3+配合 Windows 侧 capture_live.py 使用:它持续把摄像头画面存成片段,
4+本脚本轮询新片段 -> Mage-VL 帧采样分析 -> 打印解说。
5+ 
6+用法示例:
7+ HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 \
8+ python realtime_analyze.py \
9+ --model /mnt/c/Users/TL/Documents/Visio/models/Mage-VL \
10+ --live-dir /mnt/c/Users/TL/Documents/Visio/cam_live
11+"""
12+import argparse
13+import os
14+import sys
atomgit-bot
atomgit-botatomgit-bot8月14日

🔵 Low Priority

变更行:realtime_analyze.py:14 import sys 在文件中从未被引用,属于死代码/未引用导入,lint 会告警且无任何用途。

建议:删除第 14 行未使用的 import sys。

likedislike
不准确?
15+import time
16+import glob
17+from pathlib import Path
18+ 
19+os.environ["HF_HUB_OFFLINE"] = "1"
20+os.environ["TRANSFORMERS_OFFLINE"] = "1"
21+ 
22+import torch # noqa: E402
23+from transformers import AutoModelForCausalLM, AutoProcessor # noqa: E402
24+ 
25+DEFAULT_QUESTION = (
26+ "实时解说这段摄像头画面:请描述画面里发生了什么。"
27+ "特别注意:有没有发光的 LCD 屏幕在播放 GIF 动图?上面显示什么?"
28+ "有没有 LED 灯?它在闪烁还是常亮?"
29+)
30+ 
31+ 
32+def parse_args() -> argparse.Namespace:
33+ p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
34+ p.add_argument("--model", default="microsoft/Mage-VL",
35+ help="Mage-VL 模型路径(建议本地路径)")
36+ p.add_argument("--live-dir", default="/mnt/c/Users/TL/Documents/Visio/cam_live",
37+ help="capture_live.py 输出片段的目录")
38+ p.add_argument("--num-frames", type=int, default=12,
39+ help="每个片段采样的帧数")
40+ p.add_argument("--max-new-tokens", type=int, default=120,
41+ help="解说最大 token 数")
42+ p.add_argument("--question", default=DEFAULT_QUESTION,
43+ help="分析每个片段时使用的提示词")
44+ return p.parse_args()
45+ 
46+ 
47+def analyze_segment(model, processor, video_path: str, num_frames: int,
48+ max_new_tokens: int, question: str) -> str:
49+ import cv2
50+ import numpy as np
51+ from PIL import Image
52+ 
53+ # 帧采样
54+ cap = cv2.VideoCapture(video_path)
55+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
56+ if total <= 0:
57+ cap.release()
58+ return "(无法读取片段)"
59+ indices = np.linspace(0, total - 1, min(num_frames, total), dtype=int)
60+ frames = []
61+ for i in indices:
62+ cap.set(cv2.CAP_PROP_POS_FRAMES, int(i))
63+ ok, f = cap.read()
64+ if ok:
65+ frames.append(Image.fromarray(cv2.cvtColor(f, cv2.COLOR_BGR2RGB)))
66+ cap.release()
67+ 
68+ messages = [{"role": "user", "content": [
69+ {"type": "video"}, {"type": "text", "text": question},
70+ ]}]
71+ text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
72+ inputs = processor(text=[text], videos=[frames], return_tensors="pt", padding=True)
73+ inputs = {k: (v.to(model.device) if hasattr(v, "to") else v) for k, v in inputs.items()}
74+ if "pixel_values" in inputs:
75+ inputs["pixel_values"] = inputs["pixel_values"].to(model.dtype)
76+ with torch.inference_mode():
77+ out = model.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False)
78+ return processor.tokenizer.decode(out[0, inputs["input_ids"].shape[1]:], skip_special_tokens=True).strip()
79+ 
80+ 
81+def main():
82+ args = parse_args()
83+ live_dir = Path(args.live_dir)
84+ live_dir.mkdir(parents=True, exist_ok=True)
85+ 
86+ print(f"[信息] 加载模型(首次约 1 分钟,之后常驻): {args.model} ...", flush=True)
87+ processor = AutoProcessor.from_pretrained(args.model, trust_remote_code=True)
88+ model = AutoModelForCausalLM.from_pretrained(
89+ args.model, trust_remote_code=True, torch_dtype="auto",
90+ device_map="auto", attn_implementation="sdpa",
91+ ).eval()
92+ print(f"[信息] 模型就绪 {model.device},开始实时监听 {live_dir}/", flush=True)
93+ 
94+ seen = set()
95+ while True:
96+ segs = sorted(glob.glob(str(live_dir / "seg_*.mp4")))
97+ new = [s for s in segs if s not in seen]
98+ for s in new:
99+ seen.add(s)
100+ t = time.strftime("%H:%M:%S")
101+ print(f"\n[{t}] 分析 {Path(s).name} ...", flush=True)
102+ try:
103+ desc = analyze_segment(model, processor, s,
104+ args.num_frames, args.max_new_tokens, args.question)
105+ print(f"[解说] {desc}", flush=True)
106+ except Exception as e:
107+ print(f"[错误] {e}", flush=True)
108+ time.sleep(1)
109+ 
110+ 
111+if __name__ == "__main__":
112+ main()