# 门店视频 AI 分析系统 PoC 实施方案 生成日期:2026-06-15 ## 1. 系统架构 ### 1.1 整体 Pipeline ```text [门店/摄像头配置] | v [海康云眸接入层] - 获取 access_token - 查询云录像回放 URL / RTSP / HLS / MP4 - 按 15-60 分钟切片拉取 | v [本地视频缓存层: SSD] - raw_segments/ - segment_manifest.sqlite - 记录下载、抽帧、推理状态 | v [FFmpeg + NVDEC 抽帧层] - GPU 硬解码 H.264/H.265 - 1 FPS 抽帧 - 输出带时间戳的 JPEG 帧 | v [Clip 构建层] - 10-20 秒窗口 - 每个 clip 8-16 帧 - 生成 clip manifest | v [4B LoRA 模型推理层] - FP16 - batch size 1-4 - clip 级推理 | v [结果标准化层] - clip_result.jsonl - schema 校验 - JSON 解析失败重试 | v [门店级聚合层] - 事件合并 - 时间聚合 - 输出 store_result.json ``` ### 1.2 数据流向 系统按门店、摄像头、日期组织任务。 一个最小任务单元建议定义为: ```text store_id + camera_id + business_date ``` 单路 24 小时视频不要一次性处理,而是拆成多个 segment: ```text store_001 / cam_01 / 2026-06-14 -> 24 个 1 小时 segment -> 每个 segment 独立拉取、解码、抽帧 -> 所有帧按时间顺序组成 clip -> clip 级模型推理 -> 汇总为门店级 JSON ``` 核心原则: - 视频接入和模型推理解耦。 - 原始视频、抽帧结果、推理结果都可断点续跑。 - 模型只对 clip 推理,不逐帧调用 LLM。 - PoC 阶段优先可观测、可复跑,不追求极致流水线性能。 ## 2. 模块拆解 ### 2.1 视频接入层 #### 目标 从海康云眸获取指定门店、指定摄像头、指定时间范围内的 24 小时录像。 #### 推荐接入方式 优先级如下: ```text 1. 云录像文件下载 URL / MP4 URL 2. HLS 回放 URL 3. RTSP 回放 URL 4. 直播 RTSP 本地录制 ``` PoC 推荐优先使用云录像回放或可下载视频文件。直播 RTSP 只能从当前时间开始录制,不适合补历史 24 小时数据。 #### 鉴权假设 海康云眸常见接入流程可抽象为: ```text app_key / app_secret | v access_token | v camera_id + begin_time + end_time | v playback_url ``` 系统应把 token 和 URL 视为有有效期的临时凭证。 建议保存: ```text camera_id segment_start segment_end playback_url_created_at playback_url_expire_at fetch_status retry_count last_error ``` #### 24 小时录像拉取策略 不要一次请求 24 小时录像。 推荐拆分: ```text 默认:24 个 1 小时 segment 网络不稳定时:48 个 30 分钟 segment URL 过期较快时:96 个 15 分钟 segment ``` 每个 segment 独立处理: ```text pending -> fetching -> fetched -> sampling -> sampled -> inferencing -> inferred -> aggregated ``` ### 2.2 视频处理层 #### FFmpeg + NVDEC 硬解码 Ubuntu 24 上需要确认: ```bash ffmpeg -hwaccels ffmpeg -decoders | grep cuvid ``` H.264 视频示例: ```bash ffmpeg \ -hide_banner -nostdin -y \ -rtsp_transport tcp \ -hwaccel cuda \ -c:v h264_cuvid \ -i "$INPUT" \ -vf "fps=1,scale=640:-2" \ -q:v 4 \ "$FRAME_DIR/%06d.jpg" ``` H.265 视频示例: ```bash ffmpeg \ -hide_banner -nostdin -y \ -rtsp_transport tcp \ -hwaccel cuda \ -c:v hevc_cuvid \ -i "$INPUT" \ -vf "fps=1,scale=640:-2" \ -q:v 4 \ "$FRAME_DIR/%06d.jpg" ``` 如果输入是本地 MP4 文件,可以去掉 `-rtsp_transport tcp`。 #### 1 FPS 抽帧策略 24 小时视频按 1 FPS 抽帧: ```text 1 小时:3600 张帧 24 小时:86400 张帧 2 路摄像头:172800 张帧 ``` PoC 阶段建议输出 JPEG: ```text 质量:-q:v 4 分辨率:scale=640:-2 或按模型输入要求调整 命名:按 segment 内序号或绝对时间戳 ``` 更推荐保存时间戳映射: ```json {"frame_path":"frames_1fps/000001.jpg","timestamp":"2026-06-14T00:00:01+08:00","segment_id":"seg_0000"} ``` #### 视频缓存策略 本地 SSD 目录建议: ```text /data/video-ai/ stores/{store_id}/ cameras/{camera_id}/ {business_date}/ raw_segments/ 00_00_00__01_00_00.mp4 frames_1fps/ 00_00_00.jpg 00_00_01.jpg manifests/ segments.sqlite frames.jsonl clips.jsonl results/ clip_results.jsonl store_result.json ``` 缓存策略: ```text raw video segment: SSD 临时缓存,抽帧成功后可删除 1 FPS frames: SSD 中间产物,推理完成后保留 1-7 天 clip tensor: 不落盘,运行时构建 result JSONL: 长期保留 ``` PoC 不建议使用全内存缓存。24 小时视频帧数较多,磁盘缓存更适合断点续跑和调试。 ### 2.3 Clip 构建层 #### Clip 长度建议 PoC 默认: ```text clip_length = 10 秒 stride = 10 秒 frames_per_clip = 8 或 10 ``` 对应规模: ```text 1 小时视频: 10 秒 clip -> 360 个 clip 20 秒 clip -> 180 个 clip 24 小时视频: 10 秒 clip -> 8640 个 clip 20 秒 clip -> 4320 个 clip ``` 如果模型推理较慢,可以切换为: ```text clip_length = 20 秒 stride = 20 秒 frames_per_clip = 12 或 16 ``` #### 帧采样策略 不要用编码关键帧作为模型输入采样依据。关键帧是视频压缩概念,不一定对应行为变化。 推荐均匀采样: ```text 10 秒 clip: 1 FPS 得到 10 张帧 模型支持 10 帧时全部输入 模型只支持 8 帧时均匀采样 8 张 20 秒 clip: 1 FPS 得到 20 张帧 均匀采样 12 或 16 张 ``` #### Clip Manifest 建议为每个 clip 生成清单: ```json { "clip_id": "store_001_cam_01_20260614_123120", "store_id": "store_001", "camera_id": "cam_01", "start_time": "2026-06-14T12:31:20+08:00", "end_time": "2026-06-14T12:31:30+08:00", "frame_paths": [ "frames_1fps/12_31_20.jpg", "frames_1fps/12_31_21.jpg" ], "status": "pending" } ``` ### 2.4 模型推理层 #### 3080 20GB 上运行 4B LoRA 显存粗略估算: ```text 4B FP16 base model: 约 8GB LoRA adapter: 通常 < 1GB vision encoder / projector / activation / KV cache: 4-8GB 运行余量: 3-6GB ``` 推荐启动参数: ```text dtype: FP16 batch_size: 1 frames_per_clip: 8 image_size: 336 或 448 max_new_tokens: 256-512 ``` 稳定后逐步尝试: ```text batch_size: 1 -> 2 -> 4 frames_per_clip: 8 -> 12 -> 16 image_size: 336 -> 448 -> 640 ``` 出现 OOM 时按顺序回退: ```text 1. batch_size 降低 2. frames_per_clip 降低 3. image_size 降低 4. max_new_tokens 降低 5. 必要时使用 8bit / 4bit 量化 ``` #### 推荐推理框架 PoC 首选: ```text PyTorch + Transformers + PEFT ``` 原因: - 4B LoRA 加载简单。 - 多图 / 视频帧输入适配成本低。 - 更容易控制显存、batch、prompt、输出解析。 暂不首选: ```text vLLM ``` 原因: - vLLM 更适合文本 LLM 高吞吐服务。 - 多模态模型和 LoRA 的支持取决于具体模型。 - 离线 PoC 的主要目标是链路跑通,不是低延迟在线服务。 #### 推理输入 每个 batch 包含多个 clip: ```json [ { "clip_id": "store_001_cam_01_20260614_123120", "frames": ["...jpg", "...jpg"], "prompt": "请分析该门店监控片段..." } ] ``` Prompt 应约束输出为固定 JSON: ```text 只输出 JSON。 event_type 必须来自枚举。 没有事件时输出 events: []。 不要输出解释性文字。 ``` #### 推理输出 clip 级输出建议保存为 JSONL: ```json { "clip_id": "store_001_cam_01_20260614_123120", "camera_id": "cam_01", "start_time": "2026-06-14T12:31:20+08:00", "end_time": "2026-06-14T12:31:30+08:00", "status": "ok", "events": [ { "event_type": "queue_detected", "confidence": 0.86, "severity": "medium", "attributes": { "estimated_people": 5, "location": "checkout_area" } } ], "raw_response": null } ``` JSON 解析失败时: ```text 1. 用更严格 prompt 重试一次 2. 仍失败则 status = parse_failed 3. 保存 raw_response 4. 聚合层跳过该 clip,但记录 failed_clip_count ``` ### 2.5 输出结构设计 #### 门店级 JSON Schema ```json { "store_id": "store_001", "business_date": "2026-06-14", "timezone": "Asia/Shanghai", "pipeline_version": "poc-v1", "cameras": [ { "camera_id": "cam_01", "video_start": "2026-06-14T00:00:00+08:00", "video_end": "2026-06-15T00:00:00+08:00", "processed_clip_count": 8640, "failed_clip_count": 12 } ], "summary": { "total_events": 128, "event_counts": { "customer_enter": 53, "staff_absent": 8, "queue_detected": 12 } }, "events": [ { "event_id": "evt_000001", "camera_id": "cam_01", "event_type": "queue_detected", "start_time": "2026-06-14T12:31:20+08:00", "end_time": "2026-06-14T12:32:10+08:00", "duration_seconds": 50, "confidence": 0.86, "severity": "medium", "attributes": { "estimated_people": 5, "location": "checkout_area" }, "evidence": { "clip_ids": ["store_001_cam_01_20260614_123120"], "frame_refs": ["frames_1fps/12_31_20.jpg"] } } ], "processing": { "started_at": "2026-06-15T01:00:00+08:00", "finished_at": "2026-06-15T05:40:00+08:00", "status": "completed_with_warnings" } } ``` #### 事件级结构 事件字段建议固定: ```text event_id store_id camera_id event_type start_time end_time duration_seconds confidence severity attributes evidence ``` event_type 必须枚举化,例如: ```text customer_enter customer_leave queue_detected staff_absent staff_present area_crowded abnormal_behavior unknown ``` PoC 阶段不要让模型自由发明事件类型。 #### 时间聚合规则 合并条件: ```text same camera_id same event_type 相邻事件间隔 <= 30 秒 ``` 合并方式: ```text start_time = 最早 start_time end_time = 最晚 end_time duration_seconds = end_time - start_time confidence = max(confidence) 或按 clip 时长加权平均 evidence.clip_ids = 合并所有 clip_id ``` ## 3. 性能估算 ### 3.1 数据规模 单路摄像头: ```text 1 小时视频: 1 FPS -> 3600 张帧 10 秒 clip -> 360 个 clip 20 秒 clip -> 180 个 clip 24 小时视频: 1 FPS -> 86400 张帧 10 秒 clip -> 8640 个 clip 20 秒 clip -> 4320 个 clip ``` 双路摄像头: ```text 24 小时: 1 FPS -> 172800 张帧 10 秒 clip -> 17280 个 clip ``` ### 3.2 单路 1 小时处理时间 如果视频已缓存到本地 SSD: ```text NVDEC 抽帧: 3-10 分钟 模型推理: 每 clip 0.5 秒 -> 约 3 分钟 每 clip 1.0 秒 -> 约 6 分钟 每 clip 2.0 秒 -> 约 12 分钟 聚合: <1 分钟 合计: 约 8-25 分钟 / 小时视频 ``` 如果海康云眸回放只能按 1x 实时速度拉流: ```text 1 小时录像最少需要 1 小时获取 24 小时录像最少需要 24 小时获取 ``` 这需要在 PoC 第一天优先验证。 ### 3.3 单门店 24 小时处理时间 单路摄像头: ```text 视频可快速下载: 约 3-10 小时 视频只能 1x 回放: 至少 24 小时 ``` 双路摄像头: ```text 视频可快速下载: 约 6-20 小时 视频只能 1x 回放: 至少 48 路小时 ``` ### 3.4 GPU 瓶颈分析 大概率瓶颈: ```text 1. 4B LoRA 多帧推理 2. 云录像下载速度 3. JPEG 编码和磁盘写入 ``` 一般不是瓶颈: ```text NVDEC 解码能力 门店级 JSON 聚合 clip manifest 构建 ``` ## 4. PoC 实施步骤 ### Day 1:打通视频接入和 GPU 解码 任务: ```text 1. 准备 Ubuntu 24 + NVIDIA driver + CUDA runtime。 2. 确认 FFmpeg 支持 NVDEC。 3. 从海康云眸获取 10 分钟历史回放 URL。 4. 使用 FFmpeg 拉取或直接抽帧。 5. 验证输出帧数量。 ``` 命令: ```bash ffmpeg -hwaccels ffmpeg -decoders | grep cuvid ``` 验收标准: ```text 10 分钟视频生成约 600 张 1 FPS 帧 确认使用 h264_cuvid 或 hevc_cuvid 确认不是 CPU 解码 ``` ### Day 2:实现 segment 化和本地缓存 任务: ```text 1. 把 24 小时拆成 24 个 1 小时 segment。 2. 每个 segment 独立请求 playback_url。 3. 每个 segment 独立抽帧。 4. 保存 segment 状态。 5. 抽帧成功后生成 frames.jsonl。 ``` segment 状态: ```text pending fetching fetched sampling sampled failed ``` 验收标准: ```text 能处理 1 小时录像 中断后重跑不会重复处理已完成 segment 失败 segment 有 retry_count 和 last_error ``` ### Day 3:实现 Clip 构建和模型推理 任务: ```text 1. 读取 frames.jsonl。 2. 按 10 秒窗口构建 clip。 3. 每个 clip 均匀采样 8-10 帧。 4. 加载 4B base model + LoRA adapter。 5. 使用 FP16 推理。 6. batch_size 从 1 开始。 7. 输出 clip_results.jsonl。 ``` 验收标准: ```text 1 小时视频生成 360 个 10 秒 clip 模型完成 clip 级推理 JSON 可解析率 > 95% RTX 3080 20GB 不 OOM ``` ### Day 4:实现聚合、容错和性能统计 任务: ```text 1. 实现 clip_results.jsonl 到 store_result.json 的聚合。 2. 增加 JSON schema 校验。 3. 解析失败的 clip 重试一次。 4. 统计下载、抽帧、推理耗时。 5. 记录 GPU 峰值显存。 ``` 需要记录的指标: ```text download_time_seconds decode_time_seconds frames_per_second clips_per_second gpu_memory_peak_mb failed_segments failed_clips parse_failed_clips ``` 验收标准: ```text 1 小时完整链路跑通 生成 store_result.json 失败任务可断点续跑 ``` ### Day 5:跑完整 24 小时 任务: ```text 1. 跑单路摄像头 24 小时。 2. 跑双路摄像头 24 小时。 3. 记录全链路耗时。 4. 固化 PoC 参数。 5. 输出失败清单和优化建议。 ``` 最终参数需要确认: ```text clip_length stride frames_per_clip batch_size image_size raw segment 是否保留 frames 保留天数 ``` 验收标准: ```text 单门店 1-2 路摄像头 24 小时离线分析完成 输出门店级 JSON 有可复跑的 segment 和 clip 状态 ``` ## 5. 关键设计决策说明 ### 5.1 先单机跑通 PoC 只做 1 家门店,单机单 GPU 足够验证链路。 不建议一开始引入: ```text Kafka Spark Kubernetes 复杂分布式任务系统 ``` PoC 阶段使用: ```text 本地文件系统 SQLite 或 JSONL manifest 单进程或少量 worker ``` ### 5.2 使用磁盘帧缓存 原因: ```text 1. 方便调试模型输入。 2. 模型调参时不用重复拉视频。 3. 海康 playback_url 过期后不影响后续实验。 4. 支持断点续跑。 ``` 后续链路稳定后,可以优化为: ```text FFmpeg pipe -> frame queue -> model worker ``` 但 PoC 不优先做。 ### 5.3 默认 10 秒 clip 10 秒 clip 的优势: ```text 语义粒度足够细 事件定位较准确 推理量可控 方便聚合 ``` 如果吞吐不够,切到 20 秒 clip: ```text clip 数量减少 50% 事件时间定位精度下降 ``` ### 5.4 小 batch 推理 RTX 3080 20GB 显存有限。 建议从保守参数开始: ```text batch_size = 1 frames_per_clip = 8 image_size = 336 或 448 ``` 稳定后再提高 batch。 ### 5.5 结构化输出优先 模型输出必须被工程系统消费。 因此: ```text 输出 JSON event_type 枚举 schema 校验 失败重试 保留 raw_response ``` 不要依赖自然语言总结作为主输出。 ## 6. 故障与容错设计 ### 6.1 RTSP / 回放 URL 失败 处理方式: ```text 1. 重新获取 playback_url。 2. 使用 -rtsp_transport tcp。 3. 将 1 小时 segment 拆成 30 或 15 分钟。 4. 最多重试 3 次。 5. 仍失败则标记 failed,不阻塞其他 segment。 ``` ### 6.2 解码失败 处理方式: ```text 1. 使用 ffprobe 获取 codec。 2. H.264 使用 h264_cuvid。 3. H.265 使用 hevc_cuvid。 4. 失败后缩短 segment 重试。 5. 仍失败则记录 ffmpeg stderr。 ``` 由于项目约束要求必须 GPU 解码,不建议默认 fallback 到 CPU 解码。 ### 6.3 模型 OOM 处理顺序: ```text 1. batch_size: 4 -> 2 -> 1 2. frames_per_clip: 16 -> 12 -> 8 3. image_size: 640 -> 448 -> 336 4. max_new_tokens: 512 -> 256 5. 必要时使用 8bit / 4bit 量化 ``` 每次 OOM 后应: ```text torch.cuda.empty_cache() 记录失败 clip 用更小配置重试 ``` ### 6.4 JSON 解析失败 处理方式: ```text 1. 严格 prompt 重试一次。 2. 仍失败则 status = parse_failed。 3. 保存 raw_response。 4. 聚合层跳过该 clip。 5. 门店级结果记录 failed_clip_count。 ``` ### 6.5 断点续跑 每层都要有 manifest: ```text segments.sqlite: 记录每个视频段状态 frames.jsonl: 记录每张帧和时间戳 clips.jsonl: 记录每个 clip 和输入帧 clip_results.jsonl: 记录每个 clip 的推理结果 ``` 重跑逻辑: ```text 已有 sampled segment 不重复抽帧 已有 inferred clip 不重复推理 只补 failed / missing / parse_failed ``` ## 7. 可扩展性设计 ### 7.1 从 1 家门店到 26 家门店 任务分片方式: ```text store_id + camera_id + business_date ``` 26 家门店规模: ```text 26 家 * 1-2 路摄像头 = 26-52 路摄像头 ``` 如果每路 24 小时需要 3-10 GPU 小时: ```text 每日全量处理需要 78-520 GPU 小时 ``` 单张 RTX 3080 不适合长期处理 26 家全量 24 小时视频。 ### 7.2 单机阶段 结构: ```text 1 个 downloader 1 个 ffmpeg sampler 1 个 model worker 1 个 aggregator ``` 建议: ```text 下载和抽帧可以提前跑 模型 worker 独占 GPU 不要多个模型进程抢同一张 3080 ``` ### 7.3 单机多 GPU 阶段 结构: ```text 1 个调度进程 N 个 GPU worker 每张 GPU 一个模型实例 ``` 任务分配: ```text 按 camera-day 分配给不同 GPU 每个 GPU worker 独立读取 clip manifest ``` ### 7.4 多机多 GPU 阶段 最小生产化结构: ```text Postgres: 任务状态表 对象存储 / NAS: 原始视频、帧、结果 多台 GPU 服务器: 拉取任务 独立处理 写回状态 ``` 暂不需要: ```text Kafka Spark 复杂实时流处理 ``` ## 8. 风险与解决方案 ### 风险 1:海康云眸回放速度受限 影响: ```text 如果只能 1x 回放,24 小时视频至少需要 24 小时获取。 ``` 解决: ```text PoC 第一天验证是否支持快速下载历史录像。 优先使用云录像文件下载 URL。 如果只能 RTSP 回放,需要重新评估每日处理窗口。 ``` ### 风险 2:RTX 3080 显存不足 影响: ```text 4B LoRA + 多帧输入可能 OOM。 ``` 解决: ```text batch_size 从 1 开始。 frames_per_clip 从 8 开始。 image_size 从 336 或 448 开始。 必要时使用 8bit / 4bit 量化。 ``` ### 风险 3:模型对门店事件识别不稳定 影响: ```text 输出事件类型不一致,难以聚合。 ``` 解决: ```text 固定 event_type 枚举。 固定 JSON schema。 在 prompt 中要求没有事件时返回 events: []。 保存证据帧用于人工复核。 ``` ### 风险 4:抽帧结果和真实时间不对齐 影响: ```text 事件时间戳错误。 ``` 解决: ```text 每个 segment 必须记录 segment_start。 帧时间戳由 segment_start + frame_index 秒计算。 如 FFmpeg 能输出 pts_time,则优先使用 pts_time。 ``` ### 风险 5:磁盘占用增长过快 影响: ```text 24 小时、多摄像头 JPEG 帧会占用大量 SSD。 ``` 解决: ```text raw segment 抽帧成功后删除。 frames 推理完成后保留 1-7 天。 只长期保留结果 JSON 和事件证据帧。 ``` ## 9. 推荐 PoC 默认参数 ```text segment_length: 1 hour fallback_segment_length: 15 minutes decode: NVDEC h264_cuvid / hevc_cuvid frame_rate: 1 FPS frame_format: JPEG frame_width: 640 clip_length: 10 seconds clip_stride: 10 seconds frames_per_clip: 8 model_dtype: FP16 batch_size: 1 max_new_tokens: 256 retry_count: 3 for segment parse_retry_count: 1 for model output ``` ## 10. 最小可行交付物 3-5 天 PoC 结束时应交付: ```text 1. 能处理单路 24 小时视频的离线脚本或命令入口。 2. 能处理 1 家门店 1-2 路摄像头。 3. 使用 FFmpeg + NVDEC 完成 1 FPS 抽帧。 4. 使用 4B LoRA 模型完成 clip 级推理。 5. 输出 clip_results.jsonl。 6. 输出 store_result.json。 7. 支持 segment 和 clip 断点续跑。 8. 有性能统计报告。 9. 有失败 segment / failed clip 清单。 ``` 不在 PoC 范围: ```text 实时告警 Web 管理后台 复杂多租户权限 分布式调度平台 自动模型训练闭环 ```