1194 lines
20 KiB
Markdown
1194 lines
20 KiB
Markdown
# 门店视频 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 管理后台
|
||
复杂多租户权限
|
||
分布式调度平台
|
||
自动模型训练闭环
|
||
```
|
||
|