cmvr_edge_ai/README.md

629 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# cmvr-edge-ai
`cmvr-edge-ai` 是部署在机器人边缘端的、配置驱动的 AI 流水线运行时。它把上游传感器、AI 算法和下游机器人/平台接口拆成可组合节点,通过 YAML 把节点连接成有向无环图DAG。协议只出现在边界连接器中算法节点使用统一的内部消息因此以后增加 HTTP、gRPC、UDP 或 QUIC 时不需要改动算法本身。
当前版本已经包含可运行的检测链路和框架基础设施:
- 严格 YAML 配置校验、环境变量展开和 DAG 编译检查;
- `Source -> Operator -> Sink` 异步运行时;
- 每条边独立的有界队列和明确的溢出策略;
- 版本化插件注册表、检测模型注册表和 Python entry point 扩展机制;
- cmvr-es RGB 相机 gRPC Source带退避上限的指数重连、AGV gRPC Sink
- PyAV H264/H265 有状态解码、通用检测模型节点和重复命中规则节点;
- Construction PPE、六类 PPE YOLOv8n、People-Talking YOLOv8x 与 YOLOv8n Mobile Phone 模型注册,以及按标签、置信度和最大 FPS 的部署配置;
- 平台 HTTP JSON Sink支持有限重试、`raise/log_and_drop` 失败策略,使用告警
`event_id` 作为幂等键,并可把已配置的 gRPC endpoint IP 作为 `grpc_ip` 上报;
- `RobotCommand -> ApprovedRobotCommand` 安全门和无重试的类型化 AGV 命令映射;
- 文本/JSON 日志、共享 gRPC Channel 与 HTTP Client
- `serve` 被动调用入口、版本化推理契约和能力目录,以及只按稳定业务 `category`
选择服务端路由的 Detect Client SDK
- `gauge.analog` 被动调用适配:主服务保持 Python 3.10ETHZ Analog Gauge Reader
通过独立的 Python 3.8 uv 环境和常驻二进制协议 worker 运行;
- 纯 ONNX Runtime 的 YOLOv8 推理 backend 与受控导出工具;原 `.pt` `@1` 版本作为
导出/回滚源保留,三个部署配置已使用带 manifest/SHA256 校验的 `@2` ONNX 制品;
- 可直接执行的最小测试 fixture以及包含 PPE/电话使用检测的主动推送部署配置。
`configs/active_detection.yaml` 是主动检测部署配置。它从同一个相机 decoder 并行运行 Construction PPE 与
People-Talking 两个 detector各自经过独立时间窗口规则后向同一个告警平台上报
`model_id``rule_id` 用于区分事件。六类 PPE 模型仍保留在注册表中,但不在当前
Pipeline 图中实例化。
VAD/ASR/LLM/TTS 尚未内置,因此仓库不再保留不能提供真实能力的 Talk 占位 Pipeline。
主动推送与被动调用可以在同一份配置、同一进程中并存:原有相机 Pipeline 持续向平台
推送事件;被动 Detect Server 则接收单次 HTTP 请求,经服务端拥有的 Pipeline 完成推理并
返回响应。两者共用版本化契约和能力注册表,但不会把模型 ID、Pipeline ID 或权重路径
暴露为客户端可选参数。
## 架构概览
```mermaid
flowchart LR
A["cmvr-es / 平台<br/>gRPC、HTTP、未来 UDP/QUIC"] --> B["Source 连接器"]
B --> C["每条边独立的有界队列"]
C --> D["Operator DAG<br/>解码、检测、VAD、ASR、LLM、TTS、策略"]
D --> E["Safety Gate"]
D --> F["平台 Sink"]
E --> G["机器人执行器 Sink"]
```
内部节点传递 `Envelope[T]`,其中包含载荷、`schema/version`、采集时间、序号、截止时间、`trace_id` 和 `session_id`。连接器负责 protobuf/HTTP JSON 与内部契约之间的转换AI 插件不应直接依赖 cmvr-es protobuf。
完整设计和配置字段见 [docs/architecture.md](docs/architecture.md)。
## 目录
```text
cmvr_edge_ai/
├── .python-version # uv 默认 Python 3.10
├── uv.lock # 所有 profile 的可复现依赖锁
├── configs/
│ ├── README.md # 配置命名、归属与路径规则
│ ├── active_detection.yaml # 主动检测与平台告警
│ ├── server_detect.yaml # 被动 PPE/手机检测 HTTP 服务
│ ├── server_gauge.yaml # 被动 Analog Gauge HTTP 服务
│ └── detection_viewer.yaml # 远端相机 -> 双模型 -> OpenCV 窗口
├── server/
│ ├── detect/ # 被动检测服务说明与 OpenCV Viewer
│ ├── gauge/ # 仪表读数服务及独立 Python 3.8 uv worker
│ └── talk/ # 对话能力边界说明(当前未实现)
├── client/
│ └── detect/ # 不依赖 Pipeline 的 HTTP Detect Client 示例
├── models/
│ └── detection/ # 按模型 ID/版本组织的检测模型制品库
│ ├── construction-ppe-yolov8/v1,v2/ # PT 源 + ONNX/manifest/model card
│ ├── ppe-6classes-yolov8n/v1,v2/ # PT 源 + ONNX/manifest/model card
│ ├── people-talking-yolov8x/v1,v2/ # PT 源 + ONNX/manifest/model card
│ └── yolov8n-mobile-phone/v2/ # ONNXv1 PT 保留在父目录
├── scripts/
│ ├── bootstrap.sh # 一键创建 uv 环境、生成 bindings 并自检
│ ├── export_detection_onnx.py # 受控导出检测模型 @2 ONNX 制品与 manifest
│ └── generate_cmvr_stubs.py # 从 cmvr-es proto 生成 Python bindings
├── src/cmvr_edge_ai/
│ ├── config/ # 配置模型、加载与环境变量展开
│ ├── core/ # Envelope、组件接口、队列和 DAG 运行时
│ ├── contracts/ # 内部消息及 inference/catalog 公共 wire 契约
│ ├── capabilities/ # 稳定 category、服务模式与模型能力注册表
│ ├── client/ # 远程调用 SDK当前包含 Detect HTTP Client
│ ├── server/ # 请求 broker、边界节点、能力目录与 HTTP API
│ ├── detection/ # 模型注册、视频解码、推理与时间窗口规则
│ ├── gauge/ # Analog Gauge 主进程 adapter 与 Python 3.8 worker
│ ├── plugins/ # 插件注册、发现和内置基础插件
│ ├── connectors/ # cmvr-es 与平台边界连接器
│ ├── transports/ # 已实现的 gRPC/HTTP 连接池
│ ├── workers/ # 显式线程 offload 与常驻进程 Worker 工具
│ ├── observability/ # 低开销文本/JSON 日志
│ ├── application.py # 多 Pipeline 与共享网络客户端的所有者
│ ├── compiler.py # 配置到可执行 DAG 的编译器
│ └── cli.py # validate/run/serve/plugins/models
└── tests/
└── fixtures/
└── minimal_pipeline.yaml # 不依赖外部服务的框架/CLI 自检配置
```
## 快速开始
项目使用 `uv` 管理 Python、`.venv` 和锁定依赖。`.python-version` 默认选择
Python 3.10,支持范围是 3.103.12首次执行时uv 会在本机没有合适解释器时
自动安装。先确认已经安装 uv
```bash
uv --version
```
未安装时可使用 uv 官方安装器:
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
只验证框架核心时,一条命令创建最小环境并自检:
```bash
cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
bash scripts/bootstrap.sh --profile core
```
`uv.lock` 是安装的唯一版本来源bootstrap 使用 `uv sync --locked`,不会在用户
机器上重新选择依赖版本。无需 `source .venv/bin/activate`,统一通过
`uv run --no-sync` 使用已经安装好的环境:
配置中的相对文件路径按进程启动时的当前工作目录(`cwd`)解析,不是按 YAML
文件所在目录解析。因此本文的 bootstrap、validate、run 和 Viewer 命令都应从仓库根目录
`/home/xtkuang/Projects/cmvr/cmvr_edge_ai` 执行;从其他目录启动时,必须把
配置中的模型等文件路径改为正确的绝对路径。
```bash
uv run --no-sync cmvr-edge-ai validate \
--config tests/fixtures/minimal_pipeline.yaml
uv run --no-sync cmvr-edge-ai plugins
uv run --no-sync cmvr-edge-ai run \
--config tests/fixtures/minimal_pipeline.yaml \
--log-level INFO \
--log-format text
```
预期会看到两条日志,内容分别包含 `framework-ready``bounded-dag-running`。该 Source 是有限数据源,数据处理完后进程会自然退出。
bootstrap 支持以下环境:
| Profile | 安装内容 | 命令 |
|---|---|---|
| `core` | 框架核心与最小测试 fixture | `bash scripts/bootstrap.sh --profile core` |
| `detection-cpu` | gRPC、HTTP、PyAV、Pillow 和 CPU ONNX Runtime默认值 | `bash scripts/bootstrap.sh` |
| `dev` | `detection-cpu` 加锁定的 ONNX exporter、测试和 protobuf codegen | `bash scripts/bootstrap.sh --profile dev` |
如果只希望安装、不执行自检,可加 `--skip-check`。完整参数通过以下命令查看:
```bash
bash scripts/bootstrap.sh --help
```
不要使用 `uv sync --all-extras`:旧 PT adapter 的 `yolo`/`yolo-cpu` 与构建期
`onnx-export-cpu` 具有互斥依赖来源。请使用 bootstrap profile 或只选择目标 extra。
告警图片的画框和 JPEG 编码由独立的 `image` extra 提供;默认的
`detection-cpu`/`dev` profile 已安装它,手动组合检测环境时也必须选择
`--extra image`
启动被动 HTTP 服务需要 `server` extra使用仓库提供的 Detect Client 还需要 `http`
extra。CPU ONNX 检测服务可显式安装以下组合:
```bash
uv sync --locked --extra server --extra http --extra onnx-cpu
```
CLI 的五个子命令如下:
| 命令 | 用途 |
|---|---|
| `validate -c FILE [--pipeline ID]` | 加载配置、展开环境变量、构造插件并校验 DAG不启动 Pipeline |
| `run -c FILE [--pipeline ID]` | 启动选中的 Pipeline未指定时启动所有 `enabled: true` 的 Pipeline |
| `serve -c FILE` | 启动所有已启用 Pipeline 和远程推理 HTTP API要求 `server.enabled: true` |
| `plugins` | 列出内置插件和已安装 entry point 插件 |
| `models` | 列出检测模型 ID、名称、backend 和注册的全部标签 |
`--pipeline` 可以重复传给 `validate``run`。`run`、`serve` 还支持 `--log-level`
`--log-format text|json``serve` 按配置启动全部已启用 Pipeline不接受客户端或命令行
覆盖单条 route 的模型选择。配置错误退出码为 `2`,运行错误为 `1`,键盘中断为 `130`
主动推送配置为 `configs/active_detection.yaml`,当前只包含 `detection` Pipeline。
部署时可以显式传 `--pipeline detection`;省略时会启动配置中所有已启用 Pipeline。
## 被动检测 HTTP 服务
被动调用使用统一的 `InferenceRequest/v1`、`InferenceResponse/v1` 和能力目录契约。
阶段一提供两个入口:
| HTTP 入口 | 用途 |
|---|---|
| `GET /v1/models` | 返回 `cmvr.model-catalog/v1`,分别列出 `active_push``passive_invoke` 能力及部署状态 |
| `POST /v1/inference` | 接收 `cmvr.inference-request/v1` JSON并返回 `cmvr.inference-response/v1` 或稳定的错误响应 |
客户端只提交稳定业务 category例如 `detect.ppe``detect.mobile_phone`。客户端不允许
指定 `model_id`、Pipeline ID 或权重路径;`server.routes.<category>` 与对应 Pipeline 配置
决定实际模型、制品、provider 和队列容量。这样服务端可以升级实现而不改变调用方协议。
从仓库根目录校验并启动示例服务:
```bash
uv run --no-sync cmvr-edge-ai validate \
--config configs/server_detect.yaml
uv run --no-sync cmvr-edge-ai serve \
--config configs/server_detect.yaml \
--log-level INFO \
--log-format text
```
服务启动后,可在另一终端查询能力并通过 Detect Client 调用 `POST /v1/inference`
```bash
curl -sS http://127.0.0.1:8081/v1/models
CMVR_DETECT_BASE_URL=http://127.0.0.1:8081 \
uv run --no-sync python client/detect/example.py \
/path/to/image.jpg detect.ppe
```
存活与就绪探针分别是 `GET /health/live``GET /health/ready`。完整服务端配置和链路见
[server/detect/README.md](server/detect/README.md)SDK 契约与错误分类见
[client/detect/README.md](client/detect/README.md)。
默认示例仅监听 loopback。监听局域网或其他非 loopback 地址时,配置必须包含
`server.http.bearer_token`,并默认要求 `tls_certfile``tls_keyfile`;只有受信隔离网络
才能显式设置 `allow_insecure_remote: true` 使用明文 HTTP。除 `/health/live`
`/health/ready` 外,所有 HTTP 入口都要求 `Authorization: Bearer <token>`
被动调用链采用 `request/block` 单线性 DAG。每个 Operator 必须声明并在运行时满足
`exactly_one` 输出;单条请求的解码/模型异常会返回 500但不会终止可复用 Pipeline。
broker 使用每次提交生成的 invocation token 防止迟到结果串到复用的 `request_id`;已经
进入模型的超时任务会继续占用该 route 容量,直到真实执行结束,避免超时请求堆积。
## Analog Gauge 被动服务
[ethz-asl/analog_gauge_reader](https://github.com/ethz-asl/analog_gauge_reader) 使用同一个
`POST /v1/inference` URL客户端只需选择 `category: gauge.analog`。它返回通用
`scalar` 输出,并可按请求返回 `original`、`annotated` 和 `diagnostics` artifact。
该上游项目依赖 Python 3.8、Torch 2.0、MMCV/MMOCR与主项目 Python 3.10 环境隔离。
首次部署从仓库根目录执行:
```bash
bash server/gauge/worker/bootstrap.sh
uv run --no-sync cmvr-edge-ai validate \
--config configs/server_gauge.yaml
uv run --no-sync cmvr-edge-ai serve \
--config configs/server_gauge.yaml \
--log-level INFO
```
worker 在进程内只串行处理一个请求,模型只加载一次;请求超时或协议失配时会终止被污染
的子进程,并在下一次请求时重新创建。项目路径和三份权重路径只存在于服务端配置,调用方
不能覆盖。完整准备步骤、请求 JSON 和当前 Linux x86_64 限制见
[server/gauge/README.md](server/gauge/README.md)。
## ONNX 检测迁移
四个检测模型都保留 `ultralytics-yolo``@1` 注册,同时提供
`onnxruntime-yolov8``@2` 注册。`@2` backend 直接执行静态 batch=1、NMS 外置的
YOLOv8 ONNX 图,运行环境只需 NumPy、Pillow 和 ONNX Runtime不导入 Torch 或
Ultralytics
```bash
uv sync --locked --extra onnx-cpu
```
导出是单独的构建步骤,必须使用锁定的 exporter 环境;以下命令以 Mobile Phone 模型为例:
```bash
uv sync --locked --extra onnx-export-cpu
uv run --no-sync python scripts/export_detection_onnx.py \
--model-id yolov8n-mobile-phone@2
```
脚本会校验来源标签、静态输入输出和 ONNX metadata清除训练机路径/时间戳,并在
`v2/` 写入 `model.onnx` 与 SHA256 `manifest.json`。四份制品已经生成,三个部署 YAML
已切换到 `@2`;运行时会在 session 创建前验证 manifest、模型身份与制品哈希。当前已
完成真实 ORT smoke但正式现场使用仍要用授权图片完成 `.pt(rect=False)`/ONNX 框与
置信度一致性、延迟、峰值内存和长期稳定性验收。YOLOv8x People-Talking 计算量较大,
格式转换本身不会把它变成轻量网络。
具体状态与普通 Git 制品规则见 [models/detection/README.md](models/detection/README.md)。
## 运行视觉检测链路
默认 bootstrap 就是当前 YAML 使用的 CPU 检测环境。它会从相邻的
`../cmvr-es` 读取 proto、用锁定的 `grpcio-tools` 生成 bindings然后安装完整
检测依赖并校验最小测试 fixture 和统一配置中的 PPE 链路:
```bash
cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
bash scripts/bootstrap.sh
```
如果 cmvr-es 不在相邻目录,显式指定路径:
```bash
bash scripts/bootstrap.sh \
--cmvr-es-root /home/xtkuang/Projects/cmvr/cmvr-es
```
bootstrap 默认使用 portable codegen所以不要求 cmvr-es 已经编译出 `protoc`
要跳过生成(例如部署包已经包含匹配版本的 bindings使用
`--skip-codegen`。手动生成时可以执行:
```bash
uv sync --locked --only-group codegen
.venv/bin/python scripts/generate_cmvr_stubs.py \
--cmvr-es-root /home/xtkuang/Projects/cmvr/cmvr-es \
--portable
```
生成文件默认写入 `src/cmvr/...`,使 `cmvr.api.*_pb2` 可以被连接器导入。
生成后应再次执行目标 profile 的 `uv sync --locked`让可编辑安装识别新包bootstrap
已经按这个顺序处理。
`configs/active_detection.yaml``detection` Pipeline 中配置部署参数:
```yaml
endpoints:
cmvr_es:
target: 192.168.0.119:50052
ppe_alert_platform:
base_url: http://192.168.0.222:13080
pipelines:
detection:
nodes:
camera:
with:
device_id: wrist_cam
stream_log_interval_s: 5
detector:
with:
model: construction-ppe-yolov8@2
attach_frame: true
inference_log_interval_s: 5
model_options:
weights: models/detection/construction-ppe-yolov8/v2/model.onnx
providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
repeat_gate:
with:
alert_image:
enabled: true
jpeg_quality: 85
phone_detector:
with:
model: people-talking-yolov8x@2
detect_labels: [talking on phone]
max_fps: 5
attach_frame: true
model_options:
weights: models/detection/people-talking-yolov8x/v2/model.onnx
providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
phone_repeat_gate:
with:
alert_image:
enabled: true
jpeg_quality: 85
alert_platform:
with:
endpoint: ppe_alert_platform
grpc_endpoint: cmvr_es
failure_mode: log_and_drop
```
然后验证并运行检测链路,不再要求预先导出环境变量:
```bash
uv run --no-sync cmvr-edge-ai models
uv run --no-sync cmvr-edge-ai validate \
--config configs/active_detection.yaml \
--pipeline detection
uv run --no-sync cmvr-edge-ai run --config configs/active_detection.yaml \
--pipeline detection \
--log-level INFO \
--log-format json
```
启动后,`detector` 和 `phone_detector` 会分别输出一条 `detection model loaded`,表示
两份权重已经成功加载。收到解码帧并完成真实 `predict` 后,会分别输出
`detection inference`,之后
`inference_log_interval_s` 聚合输出一次;其中 `window_frames` 是本周期推理帧数,
`window_detections` 是检测框总数,`hit_labels` 是各标签的检测框累计数。持续只有 loaded 而没有
inference说明相机或 decoder 尚未把帧送到模型inference 中 detection 为 0 只表示
当前阈值下没有命中。短时调试可设为 `1` 秒,生产环境可设为 `30``60` 秒,省略则关闭
周期推理日志。这里使用标准日志而不是裸 `print`,因此与 `--log-format json` 兼容。
如果需要直接观察两个模型在同一帧上的画框结果,使用独立的 OpenCV Demo。它连接同一个
cmvr-es gRPC CameraService共享解码帧并行执行 PPE 与 YOLOv8n Mobile Phone再把两个结果
合并到一个窗口;它不经过重复触发规则,也不会访问 HTTP 平台:
```bash
uv run --no-sync python server/detect/show_detections.py \
--config configs/detection_viewer.yaml \
--pipeline detection_show \
--log-level INFO
```
运行前在 `configs/detection_viewer.yaml` 中配置远端地址、`device_id` 和两份模型
权重;按 `q``Esc` 退出。框标签使用 `PPE:`/`PHONE:` 前缀,手机框标签为
`PHONE:mobile_phone`,窗口刷新率由较慢的模型分支决定。详细说明见
[server/detect/README.md](server/detect/README.md#opencv-实时画框-viewer)。
相机连接器会在每次首次连接或重连时先发 `CameraService.StartCamera`,收到成功反馈后
才建立 `GetRGBImageStream`。终端会依次出现 `camera start requested/succeeded`
`camera stream opening`、`camera stream first frame` 和周期性的 `camera stream progress`
如果只有 opening 而没有 first frameprogress 中会持续显示
`first_frame_received=false window_frames=0`用于区分“RPC 已建立但相机没有出帧”。
运行前需要确认 Construction PPE 与 People-Talking 的 `model.onnx`、`manifest.json`
存在cmvr-es 已启用配置相机,并且告警平台 `/v1/detection-alerts` 可访问。默认
`detection-cpu` profile 只安装 NumPy、Pillow 和 ONNX Runtime不安装 Torch 或
Ultralytics。CUDA/TensorRT、Jetson 和其他 provider 必须建立与目标设备匹配的独立
依赖/锁文件并重新验收,不能只修改 `providers` 就认为硬件环境已经就绪。
`detection.model@1` 根据 `model``DetectionModelRegistry` 解析模型。`detect_labels` 只选择需要检测的标签,省略时检测注册模型的全部标签;`confidence` 是全局阈值,也可以用 `label_confidence` 为个别标签覆盖。当前四个内置模型都可以通过 `cmvr-edge-ai models` 查看,模型制品和独立 model card 位于仓库内:
- [Construction PPE ONNX v2](models/detection/construction-ppe-yolov8/v2/README.md):包含正向 PPE、`No-*` 违规类和施工现场设备类;
- [PPE YOLOv8n 6 Classes ONNX v2](models/detection/ppe-6classes-yolov8n/v2/README.md):六类正向装备检测模型,当前未实例化;
- [People Talking ONNX v2](models/detection/people-talking-yolov8x/v2/README.md):电话使用行为检测,只选择 `talking on phone`
- [YOLOv8n Mobile Phone ONNX v2](models/detection/yolov8n-mobile-phone/v2/README.md)Viewer 和被动服务使用的单类手机检测。
注册 ID 中 `@N` 与版本目录 `vN` 对应Mobile Phone 的 v1 PT 是历史扁平布局v2 已
恢复版本目录。实际 `weights` 路径仍由部署 YAML 显式指定。标签顺序、训练指标、局限
和许可声明以各版本 model card 为准。
`attach_frame: true` 让检测结果临时携带对应的解码帧,供后续告警节点使用;因此原 detector 到 repeat gate 的队列应保持较小,避免堆积未压缩图像。
六类模型的标签是 `Gloves`、`Vest`、`goggles`、`helmet`、`mask` 和 `safety_shoe`
语义是“画面中检测到了该装备”,不是“人员缺少该装备”。它没有 `Person``No-*`
类,也没有人员与装备关联能力,因此不能只靠配置推断某个人未佩戴 PPE。该模型当前仅
注册、未被 `configs/active_detection.yaml``detection` Pipeline 引用;需要恢复第二分支时,应同时配置 detector、
8082 endpoint、HTTP Sink 和两条关联 edge。
当前 Construction PPE 和 People-Talking 分支从 decoder 输出端口 fan-out共享同一个
相机订阅和 PyAV decoder两个模型仍会分别加载和推理并共享应用的有界线程池。
People-Talking 是视觉行为判断,不能证明电话已接通,也不能区分来电和去电。
`detection.repeat_gate@1` 只在一个规则的 `window_ms` 内看到至少 `min_hits` 个不同帧后生成 `DetectionAlert/v1`。同一帧有多个同类框仍只算一次;触发后进入 `cooldown_ms`,冷却期间不累计,结束后必须重新满足次数。`scope: source` 按相机统计;`scope: track` 按 `track_id` 统计,但当前 YOLO adapter 只做逐帧检测,不产生 `track_id`,因此使用 track 规则前必须增加跟踪/关联节点。
启用 `alert_image`repeat gate 只在规则真正触发时使用 Pillow 对阈值帧画框并编码 JPEG不会给每一帧都生成图片。告警的 `detections` 和图片中的 bounding boxes 都来自达到 `min_hits` 的阈值帧;窗口内更早帧只参与 `hit_count`、时间范围和最大置信度统计。
`alert_platform.with.grpc_endpoint: cmvr_es` 会读取 `endpoints.cmvr_es.target`,从
`192.168.0.119:50052` 这样的字面 IP target 中去掉端口,并把结果放在 HTTP JSON
顶层。`source_id` 仍是相机 ID二者不会互相覆盖
```json
{
"schema": "DetectionAlert/v1",
"source_id": "wrist_cam",
"grpc_ip": "192.168.0.119",
"payload": {"event_id": "...", "image": {"encoding": "base64", "data": "..."}}
}
```
被引用 target 必须是单个字面 IPv4/IPv6 加端口hostname、Unix socket 和多地址
target 会在 Sink setup 时被拒绝,避免 DNS 地址变化造成边缘端身份漂移。
HTTP JSON 中完整图片对象位于 `payload.image`
```json
{
"image": {
"media_type": "image/jpeg",
"width": 1280,
"height": 720,
"encoding": "base64",
"data": "..."
}
}
```
这里的外层对象是 `DetectionAlert` payload 的片段HTTP JSON Sink 会把内部 JPEG
`bytes` 转成上述扁平 Base64 图片对象。未启用图片,或运行时因第三方结果未附带
帧、坏帧等原因渲染失败时,告警仍会发送且 `payload.image``null`
同一阈值帧若同时触发多条规则或多个 track只编码一次相关框的并集并让这些
告警共享同一个不可变 JPEG 对象,以限制边缘端瞬时 CPU 和内存开销。
Talk Server 当前只保留能力边界说明;在真实音频 proto、VAD/ASR/LLM/TTS 节点和远程
调用契约落地前,不提供容易被误认为可部署服务的占位配置。
## 配置最小示例
```yaml
api_version: cmvr.edge.ai/v1
runtime:
thread_workers: 2
shutdown_timeout_s: 5
pipelines:
example:
enabled: true
nodes:
source:
uses: core.sequence_source@1
with:
items: [hello]
schema_name: TextEvent
schema_version: 1
sink:
uses: core.log_sink@1
edges:
- from: source.output
to: sink.input
qos:
profile: request
capacity: 4
overflow: block
```
配置模型是严格的,多余字段会报错。字符串支持以下环境变量形式:
- `${NAME}`:变量必须存在;
- `${NAME:-default}`:未设置或为空时使用默认值;
- `env://NAME`:整个字符串取自必填环境变量。
v1 支持五个 `qos.profile`,并在编译期约束其溢出策略:编码 H264/H265 在解码前必须使用 `video_contiguous`,只能阻塞或拒绝;解码后的完整图像可使用 `realtime_latest` 丢旧帧控制延迟;`audio_contiguous` 和 `request` 只能阻塞或拒绝;`telemetry` 支持全部策略。未填写 QoS 时使用无损的 `request + block + capacity=1`。`reject` 会抛异常,`error` 是它的兼容别名。`max_age_ms`、`put_timeout_ms` 已预留,设置后 `validate` 会拒绝配置。完整矩阵见架构文档。
## 注册新的检测模型
检测模型和 DAG 插件是两层注册:流水线固定使用通用的 `detection.model@1`,具体模型通过 `DetectionModelRegistry` 注册 `DetectionModelSpec`。每个 spec 必须给出版本化 `model_id`、面向运维的 `name`、有序且唯一的 `supported_labels`、`backend` 和 factory。factory 返回实现 `load/predict/close``DetectionModel`;部署 YAML 中的 `model_options` 原样交给它。第三方模型包可以使用 `cmvr_edge_ai.detection_models` entry point 发布 spec 或注册回调。安装后先执行 `cmvr-edge-ai models`,再让配置引用其中的精确模型 ID。
仓库自带制品统一放在 `models/detection/<model-name>/vN/`,并在每个版本目录保存
`README.md` model card。新的 `model_id` 尾部 `@N` 应与制品目录 `vN` 保持一致。
模型实际输出的标签仍会在通用 Operator 边界二次校验和过滤;模型返回未注册标签会让节点失败。直接相连的重复规则若引用了 detector 没有选择的标签,也会在 `validate` 阶段被编译器拒绝。
## 开发插件
插件必须使用带版本的稳定 ID例如 `example.text_upper@1`,并声明节点种类、输入/输出端口及 schema。工厂签名固定为 `(node_id, params)`;组件分别继承 `Source`、`Operator` 或 `Sink`
```python
from collections.abc import Mapping
from typing import Any
from cmvr_edge_ai.contracts import TextEvent
from cmvr_edge_ai.core import Emission, Envelope, Operator
from cmvr_edge_ai.plugins import PluginKind, PluginRegistry, PluginSpec
class UppercaseOperator(Operator):
def __init__(self, node_id: str, params: Mapping[str, Any]) -> None:
self.node_id = node_id
async def process(
self, envelope: Envelope[Any], input_port: str = "input"
) -> Emission:
event = envelope.payload
if not isinstance(event, TextEvent):
raise TypeError("expected TextEvent")
result = TextEvent(text=event.text.upper(), role=event.role, final=event.final)
return Emission(
"output",
envelope.with_payload(result, schema_name="TextEvent", schema_version=1),
)
def register_plugins(registry: PluginRegistry) -> None:
registry.register(
PluginSpec(
plugin_id="example.text_upper@1",
kind=PluginKind.OPERATOR,
factory=UppercaseOperator,
inputs={"input": "TextEvent/v1"},
outputs={"output": "TextEvent/v1"},
description="Uppercase text events",
)
)
```
在插件包的 `pyproject.toml` 中注册:
```toml
[project.entry-points."cmvr_edge_ai.plugins"]
example = "my_cmvr_plugin.plugins:register_plugins"
```
安装插件包后,用下面的命令确认发现成功:
```bash
uv run --no-sync cmvr-edge-ai plugins
```
配置文件不能通过 `module:Class` 任意导入代码;只会使用内置或已安装 entry point 注册的插件。插件完整生命周期、返回值规范和连接器开发约定见 [docs/architecture.md](docs/architecture.md#5-插件开发约定)。
## 机器人控制安全边界
控制链固定为:
```text
Policy -> RobotCommand/v1
-> safety.robot_command_gate@1
-> ApprovedRobotCommand/v1
-> cmvr.grpc.agv_command_sink@1
```
AGV Sink 只接受 `ApprovedRobotCommand`,原始 `RobotCommand` 会在运行时被拒绝。配置编译器还要求安全门是执行器的直接前驱;安全门与 actuator 之间不能插入其他节点,也不能增加绕过安全门的输入边。内置安全门负责过期检查和默认 5 秒的最大 TTL、动作/设备白名单、参数范围、最小发送间隔及默认开启的单调序号检查;不合格命令会被丢弃并记录 warning不会因为一条业务拒绝停止整条 Pipeline。
AGV Sink 必须绑定固定的非空 `device_id`。`set_velocity` 默认禁用;`unsafe_allow_unleased_velocity: true` 只是面向隔离测试场景的显式逃生开关,并且仍要求 Sink 配置 `vx/vy/wz``velocity_limits`。它只能限制当前客户端发出的数值,并在正常关闭时尽力调用 `stopVelocityControl`;进程崩溃、`SIGKILL`、断电或网络分区时无法保证停车。
真实机器人要启用持续速度控制,必须先在 cmvr-es 服务端实现带过期时间的 lease/deadman客户端停止续租后由 cmvr-es 在独立于 edge-ai 进程的安全路径中自动清零速度并停车。客户端的安全门、TTL 和 shutdown hook 不能替代这项服务端保护。
## 当前实现边界
- 音频内部契约 `AudioChunk/v1` 已定义,但 cmvr-es 麦克风/扬声器双向流 proto 和连接器尚未落地。
- UDP 与 QUIC 目录目前是扩展占位,没有可用传输实现。
- 运行时 v1 只支持 `execution.mode: async|inline`,两者当前都是单 task、单并发执行。`thread`、`process`、`model_worker` 是保留值;`concurrency != 1`、非空 `max_in_flight/timeout_s`、`ordered: false` 也是保留配置,都会在编译期被拒绝。插件仍可在组件内部显式使用 `workers.run_blocking()`、`PersistentProcessWorker` 或自有模型 Worker但不能把 `execution` 声明误当作自动调度。
- v1 对未实现的声明采取 fail-closed显式设置 `runtime.max_processes/process_start_method/health_bind/reserved_memory_mb`、非默认 pipeline `priority` 或任何非空 `resources` 都会在编译期被拒绝。`serve` 已提供 `/health/live``/health/ready`;详细节点健康信息与队列统计仍只能通过 Python API 获取。
- 当前没有配置热更新、配置 overlay、持久化 outbox/spool 或共享内存图像池。HTTP Sink
只有当前进程内的有限重试;默认 `failure_mode: raise` 会终止 Pipeline当前检测配置的
`log_and_drop` 则在重试耗尽后记录 WARNING 并丢弃该告警。两种模式在进程退出或断电时
都可能丢失未上报数据。
- 相机 Source 可以重连,但 cmvr-es 当前服务端通过 `getLatestEncodedFrame` 获取最新编码数据;如果上游在 edge-ai 收到之前已跳过 H264/H265 参考包,`video_contiguous` 无法补回数据,解码器只能在错误后重置并等待关键帧。正式部署应验证 cmvr-es 输出的是连续 access unit 流,或改为对 AI 友好的原始/JPEG/可检测不连续性的接口。
- 内置 YOLO adapter 不运行 tracker所有检测的 `track_id` 都为空;当前示例因此使用 `scope: source`
- PPE 权重仓库对权重的许可说明与 Ultralytics runtime/checkpoint 中的 AGPL 信息需要在商业交付前核对,并同时确认训练数据和权重分发许可。
- 端口 schema 在编译期按字符串匹配;运行时不会自动验证 Python payload 类型,插件必须在边界处主动检查。
- AGV 执行器不会自动重试。安全门已提供动作/设备白名单、TTL、参数范围、最小间隔和单调序号检查但设备状态、command ID 去重、来源仲裁、服务端 lease/deadman 和急停优先级仍需按机器人型号补齐。服务端 deadman 未完成前,禁止把 `set_velocity` 用于真实机器人持续运动。
这些限制及推荐演进顺序在 [docs/architecture.md](docs/architecture.md#9-当前限制与演进顺序) 中有更详细说明。