720 lines
22 KiB
Markdown
720 lines
22 KiB
Markdown
# Project Documentation
|
||
|
||
## Goal
|
||
|
||
本项目是在 `/Users/yoilun/AI-train/video-ai-analysis-poc` 中实现视频离线批处理分析 PoC。`v1.0` 已支持本地视频文件夹;`v1.1` 新增海康云存储录像下载作为视频来源,下载完成后复用现有抽帧、clip、VLM 推理和聚合流程。
|
||
|
||
必须支持:
|
||
|
||
- 选择一个本地视频文件夹。
|
||
- 直接调用海康云存储录像下载 API 获取录像下载地址并下载视频。
|
||
- AccessToken 通过 config 或环境变量配置,不写入测试夹具和文档样例。
|
||
- 设备序列号和通道可配置,并支持多设备。
|
||
- 分析时间段包含年月日,支持 `YYYY-MM-DD HH:MM:SS` 配置。
|
||
- 海康 API 单次最多下载 1 小时,超过 1 小时的时间段必须拆成多个不超过 3600 秒的请求;默认示例使用 600 秒分片,真实 smoke 中比 3600 秒更稳定。
|
||
- 自动发现文件夹内所有常见视频文件。
|
||
- 对每个视频按 1 FPS 抽帧,按 10-20 秒 clip 组织输入。
|
||
- 使用已有 4B VLM 模型能力,兼容 `memai-zhengxin-v3-20260413` 的 OpenAI-compatible vLLM 接口。
|
||
- prompt 通过 config 调整。
|
||
- 输出结构化 JSON/JSONL。
|
||
- 输出中必须包含监控画面的时间轴,包括视频、clip、frame 和事件的时间定位。
|
||
|
||
## v1.1 Hik Cloud Storage Source
|
||
|
||
海康文档 `录像下载流程_1.pdf` 的“2、获取录像下载地址”定义:
|
||
|
||
```text
|
||
POST https://api2.hik-cloud.com/v1/carrier/cstorage/open/play/download
|
||
Authorization: bearer <AccessToken>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
请求 body:
|
||
|
||
```json
|
||
{
|
||
"deviceSerial": "EXAMPLE_DEVICE_SERIAL",
|
||
"channelNo": 1,
|
||
"timeBegin": 1764856787,
|
||
"timeEnd": 1764856978
|
||
}
|
||
```
|
||
|
||
成功返回 `data.url`、`actualBeginTime`、`actualEndTime`。错误码 `80430002` 包含起止时间大于 3600 秒的参数错误,错误码 `80438027` 表示起始时间内没有录像。
|
||
|
||
配置示例:
|
||
|
||
```yaml
|
||
source:
|
||
mode: hik_cloud # local | hik_cloud
|
||
|
||
hik_cloud:
|
||
api_base_url: https://api2.hik-cloud.com
|
||
download_path: /v1/carrier/cstorage/open/play/download
|
||
access_token: null
|
||
access_token_env: HIK_CLOUD_ACCESS_TOKEN
|
||
chunk_seconds: 600
|
||
timeout_seconds: 60
|
||
download_timeout_seconds: 600
|
||
devices:
|
||
- device_serial: EXAMPLE_DEVICE_SERIAL
|
||
channel_no: 1
|
||
name: store-front
|
||
time_ranges:
|
||
- begin: "2026-02-03 09:00:00"
|
||
end: "2026-02-03 11:30:00"
|
||
```
|
||
|
||
云下载输出:
|
||
|
||
- `hik_cloud_download_manifest.jsonl`:每个设备/通道/时间分片的请求、实际时间、状态和错误。`--dry-run` 云模式只请求下载地址并写入 `address_ok` / failure 状态,不下载 mp4,不 probe。
|
||
- `downloads/hik_cloud/<device_serial>/ch<channel_no>/*.mp4`:下载后供现有分析链路消费的视频文件。
|
||
- `video_manifest.jsonl`:保留现有契约,并附加云来源元数据。
|
||
|
||
运行本地文件夹模式:
|
||
|
||
```bash
|
||
python3 -B -m video_ai_analysis_poc.cli \
|
||
--config config/local_batch.yaml \
|
||
--input-dir /path/to/local/videos \
|
||
--output-dir ./outputs/local-batch
|
||
```
|
||
|
||
运行海康云存储模式时,复制配置文件并设置 `source.mode: hik_cloud`,AccessToken 优先通过环境变量提供:
|
||
|
||
```bash
|
||
export HIK_CLOUD_ACCESS_TOKEN='<redacted>'
|
||
python3 -B -m video_ai_analysis_poc.cli \
|
||
--config /path/to/hik-cloud.yaml \
|
||
--output-dir ./outputs/hik-cloud
|
||
```
|
||
|
||
`--dry-run` 会请求海康下载地址并写 `hik_cloud_download_manifest.jsonl`,但不会下载视频文件、probe、抽帧、推理或聚合。`--until clips` 会在下载、探测、抽帧和 clip manifest 后停止;`--until inference` 会继续运行模型推理并写入 `clip_results.jsonl`。
|
||
|
||
真实远端 smoke 观察到同一 1 小时时间段直接按 3600 秒下载时,云端返回的 MP4 缺少 `moov` atom,`ffprobe` 无法解析;改用 600 秒分片后 6 个分片均可探测并进入抽帧。抽帧阶段会根据云下载记录的 `actual_begin/actual_end` 或 `requested_begin/requested_end` 给 FFmpeg 加输出帧数上限,避免海康 MP4 异常时间戳导致 `fps=1` 复制出过量帧。
|
||
|
||
海康云存储安全规则:
|
||
|
||
- 不提交真实 AccessToken。
|
||
- 优先使用 `hik_cloud.access_token_env: HIK_CLOUD_ACCESS_TOKEN`。
|
||
- 不记录 Authorization header。
|
||
- 不持久化签名下载 URL query,例如 `sign`、`sig`、`token`、`access_token`。
|
||
- `access_token.md` 是敏感验证文件,只能用于远端真实 smoke,不复制进文档、测试或输出样例。
|
||
|
||
## Directory Boundaries
|
||
|
||
```text
|
||
/Users/yoilun/AI-train/video-ai-analysis-poc
|
||
本次 PoC 项目目录,后续代码、配置、计划、文档都放这里。
|
||
|
||
/Users/yoilun/AI-train/zhengxin-vlm-0413
|
||
外部模型和参考实现目录,不是本次项目目录。
|
||
```
|
||
|
||
硬性边界:
|
||
|
||
- 不在 `zhengxin-vlm-0413` 中创建本项目文件。
|
||
- 不修改 `zhengxin-vlm-0413/models/**`。
|
||
- 不修改 `zhengxin-vlm-0413/service/config.yaml`、`service/config.yaml-bk`、`docker/.env`。
|
||
- 不把参考项目真实 RTSP、Webhook、token、Cookie、密码写入本项目示例配置、测试夹具、文档或输出样例。
|
||
- 输出目录只能是用户显式传入目录,或本项目内 `outputs/`。
|
||
- 不覆盖用户原始视频文件。
|
||
|
||
## Inference Architecture Decision
|
||
|
||
本 PoC 明确选择:
|
||
|
||
```text
|
||
OpenAI-compatible vLLM API
|
||
```
|
||
|
||
不在 PoC 第一版中直接加载 PyTorch + Transformers + PEFT。原因:
|
||
|
||
- 用户说明测试环境已有模型。
|
||
- 参考项目已经使用 vLLM OpenAI-compatible API。
|
||
- 本地视频批处理的主要目标是打通工程链路,而不是重新实现模型服务。
|
||
|
||
配置字段固定为:
|
||
|
||
```yaml
|
||
vlm:
|
||
api_base_url: http://localhost:8679
|
||
chat_completions_path: /v1/chat/completions
|
||
```
|
||
|
||
代码拼接规则:
|
||
|
||
```text
|
||
chat_url = api_base_url.rstrip("/") + chat_completions_path
|
||
```
|
||
|
||
不要在配置中同时传完整 endpoint 和 base URL,避免出现 `/v1/chat/completions/v1/chat/completions` 之类的双拼路径。
|
||
|
||
## Target File Structure
|
||
|
||
```text
|
||
video-ai-analysis-poc/
|
||
agent.md
|
||
task_plan.md
|
||
findings.md
|
||
progress.md
|
||
memories.md
|
||
video_ai_analysis_system_plan.md
|
||
config/
|
||
local_batch.yaml
|
||
video_ai_analysis_poc/
|
||
__init__.py
|
||
cli.py
|
||
config.py
|
||
paths.py
|
||
discovery.py
|
||
probe.py
|
||
ffmpeg_sampler.py
|
||
frames.py
|
||
clips.py
|
||
vlm_client.py
|
||
result_parser.py
|
||
aggregator.py
|
||
manifest.py
|
||
logging_utils.py
|
||
schemas/
|
||
clip_result.schema.json
|
||
video_result.schema.json
|
||
folder_summary.schema.json
|
||
tests/
|
||
test_config.py
|
||
test_discovery.py
|
||
test_probe.py
|
||
test_clips.py
|
||
test_result_parser.py
|
||
test_aggregator.py
|
||
outputs/
|
||
.gitkeep
|
||
```
|
||
|
||
## Module Boundaries
|
||
|
||
### `config.py`
|
||
|
||
- 加载 `config/local_batch.yaml`。
|
||
- 合并 CLI 参数覆盖项。
|
||
- 校验必填字段、数值范围、路径安全。
|
||
- 不访问视频、不调用 FFmpeg、不调用模型。
|
||
|
||
### `paths.py`
|
||
|
||
- 生成稳定 `video_id`、`clip_id`。
|
||
- 生成输出目录结构。
|
||
- 防止输出目录指向参考模型目录或覆盖输入视频目录。
|
||
|
||
### `discovery.py`
|
||
|
||
- 只负责按 `input.dir`、`recursive`、`extensions` 发现视频。
|
||
- 输出 `video_manifest.jsonl`。
|
||
- 不做 ffprobe,不做抽帧,不调用模型。
|
||
|
||
### `probe.py`
|
||
|
||
- 包装 `ffprobe`。
|
||
- 输出 `duration_seconds`、`codec_name`、`width`、`height`、`fps`、`format_name`、`start_time`。
|
||
- 损坏或不支持视频标记 `probe_failed`,记录 `last_error`,不阻塞其他视频。
|
||
|
||
### `ffmpeg_sampler.py`
|
||
|
||
- 使用 FFmpeg + NVDEC 做 1 FPS 抽帧。
|
||
- 根据 codec 选择 `h264_cuvid` / `hevc_cuvid`。
|
||
- 默认 `allow_cpu_fallback: false`。
|
||
- 输出 JPEG 和 `frame_manifest.jsonl`。
|
||
- 保存 FFmpeg stderr 摘要,作为实际使用 GPU 解码的证据。
|
||
|
||
### `frames.py`
|
||
|
||
- 计算 frame 的相对秒数和 timecode。
|
||
- 维护 frame 文件路径、offset、timecode。
|
||
- 优先使用可获得的 `pts_time`,否则使用抽帧序号按 FPS 推导相对时间。
|
||
|
||
### `clips.py`
|
||
|
||
- 读取 `frame_manifest.jsonl`。
|
||
- 按 `clip.length_seconds` 和 `clip.stride_seconds` 构建 clip。
|
||
- 从 1 FPS 帧中均匀采样 `frames_per_clip`。
|
||
- 输出 `clip_manifest.jsonl`,必须包含参与推理的实际帧时间。
|
||
|
||
### `vlm_client.py`
|
||
|
||
- 调用 OpenAI-compatible `/v1/chat/completions`。
|
||
- 多帧使用 `image_url`,默认 `data:image/jpeg;base64`。
|
||
- prompt 来自 config,不硬编码。
|
||
- 不解析业务事件,只返回 raw response、latency 和 HTTP 状态。
|
||
- 阶段 4 实现使用 Python 标准库 `urllib`,并暴露可注入 HTTP 函数以便测试 mock;默认 URL 拼接为 `vlm.api_base_url.rstrip("/") + vlm.chat_completions_path`。
|
||
|
||
### `result_parser.py`
|
||
|
||
- 从 raw response 中提取严格 JSON。
|
||
- 校验 `schema_version`、`events`、`screen_time`、事件枚举等字段。
|
||
- 解析失败触发一次严格 prompt 重试。
|
||
- 仍失败写 `parse_failed`,保留 `raw_response`。
|
||
- 阶段 4 实现支持 raw JSON、markdown/prose 中嵌入 JSON,输出 clip 级 `monitoring_timeline`、`events`、`raw_response`、`processing` 和 `error` 字段。
|
||
|
||
### `aggregator.py`
|
||
|
||
- 消费 `video_manifest.jsonl`、`clip_manifest.jsonl` 和 `clip_results.jsonl`。
|
||
- 聚合为 `videos/<video_id>/video_result.json` 和输出根目录下的 `folder_summary.json`。
|
||
- 按 `merge_gap_seconds` 合并同视频、同类型、相邻时间范围接近的事件。
|
||
- 保留事件相对时间轴、screen_time、clip evidence 和 frame evidence。
|
||
- 统计 `parse_failed` / `inference_failed` clip 数量。
|
||
|
||
### `manifest.py`
|
||
|
||
- 负责 JSONL 读写和状态字段。
|
||
- 支持断点续跑。
|
||
- 每条记录包含 `status`、`retry_count`、`last_error`。
|
||
|
||
## Config Schema
|
||
|
||
`config/local_batch.yaml` 建议字段:
|
||
|
||
```yaml
|
||
input:
|
||
dir: /path/to/videos
|
||
recursive: true
|
||
extensions: [".mp4", ".mov", ".mkv", ".avi", ".flv", ".ts", ".m4v"]
|
||
|
||
source:
|
||
mode: local
|
||
|
||
output:
|
||
dir: ./outputs/local-batch
|
||
overwrite: false
|
||
resume: true
|
||
keep_frames: true
|
||
|
||
hik_cloud:
|
||
api_base_url: https://api2.hik-cloud.com
|
||
download_path: /v1/carrier/cstorage/open/play/download
|
||
access_token: null
|
||
access_token_env: HIK_CLOUD_ACCESS_TOKEN
|
||
chunk_seconds: 600
|
||
timeout_seconds: 60
|
||
download_timeout_seconds: 600
|
||
devices:
|
||
- device_serial: EXAMPLE_DEVICE_SERIAL
|
||
channel_no: 1
|
||
name: example-device
|
||
time_ranges:
|
||
- begin: "2026-02-03 09:00:00"
|
||
end: "2026-02-03 10:00:00"
|
||
|
||
ffprobe:
|
||
timeout_seconds: 30
|
||
|
||
ffmpeg:
|
||
prefer_nvdec: true
|
||
allow_cpu_fallback: false
|
||
hwaccel: cuda
|
||
codec_decoders:
|
||
h264: h264_cuvid
|
||
hevc: hevc_cuvid
|
||
frame_fps: 1
|
||
frame_width: 640
|
||
jpeg_quality: 4
|
||
timeout_seconds_per_video: 3600
|
||
|
||
clip:
|
||
length_seconds: 10
|
||
stride_seconds: 10
|
||
frames_per_clip: 8
|
||
min_frames_per_clip: 4
|
||
|
||
vlm:
|
||
api_base_url: http://localhost:8679
|
||
chat_completions_path: /v1/chat/completions
|
||
model: memai-zhengxin-v3-20260413
|
||
timeout_seconds: 120
|
||
max_tokens: 512
|
||
temperature: 0
|
||
batch_size: 1
|
||
image_transport: data_uri
|
||
retries: 1
|
||
|
||
prompt:
|
||
system: "You are a store video analysis assistant. Return strict JSON only."
|
||
user: "Analyze this clip. Return events and screen_time. If no event, return events: []."
|
||
|
||
schema:
|
||
version: local-batch-v1
|
||
event_types:
|
||
- customer_enter
|
||
- customer_leave
|
||
- queue_detected
|
||
- staff_absent
|
||
- staff_present
|
||
- area_crowded
|
||
- abnormal_behavior
|
||
- unknown
|
||
require_strict_json: true
|
||
parse_retry: 1
|
||
merge_gap_seconds: 30
|
||
|
||
runtime:
|
||
timezone: Asia/Shanghai
|
||
log_level: INFO
|
||
```
|
||
|
||
## File Contracts
|
||
|
||
### `video_manifest.jsonl`
|
||
|
||
One line per discovered video:
|
||
|
||
```json
|
||
{
|
||
"video_id": "stable_hash_or_slug",
|
||
"source_path": "/path/to/video.mp4",
|
||
"status": "pending",
|
||
"probe": null,
|
||
"retry_count": 0,
|
||
"last_error": null
|
||
}
|
||
```
|
||
|
||
### `frame_manifest.jsonl`
|
||
|
||
One line per sampled frame:
|
||
|
||
```json
|
||
{
|
||
"video_id": "stable_hash_or_slug",
|
||
"frame_id": "stable_hash_or_slug_f000120",
|
||
"frame_path": "frames/stable_hash_or_slug/000120.jpg",
|
||
"offset_seconds": 120.0,
|
||
"timecode": "00:02:00",
|
||
"pts_time": 120.0,
|
||
"status": "sampled"
|
||
}
|
||
```
|
||
|
||
### `clip_manifest.jsonl`
|
||
|
||
One line per clip:
|
||
|
||
```json
|
||
{
|
||
"video_id": "stable_hash_or_slug",
|
||
"clip_id": "stable_hash_or_slug_c000012",
|
||
"clip_start_seconds": 120.0,
|
||
"clip_end_seconds": 130.0,
|
||
"clip_start_timecode": "00:02:00",
|
||
"clip_end_timecode": "00:02:10",
|
||
"frame_times": [
|
||
{
|
||
"frame_path": "frames/stable_hash_or_slug/000120.jpg",
|
||
"offset_seconds": 120.0,
|
||
"timecode": "00:02:00"
|
||
}
|
||
],
|
||
"status": "pending",
|
||
"retry_count": 0,
|
||
"last_error": null
|
||
}
|
||
```
|
||
|
||
### `clip_results.jsonl`
|
||
|
||
One line per inferred clip:
|
||
|
||
```json
|
||
{
|
||
"schema_version": "local-batch-v1",
|
||
"video_id": "stable_hash_or_slug",
|
||
"video_path": "/path/to/video.mp4",
|
||
"clip_id": "stable_hash_or_slug_c000012",
|
||
"status": "ok",
|
||
"monitoring_timeline": {
|
||
"timezone": "Asia/Shanghai",
|
||
"video_start_time": null,
|
||
"clip_start_seconds": 120.0,
|
||
"clip_end_seconds": 130.0,
|
||
"clip_start_timecode": "00:02:00",
|
||
"clip_end_timecode": "00:02:10",
|
||
"frame_times": [
|
||
{
|
||
"frame_path": "frames/stable_hash_or_slug/000120.jpg",
|
||
"offset_seconds": 120.0,
|
||
"timecode": "00:02:00"
|
||
}
|
||
],
|
||
"screen_time": "2026-06-14 12:31:20"
|
||
},
|
||
"events": [
|
||
{
|
||
"event_type": "queue_detected",
|
||
"start_time": null,
|
||
"end_time": null,
|
||
"start_offset_seconds": 120.0,
|
||
"end_offset_seconds": 130.0,
|
||
"confidence": 0.86,
|
||
"severity": "medium",
|
||
"attributes": {},
|
||
"evidence": {
|
||
"clip_id": "stable_hash_or_slug_c000012",
|
||
"frame_paths": ["frames/stable_hash_or_slug/000120.jpg"]
|
||
}
|
||
}
|
||
],
|
||
"raw_response": null,
|
||
"processing": {
|
||
"started_at": "2026-06-15T10:00:00+08:00",
|
||
"finished_at": "2026-06-15T10:00:02+08:00",
|
||
"latency_ms": 1800
|
||
},
|
||
"error": null
|
||
}
|
||
```
|
||
|
||
### `video_result.json`
|
||
|
||
Written to:
|
||
|
||
```text
|
||
videos/<video_id>/video_result.json
|
||
```
|
||
|
||
Required top-level fields:
|
||
|
||
```text
|
||
schema_version
|
||
video_id
|
||
video_path
|
||
probe
|
||
monitoring_timeline.video_start_time
|
||
monitoring_timeline.video_duration_seconds
|
||
clip_count
|
||
failed_clip_count
|
||
event_counts
|
||
events
|
||
outputs.clip_results_jsonl
|
||
processing
|
||
```
|
||
|
||
### `folder_summary.json`
|
||
|
||
Required top-level fields:
|
||
|
||
```text
|
||
schema_version
|
||
input_dir
|
||
video_count
|
||
processed_video_count
|
||
failed_video_count
|
||
event_counts
|
||
videos
|
||
processing
|
||
```
|
||
|
||
## Timeline Rules
|
||
|
||
时间轴必须区分三类时间:
|
||
|
||
- 视频相对时间:`offset_seconds`、`timecode`。
|
||
- 画面 OCR 时间:`screen_time` 或模型输出里的 `画面时间`。
|
||
- 处理时间:`processing.started_at`、`processing.finished_at`。
|
||
|
||
本地视频没有可靠业务开始时间时:
|
||
|
||
- `video_start_time` 必须为 `null`。
|
||
- 不允许伪造绝对时间。
|
||
- 事件必须保留 `start_offset_seconds` 和 `end_offset_seconds`。
|
||
|
||
参与推理的实际帧时间必须写入 `frame_times`。不能只写 clip 起止时间。
|
||
|
||
## Reference Code Usage
|
||
|
||
可以参考:
|
||
|
||
- `zhengxin-vlm-0413/shared/vlm_client.py` 的 OpenAI-compatible payload 结构。
|
||
- `zhengxin-vlm-0413/shared/frame_utils.py` 的 base64 data URI 处理方式。
|
||
- `zhengxin-vlm-0413/service/config.yaml` 的 prompt 配置风格。
|
||
|
||
不能直接复用为核心实现:
|
||
|
||
- `frame_utils.extract_frames_from_video`,因为它是整段均匀抽 8 帧,不满足 1 FPS、clip manifest、时间轴要求。
|
||
- `vlm_client.extract_action`,因为它只解析 `Action`,不能覆盖本项目完整事件和时间轴 schema。
|
||
- `rtsp_service.py` 主循环,因为它服务实时 RTSP,不适合离线文件夹批处理。
|
||
|
||
## Validation Matrix
|
||
|
||
### Phase 1 Architecture Validation
|
||
|
||
阶段 1 complete 条件:
|
||
|
||
- `docs/project.md` 固化模块边界、文件输出契约、config schema、时间轴 schema、安全边界和验证矩阵。
|
||
- 推理接口选择已明确为 OpenAI-compatible vLLM。
|
||
- API URL 字段语义已固定为 `api_base_url` + `chat_completions_path`。
|
||
- 已声明参考 `frame_utils.py` / `vlm_client.py` 哪些可借鉴、哪些不能直接复用。
|
||
- 已列出阶段 2-6 的 smoke test 输入、命令、期望输出字段和失败判定标准。
|
||
- 子 agent 审查结论记录到 `progress.md`。
|
||
|
||
### Phase 2 Validation
|
||
|
||
目标:本地视频发现、ffprobe、manifest、CLI 骨架。
|
||
|
||
命令:
|
||
|
||
```bash
|
||
python3 -m py_compile video_ai_analysis_poc/*.py
|
||
python3 -m video_ai_analysis_poc.cli --config config/local_batch.yaml --input-dir /path/to/videos --output-dir ./outputs/local-batch --dry-run
|
||
```
|
||
|
||
期望:
|
||
|
||
- 生成 `video_manifest.jsonl`。
|
||
- 损坏/不支持视频被标记失败,不阻塞其他视频。
|
||
- 不读取或写入参考模型目录。
|
||
|
||
### Phase 3 Validation
|
||
|
||
目标:FFmpeg/NVDEC 1 FPS 抽帧和 clip 构建。
|
||
|
||
命令:
|
||
|
||
```bash
|
||
ffmpeg -hwaccels
|
||
ffmpeg -decoders | grep cuvid
|
||
python3 -m video_ai_analysis_poc.cli --config config/local_batch.yaml --input-dir /path/to/short-videos --output-dir ./outputs/local-batch --until clips
|
||
```
|
||
|
||
期望:
|
||
|
||
- 对一个样例视频实际运行带 `-hwaccel cuda` 和 `h264_cuvid` 或 `hevc_cuvid` 的抽帧命令。
|
||
- 保存 FFmpeg stderr 或日志中的解码器证据。
|
||
- 生成 `frame_manifest.jsonl` 和 `clip_manifest.jsonl`。
|
||
- `clip_manifest.jsonl` 包含 `frame_times`。
|
||
|
||
### Phase 4 Validation
|
||
|
||
目标:vLLM OpenAI-compatible API、prompt 配置、JSON 解析重试。
|
||
|
||
命令:
|
||
|
||
```bash
|
||
curl http://localhost:8679/v1/models
|
||
python3 -m video_ai_analysis_poc.cli --config config/local_batch.yaml --input-dir /path/to/short-videos --output-dir ./outputs/local-batch --until inference --limit-clips 3
|
||
```
|
||
|
||
期望:
|
||
|
||
- prompt 从 config 读取。
|
||
- 请求 URL 使用 `api_base_url + chat_completions_path`。
|
||
- 生成 `clip_results.jsonl`。
|
||
- 每条结果包含 `monitoring_timeline.frame_times` 和 `screen_time` 字段。
|
||
|
||
### Phase 5 Validation
|
||
|
||
目标:clip/video/folder 聚合和 schema 校验。
|
||
|
||
命令:
|
||
|
||
```bash
|
||
python3 -m video_ai_analysis_poc.cli --config config/local_batch.yaml --input-dir /path/to/short-videos --output-dir ./outputs/local-batch
|
||
python3 -m json.tool ./outputs/local-batch/folder_summary.json >/dev/null
|
||
```
|
||
|
||
期望:
|
||
|
||
- 默认 CLI 运行不传 `--dry-run` 或 `--until` 时,会执行到 inference 并继续 aggregation。
|
||
- `--until clips` 和 `--until inference` 仍停在各自阶段,不写聚合输出。
|
||
- 生成 `videos/<video_id>/video_result.json`。
|
||
- 生成 `folder_summary.json`。
|
||
- 事件聚合保留相对时间轴。
|
||
- JSON 可被标准工具解析。
|
||
|
||
### Phase 6 Validation
|
||
|
||
目标:测试环境 smoke test 与文档更新。
|
||
|
||
远端环境:
|
||
|
||
```text
|
||
ssh xiaozheng@192.168.5.100
|
||
/home/xiaozheng/video-ai-analysis-poc
|
||
```
|
||
|
||
模型服务:
|
||
|
||
```bash
|
||
ssh xiaozheng@192.168.5.100 'curl http://localhost:8679/v1/models'
|
||
```
|
||
|
||
当前服务状态:
|
||
|
||
- 容器:`zhengxin-vllm`
|
||
- 镜像:`vllm/vllm-openai:v0.14.1`
|
||
- 端口:`8679`
|
||
- 模型:`memai-zhengxin-v3-20260413`
|
||
- 模型目录挂载:`/home/xiaozheng/zhengxin-vlm-0413/models:/models:ro`
|
||
|
||
远端能力验证命令:
|
||
|
||
```bash
|
||
ssh xiaozheng@192.168.5.100 'nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader'
|
||
ssh xiaozheng@192.168.5.100 'ffmpeg -hwaccels'
|
||
ssh xiaozheng@192.168.5.100 'ffmpeg -decoders'
|
||
```
|
||
|
||
已验证:
|
||
|
||
- GPU: `NVIDIA GeForce RTX 3080`, `20480 MiB`, driver `595.71.05`。
|
||
- FFmpeg 6.1.1 支持 `cuda` hwaccel。
|
||
- FFmpeg decoders 包含 `h264_cuvid` 和 `hevc_cuvid`。
|
||
- `/v1/models` 返回模型 id `memai-zhengxin-v3-20260413`。
|
||
- `/v1/chat/completions` 安全 quoted health check 返回 `OK`。
|
||
|
||
远端 smoke 输入:
|
||
|
||
```text
|
||
/tmp/video-ai-analysis-poc-smoke.h1cZUR/input/sample_h264.mp4
|
||
```
|
||
|
||
远端 smoke 输出:
|
||
|
||
```text
|
||
/tmp/video-ai-analysis-poc-smoke.h1cZUR/output
|
||
```
|
||
|
||
远端批处理命令:
|
||
|
||
```bash
|
||
ssh xiaozheng@192.168.5.100 'PYTHONPATH=/home/xiaozheng/video-ai-analysis-poc python3 -B -m unittest discover -s /home/xiaozheng/video-ai-analysis-poc/tests -v'
|
||
ssh xiaozheng@192.168.5.100 'python3 -B -m compileall -q /home/xiaozheng/video-ai-analysis-poc/video_ai_analysis_poc'
|
||
ssh xiaozheng@192.168.5.100 'PYTHONPATH=/home/xiaozheng/video-ai-analysis-poc python3 -B -m video_ai_analysis_poc.cli --config /home/xiaozheng/video-ai-analysis-poc/config/local_batch.yaml --input-dir /tmp/video-ai-analysis-poc-smoke.h1cZUR/input --output-dir /tmp/video-ai-analysis-poc-smoke.h1cZUR/output --until clips'
|
||
ssh xiaozheng@192.168.5.100 'PYTHONPATH=/home/xiaozheng/video-ai-analysis-poc python3 -B -m video_ai_analysis_poc.cli --config /home/xiaozheng/video-ai-analysis-poc/config/local_batch.yaml --input-dir /tmp/video-ai-analysis-poc-smoke.h1cZUR/input --output-dir /tmp/video-ai-analysis-poc-smoke.h1cZUR/output --until inference --limit-clips 1'
|
||
ssh xiaozheng@192.168.5.100 'PYTHONPATH=/home/xiaozheng/video-ai-analysis-poc python3 -B -m video_ai_analysis_poc.cli --config /home/xiaozheng/video-ai-analysis-poc/config/local_batch.yaml --input-dir /tmp/video-ai-analysis-poc-smoke.h1cZUR/input --output-dir /tmp/video-ai-analysis-poc-smoke.h1cZUR/output'
|
||
```
|
||
|
||
已验证输出:
|
||
|
||
- `video_manifest.jsonl`: 1 条视频记录。
|
||
- `frame_manifest.jsonl`: 12 条 sampled frame 记录。
|
||
- `clip_manifest.jsonl`: 1 条 clip 记录。
|
||
- frame manifest 中持久化 `hwaccel: cuda`、`decoder: h264_cuvid`、`ffmpeg_command` 和 FFmpeg stderr 摘要。
|
||
- `clip_results.jsonl`: 1 条记录,`status: ok`,包含 `monitoring_timeline.frame_times`。
|
||
- `videos/<video_id>/video_result.json`: JSON 可解析,`failed_clip_count: 0`。
|
||
- `folder_summary.json`: JSON 可解析,`video_count: 1`、`processed_video_count: 1`。
|
||
- 本地视频没有可靠业务开始时间时,`monitoring_timeline.video_start_time` 输出 `null`;ffprobe 的 `start_time: 0.0` 只保留在 `probe`。
|
||
|
||
远端验证约束:
|
||
|
||
- 只写入明确输出目录。
|
||
- 不覆盖远端已有模型、配置和视频。
|
||
- 不复制真实凭据到日志或文档。
|
||
|
||
## Known Risks
|
||
|
||
- HEVC decoder 可用性已验证,但实际 smoke 只覆盖 H.264 样例视频。
|
||
- 24 小时真实门店视频吞吐量尚未压测。
|
||
- 海康云眸云录像/RTSP 接入仍在当前本地文件夹 PoC 范围之外。
|
||
- 本地视频可能没有画面内时间戳,必须同时保留相对时间。
|
||
- 模型事件质量尚未用真实门店素材验收;合成测试图没有业务事件,输出空事件是合理结果。
|
||
- 远端 vLLM 容器当前为手工启动,不是生产级 systemd/compose 托管。
|