refactor edge ai server and add onnx models

This commit is contained in:
xtkuang 2026-07-22 15:17:34 +08:00
parent 76189302a9
commit 9c0b2a3a8a
482 changed files with 18156 additions and 984 deletions

0
.gitattributes vendored
View File

2
.gitignore vendored
View File

@ -3,3 +3,5 @@
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*.egg-info/ *.egg-info/
/build/
/dist/

292
README.md
View File

@ -11,19 +11,28 @@
- cmvr-es RGB 相机 gRPC Source带退避上限的指数重连、AGV gRPC Sink - cmvr-es RGB 相机 gRPC Source带退避上限的指数重连、AGV gRPC Sink
- PyAV H264/H265 有状态解码、通用检测模型节点和重复命中规则节点; - PyAV H264/H265 有状态解码、通用检测模型节点和重复命中规则节点;
- Construction PPE、六类 PPE YOLOv8n、People-Talking YOLOv8x 与 YOLOv8n Mobile Phone 模型注册,以及按标签、置信度和最大 FPS 的部署配置; - Construction PPE、六类 PPE YOLOv8n、People-Talking YOLOv8x 与 YOLOv8n Mobile Phone 模型注册,以及按标签、置信度和最大 FPS 的部署配置;
- 平台 HTTP JSON Sink支持有限重试、`raise/log_and_drop` 失败策略,使用告警 - 平台 HTTP JSON Sink支持有限重试、`raise/log_and_drop` 失败策略,使用告警
`event_id` 作为幂等键; `event_id` 作为幂等键,并可把已配置的 gRPC endpoint IP 作为 `grpc_ip` 上报
- `RobotCommand -> ApprovedRobotCommand` 安全门和无重试的类型化 AGV 命令映射; - `RobotCommand -> ApprovedRobotCommand` 安全门和无重试的类型化 AGV 命令映射;
- 文本/JSON 日志、共享 gRPC Channel 与 HTTP Client - 文本/JSON 日志、共享 gRPC Channel 与 HTTP Client
- 可直接执行的最小测试 fixture以及包含 PPE/电话使用检测和对话占位链路的统一部署配置。 - `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/edge_ai.yaml` 是统一部署配置,其中同时定义 `detection``talk` 两个 `configs/active_detection.yaml` 是主动检测部署配置。它从同一个相机 decoder 并行运行 Construction PPE 与
Pipeline。`detection` 从同一个相机 decoder 并行运行 Construction PPE 与
People-Talking 两个 detector各自经过独立时间窗口规则后向同一个告警平台上报 People-Talking 两个 detector各自经过独立时间窗口规则后向同一个告警平台上报
`model_id``rule_id` 用于区分事件。六类 PPE 模型仍保留在注册表中,但不在当前 `model_id``rule_id` 用于区分事件。六类 PPE 模型仍保留在注册表中,但不在当前
Pipeline 图中实例化。 Pipeline 图中实例化。
VAD/ASR/LLM/TTS 尚未内置;`talk` 仍使用模拟音频数据,等待 cmvr-es 音频双向流 VAD/ASR/LLM/TTS 尚未内置,因此仓库不再保留不能提供真实能力的 Talk 占位 Pipeline。
proto 落地。
主动推送与被动调用可以在同一份配置、同一进程中并存:原有相机 Pipeline 持续向平台
推送事件;被动 Detect Server 则接收单次 HTTP 请求,经服务端拥有的 Pipeline 完成推理并
返回响应。两者共用版本化契约和能力注册表,但不会把模型 ID、Pipeline ID 或权重路径
暴露为客户端可选参数。
## 架构概览 ## 架构概览
@ -48,36 +57,44 @@ cmvr_edge_ai/
├── .python-version # uv 默认 Python 3.10 ├── .python-version # uv 默认 Python 3.10
├── uv.lock # 所有 profile 的可复现依赖锁 ├── uv.lock # 所有 profile 的可复现依赖锁
├── configs/ ├── configs/
│ ├── edge_ai.yaml # detection + talk 统一部署配置 │ ├── README.md # 配置命名、归属与路径规则
│ └── debug/ │ ├── active_detection.yaml # 主动检测与平台告警
│ └── detection_viewer.yaml # 远端相机 -> PPE+电话检测 -> 合并画框窗口 │ ├── server_detect.yaml # 被动 PPE/手机检测 HTTP 服务
├── detect_server/ │ ├── server_gauge.yaml # 被动 Analog Gauge HTTP 服务
│ ├── README.md # PPE/电话检测链路与 Viewer 使用说明 │ └── detection_viewer.yaml # 远端相机 -> 双模型 -> OpenCV 窗口
│ └── show_detections.py # OpenCV 实时检测结果 Demo ├── server/
│ ├── detect/ # 被动检测服务说明与 OpenCV Viewer
│ ├── gauge/ # 仪表读数服务及独立 Python 3.8 uv worker
│ └── talk/ # 对话能力边界说明(当前未实现)
├── client/
│ └── detect/ # 不依赖 Pipeline 的 HTTP Detect Client 示例
├── models/ ├── models/
│ └── detection/ # 按模型 ID/版本组织的检测模型制品库 │ └── detection/ # 按模型 ID/版本组织的检测模型制品库
│ ├── construction-ppe-yolov8/v1/ # best.pt + 独立 model card │ ├── construction-ppe-yolov8/v1,v2/ # PT 源 + ONNX/manifest/model card
│ ├── ppe-6classes-yolov8n/v1/ # best.pt + 独立 model card │ ├── ppe-6classes-yolov8n/v1,v2/ # PT 源 + ONNX/manifest/model card
│ ├── people-talking-yolov8x/v1/ # best.pt + 独立 model card │ ├── people-talking-yolov8x/v1,v2/ # PT 源 + ONNX/manifest/model card
│ └── yolov8n-mobile-phone/ # Viewer 手机目标检测权重 + model card │ └── yolov8n-mobile-phone/v2/ # ONNXv1 PT 保留在父目录
├── talk_server/
│ └── nodes/ # 对话插件预留目录
├── scripts/ ├── scripts/
│ ├── bootstrap.sh # 一键创建 uv 环境、生成 bindings 并自检 │ ├── bootstrap.sh # 一键创建 uv 环境、生成 bindings 并自检
│ ├── export_detection_onnx.py # 受控导出检测模型 @2 ONNX 制品与 manifest
│ └── generate_cmvr_stubs.py # 从 cmvr-es proto 生成 Python bindings │ └── generate_cmvr_stubs.py # 从 cmvr-es proto 生成 Python bindings
├── src/cmvr_edge_ai/ ├── src/cmvr_edge_ai/
│ ├── config/ # 配置模型、加载与环境变量展开 │ ├── config/ # 配置模型、加载与环境变量展开
│ ├── core/ # Envelope、组件接口、队列和 DAG 运行时 │ ├── core/ # Envelope、组件接口、队列和 DAG 运行时
│ ├── contracts/ # 协议无关的图像、音频、AI 和控制契约 │ ├── contracts/ # 内部消息及 inference/catalog 公共 wire 契约
│ ├── capabilities/ # 稳定 category、服务模式与模型能力注册表
│ ├── client/ # 远程调用 SDK当前包含 Detect HTTP Client
│ ├── server/ # 请求 broker、边界节点、能力目录与 HTTP API
│ ├── detection/ # 模型注册、视频解码、推理与时间窗口规则 │ ├── detection/ # 模型注册、视频解码、推理与时间窗口规则
│ ├── gauge/ # Analog Gauge 主进程 adapter 与 Python 3.8 worker
│ ├── plugins/ # 插件注册、发现和内置基础插件 │ ├── plugins/ # 插件注册、发现和内置基础插件
│ ├── connectors/ # cmvr-es 与平台边界连接器 │ ├── connectors/ # cmvr-es 与平台边界连接器
│ ├── transports/ # gRPC/HTTP 连接池UDP/QUIC 扩展位置 │ ├── transports/ # 已实现的 gRPC/HTTP 连接池
│ ├── workers/ # 显式线程 offload 与常驻进程 Worker 工具 │ ├── workers/ # 显式线程 offload 与常驻进程 Worker 工具
│ ├── observability/ # 低开销文本/JSON 日志 │ ├── observability/ # 低开销文本/JSON 日志
│ ├── application.py # 多 Pipeline 与共享网络客户端的所有者 │ ├── application.py # 多 Pipeline 与共享网络客户端的所有者
│ ├── compiler.py # 配置到可执行 DAG 的编译器 │ ├── compiler.py # 配置到可执行 DAG 的编译器
│ └── cli.py # validate/run/plugins/models │ └── cli.py # validate/run/serve/plugins/models
└── tests/ └── tests/
└── fixtures/ └── fixtures/
└── minimal_pipeline.yaml # 不依赖外部服务的框架/CLI 自检配置 └── minimal_pipeline.yaml # 不依赖外部服务的框架/CLI 自检配置
@ -99,7 +116,7 @@ uv --version
curl -LsSf https://astral.sh/uv/install.sh | sh curl -LsSf https://astral.sh/uv/install.sh | sh
``` ```
只验证框架和模拟对话链路时,一条命令创建最小环境并自检: 只验证框架核心时,一条命令创建最小环境并自检:
```bash ```bash
cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
@ -131,9 +148,9 @@ bootstrap 支持以下环境:
| Profile | 安装内容 | 命令 | | Profile | 安装内容 | 命令 |
|---|---|---| |---|---|---|
| `core` | 框架核心、最小测试 fixture 和模拟 talk 链路 | `bash scripts/bootstrap.sh --profile core` | | `core` | 框架核心与最小测试 fixture | `bash scripts/bootstrap.sh --profile core` |
| `detection-cpu` | gRPC、HTTP、PyAV、Pillow 告警图片固定版本 CPU YOLO默认值 | `bash scripts/bootstrap.sh` | | `detection-cpu` | gRPC、HTTP、PyAV、Pillow 和 CPU ONNX Runtime;默认值 | `bash scripts/bootstrap.sh` |
| `dev` | `detection-cpu` 加测试和 protobuf codegen 工具,并运行完整测试 | `bash scripts/bootstrap.sh --profile dev` | | `dev` | `detection-cpu`锁定的 ONNX exporter、测试和 protobuf codegen | `bash scripts/bootstrap.sh --profile dev` |
如果只希望安装、不执行自检,可加 `--skip-check`。完整参数通过以下命令查看: 如果只希望安装、不执行自检,可加 `--skip-check`。完整参数通过以下命令查看:
@ -141,27 +158,133 @@ bootstrap 支持以下环境:
bash scripts/bootstrap.sh --help bash scripts/bootstrap.sh --help
``` ```
不要使用 `uv sync --all-extras``yolo` 与 `yolo-cpu` 是为不同 PyTorch 来源准备的 不要使用 `uv sync --all-extras`旧 PT adapter 的 `yolo`/`yolo-cpu` 与构建期
互斥环境。请使用 bootstrap profile或显式只选择其中一个 extra。 `onnx-export-cpu` 具有互斥依赖来源。请使用 bootstrap profile 或只选择目标 extra。
告警图片的画框和 JPEG 编码由独立的 `image` extra 提供;默认的 告警图片的画框和 JPEG 编码由独立的 `image` extra 提供;默认的
`detection-cpu`/`dev` profile 已安装它,手动组合检测环境时也必须选择 `detection-cpu`/`dev` profile 已安装它,手动组合检测环境时也必须选择
`--extra image` `--extra image`
CLI 的四个子命令如下: 启动被动 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 | | `validate -c FILE [--pipeline ID]` | 加载配置、展开环境变量、构造插件并校验 DAG不启动 Pipeline |
| `run -c FILE [--pipeline ID]` | 启动选中的 Pipeline未指定时启动所有 `enabled: true` 的 Pipeline | | `run -c FILE [--pipeline ID]` | 启动选中的 Pipeline未指定时启动所有 `enabled: true` 的 Pipeline |
| `serve -c FILE` | 启动所有已启用 Pipeline 和远程推理 HTTP API要求 `server.enabled: true` |
| `plugins` | 列出内置插件和已安装 entry point 插件 | | `plugins` | 列出内置插件和已安装 entry point 插件 |
| `models` | 列出检测模型 ID、名称、backend 和注册的全部标签 | | `models` | 列出检测模型 ID、名称、backend 和注册的全部标签 |
`--pipeline` 可以重复传入。`run` 还支持 `--log-level``--log-format text|json`。配置错误退出码为 `2`,运行错误为 `1`,键盘中断为 `130` `--pipeline` 可以重复传给 `validate``run`。`run`、`serve` 还支持 `--log-level`
`--log-format text|json``serve` 按配置启动全部已启用 Pipeline不接受客户端或命令行
覆盖单条 route 的模型选择。配置错误退出码为 `2`,运行错误为 `1`,键盘中断为 `130`
生产配置 `configs/edge_ai.yaml` 在一个 YAML 中同时定义 `detection``talk`。部署时 主动推送配置为 `configs/active_detection.yaml`,当前只包含 `detection` Pipeline。
建议显式传 `--pipeline detection``--pipeline talk`,这样进程只加载并运行选中的 部署时可以显式传 `--pipeline detection`;省略时会启动配置中所有已启用 Pipeline。
链路;需要同进程运行两条链路时,可以重复传两个 `--pipeline`。如果完全省略
`--pipeline`,运行时会启动配置中所有 `enabled: true` 的 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)。
## 运行视觉检测链路 ## 运行视觉检测链路
@ -196,29 +319,32 @@ uv sync --locked --only-group codegen
生成后应再次执行目标 profile 的 `uv sync --locked`让可编辑安装识别新包bootstrap 生成后应再次执行目标 profile 的 `uv sync --locked`让可编辑安装识别新包bootstrap
已经按这个顺序处理。 已经按这个顺序处理。
`configs/edge_ai.yaml` 的 `detection` Pipeline 中配置部署参数: `configs/active_detection.yaml` 的 `detection` Pipeline 中配置部署参数:
```yaml ```yaml
endpoints: endpoints:
cmvr_es: cmvr_es:
target: 127.0.0.1:50052 target: 192.168.0.119:50052
ppe_alert_platform: ppe_alert_platform:
base_url: http://127.0.0.1:8081 base_url: http://192.168.0.222:13080
pipelines: pipelines:
detection: detection:
nodes: nodes:
camera: camera:
with: with:
device_id: right_hand_cam device_id: wrist_cam
stream_log_interval_s: 5 stream_log_interval_s: 5
detector: detector:
with: with:
model: construction-ppe-yolov8@2
attach_frame: true attach_frame: true
inference_log_interval_s: 5 inference_log_interval_s: 5
model_options: model_options:
weights: models/detection/construction-ppe-yolov8/v1/best.pt weights: models/detection/construction-ppe-yolov8/v2/model.onnx
device: cpu providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
repeat_gate: repeat_gate:
with: with:
alert_image: alert_image:
@ -226,13 +352,15 @@ pipelines:
jpeg_quality: 85 jpeg_quality: 85
phone_detector: phone_detector:
with: with:
model: people-talking-yolov8x@1 model: people-talking-yolov8x@2
detect_labels: [talking on phone] detect_labels: [talking on phone]
max_fps: 5 max_fps: 5
attach_frame: true attach_frame: true
model_options: model_options:
weights: models/detection/people-talking-yolov8x/v1/best.pt weights: models/detection/people-talking-yolov8x/v2/model.onnx
device: cpu providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
phone_repeat_gate: phone_repeat_gate:
with: with:
alert_image: alert_image:
@ -241,6 +369,7 @@ pipelines:
alert_platform: alert_platform:
with: with:
endpoint: ppe_alert_platform endpoint: ppe_alert_platform
grpc_endpoint: cmvr_es
failure_mode: log_and_drop failure_mode: log_and_drop
``` ```
@ -249,9 +378,9 @@ pipelines:
```bash ```bash
uv run --no-sync cmvr-edge-ai models uv run --no-sync cmvr-edge-ai models
uv run --no-sync cmvr-edge-ai validate \ uv run --no-sync cmvr-edge-ai validate \
--config configs/edge_ai.yaml \ --config configs/active_detection.yaml \
--pipeline detection --pipeline detection
uv run --no-sync cmvr-edge-ai run --config configs/edge_ai.yaml \ uv run --no-sync cmvr-edge-ai run --config configs/active_detection.yaml \
--pipeline detection \ --pipeline detection \
--log-level INFO \ --log-level INFO \
--log-format json --log-format json
@ -271,16 +400,16 @@ cmvr-es gRPC CameraService共享解码帧并行执行 PPE 与 YOLOv8n Mobile
合并到一个窗口;它不经过重复触发规则,也不会访问 HTTP 平台: 合并到一个窗口;它不经过重复触发规则,也不会访问 HTTP 平台:
```bash ```bash
uv run --no-sync python detect_server/show_detections.py \ uv run --no-sync python server/detect/show_detections.py \
--config configs/debug/detection_viewer.yaml \ --config configs/detection_viewer.yaml \
--pipeline detection_show \ --pipeline detection_show \
--log-level INFO --log-level INFO
``` ```
运行前在 `configs/debug/detection_viewer.yaml` 中配置远端地址、`device_id` 和两份模型 运行前在 `configs/detection_viewer.yaml` 中配置远端地址、`device_id` 和两份模型
权重;按 `q``Esc` 退出。框标签使用 `PPE:`/`PHONE:` 前缀,手机框标签为 权重;按 `q``Esc` 退出。框标签使用 `PPE:`/`PHONE:` 前缀,手机框标签为
`PHONE:mobile_phone`,窗口刷新率由较慢的模型分支决定。详细说明见 `PHONE:mobile_phone`,窗口刷新率由较慢的模型分支决定。详细说明见
[detect_server/README.md](detect_server/README.md#实时画框-demo)。 [server/detect/README.md](server/detect/README.md#opencv-实时画框-viewer)。
相机连接器会在每次首次连接或重连时先发 `CameraService.StartCamera`,收到成功反馈后 相机连接器会在每次首次连接或重连时先发 `CameraService.StartCamera`,收到成功反馈后
才建立 `GetRGBImageStream`。终端会依次出现 `camera start requested/succeeded` 才建立 `GetRGBImageStream`。终端会依次出现 `camera start requested/succeeded`
@ -288,33 +417,28 @@ uv run --no-sync python detect_server/show_detections.py \
如果只有 opening 而没有 first frameprogress 中会持续显示 如果只有 opening 而没有 first frameprogress 中会持续显示
`first_frame_received=false window_frames=0`用于区分“RPC 已建立但相机没有出帧”。 `first_frame_received=false window_frames=0`用于区分“RPC 已建立但相机没有出帧”。
运行前需要确认 Construction PPE 与 People-Talking 权重存在、cmvr-es 已启用 `right_hand_cam`8081 平台的 运行前需要确认 Construction PPE 与 People-Talking 的 `model.onnx`、`manifest.json`
`/v1/detection-alerts` 可访问。默认 `yolo-cpu` profile 将 PyTorch 2.7.0 和 存在cmvr-es 已启用配置相机,并且告警平台 `/v1/detection-alerts` 可访问。默认
torchvision 0.22.0 绑定到官方 CPU wheel并固定 checkpoint 记录的 Ultralytics `detection-cpu` profile 只安装 NumPy、Pillow 和 ONNX Runtime不安装 Torch 或
8.4.31;同时显式安装 People-Talking checkpoint 反序列化所需的 `dill` Ultralytics。CUDA/TensorRT、Jetson 和其他 provider 必须建立与目标设备匹配的独立
`model_options.device` 在该环境中应保持 `cpu``half` 应保持 `false` 依赖/锁文件并重新验收,不能只修改 `providers` 就认为硬件环境已经就绪。
x86 CUDA 和 Jetson/JetPack 的 PyTorch wheel 与驱动强绑定,不能复用这个 CPU
profile。项目保留了不绑定 CPU index 的 `yolo` extra 作为设备专用环境的基础,但
GPU 部署前应为目标设备建立单独的 uv source/lock或使用 NVIDIA 容器),再把 YAML
中的 `device` 改为 `cuda:0`;不要只改 YAML 就认为 CUDA 环境已经就绪。
`detection.model@1` 根据 `model``DetectionModelRegistry` 解析模型。`detect_labels` 只选择需要检测的标签,省略时检测注册模型的全部标签;`confidence` 是全局阈值,也可以用 `label_confidence` 为个别标签覆盖。当前四个内置模型都可以通过 `cmvr-edge-ai models` 查看,模型制品和独立 model card 位于仓库内: `detection.model@1` 根据 `model``DetectionModelRegistry` 解析模型。`detect_labels` 只选择需要检测的标签,省略时检测注册模型的全部标签;`confidence` 是全局阈值,也可以用 `label_confidence` 为个别标签覆盖。当前四个内置模型都可以通过 `cmvr-edge-ai models` 查看,模型制品和独立 model card 位于仓库内:
- [Construction PPE YOLOv8 v1](models/detection/construction-ppe-yolov8/v1/README.md):包含正向 PPE、`No-*` 违规类和施工现场设备类; - [Construction PPE ONNX v2](models/detection/construction-ppe-yolov8/v2/README.md):包含正向 PPE、`No-*` 违规类和施工现场设备类;
- [PPE YOLOv8n 6 Classes v1](models/detection/ppe-6classes-yolov8n/v1/README.md):轻量的六类正向装备检测模型。 - [PPE YOLOv8n 6 Classes ONNX v2](models/detection/ppe-6classes-yolov8n/v2/README.md):六类正向装备检测模型,当前未实例化;
- [People Talking YOLOv8x v1](models/detection/people-talking-yolov8x/v1/README.md)电话使用行为检测Pipeline 只选择 `talking on phone` 类。 - [People Talking ONNX v2](models/detection/people-talking-yolov8x/v2/README.md):电话使用行为检测,只选择 `talking on phone`
- [YOLOv8n Mobile Phone v1](models/detection/yolov8n-mobile-phone/README.md)单类手机目标检测Debug Viewer 使用 `mobile_phone` - [YOLOv8n Mobile Phone ONNX v2](models/detection/yolov8n-mobile-phone/v2/README.md)Viewer 和被动服务使用的单类手机检测
注册 ID 中`@1` 表示版本 1多数制品使用 `v1` 子目录,当前 YOLOv8n Mobile 注册 ID 中 `@N` 与版本目录 `vN` 对应Mobile Phone 的 v1 PT 是历史扁平布局v2 已
Phone 保留导入时的扁平目录和上游文件名。实际 `weights` 路径仍由部署 YAML 显式 恢复版本目录。实际 `weights` 路径仍由部署 YAML 显式指定。标签顺序、训练指标、局限
指定。标签顺序、训练指标、局限和许可声明以上述 model card 为准。 和许可声明以各版本 model card 为准。
`attach_frame: true` 让检测结果临时携带对应的解码帧,供后续告警节点使用;因此原 detector 到 repeat gate 的队列应保持较小,避免堆积未压缩图像。 `attach_frame: true` 让检测结果临时携带对应的解码帧,供后续告警节点使用;因此原 detector 到 repeat gate 的队列应保持较小,避免堆积未压缩图像。
六类模型的标签是 `Gloves`、`Vest`、`goggles`、`helmet`、`mask` 和 `safety_shoe` 六类模型的标签是 `Gloves`、`Vest`、`goggles`、`helmet`、`mask` 和 `safety_shoe`
语义是“画面中检测到了该装备”,不是“人员缺少该装备”。它没有 `Person``No-*` 语义是“画面中检测到了该装备”,不是“人员缺少该装备”。它没有 `Person``No-*`
类,也没有人员与装备关联能力,因此不能只靠配置推断某个人未佩戴 PPE。该模型当前仅 类,也没有人员与装备关联能力,因此不能只靠配置推断某个人未佩戴 PPE。该模型当前仅
注册、未被 `configs/edge_ai.yaml` 的 `detection` Pipeline 引用;需要恢复第二分支时,应同时配置 detector、 注册、未被 `configs/active_detection.yaml` 的 `detection` Pipeline 引用;需要恢复第二分支时,应同时配置 detector、
8082 endpoint、HTTP Sink 和两条关联 edge。 8082 endpoint、HTTP Sink 和两条关联 edge。
当前 Construction PPE 和 People-Talking 分支从 decoder 输出端口 fan-out共享同一个 当前 Construction PPE 和 People-Talking 分支从 decoder 输出端口 fan-out共享同一个
@ -323,7 +447,25 @@ 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 规则前必须增加跟踪/关联节点。 `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`、时间范围和最大置信度统计。HTTP JSON 中图片位于 `payload.image` 启用 `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 ```json
{ {
@ -343,16 +485,8 @@ People-Talking 是视觉行为判断,不能证明电话已接通,也不能
同一阈值帧若同时触发多条规则或多个 track只编码一次相关框的并集并让这些 同一阈值帧若同时触发多条规则或多个 track只编码一次相关框的并集并让这些
告警共享同一个不可变 JPEG 对象,以限制边缘端瞬时 CPU 和内存开销。 告警共享同一个不可变 JPEG 对象,以限制边缘端瞬时 CPU 和内存开销。
对话占位链路不依赖音频 proto Talk Server 当前只保留能力边界说明;在真实音频 proto、VAD/ASR/LLM/TTS 节点和远程
调用契约落地前,不提供容易被误认为可部署服务的占位配置。
```bash
uv run --no-sync cmvr-edge-ai validate \
--config configs/edge_ai.yaml \
--pipeline talk
uv run --no-sync cmvr-edge-ai run \
--config configs/edge_ai.yaml \
--pipeline talk
```
## 配置最小示例 ## 配置最小示例
@ -480,7 +614,7 @@ AGV Sink 必须绑定固定的非空 `device_id`。`set_velocity` 默认禁用
- 音频内部契约 `AudioChunk/v1` 已定义,但 cmvr-es 麦克风/扬声器双向流 proto 和连接器尚未落地。 - 音频内部契约 `AudioChunk/v1` 已定义,但 cmvr-es 麦克风/扬声器双向流 proto 和连接器尚未落地。
- UDP 与 QUIC 目录目前是扩展占位,没有可用传输实现。 - 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 只支持 `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` 都会在编译期被拒绝。健康状态和队列统计目前只能通过 Python API 获取。 - 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 - 当前没有配置热更新、配置 overlay、持久化 outbox/spool 或共享内存图像池。HTTP Sink
只有当前进程内的有限重试;默认 `failure_mode: raise` 会终止 Pipeline当前检测配置的 只有当前进程内的有限重试;默认 `failure_mode: raise` 会终止 Pipeline当前检测配置的
`log_and_drop` 则在重试耗尽后记录 WARNING 并丢弃该告警。两种模式在进程退出或断电时 `log_and_drop` 则在重试耗尽后记录 WARNING 并丢弃该告警。两种模式在进程退出或断电时

31
client/detect/README.md Normal file
View File

@ -0,0 +1,31 @@
# Detect Client
这里放置不依赖 pipeline 的 Detect HTTP SDK 使用示例。SDK 只负责:
- `list_models()` 查询 `active_push``passive_invoke` 能力;
- `infer(category, image, media_type, parameters, image_roles)` 调用
`POST /v1/inference`
- 严格校验版本化响应并把连接、HTTP 状态和协议错误分成不同异常。
客户端不得传入 `model_id`、权重路径或 pipeline 配置。模型选择和权重生命周期属于
`server/detect`;客户端只使用服务端公开的稳定业务 `category`
安装 HTTP extra 后运行 [example.py](example.py)
```bash
uv sync --extra http
CMVR_DETECT_BASE_URL=http://127.0.0.1:8081 \
uv run --no-sync python client/detect/example.py ./sample.jpg detect.ppe
```
远程服务配置了 `server.http.bearer_token` 时,通过环境变量传入同一 tokenTLS 部署应把
base URL 改为 `https://`
```bash
CMVR_DETECT_BASE_URL=https://edge.example.com:8081 \
CMVR_DETECT_BEARER_TOKEN='deployment-secret' \
uv run --no-sync python client/detect/example.py ./sample.jpg detect.ppe
```
SDK 的生产代码位于 `cmvr_edge_ai.client.detect`。base URL、认证 header 和 timeout
属于调用应用配置;本目录不复制服务端 category/model/weights 配置。

40
client/detect/example.py Normal file
View File

@ -0,0 +1,40 @@
"""Call a Detect Server without constructing an edge-AI pipeline."""
from __future__ import annotations
import asyncio
import os
import sys
from pathlib import Path
from cmvr_edge_ai.client.detect import DetectClient
async def main() -> None:
if len(sys.argv) != 3:
raise SystemExit("usage: example.py IMAGE_PATH CATEGORY")
image_path = Path(sys.argv[1])
category = sys.argv[2]
base_url = os.environ.get("CMVR_DETECT_BASE_URL", "http://127.0.0.1:8081")
bearer_token = os.environ.get("CMVR_DETECT_BEARER_TOKEN")
headers = (
{}
if bearer_token is None
else {"Authorization": f"Bearer {bearer_token}"}
)
async with DetectClient(base_url, headers=headers) as client:
models = await client.list_models()
print(models)
result = await client.infer(
category,
image_path.read_bytes(),
media_type="image/jpeg",
parameters={},
image_roles=("annotated", "original"),
)
print(result)
if __name__ == "__main__":
asyncio.run(main())

23
configs/README.md Normal file
View File

@ -0,0 +1,23 @@
# 配置目录
所有可部署、可直接传给 `cmvr-edge-ai --config` 的配置统一保存在本目录:
| 文件 | 模式 | 用途 |
|---|---|---|
| `active_detection.yaml` | 主动推送 | 相机检测告警链路 |
| `server_detect.yaml` | 被动调用 | PPE 与手机检测 HTTP 服务 |
| `server_gauge.yaml` | 被动调用 | Analog Gauge HTTP 服务 |
| `detection_viewer.yaml` | 本地调试 | OpenCV 双模型实时画框 |
三份检测配置均使用 `@2` `onnxruntime-yolov8` 制品;`model_options` 必须使用
`providers/intra_op_threads/inter_op_threads`,不能保留 PT adapter 的 `device/half`
所有仓库内命令都以项目根目录为当前工作目录。配置中的模型权重路径也按项目根目录
解析,而不是按 YAML 文件所在目录解析。
`tests/fixtures/minimal_pipeline.yaml` 是自动化测试夹具,不是部署配置;
`server/gauge/worker/pyproject.toml``uv.lock` 是隔离 Worker 的环境清单,也不属于
运行时 YAML 配置。
Talk Server 尚未实现,因此不保留不能运行的占位 YAML。实现真实音频协议与模型节点后
再新增 `configs/server_talk.yaml`

View File

@ -45,7 +45,7 @@ pipelines:
detector: detector:
uses: detection.model@1 uses: detection.model@1
with: with:
model: construction-ppe-yolov8@1 model: construction-ppe-yolov8@2
# Omitting detect_labels means all registered labels. This example # Omitting detect_labels means all registered labels. This example
# asks the backend to return only PPE violations used by the rules. # asks the backend to return only PPE violations used by the rules.
detect_labels: detect_labels:
@ -66,14 +66,15 @@ pipelines:
# annotated alert image can be rendered only when a rule triggers. # annotated alert image can be rendered only when a rule triggers.
attach_frame: true attach_frame: true
model_options: model_options:
# Model artifact and inference device are deployment configuration, # Model artifact and inference provider are deployment configuration,
# not process environment requirements. This repository-relative # not process environment requirements. This repository-relative
# path requires launching cmvr-edge-ai from the repository root. # path requires launching cmvr-edge-ai from the repository root.
weights: models/detection/construction-ppe-yolov8/v1/best.pt weights: models/detection/construction-ppe-yolov8/v2/model.onnx
device: cpu providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
imgsz: 640 imgsz: 640
iou: 0.70 iou: 0.70
half: false
max_det: 100 max_det: 100
repeat_gate: repeat_gate:
@ -136,50 +137,54 @@ pipelines:
min_confidence: 0.50 min_confidence: 0.50
scope: source scope: source
# phone_detector: phone_detector:
# uses: detection.model@1 uses: detection.model@1
# with: with:
# model: people-talking-yolov8x@1 model: people-talking-yolov8x@2
# # The checkpoint also contains a generic class named "label". It is # The source model also contains a generic class named "label". It is
# # retained in model registration for class-ID safety but is not an # retained in model registration for class-ID safety but is not an
# # actionable phone-use event, so this branch selects only class 1. # actionable phone-use event, so this branch selects only class 1.
# detect_labels: detect_labels:
# - talking on phone - talking on phone
# confidence: 0.50 confidence: 0.50
# # YOLOv8x is substantially heavier than the PPE model. Start with a # YOLOv8x is substantially heavier than the PPE model. Start with a
# # conservative CPU rate and tune only after measuring target hardware. # conservative CPU rate and tune only after measuring target hardware.
# max_fps: 5 max_fps: 5
# inference_log_interval_s: 5 inference_log_interval_s: 5
# attach_frame: true attach_frame: true
# model_options: model_options:
# weights: models/detection/people-talking-yolov8x/v1/best.pt weights: models/detection/people-talking-yolov8x/v2/model.onnx
# device: cpu providers: [CPUExecutionProvider]
# imgsz: 640 intra_op_threads: 1
# iou: 0.70 inter_op_threads: 1
# half: false imgsz: 640
# max_det: 100 iou: 0.70
max_det: 100
# phone_repeat_gate: phone_repeat_gate:
# uses: detection.repeat_gate@1 uses: detection.repeat_gate@1
# with: with:
# time_source: received time_source: received
# alert_image: alert_image:
# enabled: true enabled: true
# jpeg_quality: 85 jpeg_quality: 85
# rules: rules:
# - id: talking-on-phone - id: talking-on-phone
# labels: [talking on phone] labels: [talking on phone]
# min_hits: 3 min_hits: 3
# window_ms: 2000 window_ms: 2000
# cooldown_ms: 30000 cooldown_ms: 30000
# min_confidence: 0.50 min_confidence: 0.50
# scope: source scope: source
alert_platform: alert_platform:
uses: platform.http_json_sink@1 uses: platform.http_json_sink@1
with: with:
endpoint: ppe_alert_platform endpoint: ppe_alert_platform
path: /v1/detection-alerts path: /v1/detection-alerts
# Include the IP from endpoints.cmvr_es.target in every platform
# report so the platform can identify the originating edge device.
grpc_endpoint: cmvr_es
# Platform outages must not stop camera capture or inference. After # Platform outages must not stop camera capture or inference. After
# bounded retries, log a WARNING and drop only this report. # bounded retries, log a WARNING and drop only this report.
failure_mode: log_and_drop failure_mode: log_and_drop
@ -223,13 +228,13 @@ pipelines:
capacity: 2 capacity: 2
overflow: drop_oldest overflow: drop_oldest
# - from: phone_detector.detections - from: phone_detector.detections
# to: phone_repeat_gate.detections to: phone_repeat_gate.detections
# qos: qos:
# profile: telemetry profile: telemetry
# # attach_frame carries the decoded image until the rule is evaluated. # attach_frame carries the decoded image until the rule is evaluated.
# capacity: 2 capacity: 2
# overflow: drop_oldest overflow: drop_oldest
- from: repeat_gate.alerts - from: repeat_gate.alerts
to: alert_platform.input to: alert_platform.input
@ -238,45 +243,9 @@ pipelines:
capacity: 64 capacity: 64
overflow: block overflow: block
# - from: phone_repeat_gate.alerts - from: phone_repeat_gate.alerts
# to: alert_platform.input to: alert_platform.input
# qos:
# profile: telemetry
# capacity: 64
# overflow: block
talk:
enabled: true
nodes:
audio_stream_placeholder:
# Replace with cmvr.grpc.microphone_audio_stream@1 when its proto lands.
uses: core.sequence_source@1
with:
items:
- simulated-audio-chunk
schema_name: AudioChunk
schema_version: 1
dialogue_placeholder:
# The real chain will be VAD -> ASR -> dialogue -> TTS.
uses: core.passthrough@1
output:
uses: core.log_sink@1
with:
logger: cmvr_edge_ai.talk
edges:
- from: audio_stream_placeholder.output
to: dialogue_placeholder.input
qos: qos:
profile: audio_contiguous profile: telemetry
capacity: 16 capacity: 64
overflow: block
- from: dialogue_placeholder.output
to: output.input
qos:
profile: request
capacity: 8
overflow: block overflow: block

View File

@ -41,7 +41,7 @@ pipelines:
detector: detector:
uses: detection.model@1 uses: detection.model@1
with: with:
model: construction-ppe-yolov8@1 model: construction-ppe-yolov8@2
detect_labels: detect_labels:
- No-Boots - No-Boots
- No-Ear-Protection - No-Ear-Protection
@ -56,28 +56,30 @@ pipelines:
attach_frame: true attach_frame: true
model_options: model_options:
# Repository-relative path; launch the viewer from the repository root. # Repository-relative path; launch the viewer from the repository root.
weights: models/detection/construction-ppe-yolov8/v1/best.pt weights: models/detection/construction-ppe-yolov8/v2/model.onnx
device: cpu providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
imgsz: 640 imgsz: 640
iou: 0.70 iou: 0.70
half: false
max_det: 100 max_det: 100
phone_detector: phone_detector:
uses: detection.model@1 uses: detection.model@1
with: with:
model: yolov8n-mobile-phone@1 model: yolov8n-mobile-phone@2
detect_labels: detect_labels:
- mobile_phone - mobile_phone
confidence: 0.50 confidence: 0.50
inference_log_interval_s: 5 inference_log_interval_s: 5
attach_frame: true attach_frame: true
model_options: model_options:
weights: models/detection/yolov8n-mobile-phone/yolov8n-mobile-phone.pt weights: models/detection/yolov8n-mobile-phone/v2/model.onnx
device: cpu providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
imgsz: 640 imgsz: 640
iou: 0.70 iou: 0.70
half: false
max_det: 100 max_det: 100
viewer: viewer:

148
configs/server_detect.yaml Normal file
View File

@ -0,0 +1,148 @@
# Passive Detect Server deployment example.
api_version: cmvr.edge.ai/v1
runtime:
thread_workers: 2
server:
enabled: true
http:
bind: 127.0.0.1
port: 8081
# 绑定局域网地址时必须设置 bearer_token并配置下面两项 TLS只有受信隔离网络
# 才应改用 allow_insecure_remote: true 明文传输 token。
# bearer_token: env://CMVR_EDGE_AI_BEARER_TOKEN
# tls_certfile: /etc/cmvr-edge-ai/tls/server.crt
# tls_keyfile: /etc/cmvr-edge-ai/tls/server.key
request_timeout_s: 30
max_request_bytes: 16777216
max_image_bytes: 10485760
routes:
detect.ppe:
pipeline: detect_ppe
model_id: construction-ppe-yolov8@2
queue_capacity: 4
detect.mobile_phone:
pipeline: detect_mobile_phone
model_id: yolov8n-mobile-phone@2
queue_capacity: 4
# Public clients know only the route categories above. Model IDs, weights and
# devices remain in this server-owned configuration.
pipelines:
detect_ppe:
nodes:
request_source:
uses: server.request_source@1
with:
category: detect.ppe
image_decoder:
uses: media.image_decoder.pillow@1
with:
input_name: image
pixel_format: BGR8
max_pixels: 25000000
accepted_media_types: [image/jpeg, image/png]
detector:
uses: detection.model@1
with:
model: construction-ppe-yolov8@2
confidence: 0.50
# Passive requests must all produce a response, so this pipeline does
# not configure max_fps or any frame-dropping edge.
attach_frame: true
model_options:
weights: models/detection/construction-ppe-yolov8/v2/model.onnx
providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
imgsz: 640
iou: 0.70
max_det: 100
response:
uses: server.detection_response@1
with:
category: detect.ppe
backend: onnxruntime-yolov8
jpeg_quality: 85
response_sink:
uses: server.response_sink@1
edges:
- from: request_source.requests
to: image_decoder.requests
qos: &ppe_request_qos
profile: request
capacity: 4
overflow: block
- from: image_decoder.frames
to: detector.frames
qos: *ppe_request_qos
- from: detector.detections
to: response.detections
qos: *ppe_request_qos
- from: response.responses
to: response_sink.responses
qos: *ppe_request_qos
detect_mobile_phone:
nodes:
request_source:
uses: server.request_source@1
with:
category: detect.mobile_phone
image_decoder:
uses: media.image_decoder.pillow@1
with:
input_name: image
pixel_format: BGR8
max_pixels: 25000000
accepted_media_types: [image/jpeg, image/png]
detector:
uses: detection.model@1
with:
model: yolov8n-mobile-phone@2
detect_labels: [mobile_phone]
confidence: 0.50
attach_frame: true
model_options:
weights: models/detection/yolov8n-mobile-phone/v2/model.onnx
providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
imgsz: 640
iou: 0.70
max_det: 100
response:
uses: server.detection_response@1
with:
category: detect.mobile_phone
backend: onnxruntime-yolov8
jpeg_quality: 85
response_sink:
uses: server.response_sink@1
edges:
- from: request_source.requests
to: image_decoder.requests
qos: &phone_request_qos
profile: request
capacity: 4
overflow: block
- from: image_decoder.frames
to: detector.frames
qos: *phone_request_qos
- from: detector.detections
to: response.detections
qos: *phone_request_qos
- from: response.responses
to: response_sink.responses
qos: *phone_request_qos

66
configs/server_gauge.yaml Normal file
View File

@ -0,0 +1,66 @@
api_version: cmvr.edge.ai/v1
runtime:
thread_workers: 1
shutdown_timeout_s: 15
server:
enabled: true
http:
bind: 127.0.0.1
port: 8081
# 绑定局域网地址时必须设置 bearer_token并配置下面两项 TLS只有受信隔离网络
# 才应改用 allow_insecure_remote: true 明文传输 token。
# bearer_token: env://CMVR_EDGE_AI_BEARER_TOKEN
# tls_certfile: /etc/cmvr-edge-ai/tls/server.crt
# tls_keyfile: /etc/cmvr-edge-ai/tls/server.key
# 首次模型初始化和 OCR 都比较慢HTTP timeout 必须大于 worker timeout。
request_timeout_s: 360
max_request_bytes: 33554432
max_image_bytes: 25165824
routes:
gauge.analog:
pipeline: gauge_analog
model_id: ethz-analog-gauge-reader@1
queue_capacity: 1
pipelines:
gauge_analog:
nodes:
request_source:
uses: server.request_source@1
with:
category: gauge.analog
gauge_reader:
uses: gauge.analog_reader@1
with:
category: gauge.analog
model_id: ethz-analog-gauge-reader@1
# 这些路径由服务端拥有,远程调用方不能覆盖。
python_executable: /home/xtkuang/Projects/cmvr/cmvr_edge_ai/server/gauge/worker/.venv/bin/python
project_root: /home/xtkuang/Projects/cmvr/changan_robot/analog_gauge_reader
detection_model_path: models/gauge_detection_model.pt
key_point_model_path: models/key_point_model.pt
segmentation_model_path: models/segmentation_model.pt
startup_timeout_s: 300
request_timeout_s: 300
shutdown_timeout_s: 10
max_header_bytes: 1048576
max_blob_bytes: 67108864
# 先读取 JPEG/PNG 头检查像素数,再交给 Pillow/OpenCV 和上游模型。
max_pixels: 25000000
response_sink:
uses: server.response_sink@1
edges:
- from: request_source.requests
to: gauge_reader.requests
qos: &gauge_request_qos
profile: request
capacity: 1
overflow: block
- from: gauge_reader.responses
to: response_sink.responses
qos: *gauge_request_qos

View File

@ -1,410 +0,0 @@
# PPE 与电话使用检测流水线
`configs/edge_ai.yaml` 中的 `detection` Pipeline 是当前可运行的园区视觉告警链路。
它与 `talk` Pipeline 共用一个部署 YAML并从同一个 decoder 并行运行 Construction
PPE 与 People-Talking 两个模型。两个 detector 使用独立 repeat gate再把带有不同
`model_id`/`rule_id` 的告警交给同一个平台 Sink
```text
cmvr-es CameraService
-> H264/H265 ImageFrame/v1
-> media.video_decoder.pyav@1
-> BGR8 ImageFrame/v1
|-> construction-ppe-yolov8@1 -> PPE repeat gate --|
`-> people-talking-yolov8x@1 -> phone repeat gate --+-> DetectionAlert/v1
`-> POST /v1/detection-alerts
```
## 安装与启动
从仓库根目录执行一键安装。默认 profile 安装锁定的 CPU 检测环境、生成 cmvr-es
bindings并验证最小测试 fixture 与统一部署配置中的检测链路:
```bash
cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
bash scripts/bootstrap.sh
```
cmvr-es 不在相邻的 `../cmvr-es` 时指定实际路径:
```bash
bash scripts/bootstrap.sh \
--cmvr-es-root /home/xtkuang/Projects/cmvr/cmvr-es
```
环境固定 Python 3.10、Ultralytics 8.4.31、PyTorch 2.7.0 CPU 和 torchvision
0.22.0,通过 `image` extra 安装 Pillow 以生成告警图片,并显式安装 People-Talking
checkpoint 反序列化所需的 `dill`;所有具体包版本记录在仓库的 `uv.lock` 中。
GPU/Jetson 需要单独适配与驱动或 JetPack 匹配的 PyTorch
wheel不能直接复用 `detection-cpu` profile。手动组合依赖时必须显式增加
`--extra image`,不能只依赖 YOLO 间接安装 Pillow。
直接编辑 `configs/edge_ai.yaml``detection` Pipeline 的部署参数:
```yaml
endpoints:
cmvr_es:
target: 127.0.0.1:50052
ppe_alert_platform:
base_url: http://127.0.0.1:8081
pipelines:
detection:
nodes:
camera:
with:
device_id: right_hand_cam
stream_log_interval_s: 5
detector:
with:
attach_frame: true
inference_log_interval_s: 5
model_options:
weights: models/detection/construction-ppe-yolov8/v1/best.pt
device: cpu
repeat_gate:
with:
alert_image:
enabled: true
jpeg_quality: 85
phone_detector:
with:
model: people-talking-yolov8x@1
detect_labels: [talking on phone]
max_fps: 5
attach_frame: true
model_options:
weights: models/detection/people-talking-yolov8x/v1/best.pt
device: cpu
phone_repeat_gate:
with:
alert_image:
enabled: true
jpeg_quality: 85
alert_platform:
with:
endpoint: ppe_alert_platform
failure_mode: log_and_drop
```
上述相对权重路径按进程启动时的当前工作目录(`cwd`)解析,不是按
`configs/edge_ai.yaml` 所在目录解析。下面的 validate、run 和 Viewer 命令都应先
`cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai`;如果必须在其他 `cwd` 启动,请在
YAML 中使用正确的绝对权重路径。
完成配置后启动,不需要再通过 shell `export` 传入这些值:
```bash
uv run --no-sync cmvr-edge-ai models
uv run --no-sync cmvr-edge-ai validate \
-c configs/edge_ai.yaml \
--pipeline detection
uv run --no-sync cmvr-edge-ai run -c configs/edge_ai.yaml \
--pipeline detection \
--log-level INFO \
--log-format json
```
运行时会看到类似下面两类 JSON 日志:
```json
{"level":"INFO","logger":"cmvr_edge_ai.detection.operator","message":"detection model loaded node=detector model=construction-ppe-yolov8@1 ..."}
{"level":"INFO","logger":"cmvr_edge_ai.detection.operator","message":"detection inference node=detector model=construction-ppe-yolov8@1 total_frames=1 window_frames=1 window_detections=2 hit_labels=No-Helmet:2 ..."}
```
- `detection model loaded`checkpoint 已成功加载并完成标签顺序校验;当前 detector
应出现一条;
- `detection inference`:模型确实收到解码帧并执行了 `predict`;第一帧立即输出,
后续按配置周期聚合;
- `window_detections=0 hit_labels=none`:模型在工作,但本周期没有高于配置阈值的命中;
- 只有 loaded、长期没有 inference优先检查 cmvr-es 相机流、decoder 和关键帧;
- 短时观察可把 `inference_log_interval_s` 改为 `1` 秒,长期运行建议 `30``60` 秒,
省略该字段会关闭周期推理日志。
运行前确认 cmvr-es 已启用 `right_hand_cam`、两份生产权重存在,并且 8081
平台接受 `POST /v1/detection-alerts`。默认 profile 下 `model_options.device` 应设为
`cpu`;只有完成设备专用的 CUDA/Jetson PyTorch 环境适配后,才能改为 `cuda:0` 等值。
## 实时画框 Demo
`configs/debug/detection_viewer.yaml``show_detections.py` 提供一个不访问 HTTP 平台的
独立调试链路:
```text
cmvr-es CameraService -> PyAV decoder -> latest decoded frame
|-> Construction PPE --|
`-> YOLOv8n Mobile ----+-> same-frame join
-> OpenCV window
```
先编辑 `configs/debug/detection_viewer.yaml` 中的远端 cmvr-es 地址、相机 ID 和权重路径:
```yaml
endpoints:
cmvr_es:
target: 192.168.0.119:50052
pipelines:
detection_show:
nodes:
camera:
with:
device_id: wrist_cam
detector:
with:
model_options:
weights: models/detection/construction-ppe-yolov8/v1/best.pt
device: cpu
phone_detector:
with:
model: yolov8n-mobile-phone@1
detect_labels: [mobile_phone]
model_options:
weights: models/detection/yolov8n-mobile-phone/yolov8n-mobile-phone.pt
device: cpu
```
Demo 只在共享 fan-out 之前通过 `realtime_latest + drop_oldest` 选择最新解码帧,之后用
两条 `block` 边把同一个 `ImageFrame` 交给两个 detector。两个 detector 都不设置独立
`max_fps`避免各自跳过不同帧。Viewer 按 frame identity 等待两个结果配齐,再在同一
张图上绘制;因此预览吞吐由较慢的模型分支决定。
只检查配置和插件连线,不连接相机、不加载模型、也不创建窗口:
```bash
uv run --no-sync python detect_server/show_detections.py \
--config configs/debug/detection_viewer.yaml \
--validate-only
```
启动实时显示:
```bash
uv run --no-sync python detect_server/show_detections.py \
--config configs/debug/detection_viewer.yaml \
--pipeline detection_show \
--log-level INFO \
--log-format json
```
相机 Source 会先调用 `StartCamera`,成功后再建立 gRPC 视频流。窗口只在同一帧的两个
推理结果都到达后刷新PPE 框显示为 `PPE:<label>`,手机框显示为
`PHONE:mobile_phone`,顶部同时显示两模型的框数和推理耗时。即使两模型都没有框,
配对完成后也会刷新。按 `q`、`Q`、`Esc` 或关闭窗口可安全退出。这个 Demo 绕过
`repeat_gate` 和 HTTP Sink因此只用于观察模型效果不代表某条告警规则已满足。
OpenCV 窗口出现在运行命令的机器上。无桌面的边缘设备不能直接显示;通过 SSH 运行时
需要启用 X11 转发并确保 `DISPLAY` 可用,否则程序会给出明确错误并退出。依赖缺失时先
执行 `bash scripts/bootstrap.sh`
## 模型与标签
`detection.model@1` 不绑定某一个框架;它通过 `DetectionModelRegistry` 查找配置中的
`model`。每个 `DetectionModelSpec` 注册版本化模型 ID、模型名称、backend 和有序
`supported_labels`。注册表当前包含四个内置模型:生产 Pipeline 引用 Construction PPE
与 People-TalkingDebug Viewer 则并行运行 Construction PPE 与 YOLOv8n Mobile Phone
- [`construction-ppe-yolov8@1`](../models/detection/construction-ppe-yolov8/v1/README.md)
对应制品目录 `v1`19 类,包含用于违规告警的 `No-*` 标签;
- [`ppe-6classes-yolov8n@1`](../models/detection/ppe-6classes-yolov8n/v1/README.md)
对应制品目录 `v1`,包含 `Gloves`、`Vest`、`goggles`、`helmet`、`mask`、
`safety_shoe` 六个正向装备标签。
- [`people-talking-yolov8x@1`](../models/detection/people-talking-yolov8x/v1/README.md)
对应制品目录 `v1`checkpoint 标签为 `label`、`talking on phone`;生产配置只选择
语义明确的 `talking on phone`
- [`yolov8n-mobile-phone@1`](../models/detection/yolov8n-mobile-phone/README.md)
checkpoint 单类标签为 `mobile_phone`Debug Viewer 用它显示画面中的手机物体。
模型 ID 尾部的 `@1` 表示注册版本 1多数制品使用 `v1` 子目录,当前
YOLOv8n Mobile Phone 保留导入时的扁平目录和上游文件名。运行时不会根据 ID 自动
拼接路径,仍由 YAML 中的 `model_options.weights` 显式指定。训练信息、完整标签顺序、
性能、限制和许可信息请查看各自的 model card。
可用 `cmvr-edge-ai models` 核对 ID、名称、backend 和标签顺序。六类模型只表达
“检测到某件装备”,不包含 `Person``No-*` 类,也不执行人员/PPE 关联;所以它
不能直接判断某个人缺少装备。需要这种语义时,仍应增加人员检测、空间关联和缺失
判定节点,不能把“没有检测到 helmet”直接当成“人员未戴安全帽”。
People-Talking 是纯视觉检测:它只能判断画面是否像“正在用手机通话”,不能读取真实
通话状态,不能区分接听/拨出,也不能证明电话已经接通。上游 checkpoint 中含义不清的
`label` 类为了保持 class ID 对齐仍在注册表中,但不会进入当前告警规则。
YOLOv8n Mobile Phone 只检测手机物体,不能据此判断人员正在接打电话。合成全黑图的
本地 smoke test 也出现过高置信度误报,因此 Viewer 默认阈值只是起点,应使用真实相机
画面校准 `confidence`
检测节点的关键参数:
| 参数 | 语义 |
|---|---|
| `model` | 必须是已注册的精确模型 ID |
| `detect_labels` | 本次部署需要的标签;省略表示模型的全部注册标签 |
| `confidence` | 所有选中标签的默认置信度阈值 |
| `label_confidence` | 可选的逐标签阈值覆盖 |
| `max_fps` | 推理启动频率上限;过密的解码帧会被跳过 |
| `inference_log_interval_s` | 可选推理摘要周期(秒);首帧立即输出,省略表示关闭周期日志 |
| `attach_frame` | 是否让 `DetectionResult` 临时附带对应解码帧;生成告警图片时必须为 `true` |
| `model_options` | backend 私有选项;当前 YOLO 支持 weights、device、imgsz、iou、half、max_det、agnostic_nms |
配置阶段会拒绝未知模型、模型不支持的 `detect_labels`、重复标签和非法阈值。YOLO 加载 checkpoint 时还会严格比较 checkpoint 的实际标签及顺序与注册信息,避免类别编号静默错位。
## 六类逐推理结果分支
这一分支当前默认关闭。恢复后,六类 detector 不连接 repeat gate而是把每次完成推理产生的
`DetectionResult/v1` 直接交给第二个 HTTP Sink。其 POST Envelope 的
`schema``DetectionResult/v1``payload` 示例为:
```json
{
"detections": [
{
"label": "helmet",
"confidence": 0.91,
"box": {"x_min": 120.0, "y_min": 60.0, "x_max": 250.0, "y_max": 220.0},
"track_id": null
}
],
"model_id": "ppe-6classes-yolov8n@1",
"inference_ms": 34.2,
"model_name": "PPE Detection YOLOv8n (6 Classes)"
}
```
该结构刻意不同于原分支的 `DetectionAlert/v1`:它没有 `rule_id`、`hit_count`、
`event_id``image`,空检测帧也会以空 `detections` 数组上报。`cooldown_ms` 是
repeat gate 的内部规则配置,不属于 `DetectionAlert` payload。由于
payload 没有 event IDHTTP Sink 的 `Idempotency-Key` 回退使用 Envelope
`trace_id`。这只是模拟的逐结果接口;若平台要求另一套字段命名或嵌套,需要增加
平台专用转换节点/Sink而不是仅修改 endpoint。
## 重复触发规则
`detection.repeat_gate@1` 按规则维护滑动时间窗口。规则字段如下:
| 参数 | 语义 |
|---|---|
| `id` | 唯一规则 ID写入告警 |
| `labels` | 任一标签匹配即视为该规则在当前帧命中 |
| `min_confidence` | 规则侧最低置信度,可高于 detector 阈值 |
| `min_hits` | 触发所需的不同帧数 |
| `window_ms` | 上述命中必须落入的时间窗口 |
| `cooldown_ms` | 告警后的静默期;期间不累计,结束后重新计数 |
| `scope` | `source` 按相机统计;`track` 按相机内的 `track_id` 分别统计 |
一次命中按 `(source_id, sequence)` 的不同视频帧计算:同一帧出现多个相同标签框只算一次,重复投递同一帧也不会增加计数。一个规则即使配置多个标签,同一 scope 在一帧内仍只增加一次。`time_source` 可选 `captured`、`received` 或优先采集时间的 `auto`;当前 cmvr-es 成功帧通常没有 `header.timestamp`,示例显式使用 `received`
`alert_image.enabled` 默认为 `false`;启用后要求 detector 同时配置
`attach_frame: true`。`jpeg_quality` 是 `1..95` 的整数,默认 85。repeat gate
只在规则达到 `min_hits`、准备输出告警时,才用 Pillow 在当前阈值帧上绘制匹配的
bounding boxes、标签和置信度并编码 JPEG不会为每个推理结果都渲染。告警里的
`detections` 和图片框均来自阈值帧;窗口内更早的命中只参与次数、时间范围和
`max_confidence` 统计。
同一阈值帧同时触发多条规则或多个 track 时repeat gate 对这些告警相关框取并集,
只画框并编码一次,然后让本帧产生的告警共享该 JPEG避免在边缘端重复编码。
非有限坐标、反向/退化框、非法置信度等 detection 会在规则计数前被忽略,并记录在
`invalid_detections` 健康指标中,避免 `NaN/Inf` 令整个 HTTP JSON 告警无法发送。
当前 YOLO adapter 是逐帧检测,不执行人员跟踪,输出的 `track_id``None`。因此示例使用 `scope: source`;如果业务要求“同一个人连续多次违规”,必须在 detector 和 repeat gate 之间加入 tracker 及人员/PPE 关联节点,之后才能使用 `scope: track`
## 相机启动与视频流日志
cmvr-es 的 `StartCamera``GetRGBImageStream` 是两个不同阶段:前者打开物理相机,
后者只启动编码和流式传输。连接器在每个首次连接或重连 session 中严格执行:
```text
StartCamera -> 检查 feedback.header.success -> GetRGBImageStream -> 等待首帧
```
`StartCamera` 使用 `cmvr_es.timeout_s`RPC 异常或 `header.success=false` 都进入同一套
指数退避重连。正常关闭只取消当前 stream不自动调用设备级 `StopCamera`,避免影响
同一相机的其他客户端。
使用 `--log-level INFO --log-format json` 时,按顺序关注:
- `camera source configured`endpoint、device 和超时配置完成;
- `camera start requested/succeeded`StartCamera 已被调用并成功;
- `camera stream opening`:流请求已创建,正在等待第一帧;
- `camera stream first frame`:已经收到真实编码帧,包含 codec、尺寸、字节数和远程序号
- `camera stream progress`:每 `stream_log_interval_s` 秒输出帧数、FPS、bitrate、关键帧、
最后序号和 `last_frame_age_s`;即使完全没帧也会输出 `first_frame_received=false`
- `camera stream disconnected`:包含错误类型、重连次数和退避时间;
- `camera stream closed`、`camera source stopped`:当前 session 和本地 Source 已清理。
INFO 日志不会打印帧二进制。示例设置 `stream_log_interval_s: 5` 便于联调,正式部署
可改为 3060 秒。
## 视频连续性和背压
H264/H265 是有参考关系的编码流,解码前不能任意丢包:
- `camera -> decoder` 使用 `video_contiguous + block`,保持 edge-ai 内部已经接受的数据连续;
- decoder 在新 session、编码参数变化、sequence gap 或解码错误后释放上下文,等待下一个关键帧;
- `decoder -> detector` 已经是完整 BGR 图像,使用 `realtime_latest + drop_oldest` 和容量 1以有限内存换取较低实时延迟
- `detector -> repeat_gate``attach_frame: true` 时携带未压缩图像,示例把队列容量限制为 2避免排队的 `DetectionResult` 长时间占用大量内存。
该边使用 `drop_oldest` 偏向低延迟;持续过载时,被丢弃的推理结果不会计入
`min_hits`。如果业务更重视每次推理结果都参与计数,可改为 `overflow: block`,但要
接受延迟向上游传播,并继续保持很小的队列容量。
恢复第二模型时,应从 decoder 输出端口 fan-out以避免第二次相机订阅和第二次
H264/H265 解码;这不会复用模型计算,两个 YOLO 实例仍会分别加载权重并共享应用的
有界线程池。
这个保证只覆盖 edge-ai 内部。当前 cmvr-es 服务端通过 `getLatestEncodedFrame` 读取最新编码数据;如果它在负载或时序竞争下已经跳过参考包,`video_contiguous` 无法恢复丢失内容,而且当前 edge-ai 的本地 sequence 不能可靠暴露这种上游跳包。此时 PyAV 可能报错decoder 会重置并等待关键帧。上线前必须用真实摄像头长时间验证连续性;更稳妥的方案是让 cmvr-es 提供连续 access unit 流,或额外提供原始/JPEG/带显式 discontinuity 的 AI 接口。
相机 Source 默认启用指数退避重连;每次重新订阅生成新的 `session_id`,使 decoder 主动重置。`reconnect_initial_s`、`reconnect_max_s` 和可选 `max_reconnect_attempts` 可配置;省略最大次数表示持续重连直到进程关闭。
## HTTP 输出可靠性
每次规则触发会产生带唯一 `event_id``DetectionAlert/v1`。HTTP Sink 优先把该
`event_id` 放入 `Idempotency-Key`,没有事件 ID 时才回退到 `trace_id`。它对连接/超时
错误和配置的临时 HTTP 状态执行有限指数退避,示例最多尝试 3 次。
`failure_mode` 支持两种明确语义:默认 `raise` 在最终投递失败时让 Pipeline 失败;当前
检测配置使用 `log_and_drop`,最终连接失败、超时或非 2xx 时输出
`HTTP report dropped ... action=drop` WARNING丢弃当前告警并继续处理后续视频帧。
`CancelledError`、序列化错误和未知本地程序异常不会被吞掉。
当前唯一 HTTP Sink 将 PPE 和电话使用告警发送到
`http://127.0.0.1:8081/v1/detection-alerts`payload 中的 `model_id``rule_id`
用于区分来源。8082 六类逐推理结果 endpoint 当前未配置。
启用告警图片后POST JSON 的 `payload.image` 结构如下:
```json
{
"media_type": "image/jpeg",
"width": 1280,
"height": 720,
"encoding": "base64",
"data": "/9j/4AAQSk..."
}
```
`width/height` 是 JPEG 的像素尺寸HTTP JSON Sink 将内部 JPEG `bytes` 特判为
上述扁平对象,`encoding` 固定为 `base64`。关闭 `alert_image`,或运行时因第三方
结果未附带帧、坏帧等原因渲染失败时,告警仍会发送且该字段为 `null`。Base64 会
额外增加约三分之一的体积,平台和反向代理需要配置足够的请求体上限。
这不是持久化 outbox`log_and_drop` WARNING 表示该告警已经永久丢失,不会自动补发;
每次告警在放弃前仍会完成有限重试,因此平台离线时 Sink 队列会短暂阻塞。进程崩溃、
断电或告警仍在内存队列中时也可能丢失。平台必须按幂等键去重;对“不可丢告警”的
部署,还需要后续增加有界磁盘 outbox、投递确认和恢复发送。
## 部署风险
- 模型 README 报告某些小样本类别存在漏检,实际园区需要按相机角度、光照、遮挡和距离重新标定置信度与 `min_hits`;规则降噪不能补偿模型系统性漏检。
- 权重目录对权重许可的描述与 Ultralytics runtime/checkpoint 中的 AGPL 信息需要在商业部署前核对;同时确认训练数据来源和权重再分发权利。
- `.pt` 使用 PyTorch checkpoint loader只加载可信构建和受控分发的权重并固定、验证实际 Ultralytics 8.x 版本。
如果检测结果还要驱动机器人,控制路径必须保持:
```text
DetectionResult -> policy -> RobotCommand/v1
-> safety.robot_command_gate@1
-> ApprovedRobotCommand/v1
-> cmvr.grpc.agv_command_sink@1
```
编译器要求 actuator 的每个直接前驱都是安全门,安全门和执行器之间不能插入普通变换节点,也不能存在绕过路径。

View File

@ -81,7 +81,11 @@ flowchart TB
- 直接连接的 `detection.model@1 -> detection.repeat_gate@1` 会检查规则标签确实包含在 detector 本次选择的标签中; - 直接连接的 `detection.model@1 -> detection.repeat_gate@1` 会检查规则标签确实包含在 detector 本次选择的标签中;
- 当前未实现的 `max_age_ms``put_timeout_ms` 不能设置。 - 当前未实现的 `max_age_ms``put_timeout_ms` 不能设置。
`validate` 会实际调用插件工厂,因此也会检查构造函数所需参数、已注册模型 ID 和标签选择;但不会执行组件的 `setup()`。YOLO 权重是否存在、网络连通性、cmvr protobuf 是否存在、PyAV/Ultralytics/Pillow 可选依赖是否可导入,以及 checkpoint 内部标签是否与注册信息一致,要到 `run` 的 setup 或首个编码帧时才检查;启用 `alert_image` 后 Pillow 会在 repeat gate 的 setup 阶段提前检查。 `validate` 会实际调用插件工厂,因此也会检查构造函数所需参数、已注册模型 ID 和标签选择;
但不会执行组件的 `setup()`。ONNX 权重/manifest 是否存在且哈希匹配、网络连通性、
cmvr protobuf、PyAV/ONNX Runtime/Pillow 可选依赖和图 metadata要到 `run` 的 setup
或首个编码帧时检查PT 回滚 adapter 的 checkpoint 标签也在 setup 检查。启用
`alert_image` 后 Pillow 会在 repeat gate 的 setup 阶段提前检查。
### 2.2 内部契约层 ### 2.2 内部契约层
@ -108,6 +112,8 @@ Envelope[T]
| `AudioChunk/v1` | `AudioChunk` | 连续音频块,包含格式、采样率和 discontinuity 标记 | | `AudioChunk/v1` | `AudioChunk` | 连续音频块,包含格式、采样率和 discontinuity 标记 |
| `DetectionResult/v1` | `DetectionResult` | 检测框、标签、置信度和推理时间;可临时附带对应解码帧供告警节点使用 | | `DetectionResult/v1` | `DetectionResult` | 检测框、标签、置信度和推理时间;可临时附带对应解码帧供告警节点使用 |
| `DetectionAlert/v1` | `DetectionAlert` | 规则 ID、模型、scope、时间窗口、命中数、置信度、阈值帧检测框、可选 JPEG 告警图和唯一 event ID | | `DetectionAlert/v1` | `DetectionAlert` | 规则 ID、模型、scope、时间窗口、命中数、置信度、阈值帧检测框、可选 JPEG 告警图和唯一 event ID |
| `InferenceRequest/v1` | `InferenceRequest` | 被动调用进入请求 Pipeline 后的统一输入;包含稳定 category、输入、业务参数和请求的 artifact roles |
| `InferenceResponse/v1` | `InferenceResponse` | 被动调用 Pipeline 的统一相关响应包含状态、输出、artifact、实际模型引用、耗时、warning 或错误 |
| `TextEvent/v1` | `TextEvent` | ASR/LLM/TTS 链路中的文本事件 | | `TextEvent/v1` | `TextEvent` | ASR/LLM/TTS 链路中的文本事件 |
| `ChatTurn/v1` | `ChatTurn` | 带 session 和历史的对话输入 | | `ChatTurn/v1` | `ChatTurn` | 带 session 和历史的对话输入 |
| `RobotCommand/v1` | `RobotCommand` | 带 TTL、序号和 command ID 的协议无关控制命令 | | `RobotCommand/v1` | `RobotCommand` | 带 TTL、序号和 command ID 的协议无关控制命令 |
@ -115,6 +121,20 @@ Envelope[T]
`PluginSpec` 中的 schema 是编译期契约字符串。当前运行时不会反射检查 payload 的 Python 类型;连接器和算法插件仍应使用 `isinstance` 或自己的严格模型在边界处失败。 `PluginSpec` 中的 schema 是编译期契约字符串。当前运行时不会反射检查 payload 的 Python 类型;连接器和算法插件仍应使用 `isinstance` 或自己的严格模型在边界处失败。
HTTP 边界不复制一套临时字典协议,而是复用 `cmvr_edge_ai.contracts` 中严格、版本化的
wire DTO
| `schema_version` | Python 类型 | HTTP 用途 |
|---|---|---|
| `cmvr.inference-request/v1` | `InferenceRequest` | `POST /v1/inference` 请求 |
| `cmvr.inference-response/v1` | `InferenceResponse` | 成功受理后的相关响应,包括业务失败状态 |
| `cmvr.inference-error/v1` | `InferenceErrorResponse` | 非 2xx 的稳定错误响应 |
| `cmvr.model-catalog/v1` | `ModelCatalog` | `GET /v1/models` 能力目录,分组列出主动推送与被动调用 |
Pipeline 端口使用 `InferenceRequest/v1`、`InferenceResponse/v1` 这样的内部 schema 名;
JSON 文档中的 `schema_version` 使用上表 `cmvr.*` 名称。两者属于同一 DTO 的内部图契约
和外部 wire 版本,不应互相替换。
### 2.3 组件层 ### 2.3 组件层
运行时只有三类组件: 运行时只有三类组件:
@ -163,6 +183,41 @@ Pipeline并由 CLI 以运行错误退出。相机 Source 和平台 HTTP Sink
有界的重连/重试语义HTTP Sink 还可显式选择 `log_and_drop`,但框架仍不提供通用节点级 有界的重连/重试语义HTTP Sink 还可显式选择 `log_and_drop`,但框架仍不提供通用节点级
supervisor 或 retry policy。 supervisor 或 retry policy。
### 2.5 主动推送与被动调用
运行时保留原有主动推送链路,同时增加共享 HTTP 入口的被动调用链路。`serve` 启动
配置中全部 `enabled: true` 的 Pipeline 和 HTTP API因此两种服务模式可以在同一
进程并存:
```mermaid
flowchart LR
subgraph Active["主动推送 active_push"]
Camera["相机 Source"] --> Decode["解码 / 模型 / 规则"] --> Platform["平台 Sink"]
end
subgraph Passive["被动调用 passive_invoke"]
Client["HTTP Client"] --> API["POST /v1/inference"] --> Broker["category 请求队列"]
Broker --> RequestSource["server.request_source@1"] --> Model["解码 / 模型"]
Model --> Response["server.detection_response@1"] --> ResponseSink["server.response_sink@1"]
ResponseSink --> Broker --> API --> Client
end
Routes["server.routes.<category>"] --> Broker
```
`cmvr_edge_ai.capabilities` 统一登记 capability 的稳定 category、模型实现和支持的服务
模式;`GET /v1/models` 返回的 `ModelCatalog` 分成 `active_push`
`passive_invoke` 两组,并把“代码已注册”和“部署已就绪”作为不同状态。一个同时支持
两种模式的 capability 可以出现在两组中。
公开路由边界是 namespaced category例如 `detect.ppe`、`detect.mobile_phone`,而不是
模型或 Pipeline 标识。客户端请求不能指定 `model_id`、Pipeline ID、权重路径或推理
设备;这些都由 `server.routes.<category>` 和服务端 Pipeline 拥有。响应和能力目录可以
报告实际模型元数据用于追踪,但该字段不是客户端选择器。
HTTP API 把每个 category 交给独立、有界的 invocation broker 队列,并等待相同请求的
相关响应。被动示例的图内边使用 `request + block`,不配置实时丢帧或 `max_fps`,保证
每个已接受请求最终得到响应或明确失败。未被 `server.routes` 引用的主动推送 Pipeline
不受被动调用图约束影响。
## 3. 配置参考 ## 3. 配置参考
### 3.1 根字段 ### 3.1 根字段
@ -173,8 +228,11 @@ supervisor 或 retry policy。
| `runtime` | 否 | 进程级运行参数 | | `runtime` | 否 | 进程级运行参数 |
| `endpoints` | 否 | 命名的外部服务连接信息 | | `endpoints` | 否 | 命名的外部服务连接信息 |
| `pipelines` | 是 | 至少一个命名 Pipeline | | `pipelines` | 是 | 至少一个命名 Pipeline |
| `server` | 否 | 被动调用 HTTP 监听和稳定 category 到服务端 Pipeline/模型的路由 |
endpoint、pipeline 和 node 名称只能包含字母、数字、`_`、`-`,且不能以数字开头。端口引用固定使用 `node.port` endpoint、pipeline 和 node 名称只能包含字母、数字、`_`、`-`,且不能以数字开头。
端口引用固定使用 `node.port`。公开 category 使用单独的小写 namespaced 格式,详见
`server.routes`
### 3.2 `runtime` ### 3.2 `runtime`
@ -194,7 +252,7 @@ v1 可配置的 runtime 字段只有 `thread_workers` 和 `shutdown_timeout_s`
| 字段 | 默认值 | 说明 | | 字段 | 默认值 | 说明 |
|---|---:|---| |---|---:|---|
| `transport` | 无 | 必填,转为小写;当前连接器支持 `grpc`、`http` | | `transport` | 无 | 必填,转为小写;当前连接器支持 `grpc`、`http` |
| `target` | `null` | gRPC 地址,例如 `127.0.0.1:50052` | | `target` | `null` | gRPC 地址,例如 `127.0.0.1:50052`;若供 HTTP Sink 上报 `grpc_ip`,必须是单个字面 IP 与端口 |
| `base_url` | `null` | HTTP 基础 URL | | `base_url` | `null` | HTTP 基础 URL |
| `bind` | `null` | 为未来 ingress/server 连接器预留 | | `bind` | `null` | 为未来 ingress/server 连接器预留 |
| `tls` | `false` | 仅用于 gRPC channel 是否使用 TLSHTTP endpoint 禁止设置该字段 | | `tls` | `false` | 仅用于 gRPC channel 是否使用 TLSHTTP endpoint 禁止设置该字段 |
@ -224,10 +282,9 @@ HTTP 是否使用 TLS 由 `base_url` 的 `https://` scheme 决定,证书验证
| `nodes` | 无 | 至少一个节点 | | `nodes` | 无 | 至少一个节点 |
| `edges` | `[]` | 有向连接列表 | | `edges` | `[]` | 有向连接列表 |
仓库的生产配置 `configs/edge_ai.yaml` 在同一个 YAML 中定义 `detection``talk` 仓库的主动推送配置为 `configs/active_detection.yaml`,当前只定义 `detection`
CLI 的 `--pipeline` 可以重复传入:显式传 `--pipeline detection``--pipeline talk` CLI 显式传 `--pipeline detection` 时只编译并运行该链路;省略参数时会启动配置中所有
只编译并运行所选链路;省略该参数时会启动所有 `enabled: true` 的 Pipeline。生产部署 `enabled: true` 的 Pipeline。不能提供真实能力的 Talk 占位图已移除。
通常应显式选择 Pipeline需要共享同一进程和网络客户端时才同时选择两条链路。
不要在活跃 Pipeline 中保留 `enabled: false` 节点;当前编译器会直接拒绝。要暂时关闭逻辑,请禁用整个 Pipeline 或从图和配置中移除该节点。 不要在活跃 Pipeline 中保留 `enabled: false` 节点;当前编译器会直接拒绝。要暂时关闭逻辑,请禁用整个 Pipeline 或从图和配置中移除该节点。
@ -301,6 +358,109 @@ v1 强制执行的 profile/overflow 矩阵:
队列记录入队、出队、水位、丢弃、拒绝和强制关闭丢弃计数,当前可通过 Python `PipelineRuntime.edge_stats()` 获取,尚未暴露为服务指标。 队列记录入队、出队、水位、丢弃、拒绝和强制关闭丢弃计数,当前可通过 Python `PipelineRuntime.edge_stats()` 获取,尚未暴露为服务指标。
### 3.7 `server`
`server` 是可选的进程级被动调用配置;只有 `cmvr-edge-ai serve` 会监听 HTTP。最小
结构如下:
```yaml
server:
enabled: true
http:
bind: 127.0.0.1
port: 8081
request_timeout_s: 30
max_request_bytes: 16777216
max_image_bytes: 10485760
access_log: false
routes:
detect.ppe:
pipeline: detect_ppe
model_id: construction-ppe-yolov8@2
queue_capacity: 4
timeout_s: 30
description: PPE detection
```
`server` 字段:
| 字段 | 默认值 | 说明 |
|---|---:|---|
| `enabled` | `false` | 是否启用被动 HTTP 服务;设为 `true` 时至少需要一条 route |
| `http` | 见下表 | 监听、请求超时与请求体限制 |
| `routes` | `{}` | 稳定 category 到 Pipeline 和模型的服务端映射 |
`server.http` 字段:
| 字段 | 默认值 | 说明 |
|---|---:|---|
| `bind` | `127.0.0.1` | 监听 host/IP不接受 URL scheme 或 path |
| `port` | `8080` | HTTP 监听端口,范围 `1..65535` |
| `bearer_token` | `null` | Bearer 凭据;配置后除 `/health/live`、`/health/ready` 外的入口都要求 `Authorization: Bearer <token>`;非 loopback 监听必须配置 |
| `tls_certfile` | `null` | Uvicorn TLS 证书文件;必须与 `tls_keyfile` 同时配置 |
| `tls_keyfile` | `null` | Uvicorn TLS 私钥文件;必须与 `tls_certfile` 同时配置 |
| `allow_insecure_remote` | `false` | 是否明确允许非 loopback 地址通过明文 HTTP 传输 Bearer token仅限受信隔离网络 |
| `request_timeout_s` | `30.0` | route 未单独设置超时时使用的请求总超时,范围 `(0, 3600]` 秒 |
| `max_request_bytes` | `16777216` | JSON 请求体上限,范围 1 KiB1 GiB |
| `max_image_bytes` | `10485760` | Base64 解码后的单张输入图像上限,且不能大于请求体上限 |
| `access_log` | `false` | 是否启用 Uvicorn access log |
`server.routes.<category>` 字段:
| 字段 | 默认值 | 说明 |
|---|---:|---|
| `pipeline` | 无 | 必填;服务端拥有的、已启用的请求/响应 Pipeline ID |
| `model_id` | 无 | 必填;服务端选择的版本化模型 ID |
| `queue_capacity` | `4` | 该 category invocation broker 的有界请求容量 |
| `timeout_s` | `null` | 可选 route 超时;省略时使用 `server.http.request_timeout_s` |
| `description` | `""` | 能力目录中的部署说明 |
category 必须是含至少一个点的小写命名空间,例如 `detect.ppe`;每条 invocation
Pipeline 只能被一个公开 category 引用。`model_id` 会出现在能力目录和响应中用于追踪,
但客户端请求中没有模型、Pipeline 或权重选择字段。
`127.0.0.1`、`::1` 和 `localhost` 可在本机调试时不配置认证。任何其他 bind包括
`0.0.0.0`、`::` 和普通 hostname都必须设置 `bearer_token`,并默认要求同时提供 TLS
证书和私钥。只有部署在受信、隔离网络且明确接受 token 明文传输风险时,才能设置
`allow_insecure_remote: true` 代替 TLS。认证失败返回 `401 cmvr.inference-error/v1`
`WWW-Authenticate: Bearer`,健康探针保持无需认证。
安全的局域网监听示例token 从进程环境读取,不写入仓库):
```yaml
server:
enabled: true
http:
bind: 0.0.0.0
bearer_token: env://CMVR_EDGE_AI_BEARER_TOKEN
tls_certfile: /etc/cmvr-edge-ai/tls/server.crt
tls_keyfile: /etc/cmvr-edge-ai/tls/server.key
```
配置模型与 DAG 编译器会 fail closed 地执行以下约束:
- route 引用的 Pipeline 必须存在且启用,同一 Pipeline 不能映射到多个 category
- 所有边都必须使用 `request + block`,避免已接受请求被 QoS 静默丢弃;
- 每条被动 Pipeline 必须恰好包含一个 `server.request_source@1` 和一个
`server.response_sink@1`;请求 source 的 category 必须与 route 一致;
- 必须恰好有一个节点输出 `InferenceResponse/v1`,其 category 也必须与 route 一致;
- 必须恰好有一个模型所有者 Operator它通过 `PluginSpec.route_model_param` 声明模型参数,
编译器对所有模型类别校验该参数与 route 的 `model_id` 一致;
- 每个 Operator 都必须声明 `invocation_cardinality: exactly_one`,运行时会先完整验证本次
调用确实只产生一个 emission再向下游路由返回 `None`、多条或惰性迭代异常都会立即
失败当前请求;
- v1 请求图必须是 source 到 sink 的单线性路径;在引入显式 join/cardinality 语义前,
禁止 fan-out 后汇合导致同一个请求被重复处理;
- `detect.*` Pipeline 必须恰好包含一个 `detection.model@1`,且不能设置会跳过请求的
`max_fps`
- source 到 sink 必须存在有向路径,所有节点都必须位于某条有效请求-响应路径上,并且
不能存在绕过必需 response 节点或 detector 的旁路。
完整的双检测示例见
[configs/server_detect.yaml](../configs/server_detect.yaml)。它为
`detect.ppe``detect.mobile_phone` 分别创建独立 Pipeline所有图内边使用
`request/block` QoS。
## 4. 内置插件与连接器 ## 4. 内置插件与连接器
运行 `cmvr-edge-ai plugins` 可查看当前实际注册结果。 运行 `cmvr-edge-ai plugins` 可查看当前实际注册结果。
@ -342,37 +502,65 @@ with:
| `detection.model@1` | `ImageFrame/v1 -> DetectionResult/v1` | 加载一个已注册模型,在线程池中推理,按部署标签和阈值二次过滤;可附带对应解码帧 | | `detection.model@1` | `ImageFrame/v1 -> DetectionResult/v1` | 加载一个已注册模型,在线程池中推理,按部署标签和阈值二次过滤;可附带对应解码帧 |
| `detection.repeat_gate@1` | `DetectionResult/v1 -> DetectionAlert/v1` | 按不同帧、时间窗口、scope 和 cooldown 把逐帧检测转换为平台告警,仅在规则触发时按需画框并编码 JPEG | | `detection.repeat_gate@1` | `DetectionResult/v1 -> DetectionAlert/v1` | 按不同帧、时间窗口、scope 和 cooldown 把逐帧检测转换为平台告警,仅在规则触发时按需画框并编码 JPEG |
核心包不会强制安装大型视觉运行时。使用解码器安装 `video` extra告警图的画框和 JPEG 编码由 Pillow 提供,安装独立的 `image` extra内置 YOLO adapter 提供通用 `yolo` 和固定 CPU wheel 的 `yolo-cpu` 两个互斥 extra。当前 PPE/电话使用检测 CPU 链路通过 `bash scripts/bootstrap.sh` 一键生成 cmvr-es bindings并按 `uv.lock` 安装 `grpc + http + video + image + yolo-cpu`。`yolo` 与 `yolo-cpu` 都显式包含 People-Talking checkpoint 反序列化所需的 `dill`。开发环境使用 `bash scripts/bootstrap.sh --profile dev`。PyAV、Pillow 和 Ultralytics 都延迟导入,因此不运行对应检测能力的对话服务不会加载它们。不要依赖 Ultralytics 间接带入 Pillow手动组合告警图片环境时必须显式选择 `--extra image`。CUDA/Jetson 必须按目标驱动或 JetPack 建立独立依赖源和锁文件。 核心包不会强制安装视觉运行时。解码器使用 `video` extra告警图使用独立的 `image`
extra当前部署检测使用 `onnx-cpu`,只安装 NumPy、Pillow 和 ONNX Runtime不导入
Torch/Ultralytics。`bash scripts/bootstrap.sh` 按 `uv.lock` 安装
`grpc + http + video + image + server + onnx-cpu``--profile dev` 改用锁定的
`onnx-export-cpu`,额外提供 Torch/Ultralytics/ONNX 构建工具。旧 `@1` 回滚路径仍可
显式选择 `yolo``yolo-cpu`。CUDA、TensorRT 与 Jetson provider 必须按目标驱动
建立独立 source/lock 并重新验证,不能只改 YAML。
具体模型不再各自注册一套 DAG 插件。`DetectionModelRegistry` 保存 `DetectionModelSpec` 具体模型不再各自注册一套 DAG 插件。`DetectionModelRegistry` 保存 `DetectionModelSpec`
| 字段 | 说明 | | 字段 | 说明 |
|---|---| |---|---|
| `model_id` | 版本化稳定 ID例如 `construction-ppe-yolov8@1` | | `model_id` | 版本化稳定 ID例如 `construction-ppe-yolov8@2` |
| `name` | 日志、CLI 和告警中的可读模型名称 | | `name` | 日志、CLI 和告警中的可读模型名称 |
| `supported_labels` | 有序、非空、无重复的标签;顺序必须与 backend 类别编号一致 | | `supported_labels` | 有序、非空、无重复的标签;顺序必须与 backend 类别编号一致 |
| `backend` | backend 标识,例如 `ultralytics-yolo` | | `backend` | backend 标识,例如 `onnxruntime-yolov8` |
| `factory` | 接收 `model_options` 并返回 `DetectionModel` 的工厂 | | `factory` | 接收 `model_options` 并返回 `DetectionModel` 的工厂 |
`cmvr-edge-ai models` 输出当前 model registry 的 ID、name、backend 和 labels。第三方包可在 `cmvr_edge_ai.detection_models` entry point 中暴露一个 `DetectionModelSpec` 或注册回调。当前内置 `construction-ppe-yolov8@1`19 类)和 `ppe-6classes-yolov8n@1``Gloves/Vest/goggles/helmet/mask/safety_shoe`YOLO adapter 在加载时严格比较 checkpoint `model.names` 与各自注册的标签及顺序,避免错误的类别编号继续运行。 `cmvr-edge-ai models` 输出 model registry 的 ID、name、backend 和 labels。四个模型都保留
`ultralytics-yolo` `@1` 导出/回滚注册,并提供 `onnxruntime-yolov8` `@2`;当前三个
部署 YAML 使用 `@2`。第三方包可通过 `cmvr_edge_ai.detection_models` entry point
暴露 `DetectionModelSpec` 或注册回调。
内置检测模型制品按 `models/detection/<model-name>/vN/` 组织,每个版本目录同时 内置检测模型制品通常`models/detection/<model-name>/vN/` 组织,每个版本目录同时
保存权重和独立 model card 保存权重和独立 model cardMobile Phone 只有 v1 PT 保留历史扁平布局
- [Construction PPE YOLOv8 v1](../models/detection/construction-ppe-yolov8/v1/README.md) - [Construction PPE YOLOv8 v1](../models/detection/construction-ppe-yolov8/v1/README.md)
- [PPE YOLOv8n 6 Classes v1](../models/detection/ppe-6classes-yolov8n/v1/README.md)。 - [PPE YOLOv8n 6 Classes v1](../models/detection/ppe-6classes-yolov8n/v1/README.md)
- [People Talking YOLOv8x v1](../models/detection/people-talking-yolov8x/v1/README.md)
- [YOLOv8n Mobile Phone v1](../models/detection/yolov8n-mobile-phone/README.md)。
- [Construction PPE YOLOv8 ONNX v2](../models/detection/construction-ppe-yolov8/v2/README.md)
- [PPE YOLOv8n 6 Classes ONNX v2](../models/detection/ppe-6classes-yolov8n/v2/README.md)
- [People Talking YOLOv8x ONNX v2](../models/detection/people-talking-yolov8x/v2/README.md)
- [YOLOv8n Mobile Phone ONNX v2](../models/detection/yolov8n-mobile-phone/v2/README.md)。
`DetectionModelSpec.model_id` 尾部的 `@N` 与制品目录的 `vN` 对应,例如 `DetectionModelSpec.model_id` 尾部的 `@N` 与制品目录的 `vN` 对应,例如
`construction-ppe-yolov8@1` 对应 `construction-ppe-yolov8/v1/`。这是注册表与制品 `construction-ppe-yolov8@1` 对应 `construction-ppe-yolov8/v1/`。这是注册表与制品
库的版本约定,运行时不会由模型 ID 自动推导权重路径;部署配置仍必须 库的版本约定;当前 Mobile Phone 制品是导入布局的显式例外。运行时不会由模型 ID
显式给出 `model_options.weights`。标签、训练来源、评估、局限和许可信息由每个 自动推导权重路径,部署配置仍必须显式给出 `model_options.weights`。标签、训练来源、
版本目录的 model card 维护,架构文档只定义制品与运行时的边界。 评估、局限和许可信息由每个 model card 维护,架构文档只定义制品与运行时的边界。
四个 `.pt` 模型的 `@1` 版本继续保留;对应 `@2` 注册使用
`onnxruntime-yolov8`。该 backend 只接受静态 `[1, 3, imgsz, imgsz]` 输入和
`[1, 4 + classes, anchors]` 原始输出,在 NumPy 中执行 letterbox、类别筛选与 NMS
因此边缘运行时不导入 Torch/Ultralytics。加载阶段先校验 manifest、artifact SHA256、
内嵌模型身份,再校验 ONNX 的 `task`、`names`、`imgsz` metadata、静态 shape、provider
和注册标签顺序,防止错误或被替换的制品静默运行。
`scripts/export_detection_onnx.py` 是构建期工具:固定 batch=1、dynamic=false、
nms=false、CPU FP32阻止 Ultralytics 自动安装未锁定依赖,清除训练机路径/时间戳,
并生成带来源/制品 SHA256 的 manifest。四份 v2 制品已经生成并完成真实 ORT smoke
现场发布仍必须补充有授权的非方形图片 parity 与目标硬件资源验收。People-Talking 的
YOLOv8x 计算量不会因为文件格式变化而消失,资源不足时应换小模型或使用现场校准 INT8。
`detection.model@1` 参数: `detection.model@1` 参数:
```yaml ```yaml
with: with:
model: construction-ppe-yolov8@1 model: construction-ppe-yolov8@2
detect_labels: [No-Helmet, No-Vest] detect_labels: [No-Helmet, No-Vest]
confidence: 0.5 confidence: 0.5
label_confidence: label_confidence:
@ -381,8 +569,10 @@ with:
inference_log_interval_s: 5 inference_log_interval_s: 5
attach_frame: true attach_frame: true
model_options: model_options:
weights: models/detection/construction-ppe-yolov8/v1/best.pt weights: models/detection/construction-ppe-yolov8/v2/model.onnx
device: cpu providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
imgsz: 640 imgsz: 640
``` ```
@ -398,7 +588,9 @@ YAML 文件的所在目录解析。仓库内配置和文档命令以仓库根目
始终输出一次 INFO。这样可以区分“权重已加载但没有输入帧”“持续推理但没有命中”与 始终输出一次 INFO。这样可以区分“权重已加载但没有输入帧”“持续推理但没有命中”与
“检测到目标”,同时避免逐帧日志拖慢边缘端; “检测到目标”,同时避免逐帧日志拖慢边缘端;
- `attach_frame` 默认 `false`;启用时 `DetectionResult` 临时引用对应的解码帧,供 repeat gate 在阈值帧上生成告警图片。由于它携带未压缩图像detector 到 repeat gate 的队列容量必须保持较小; - `attach_frame` 默认 `false`;启用时 `DetectionResult` 临时引用对应的解码帧,供 repeat gate 在阈值帧上生成告警图片。由于它携带未压缩图像detector 到 repeat gate 的队列容量必须保持较小;
- `model_options` 属于 backend内置 YOLO 支持 `weights/device/imgsz/iou/half/max_det/agnostic_nms` - `model_options` 属于 backendONNX 支持
`weights/providers/intra_op_threads/inter_op_threads/imgsz/iou/max_det/agnostic_nms`
PT 回滚 adapter 支持 `weights/device/imgsz/iou/half/max_det/agnostic_nms`
- YOLO 只接受解码后的 `BGR8/RGB8` packed buffer并把 `RGB8` 转为 backend 使用的 BGR 顺序;它不运行 tracker返回的 `track_id``None` - YOLO 只接受解码后的 `BGR8/RGB8` packed buffer并把 `RGB8` 转为 backend 使用的 BGR 顺序;它不运行 tracker返回的 `track_id``None`
示例 detector 到 repeat gate 使用小容量 `drop_oldest` 队列以控制延迟和原始帧内存; 示例 detector 到 repeat gate 使用小容量 `drop_oldest` 队列以控制延迟和原始帧内存;
@ -409,8 +601,8 @@ YAML 文件的所在目录解析。仓库内配置和文档命令以仓库根目
独立 Pipeline 独立 Pipeline
```text ```text
camera -> decoder -> construction detector -> repeat gate -> 8081 alert API camera -> decoder -> construction detector -> repeat gate -> alert API
`-> six-class detector -----------------> 8082 result API `-> six-class detector -----------------> result API
``` ```
该可选双分支方案的两条 `decoder.frames` 出边使用独立的 该可选双分支方案的两条 `decoder.frames` 出边使用独立的
@ -530,8 +722,10 @@ actuator:
`platform.http_json_sink@1` 接收任意 schema `platform.http_json_sink@1` 接收任意 schema
- 参数:`endpoint`、`path`(默认 `/`)、`max_attempts`(默认 3、`retry_initial_s`、`retry_max_s` 和 `retry_statuses` - 参数:`endpoint`、`path`(默认 `/`)、可选 `grpc_endpoint`、`failure_mode`、`max_attempts`(默认 3、`retry_initial_s`、`retry_max_s` 和 `retry_statuses`
- POST Envelope 元数据、输入端口、attributes 和 payload - POST Envelope 元数据、输入端口、attributes 和 payload
- 配置 `grpc_endpoint` 时,从被引用 gRPC endpoint 的 `target` 提取字面 IP并在
Envelope JSON 顶层增加 `grpc_ip``source_id` 仍表示相机/数据源;
- bytes 转换为 `{encoding: base64, data: ...}` - bytes 转换为 `{encoding: base64, data: ...}`
- `EncodedImage` 特判为 `media_type/width/height/encoding/data` 同层的扁平对象,避免 `payload.image.data` 再嵌套一层; - `EncodedImage` 特判为 `media_type/width/height/encoding/data` 同层的扁平对象,避免 `payload.image.data` 再嵌套一层;
- payload 有非空 `event_id` 时以它作为 `Idempotency-Key`,否则使用 `trace_id`;平台仍必须真正实现按键去重; - payload 有非空 `event_id` 时以它作为 `Idempotency-Key`,否则使用 `trace_id`;平台仍必须真正实现按键去重;
@ -540,17 +734,57 @@ actuator:
`log_and_drop` 输出 WARNING、丢弃当前报告并继续 `log_and_drop` 输出 WARNING、丢弃当前报告并继续
- 没有持久化 outbox/spool。有限重试只存在于当前进程内崩溃、断电或重启不会恢复尚未投递的告警。 - 没有持久化 outbox/spool。有限重试只存在于当前进程内崩溃、断电或重启不会恢复尚未投递的告警。
同一个通用 Sink 可以按节点配置不同的 endpoint 和 path。当前检测配置只把 例如 `grpc_endpoint: cmvr_es``endpoints.cmvr_es.target`
`DetectionAlert/v1` 发到 `http://127.0.0.1:8081/v1/detection-alerts`。若恢复六类模型, `192.168.0.119:50052` 时,请求外层包含:
```json
{
"schema": "DetectionAlert/v1",
"source_id": "wrist_cam",
"grpc_ip": "192.168.0.119",
"input_port": "input",
"payload": {"event_id": "..."},
"attributes": {}
}
```
为避免 DNS 多地址与运行时漂移,`grpc_endpoint` 引用的 target 必须是单个字面
IPv4/IPv6 加端口hostname、Unix socket 和多地址 target 会在 Sink setup 时被拒绝。
同一个通用 Sink 可以按节点配置不同的 endpoint 和 path。当前检测配置把
`DetectionAlert/v1` 发到 `ppe_alert_platform``/v1/detection-alerts`。若恢复六类模型,
可把每次推理的 `DetectionResult/v1` 发到独立 endpoint它与告警复用 Envelope JSON 可把每次推理的 `DetectionResult/v1` 发到独立 endpoint它与告警复用 Envelope JSON
外层,但 payload 契约不同,且没有 event ID幂等键会回退为 `trace_id`。Sink 不支持 外层,但 payload 契约不同,且没有 event ID幂等键会回退为 `trace_id`。Sink 不支持
通过 YAML 重命名或重排 payload 字段,如果目标平台要求自定义 wire contract应增加 通过 YAML 重命名或重排 payload 字段,如果目标平台要求自定义 wire contract应增加
平台专用转换节点或 Sink。 平台专用转换节点或 Sink。
运行时对未处理异常采用 fail-fast。HTTP Sink 默认 `failure_mode: raise`,最终发送失败 运行时对未处理异常采用 fail-fast。HTTP Sink 默认 `failure_mode: raise`,最终发送失败
会终止它所在的 Pipeline当前 8081 告警 Sink 显式使用 `log_and_drop`,所以平台离线只会 会终止它所在的 Pipeline当前告警 Sink 显式使用 `log_and_drop`,所以平台离线只会
产生 WARNING 并丢弃对应告警,不会停止相机和检测。该模式不是可靠投递机制。 产生 WARNING 并丢弃对应告警,不会停止相机和检测。该模式不是可靠投递机制。
### 4.5 被动调用边界插件与 HTTP API
被动 Pipeline 使用四个内置边界/转换插件:
| 插件 ID | 输入 -> 输出 | 用途 |
|---|---|---|
| `server.request_source@1` | 无 -> `InferenceRequest/v1` | 从对应 category 的 broker 取出已接受请求 |
| `media.image_decoder.pillow@1` | `InferenceRequest/v1 -> ImageFrame/v1` | 解码内联 JPEG/PNG并执行媒体类型、尺寸和像素数检查 |
| `server.detection_response@1` | `DetectionResult/v1 -> InferenceResponse/v1` | 规范化检测框、实际模型信息和按需生成的 `annotated/original` JPEG artifact |
| `server.response_sink@1` | `InferenceResponse/v1 -> 无` | 按内部 request ID + invocation token 完成 broker waiter把结果交还 HTTP 请求 |
`cmvr-edge-ai serve` 通过 `cmvr_edge_ai.server` 启动这些 Pipeline 和共享 HTTP API。
`POST /v1/inference` 只接受 `application/json`请求体、单张解码图像、category、输入类型、
artifact role 和业务参数都在进入 Pipeline 前校验。`GET /v1/models` 返回能力及动态部署
状态;`GET /health/live` 只表示进程可响应,`GET /health/ready` 还要求应用与所需 Pipeline
就绪。入站 HTTP 支持由 `server` extra 提供,调用端 SDK 位于
`cmvr_edge_ai.client.detect`,使用 `http` extra。
单条解码、模型或响应转换异常会通过 Envelope 中不可复用的 invocation token 只失败对应
callerPipeline 继续处理后续请求。客户端超时或取消后,已经被 source 取出的任务仍占用
该 route 的 broker capacity直到模型返回 late result/failure 或服务关闭;这样不会把
仍在执行的旧推理伪装成空闲资源并继续接收新任务。尚未进入 DAG 的排队请求则可立即移除。
## 5. 插件开发约定 ## 5. 插件开发约定
### 5.1 创建组件 ### 5.1 创建组件
@ -627,6 +861,12 @@ def register_plugins(registry: PluginRegistry) -> None:
- 执行器 Sink 加 `actuator` 标签,并只声明/接受 `ApprovedRobotCommand/v1`;安全节点加 `safety_gate` 标签并完成 `RobotCommand -> ApprovedRobotCommand` 转换;不要为了通过编译给普通变换节点冒充安全标签;连接器加 `transport:grpc` 等标签; - 执行器 Sink 加 `actuator` 标签,并只声明/接受 `ApprovedRobotCommand/v1`;安全节点加 `safety_gate` 标签并完成 `RobotCommand -> ApprovedRobotCommand` 转换;不要为了通过编译给普通变换节点冒充安全标签;连接器加 `transport:grpc` 等标签;
- 同一个 ID 重复注册会失败。 - 同一个 ID 重复注册会失败。
若 Operator 要进入 `server.routes` 引用的被动 Pipeline还必须声明
`invocation_cardinality=InvocationCardinality.EXACTLY_ONE`。其中恰好一个模型所有者还要
设置 `route_model_param`(例如 `"model_id"`),让编译器验证服务 route 与实际 adapter
使用同一模型。当前被动 v1 不接受可能返回 0/N 条的插件,也不接受分支/汇合图;主动上报
Pipeline 不受这组约束。
### 5.3 通过 entry point 发布 ### 5.3 通过 entry point 发布
第三方插件包的 `pyproject.toml` 第三方插件包的 `pyproject.toml`
@ -672,6 +912,13 @@ async def process(self, envelope, input_port="input"):
纯 Python CPU 密集任务、大模型或需要故障隔离的模型应由插件维护常驻进程/模型 Worker不要每帧创建进程或重复加载模型。框架提供 `PersistentProcessWorker` 作为小型基础设施:它使用一个常驻子进程、有界请求/结果队列、串行关联和可选超时,并把子进程异常还原为 `RemoteWorkerError`。请求一旦超时或在途取消Worker 会标记为 `poisoned` 并拒绝后续 submit避免迟到结果被误配插件必须 stop 后新建 Worker。插件仍负责在 `setup/start` 创建和启动它、在 `stop` 回收它,并确保 `spawn` 模式下 factory/payload 可序列化。 纯 Python CPU 密集任务、大模型或需要故障隔离的模型应由插件维护常驻进程/模型 Worker不要每帧创建进程或重复加载模型。框架提供 `PersistentProcessWorker` 作为小型基础设施:它使用一个常驻子进程、有界请求/结果队列、串行关联和可选超时,并把子进程异常还原为 `RemoteWorkerError`。请求一旦超时或在途取消Worker 会标记为 `poisoned` 并拒绝后续 submit避免迟到结果被误配插件必须 stop 后新建 Worker。插件仍负责在 `setup/start` 创建和启动它、在 `stop` 回收它,并确保 `spawn` 模式下 factory/payload 可序列化。
需要不同 Python/native 依赖的外部项目使用 `FramedSubprocessWorker`。它以 4 字节大端
JSON header 长度、JSON header、`blob_lengths` 和 raw blobs 传输数据,启动时要求
`ready` 握手,并串行关联请求。超时、取消、破损帧会终止当前进程组,下一次请求重新
启动,避免迟到响应串到新请求。`gauge.analog_reader@1` 使用该边界把 Python 3.8 的
ETHZ Analog Gauge Reader 与 Python 3.10 主服务隔离worker 的 stdout 专供协议,
第三方模型日志重定向到 stderr。部署与独立 `uv.lock` 位于 `server/gauge/worker/`
在 v1 配置中,节点仍必须使用 `execution.mode: async`(默认)或 `inline`,且保持默认单并发字段。`thread|process|model_worker` 以及非默认并发字段会在编译期被拒绝。上面的线程/进程 helper 是组件内部显式调用的实现细节,框架不会根据 YAML 自动 offload。插件需要自行限制 in-flight 数量并在 `stop()` 回收 Worker。若跨进程发送大图像优先传编码数据确认复制成为瓶颈后再实现固定大小共享内存池并只通过 IPC 传 slot/shape/dtype/时间戳。 在 v1 配置中,节点仍必须使用 `execution.mode: async`(默认)或 `inline`,且保持默认单并发字段。`thread|process|model_worker` 以及非默认并发字段会在编译期被拒绝。上面的线程/进程 helper 是组件内部显式调用的实现细节,框架不会根据 YAML 自动 offload。插件需要自行限制 in-flight 数量并在 `stop()` 回收 Worker。若跨进程发送大图像优先传编码数据确认复制成为瓶颈后再实现固定大小共享内存池并只通过 IPC 传 slot/shape/dtype/时间戳。
### 5.5 开发新协议连接器 ### 5.5 开发新协议连接器
@ -784,19 +1031,26 @@ AI 结果 -> Policy -> RobotCommand/v1
8. 控制链先使用空载/限速/人工急停条件验证; 8. 控制链先使用空载/限速/人工急停条件验证;
9. 使用 SIGTERM 停止并给 `shutdown_timeout_s` 留出排空时间。 9. 使用 SIGTERM 停止并给 `shutdown_timeout_s` 留出排空时间。
被动调用服务还需要安装 `server` extra在同一环境运行 Detect Client 时安装 `http`
extra`serve` 会同时启动配置中全部启用的主动/被动 Pipeline 和 HTTP API。
示例命令: 示例命令:
```bash ```bash
uv run --no-sync cmvr-edge-ai models uv run --no-sync cmvr-edge-ai models
uv run --no-sync cmvr-edge-ai validate -c configs/edge_ai.yaml --pipeline detection uv run --no-sync cmvr-edge-ai validate -c configs/active_detection.yaml --pipeline detection
uv run --no-sync cmvr-edge-ai run -c configs/edge_ai.yaml \ uv run --no-sync cmvr-edge-ai run -c configs/active_detection.yaml \
--pipeline detection \ --pipeline detection \
--log-level INFO \ --log-level INFO \
--log-format json --log-format json
uv run --no-sync cmvr-edge-ai run -c configs/edge_ai.yaml \ uv run --no-sync cmvr-edge-ai validate -c configs/server_detect.yaml
--pipeline talk \ uv run --no-sync cmvr-edge-ai serve -c configs/server_detect.yaml \
--log-level INFO \ --log-level INFO \
--log-format text --log-format text
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
``` ```
当前日志可输出文本或单行 JSON。日志中不要写入音频原始数据、图像 base64、认证 metadata 或用户隐私内容;生产插件应只记录 trace ID、schema、耗时、尺寸、丢弃计数和经过脱敏的错误信息。 当前日志可输出文本或单行 JSON。日志中不要写入音频原始数据、图像 base64、认证 metadata 或用户隐私内容;生产插件应只记录 trace ID、schema、耗时、尺寸、丢弃计数和经过脱敏的错误信息。
@ -805,14 +1059,18 @@ uv run --no-sync cmvr-edge-ai run -c configs/edge_ai.yaml \
### 9.1 当前限制 ### 9.1 当前限制
- cmvr-es 音频双向流 proto 尚未实现;框架只有 `AudioChunk` 契约和对话占位配置; - cmvr-es 音频双向流 proto 尚未实现;框架只有 `AudioChunk` 契约,不提供 Talk 占位配置;
- 检测链路已经提供 PyAV H264/H265 解码、双 PPE YOLO 注册/推理、单次解码 fan-out 和重复触发规则VAD、ASR、LLM、TTS 仍需插件提供; - 检测链路已经提供 PyAV H264/H265 解码、双 PPE YOLO 注册/推理、单次解码 fan-out 和重复触发规则VAD、ASR、LLM、TTS 仍需插件提供;
- UDP/QUIC 只有扩展目录,无 transport/connector - UDP/QUIC transport/connector 尚未实现
- execution v1 只支持 `async/inline` 默认单并发;其他 mode 和非默认并发字段会在编译期拒绝,线程/进程/模型 offload 必须由插件显式实现; - execution v1 只支持 `async/inline` 默认单并发;其他 mode 和非默认并发字段会在编译期拒绝,线程/进程/模型 offload 必须由插件显式实现;
- 非空 resources、非 `normal` priority 和显式保留 runtime 字段都会被拒绝;`health_bind` 未启动服务,队列指标也未导出; - 非空 resources、非 `normal` priority 和显式保留 runtime 字段都会被拒绝;
`runtime.health_bind` 仍不启动独立服务,但 `serve` 已提供 `/health/live`
`/health/ready`,详细队列指标尚未导出;
- 无热更新、overlay、通用节点级 supervisor/restart、持久化 outbox/spool相机和 HTTP 的重连/重试是 connector 内部的局部策略; - 无热更新、overlay、通用节点级 supervisor/restart、持久化 outbox/spool相机和 HTTP 的重连/重试是 connector 内部的局部策略;
- 无自动 deadline 丢弃、`max_age_ms`、`put_timeout_ms` - 无自动 deadline 丢弃、`max_age_ms`、`put_timeout_ms`
- 无共享内存池;跨进程 Worker 由插件负责; - 无共享内存池;跨进程 Worker 由插件负责;
- Analog Gauge worker 当前固定为 Linux x86_64/Python 3.8 CPU 环境,首次加载约需
1.2 GiB 级别内存;它是可工作的兼容适配,不是轻量模型;
- 配置只支持单个 YAML 加环境变量,不支持 include/merge - 配置只支持单个 YAML 加环境变量,不支持 include/merge
- 当前 cmvr-es 相机成功响应通常未填写 `header.timestamp`,因此连接器的 `captured_at_ns` 可能为 `null`;精确采集时延需要后续在帧协议中增加设备采集时钟,不能用客户端接收时间冒充; - 当前 cmvr-es 相机成功响应通常未填写 `header.timestamp`,因此连接器的 `captured_at_ns` 可能为 `null`;精确采集时延需要后续在帧协议中增加设备采集时钟,不能用客户端接收时间冒充;
- cmvr-es 当前通过 `getLatestEncodedFrame` 取得最新编码数据,可能在 edge-ai 收到之前跳过 inter-frame 参考包;本地连续队列和重连无法补回上游丢失内容,必须验证/改造服务端流语义; - cmvr-es 当前通过 `getLatestEncodedFrame` 取得最新编码数据,可能在 edge-ai 收到之前跳过 inter-frame 参考包;本地连续队列和重连无法补回上游丢失内容,必须验证/改造服务端流语义;
@ -821,17 +1079,23 @@ uv run --no-sync cmvr-edge-ai run -c configs/edge_ai.yaml \
WARNING 即表示该报告已丢失,进程崩溃或断电也可能丢失内存中的输出; WARNING 即表示该报告已丢失,进程崩溃或断电也可能丢失内存中的输出;
- 内置 YOLO adapter 不产生 `track_id`,所以 `scope: track` 需要外部 tracker 及人员/PPE 关联节点; - 内置 YOLO adapter 不产生 `track_id`,所以 `scope: track` 需要外部 tracker 及人员/PPE 关联节点;
- PPE `.pt` 只能来自可信制品源商业部署还需核对权重说明、Ultralytics runtime/checkpoint 的 AGPL/商业许可,以及训练数据和权重分发权利; - PPE `.pt` 只能来自可信制品源商业部署还需核对权重说明、Ultralytics runtime/checkpoint 的 AGPL/商业许可,以及训练数据和权重分发权利;
- 四份 FP32 ONNX 已生成且配置已切到 `@2`,但仓库没有有授权的现场正例图片;当前
smoke 只证明制品完整和可执行,不能替代非方形现场图 parity、精度与资源 gate
- FP32 ONNX 文件约为 PT 的两倍People-Talking 单文件约 273 MB 且仍是 YOLOv8x
ONNX 化减少运行依赖,不等于降低网络 FLOPs 或存储体积;
- AGV `set_velocity` 默认禁用unsafe override 和正常 shutdown stop 都不具备崩溃安全性,不能替代 cmvr-es server-side lease/deadman、机器人本体限位、急停和功能安全系统。 - AGV `set_velocity` 默认禁用unsafe override 和正常 shutdown stop 都不具备崩溃安全性,不能替代 cmvr-es server-side lease/deadman、机器人本体限位、急停和功能安全系统。
### 9.2 推荐演进顺序 ### 9.2 推荐演进顺序
1. **真实流与模型验收**:用录制数据和目标边缘设备验证 cmvr-es 编码连续性、PyAV 长时间恢复、PPE 精度/FPS/内存/显存和端到端告警; 1. **真实流与模型验收**:用录制数据和目标边缘设备验证 cmvr-es 编码连续性、PyAV 长时间恢复、PPE 精度/FPS/内存/显存和端到端告警;
2. **可靠告警投递**:在现有 event ID 和有限重试之上增加有界持久 outbox、确认、恢复发送和容量/保留策略; 2. **ONNX 现场验收与继续轻量化**:用有授权的非方形相机图完成 PT(`rect=False`)/ONNX
3. **同人违规语义**:增加 tracker 与 Worker/PPE 空间关联,验证 ID switch 后再启用 `scope: track` parity测量目标机 P50/P95/RSS再决定 People-Talking 换小模型或现场校准 INT8
4. **补齐可观测性**:导出 health、队列水位/丢弃、重连/重试、节点延迟和模型资源指标; 3. **可靠告警投递**:在现有 event ID 和有限重试之上增加有界持久 outbox、确认、恢复发送和容量/保留策略;
5. **控制安全闭环**:先在 cmvr-es 实现速度 lease/server-side deadman再补齐设备状态输入、机器人型号限值、优先级与审计然后才启用真实 AGV/机械臂动作; 4. **同人违规语义**:增加 tracker 与 Worker/PPE 空间关联,验证 ID switch 后再启用 `scope: track`
6. **接入音频双向流**proto 落地后实现麦克风 Source/扬声器 Sink严格处理 chunk 顺序、背压和 discontinuity 5. **补齐可观测性**:导出 health、队列水位/丢弃、重连/重试、节点延迟和模型资源指标;
7. **按测量结果优化并发**:先使用现有显式 thread offload再按测量结果增加常驻 model/process worker确认复制瓶颈后才加入共享内存 6. **控制安全闭环**:先在 cmvr-es 实现速度 lease/server-side deadman再补齐设备状态输入、机器人型号限值、优先级与审计然后才启用真实 AGV/机械臂动作;
8. **扩展协议**:用相同内部契约实现 QUIC/UDP connector不修改算法插件。 7. **接入音频双向流**proto 落地后实现麦克风 Source/扬声器 Sink严格处理 chunk 顺序、背压和 discontinuity
8. **按测量结果优化并发**:先使用现有显式 thread offload再按测量结果增加常驻 model/process worker确认复制瓶颈后才加入共享内存
9. **扩展协议**:用相同内部契约实现 QUIC/UDP connector不修改算法插件。
每一步都应先通过 `validate`、smoke、录制数据 replay 和资源峰值检查,再接入真实设备。 每一步都应先通过 `validate`、smoke、录制数据 replay 和资源峰值检查,再接入真实设备。

View File

@ -21,7 +21,8 @@ models/
└── <model-name>/ └── <model-name>/
└── v<version>/ └── v<version>/
├── README.md ├── README.md
└── <weights-file> ├── <weights-file>
└── manifest.json # ONNX 版本包含
``` ```
注册 ID 中的数字版本与目录版本一一对应。例如: 注册 ID 中的数字版本与目录版本一一对应。例如:
@ -37,9 +38,10 @@ construction-ppe-yolov8@1
## 大文件管理 ## 大文件管理
Detection 的 `.pt` 权重直接作为普通 Git blob 保存在仓库中,不使用 Git LFS。克隆 Detection 的 `.pt` `.onnx` 权重直接作为普通 Git blob 保存在仓库中,不使用 Git LFS。克隆
仓库后即可获得完整权重,不需要执行 `git lfs pull`。新增大权重前应同时确认 Git 仓库后即可获得完整权重,不需要执行 `git lfs pull`。新增大权重前应同时确认 Git
服务及反向代理允许对应的 push 大小和上传时长。 服务及反向代理允许对应的 push 大小和上传时长;当前 People-Talking FP32 ONNX 单文件
约 273 MB推送前尤其需要确认服务端单文件限制。
将新权重加入仓库前,先核对模型卡中的 SHA256 将新权重加入仓库前,先核对模型卡中的 SHA256

View File

@ -13,6 +13,18 @@
| `people-talking-yolov8x@1` | [people-talking-yolov8x/v1](people-talking-yolov8x/v1/README.md) | `ultralytics-yolo` | 2 | 上游通用 `label` 类及 `talking on phone` 行为类 | | `people-talking-yolov8x@1` | [people-talking-yolov8x/v1](people-talking-yolov8x/v1/README.md) | `ultralytics-yolo` | 2 | 上游通用 `label` 类及 `talking on phone` 行为类 |
| `yolov8n-mobile-phone@1` | [yolov8n-mobile-phone](yolov8n-mobile-phone/README.md) | `ultralytics-yolo` | 1 | 画面中的 `mobile_phone` 手机物体 | | `yolov8n-mobile-phone@1` | [yolov8n-mobile-phone](yolov8n-mobile-phone/README.md) | `ultralytics-yolo` | 1 | 画面中的 `mobile_phone` 手机物体 |
四个 `onnxruntime-yolov8``@2` 制品已经生成:
| 注册模型 ID | 制品与模型卡 | SHA256 |
|---|---|---|
| `construction-ppe-yolov8@2` | [construction-ppe-yolov8/v2](construction-ppe-yolov8/v2/README.md) | `b8e5d116...0b57a` |
| `ppe-6classes-yolov8n@2` | [ppe-6classes-yolov8n/v2](ppe-6classes-yolov8n/v2/README.md) | `6cb0e567...3c03b` |
| `people-talking-yolov8x@2` | [people-talking-yolov8x/v2](people-talking-yolov8x/v2/README.md) | `b4387307...e840b` |
| `yolov8n-mobile-phone@2` | [yolov8n-mobile-phone/v2](yolov8n-mobile-phone/v2/README.md) | `df170d9b...f857f` |
三个部署 YAML 已切换到 `@2`。运行时会先校验同目录 `manifest.json`、模型身份、标签、
输入尺寸和整个 ONNX 文件 SHA256再创建 ONNX Runtime session。
可通过以下命令查看运行时注册信息及有序标签: 可通过以下命令查看运行时注册信息及有序标签:
```bash ```bash
@ -28,20 +40,23 @@ nodes:
detector: detector:
uses: detection.model@1 uses: detection.model@1
with: with:
model: construction-ppe-yolov8@1 model: construction-ppe-yolov8@2
model_options: model_options:
weights: models/detection/construction-ppe-yolov8/v1/best.pt weights: models/detection/construction-ppe-yolov8/v2/model.onnx
providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
``` ```
模型加载时会严格比较 checkpoint 的类别名称和顺序与注册信息。权重不匹配时节点会停止 模型加载时会严格比较 checkpoint 的类别名称和顺序与注册信息。权重不匹配时节点会停止
启动,不能通过只修改 `detect_labels` 绕过类别校验。 启动,不能通过只修改 `detect_labels` 绕过类别校验。
当前生产 Pipeline 会从同一个 decoder 并行运行 Construction PPE 与 People-Talking 当前主动 Pipeline 会从同一个 decoder 并行运行 Construction PPE 与 People-Talking
两个 detector。People-Talking checkpoint 的 class 0 名称是含义不清的 `label`;注册 两个 detector。People-Talking checkpoint 的 class 0 名称是含义不清的 `label`;注册
表为保证 ID 对齐而保留它,但部署配置只选择 `talking on phone` 表为保证 ID 对齐而保留它,但部署配置只选择 `talking on phone`
`configs/debug/detection_viewer.yaml` 不改变生产告警链路,它使用 `configs/detection_viewer.yaml` 不改变生产告警链路,它使用
`yolov8n-mobile-phone@1` 替代 People-Talking与 Construction PPE 同帧显示。该模型 `yolov8n-mobile-phone@2` 替代 People-Talking与 Construction PPE 同帧显示。该模型
只说明画面中检测到手机物体,不能直接判断人员正在通话。 只说明画面中检测到手机物体,不能直接判断人员正在通话。
## 新增版本 ## 新增版本
@ -57,3 +72,36 @@ nodes:
6. 更新部署 YAML并先执行 `cmvr-edge-ai models``cmvr-edge-ai validate` 6. 更新部署 YAML并先执行 `cmvr-edge-ai models``cmvr-edge-ai validate`
数据集压缩包、训练集和训练过程缓存不属于部署资产,不应放入本目录。 数据集压缩包、训练集和训练过程缓存不属于部署资产,不应放入本目录。
## ONNX 导出与验收
运行 ONNX 模型的边缘环境不需要 Torch/Ultralytics
```bash
uv sync --locked --extra onnx-cpu
```
构建机安装锁定的 PT 与 ONNX 导出依赖,并一次导出一个目标版本:
```bash
uv sync --locked --extra onnx-export-cpu
uv run --no-sync python scripts/export_detection_onnx.py \
--model-id yolov8n-mobile-phone@2
```
导出脚本固定 `batch=1`、`dynamic=false`、`nms=false` 和 CPU FP32验证 source
checkpoint 标签、ONNX checker、静态图输入输出及 metadata清除训练机路径/时间戳,
然后生成普通 Git 文件 `model.onnx` 和记录来源/制品 SHA256 的 `manifest.json`。脚本
不会执行 INT8 量化,也不会自动修改部署 YAML。
当前已完成制品哈希、真实 ORT 加载/推理和配置编译测试;投入现场前仍须执行:
1. 用现场代表性图片逐图比较 `.pt` 与 ONNX 的类别、框、置信度和 NMS 后数量;
2. 在目标边缘端测量冷启动、稳态 P50/P95 延迟、峰值 RSS、CPU 占用和连续运行;
3. 复核 `manifest.json`、模型卡、许可和普通 Git 服务端的单文件大小限制;
4. 使用非方形现场图PT parity 基线必须显式 `rect=False`,与静态 640×640 ONNX
letterbox 语义保持一致。
People-Talking 的源模型是 YOLOv8x参数量和计算量远高于三个较小模型。ONNX 可减少
Python runtime 依赖,但不会自动降低网络 FLOPs如果目标设备仍不达标应重新训练
n/s 级模型或使用有现场校准集的 INT8 流程,而不是把未校准量化结果直接发布。

View File

@ -1,5 +1,8 @@
# Construction PPE YOLOv8s v1 # Construction PPE YOLOv8s v1
本版本作为可信 PT 导出源与回滚版本保留;当前部署使用
[`construction-ppe-yolov8@2`](../v2/README.md) ONNX 制品。
## 注册信息 ## 注册信息
| 字段 | 值 | | 字段 | 值 |

View File

@ -0,0 +1,33 @@
# Construction PPE YOLOv8s ONNX v2
这是 `construction-ppe-yolov8@1` 的静态 CPU FP32 ONNX 部署版本。
| 字段 | 值 |
|---|---|
| Model ID | `construction-ppe-yolov8@2` |
| Backend | `onnxruntime-yolov8` |
| 输入 | `float32[1,3,640,640]` |
| 输出 | `float32[1,23,8400]`NMS 外置 |
| ONNX opset | 17 |
| 文件大小 | 44,774,179 bytes |
| SHA256 | `b8e5d116a964d0e7091e14b5382ab9301c196ae33b363516df04fddfb0d0b57a` |
| 来源 | [`v1/best.pt`](../v1/README.md) |
`manifest.json` 记录来源与制品 SHA256、标签、输入和导出工具版本。运行时会在创建
ONNX Runtime session 前校验 manifest、模型身份和整个制品哈希。
```yaml
model: construction-ppe-yolov8@2
model_options:
weights: models/detection/construction-ppe-yolov8/v2/model.onnx
providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
imgsz: 640
iou: 0.70
max_det: 100
```
已通过 ONNX checker、静态图契约检查、CPU session 加载和合成非方形图推理 smoke。
上线前仍需使用有授权的现场图片完成 `.pt`(显式 `rect=False`)与 ONNX 的检测框、
置信度、NMS 数量及目标硬件延迟/RSS 验收。许可与数据限制继承 v1 模型卡。

View File

@ -0,0 +1,64 @@
{
"artifact": "models/detection/construction-ppe-yolov8/v2/model.onnx",
"artifact_sha256": "b8e5d116a964d0e7091e14b5382ab9301c196ae33b363516df04fddfb0d0b57a",
"backend": "onnxruntime-yolov8",
"export": {
"batch": 1,
"dynamic": false,
"format": "onnx",
"half": false,
"nms": false,
"opset": 17,
"simplify": true
},
"input": {
"layout": "NCHW",
"letterbox_color": [
114,
114,
114
],
"normalization": "uint8 / 255",
"pixel_format": "RGB",
"shape": [
1,
3,
640,
640
]
},
"labels": [
"Boots",
"Ear-Protection",
"Glass",
"Glove",
"Hard_hat",
"Mask",
"No-Boots",
"No-Ear-Protection",
"No-Glass",
"No-Glove",
"No-Helmet",
"No-Mask",
"No-Vest",
"Worker",
"Vest",
"Circular_Saw",
"Fire_Extinguisher",
"Fire_prevention_Net",
"Welding_Equipment"
],
"model_id": "construction-ppe-yolov8@2",
"schema_version": "cmvr.detection-model-manifest/v1",
"source": "models/detection/construction-ppe-yolov8/v1/best.pt",
"source_model_id": "construction-ppe-yolov8@1",
"source_sha256": "31ef3ca04a17cf545f3fcfc64c4af8993a41d52ccc460e82aff01d5354603533",
"task": "detect",
"tools": {
"onnx": "1.22.0",
"onnxruntime": "1.23.2",
"onnxslim": "0.1.94",
"torch": "2.7.0+cpu",
"ultralytics": "8.4.31"
}
}

Binary file not shown.

View File

@ -1,5 +1,8 @@
# People Talking YOLOv8x v1 # People Talking YOLOv8x v1
本版本作为可信 PT 导出源与回滚版本保留;当前部署使用
[`people-talking-yolov8x@2`](../v2/README.md) ONNX 制品。
## 注册信息 ## 注册信息
| 字段 | 值 | | 字段 | 值 |
@ -14,7 +17,7 @@
| SHA256 | `86cd63926de1c69f70dd7f8755e4de9e382bbdeb0668821b7c3846b826471526` | | SHA256 | `86cd63926de1c69f70dd7f8755e4de9e382bbdeb0668821b7c3846b826471526` |
| 基础模型 | YOLOv8x约 6815 万参数) | | 基础模型 | YOLOv8x约 6815 万参数) |
| checkpoint Ultralytics | `8.0.196` | | checkpoint Ultralytics | `8.0.196` |
| 当前部署 Ultralytics | `8.4.31` | | 回滚环境 Ultralytics | `8.4.31` |
Model ID 的 `@1` 与本目录的 `v1` 对应。权重从用户提供的 Model ID 的 `@1` 与本目录的 `v1` 对应。权重从用户提供的
`/home/xtkuang/Projects/cmvr/changan_robot/call_phone/best.pt` 复制;复制前后的大小和 `/home/xtkuang/Projects/cmvr/changan_robot/call_phone/best.pt` 复制;复制前后的大小和
@ -31,13 +34,13 @@ checkpoint 的类别编号必须与下表严格一致:
| 1 | `talking on phone` | 电话使用行为告警 | | 1 | `talking on phone` | 电话使用行为告警 |
`label` 不是背景类,不能从注册信息中删除,否则类别编号会错位并导致模型加载失败。 `label` 不是背景类,不能从注册信息中删除,否则类别编号会错位并导致模型加载失败。
生产配置通过 `detect_labels: [talking on phone]` 只请求 class 1。 v1 回滚配置通过 `detect_labels: [talking on phone]` 只请求 class 1。
## 输入与运行参数 ## 输入与运行参数
- 输入必须是已经解码的 `BGR8``RGB8` 三通道图像; - 输入必须是已经解码的 `BGR8``RGB8` 三通道图像;
- checkpoint 训练参数记录的 `imgsz``640` - checkpoint 训练参数记录的 `imgsz``640`
- 当前 CPU 部署`max_fps: 5`、`confidence: 0.50` 开始; - v1 CPU 回滚配置`max_fps: 5`、`confidence: 0.50` 开始;
- 该权重由带 `dill` 的环境序列化,项目的 `yolo`/`yolo-cpu` extra 已显式包含 - 该权重由带 `dill` 的环境序列化,项目的 `yolo`/`yolo-cpu` extra 已显式包含
`dill`,不能依赖 Ultralytics 运行时临时联网安装; `dill`,不能依赖 Ultralytics 运行时临时联网安装;
- `.pt` 由 PyTorch pickle loader 读取,只应部署经过校验的可信制品。 - `.pt` 由 PyTorch pickle loader 读取,只应部署经过校验的可信制品。
@ -93,7 +96,7 @@ phone_detector:
sha256sum models/detection/people-talking-yolov8x/v1/best.pt sha256sum models/detection/people-talking-yolov8x/v1/best.pt
uv run --no-sync cmvr-edge-ai models uv run --no-sync cmvr-edge-ai models
uv run --no-sync cmvr-edge-ai validate \ uv run --no-sync cmvr-edge-ai validate \
--config configs/edge_ai.yaml \ --config configs/active_detection.yaml \
--pipeline detection --pipeline detection
``` ```

View File

@ -0,0 +1,34 @@
# People Talking YOLOv8x ONNX v2
这是 `people-talking-yolov8x@1` 的静态 CPU FP32 ONNX 部署版本。
| 字段 | 值 |
|---|---|
| Model ID | `people-talking-yolov8x@2` |
| Backend | `onnxruntime-yolov8` |
| 输入 | `float32[1,3,640,640]` |
| 输出 | `float32[1,6,8400]`NMS 外置 |
| ONNX opset | 17 |
| 参数量/FLOPs | 68,125,494 / 257.4 GFLOPs |
| 文件大小 | 272,787,930 bytes |
| SHA256 | `b4387307dbdf1083c0229a12e833ccdc4a3ad0063d5bdb7908765072470e840b` |
| 来源 | [`v1/best.pt`](../v1/README.md) |
部署只选择 `talking on phone`;含义不清的 class 0 `label` 仅为保持类别编号一致而保留。
`manifest.json` 由运行时在 session 创建前校验。
```yaml
model: people-talking-yolov8x@2
detect_labels: [talking on phone]
model_options:
weights: models/detection/people-talking-yolov8x/v2/model.onnx
providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
imgsz: 640
```
ONNX 化移除了边缘运行时的 Torch/Ultralytics 依赖,但没有降低网络参数量或 FLOPs
FP32 文件也约为 v1 PT 的两倍。合成图 CPU smoke 可运行,但它不是现场精度或实时性
验收;资源不足时应训练 n/s 级模型或使用现场校准集开展 INT8而不是宣称格式转换已
让 YOLOv8x 轻量化。许可与数据限制继承 v1 模型卡。

View File

@ -0,0 +1,47 @@
{
"artifact": "models/detection/people-talking-yolov8x/v2/model.onnx",
"artifact_sha256": "b4387307dbdf1083c0229a12e833ccdc4a3ad0063d5bdb7908765072470e840b",
"backend": "onnxruntime-yolov8",
"export": {
"batch": 1,
"dynamic": false,
"format": "onnx",
"half": false,
"nms": false,
"opset": 17,
"simplify": true
},
"input": {
"layout": "NCHW",
"letterbox_color": [
114,
114,
114
],
"normalization": "uint8 / 255",
"pixel_format": "RGB",
"shape": [
1,
3,
640,
640
]
},
"labels": [
"label",
"talking on phone"
],
"model_id": "people-talking-yolov8x@2",
"schema_version": "cmvr.detection-model-manifest/v1",
"source": "models/detection/people-talking-yolov8x/v1/best.pt",
"source_model_id": "people-talking-yolov8x@1",
"source_sha256": "86cd63926de1c69f70dd7f8755e4de9e382bbdeb0668821b7c3846b826471526",
"task": "detect",
"tools": {
"onnx": "1.22.0",
"onnxruntime": "1.23.2",
"onnxslim": "0.1.94",
"torch": "2.7.0+cpu",
"ultralytics": "8.4.31"
}
}

Binary file not shown.

View File

@ -1,5 +1,8 @@
# PPE Detection YOLOv8n6 Classesv1 # PPE Detection YOLOv8n6 Classesv1
本版本作为可信 PT 导出源与回滚版本保留;对应 ONNX 制品见
[`ppe-6classes-yolov8n@2`](../v2/README.md)。
## 注册信息 ## 注册信息
| 字段 | 值 | | 字段 | 值 |
@ -113,7 +116,7 @@ detector:
必须自行核对当前适用条款。 必须自行核对当前适用条款。
- 原模型说明称训练数据为自定义 Roboflow 格式 PPE 数据集,但没有在本目录提供完整的 - 原模型说明称训练数据为自定义 Roboflow 格式 PPE 数据集,但没有在本目录提供完整的
数据授权链。部署方应在商业或高风险使用前核对数据来源和许可。 数据授权链。部署方应在商业或高风险使用前核对数据来源和许可。
- 本目录仅记录当前 `.pt` 权重,不表示仓库包含其他导出格式 - 本目录只记录 v1 `.pt`v2 ONNX 制品在相邻版本目录维护
- 权重来源为 Hugging Face 上的 - 权重来源为 Hugging Face 上的
[`Tanishjain9/yolov8n-ppe-detection-6classes`](https://huggingface.co/Tanishjain9/yolov8n-ppe-detection-6classes) [`Tanishjain9/yolov8n-ppe-detection-6classes`](https://huggingface.co/Tanishjain9/yolov8n-ppe-detection-6classes)
仓库内以本目录路径和上述 SHA256 作为部署制品标识。 仓库内以本目录路径和上述 SHA256 作为部署制品标识。

View File

@ -0,0 +1,30 @@
# PPE YOLOv8n 6 Classes ONNX v2
这是 `ppe-6classes-yolov8n@1` 的静态 CPU FP32 ONNX 部署版本。
| 字段 | 值 |
|---|---|
| Model ID | `ppe-6classes-yolov8n@2` |
| Backend | `onnxruntime-yolov8` |
| 输入 | `float32[1,3,640,640]` |
| 输出 | `float32[1,10,8400]`NMS 外置 |
| ONNX opset | 17 |
| 文件大小 | 10,993,196 bytes |
| SHA256 | `6cb0e567b4fbf353aeb61c271ab33c45260d6ddddbe26eb3b64c245c2683c03b` |
| 来源 | [`v1/best.pt`](../v1/README.md) |
标签顺序保持为 `Gloves, Vest, goggles, helmet, mask, safety_shoe`。`manifest.json`
记录并由运行时校验来源、标签、模型身份和制品 SHA256。
```yaml
model: ppe-6classes-yolov8n@2
model_options:
weights: models/detection/ppe-6classes-yolov8n/v2/model.onnx
providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
imgsz: 640
```
已通过 ONNX checker、CPU session 加载和合成非方形图推理 smoke当前部署 YAML
未实例化本模型。现场精度、性能和许可边界仍以 v1 模型卡及目标设备验收为准。

View File

@ -0,0 +1,51 @@
{
"artifact": "models/detection/ppe-6classes-yolov8n/v2/model.onnx",
"artifact_sha256": "6cb0e567b4fbf353aeb61c271ab33c45260d6ddddbe26eb3b64c245c2683c03b",
"backend": "onnxruntime-yolov8",
"export": {
"batch": 1,
"dynamic": false,
"format": "onnx",
"half": false,
"nms": false,
"opset": 17,
"simplify": true
},
"input": {
"layout": "NCHW",
"letterbox_color": [
114,
114,
114
],
"normalization": "uint8 / 255",
"pixel_format": "RGB",
"shape": [
1,
3,
640,
640
]
},
"labels": [
"Gloves",
"Vest",
"goggles",
"helmet",
"mask",
"safety_shoe"
],
"model_id": "ppe-6classes-yolov8n@2",
"schema_version": "cmvr.detection-model-manifest/v1",
"source": "models/detection/ppe-6classes-yolov8n/v1/best.pt",
"source_model_id": "ppe-6classes-yolov8n@1",
"source_sha256": "07172ef3ae9e256c40a1fb0ce3eefe5547d90170645aa73dded0fffc382cdb31",
"task": "detect",
"tools": {
"onnx": "1.22.0",
"onnxruntime": "1.23.2",
"onnxslim": "0.1.94",
"torch": "2.7.0+cpu",
"ultralytics": "8.4.31"
}
}

Binary file not shown.

View File

@ -1,6 +1,7 @@
# YOLOv8n Mobile Phone v1 # YOLOv8n Mobile Phone v1
这是 Debug OpenCV Viewer 使用的单类手机目标检测模型。它来自 这是单类手机目标检测模型的 v1 PT 导出源与回滚版本。当前 Debug OpenCV Viewer 使用
[`yolov8n-mobile-phone@2`](v2/README.md) ONNX 制品。v1 来自
[`IndUSV/yolov8n-mobile-phone`](https://huggingface.co/IndUSV/yolov8n-mobile-phone) [`IndUSV/yolov8n-mobile-phone`](https://huggingface.co/IndUSV/yolov8n-mobile-phone)
用于检测画面中可见的手机物体。 用于检测画面中可见的手机物体。
@ -25,7 +26,7 @@ checkpoint 只有一个类别,注册顺序必须与 class ID 完全一致:
|---:|---|---| |---:|---|---|
| 0 | `mobile_phone` | 画面中可见的手机物体 | | 0 | `mobile_phone` | 画面中可见的手机物体 |
## Viewer 配置 ## v1 回滚配置
```yaml ```yaml
phone_detector: phone_detector:
@ -64,7 +65,7 @@ sha256sum models/detection/yolov8n-mobile-phone/yolov8n-mobile-phone.pt
uv run --no-sync cmvr-edge-ai models uv run --no-sync cmvr-edge-ai models
uv run --no-sync python detect_server/show_detections.py \ uv run --no-sync python server/detect/show_detections.py \
--config configs/debug/detection_viewer.yaml \ --config configs/detection_viewer.yaml \
--validate-only --validate-only
``` ```

View File

@ -0,0 +1,31 @@
# YOLOv8n Mobile Phone ONNX v2
这是 `yolov8n-mobile-phone@1` 的静态 CPU FP32 ONNX 部署版本。
| 字段 | 值 |
|---|---|
| Model ID | `yolov8n-mobile-phone@2` |
| Backend | `onnxruntime-yolov8` |
| 输入 | `float32[1,3,640,640]` |
| 输出 | `float32[1,5,8400]`NMS 外置 |
| ONNX opset | 17 |
| 文件大小 | 12,265,264 bytes |
| SHA256 | `df170d9bc86984894797da6f2101519617669c3f589d75289acb0ba1937f857f` |
| 来源 | [`yolov8n-mobile-phone.pt`](../README.md) |
唯一标签为 `mobile_phone`。运行时在创建 session 前校验 `manifest.json`、内嵌模型
身份和制品 SHA256。
```yaml
model: yolov8n-mobile-phone@2
detect_labels: [mobile_phone]
model_options:
weights: models/detection/yolov8n-mobile-phone/v2/model.onnx
providers: [CPUExecutionProvider]
intra_op_threads: 1
inter_op_threads: 1
imgsz: 640
```
已通过 ONNX checker、CPU session 加载和合成非方形图推理 smoke。它只检测手机
物体,不能证明人员正在通话;现场阈值、误报和资源占用仍需用目标相机数据验收。

View File

@ -0,0 +1,46 @@
{
"artifact": "models/detection/yolov8n-mobile-phone/v2/model.onnx",
"artifact_sha256": "df170d9bc86984894797da6f2101519617669c3f589d75289acb0ba1937f857f",
"backend": "onnxruntime-yolov8",
"export": {
"batch": 1,
"dynamic": false,
"format": "onnx",
"half": false,
"nms": false,
"opset": 17,
"simplify": true
},
"input": {
"layout": "NCHW",
"letterbox_color": [
114,
114,
114
],
"normalization": "uint8 / 255",
"pixel_format": "RGB",
"shape": [
1,
3,
640,
640
]
},
"labels": [
"mobile_phone"
],
"model_id": "yolov8n-mobile-phone@2",
"schema_version": "cmvr.detection-model-manifest/v1",
"source": "models/detection/yolov8n-mobile-phone/yolov8n-mobile-phone.pt",
"source_model_id": "yolov8n-mobile-phone@1",
"source_sha256": "9230e4bfa7cba7134215c4c7f228b5e58760b02138788ccaff0149258c2d2e19",
"task": "detect",
"tools": {
"onnx": "1.22.0",
"onnxruntime": "1.23.2",
"onnxslim": "0.1.94",
"torch": "2.7.0+cpu",
"ultralytics": "8.4.31"
}
}

Binary file not shown.

View File

@ -27,6 +27,27 @@ video = [
image = [ image = [
"Pillow>=10,<13", "Pillow>=10,<13",
] ]
server = [
"fastapi>=0.115,<1",
"Pillow>=10,<13",
"uvicorn>=0.30,<1",
]
onnx-cpu = [
"numpy>=1.24,<3",
"onnxruntime>=1.20,<2",
"Pillow>=10,<13",
]
onnx-export-cpu = [
"dill>=0.3.8,<1",
"numpy>=1.24,<3",
"onnx>=1.17,<2",
"onnxruntime>=1.20,<2",
"onnxslim>=0.1.71,<1",
"Pillow>=10,<13",
"torch==2.7.0",
"torchvision==0.22.0",
"ultralytics==8.4.31",
]
yolo = [ yolo = [
"dill>=0.3.8,<1", "dill>=0.3.8,<1",
"numpy>=1.24,<3", "numpy>=1.24,<3",
@ -70,6 +91,10 @@ conflicts = [
{ extra = "yolo" }, { extra = "yolo" },
{ extra = "yolo-cpu" }, { extra = "yolo-cpu" },
], ],
[
{ extra = "yolo" },
{ extra = "onnx-export-cpu" },
],
] ]
required-environments = [ required-environments = [
"sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'", "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'",
@ -78,9 +103,11 @@ required-environments = [
[tool.uv.sources] [tool.uv.sources]
torch = [ torch = [
{ index = "pytorch-cpu", extra = "yolo-cpu", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" }, { index = "pytorch-cpu", extra = "yolo-cpu", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
{ index = "pytorch-cpu", extra = "onnx-export-cpu", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
] ]
torchvision = [ torchvision = [
{ index = "pytorch-cpu", extra = "yolo-cpu", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" }, { index = "pytorch-cpu", extra = "yolo-cpu", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
{ index = "pytorch-cpu", extra = "onnx-export-cpu", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
] ]
[[tool.uv.index]] [[tool.uv.index]]

View File

@ -61,9 +61,9 @@ Options:
-h, --help show this help -h, --help show this help
Profiles: Profiles:
core framework, minimal fixture, and simulated talk pipeline only core framework and minimal fixture only
detection-cpu gRPC + HTTP + PyAV + locked CPU YOLO runtime detection-cpu gRPC + HTTP server/client + PyAV + locked CPU ONNX runtime
dev detection-cpu plus tests and portable protobuf codegen tools dev detection-cpu plus locked ONNX export and test toolchains
EOF EOF
} }
@ -118,7 +118,8 @@ case "${profile}" in
--extra http --extra http
--extra video --extra video
--extra image --extra image
--extra yolo-cpu --extra server
--extra onnx-cpu
) )
needs_cmvr_bindings=true needs_cmvr_bindings=true
;; ;;
@ -131,7 +132,8 @@ case "${profile}" in
--extra http --extra http
--extra video --extra video
--extra image --extra image
--extra yolo-cpu --extra server
--extra onnx-export-cpu
) )
needs_cmvr_bindings=true needs_cmvr_bindings=true
;; ;;
@ -180,45 +182,49 @@ echo "==> validating the minimal framework fixture"
--config tests/fixtures/minimal_pipeline.yaml \ --config tests/fixtures/minimal_pipeline.yaml \
--pipeline minimal --pipeline minimal
echo "==> validating the merged talk pipeline"
.venv/bin/cmvr-edge-ai validate \
--config configs/edge_ai.yaml \
--pipeline talk
if [[ "${needs_cmvr_bindings}" == true ]]; then if [[ "${needs_cmvr_bindings}" == true ]]; then
echo "==> verifying detection model artifacts" echo "==> verifying detection model artifacts"
verify_model_artifact \ verify_model_artifact \
"construction-ppe-yolov8@1" \ "construction-ppe-yolov8@2" \
"models/detection/construction-ppe-yolov8/v1/best.pt" \ "models/detection/construction-ppe-yolov8/v2/model.onnx" \
"22537898" \ "44774179" \
"31ef3ca04a17cf545f3fcfc64c4af8993a41d52ccc460e82aff01d5354603533" "b8e5d116a964d0e7091e14b5382ab9301c196ae33b363516df04fddfb0d0b57a"
verify_model_artifact \ verify_model_artifact \
"ppe-6classes-yolov8n@1" \ "ppe-6classes-yolov8n@2" \
"models/detection/ppe-6classes-yolov8n/v1/best.pt" \ "models/detection/ppe-6classes-yolov8n/v2/model.onnx" \
"5625014" \ "10993196" \
"07172ef3ae9e256c40a1fb0ce3eefe5547d90170645aa73dded0fffc382cdb31" "6cb0e567b4fbf353aeb61c271ab33c45260d6ddddbe26eb3b64c245c2683c03b"
verify_model_artifact \ verify_model_artifact \
"people-talking-yolov8x@1" \ "people-talking-yolov8x@2" \
"models/detection/people-talking-yolov8x/v1/best.pt" \ "models/detection/people-talking-yolov8x/v2/model.onnx" \
"136694313" \ "272787930" \
"86cd63926de1c69f70dd7f8755e4de9e382bbdeb0668821b7c3846b826471526" "b4387307dbdf1083c0229a12e833ccdc4a3ad0063d5bdb7908765072470e840b"
verify_model_artifact \ verify_model_artifact \
"yolov8n-mobile-phone@1" \ "yolov8n-mobile-phone@2" \
"models/detection/yolov8n-mobile-phone/yolov8n-mobile-phone.pt" \ "models/detection/yolov8n-mobile-phone/v2/model.onnx" \
"6234666" \ "12265264" \
"9230e4bfa7cba7134215c4c7f228b5e58760b02138788ccaff0149258c2d2e19" "df170d9bc86984894797da6f2101519617669c3f589d75289acb0ba1937f857f"
echo "==> checking detection runtime imports" echo "==> checking detection runtime imports"
.venv/bin/python -c \ .venv/bin/python -c \
"import av, dill, grpc, httpx, PIL, torch, ultralytics; import cmvr.api.camera_service_pb2_grpc; print(f'torch={torch.__version__} cuda={torch.cuda.is_available()} ultralytics={ultralytics.__version__} dill={dill.__version__} pillow={PIL.__version__}')" "import av, fastapi, grpc, httpx, numpy, onnxruntime, PIL, uvicorn; import cmvr.api.camera_service_pb2_grpc; print(f'onnxruntime={onnxruntime.__version__} numpy={numpy.__version__} pillow={PIL.__version__} fastapi={fastapi.__version__} uvicorn={uvicorn.__version__}')"
echo "==> validating the PPE and phone-use detection pipeline" echo "==> validating the PPE and phone-use detection pipeline"
.venv/bin/cmvr-edge-ai validate \ .venv/bin/cmvr-edge-ai validate \
--config configs/edge_ai.yaml \ --config configs/active_detection.yaml \
--pipeline detection --pipeline detection
echo "==> validating the passive Detect Server pipelines"
.venv/bin/cmvr-edge-ai validate \
--config configs/server_detect.yaml
.venv/bin/python server/detect/show_detections.py \
--config configs/detection_viewer.yaml \
--validate-only
.venv/bin/cmvr-edge-ai models .venv/bin/cmvr-edge-ai models
fi fi
if [[ "${profile}" == dev ]]; then if [[ "${profile}" == dev ]]; then
echo "==> checking ONNX exporter imports"
.venv/bin/python -c \
"import onnx, onnxslim, torch, ultralytics; print(f'onnx={onnx.__version__} onnxslim={onnxslim.__version__} torch={torch.__version__} ultralytics={ultralytics.__version__}')"
echo "==> running tests" echo "==> running tests"
.venv/bin/pytest .venv/bin/pytest
fi fi

View File

@ -0,0 +1,359 @@
#!/usr/bin/env python3
"""Export one registered YOLOv8 detector into the strict edge ONNX contract."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any
# Ultralytics normally tries to install missing export dependencies at runtime.
# That would bypass uv.lock and make deployment artifacts irreproducible.
os.environ["YOLO_AUTOINSTALL"] = "false"
from cmvr_edge_ai.detection.models.mobile_phone import MOBILE_PHONE_LABELS
from cmvr_edge_ai.detection.models.people_talking import PEOPLE_TALKING_LABELS
from cmvr_edge_ai.detection.models.ppe_yolo import (
CONSTRUCTION_PPE_LABELS,
PPE_6CLASS_LABELS,
)
from cmvr_edge_ai.detection.models.yolo_onnx import OnnxYoloModel
@dataclass(frozen=True, slots=True)
class ExportDefinition:
source_model_id: str
target_model_id: str
source: Path
output: Path
labels: tuple[str, ...]
EXPORT_DEFINITIONS = {
"construction-ppe-yolov8@2": ExportDefinition(
source_model_id="construction-ppe-yolov8@1",
target_model_id="construction-ppe-yolov8@2",
source=Path("models/detection/construction-ppe-yolov8/v1/best.pt"),
output=Path("models/detection/construction-ppe-yolov8/v2/model.onnx"),
labels=CONSTRUCTION_PPE_LABELS,
),
"ppe-6classes-yolov8n@2": ExportDefinition(
source_model_id="ppe-6classes-yolov8n@1",
target_model_id="ppe-6classes-yolov8n@2",
source=Path("models/detection/ppe-6classes-yolov8n/v1/best.pt"),
output=Path("models/detection/ppe-6classes-yolov8n/v2/model.onnx"),
labels=PPE_6CLASS_LABELS,
),
"people-talking-yolov8x@2": ExportDefinition(
source_model_id="people-talking-yolov8x@1",
target_model_id="people-talking-yolov8x@2",
source=Path("models/detection/people-talking-yolov8x/v1/best.pt"),
output=Path("models/detection/people-talking-yolov8x/v2/model.onnx"),
labels=PEOPLE_TALKING_LABELS,
),
"yolov8n-mobile-phone@2": ExportDefinition(
source_model_id="yolov8n-mobile-phone@1",
target_model_id="yolov8n-mobile-phone@2",
source=Path(
"models/detection/yolov8n-mobile-phone/yolov8n-mobile-phone.pt"
),
output=Path("models/detection/yolov8n-mobile-phone/v2/model.onnx"),
labels=MOBILE_PHONE_LABELS,
),
}
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--model-id",
required=True,
choices=tuple(sorted(EXPORT_DEFINITIONS)),
help="registered @2 ONNX model to build",
)
parser.add_argument(
"--source",
type=Path,
help="override the registered trusted .pt source path",
)
parser.add_argument(
"--output",
type=Path,
help="override the registered v2/model.onnx destination",
)
parser.add_argument(
"--manifest",
type=Path,
help="manifest destination (default: manifest.json beside the ONNX file)",
)
parser.add_argument("--imgsz", type=int, default=640)
parser.add_argument("--opset", type=int, default=17)
parser.add_argument(
"--simplify",
action=argparse.BooleanOptionalAction,
default=True,
)
parser.add_argument(
"--force",
action="store_true",
help="replace an existing validated output and manifest",
)
return parser.parse_args(argv)
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as artifact:
for chunk in iter(lambda: artifact.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _ordered_names(value: Any) -> tuple[str, ...]:
if isinstance(value, dict):
normalized = {int(key): str(label) for key, label in value.items()}
indexes = sorted(normalized)
if indexes != list(range(len(indexes))):
raise ValueError("source checkpoint class IDs must start at zero")
return tuple(normalized[index] for index in indexes)
if isinstance(value, (list, tuple)):
return tuple(str(label) for label in value)
raise ValueError(f"source checkpoint has invalid class names: {value!r}")
def _sanitize_onnx_metadata(
onnx: Any,
path: Path,
*,
definition: ExportDefinition,
source_sha256: str,
) -> Any:
"""Remove build-host paths/timestamps and add stable CMVR identity."""
model = onnx.load(str(path))
metadata = {
str(item.key): str(item.value)
for item in getattr(model, "metadata_props", ())
}
metadata.pop("date", None)
metadata["description"] = (
f"CMVR static ONNX export for {definition.target_model_id}"
)
metadata["cmvr_model_id"] = definition.target_model_id
metadata["cmvr_source_sha256"] = source_sha256
onnx.helper.set_model_props(model, metadata)
onnx.save_model(model, str(path))
return model
def _require_export_dependencies(*, simplify: bool) -> dict[str, Any]:
try:
import onnx
import onnxruntime
import torch
import ultralytics
from ultralytics import YOLO
onnxslim = None
if simplify:
import onnxslim
except ImportError as exc:
raise RuntimeError(
"ONNX export dependencies are incomplete; run uv with the "
"onnx-export-cpu extra"
) from exc
return {
"onnx": onnx,
"onnxruntime": onnxruntime,
"onnxslim": onnxslim,
"torch": torch,
"ultralytics": ultralytics,
"YOLO": YOLO,
}
def _validate_paths(
*,
source: Path,
output: Path,
manifest: Path,
force: bool,
) -> None:
if not source.is_file():
raise FileNotFoundError(f"source checkpoint does not exist: {source}")
if source.suffix.lower() != ".pt":
raise ValueError(f"source checkpoint must end in .pt: {source}")
if output.suffix.lower() != ".onnx":
raise ValueError(f"output artifact must end in .onnx: {output}")
if output.resolve() == source.resolve():
raise ValueError("source and output paths must be different")
if manifest.resolve() in {source.resolve(), output.resolve()}:
raise ValueError("manifest path must differ from source and output")
existing = [path for path in (output, manifest) if path.exists()]
if existing and not force:
raise FileExistsError(
"refusing to replace existing export path(s) without --force: "
+ ", ".join(str(path) for path in existing)
)
with source.open("rb") as artifact:
prefix = artifact.read(128)
if b"version https://git-lfs.github.com/spec/v1" in prefix:
raise ValueError(f"source checkpoint is a Git LFS pointer: {source}")
def _write_manifest(path: Path, payload: dict[str, Any]) -> None:
with path.open("x", encoding="utf-8") as stream:
json.dump(payload, stream, ensure_ascii=False, indent=2, sort_keys=True)
stream.write("\n")
def export_model(args: argparse.Namespace) -> tuple[Path, Path]:
definition = EXPORT_DEFINITIONS[args.model_id]
source = (args.source or definition.source).expanduser()
output = (args.output or definition.output).expanduser()
manifest = (
args.manifest.expanduser()
if args.manifest is not None
else output.with_name("manifest.json")
)
if args.imgsz < 32 or args.imgsz % 32:
raise ValueError("imgsz must be at least 32 and divisible by 32")
if args.opset < 12:
raise ValueError("opset must be at least 12")
_validate_paths(
source=source,
output=output,
manifest=manifest,
force=args.force,
)
dependencies = _require_export_dependencies(simplify=args.simplify)
output.parent.mkdir(parents=True, exist_ok=True)
manifest.parent.mkdir(parents=True, exist_ok=True)
source_sha256 = _sha256(source)
with tempfile.TemporaryDirectory(
dir=output.parent,
prefix=".onnx-export-",
) as temporary:
staging = Path(temporary)
staged_source = staging / "source.pt"
shutil.copy2(source, staged_source)
model = dependencies["YOLO"](str(staged_source), task="detect")
if model.task != "detect":
raise ValueError(f"source checkpoint task must be detect, got {model.task!r}")
actual_labels = _ordered_names(model.names)
if actual_labels != definition.labels:
raise ValueError(
"source checkpoint labels do not match the registered model: "
f"expected {definition.labels!r}, got {actual_labels!r}"
)
exported = model.export(
format="onnx",
imgsz=args.imgsz,
batch=1,
dynamic=False,
simplify=args.simplify,
opset=args.opset,
nms=False,
half=False,
device="cpu",
)
staged_output = Path(str(exported)).resolve()
if not staged_output.is_file() or staged_output.suffix.lower() != ".onnx":
raise RuntimeError(
f"Ultralytics did not produce the expected ONNX file: {exported!r}"
)
try:
staged_output.relative_to(staging.resolve())
except ValueError as exc:
raise RuntimeError(
f"export escaped the atomic staging directory: {staged_output}"
) from exc
onnx = dependencies["onnx"]
exported_model = _sanitize_onnx_metadata(
onnx,
staged_output,
definition=definition,
source_sha256=source_sha256,
)
onnx.checker.check_model(exported_model)
runtime = OnnxYoloModel(
{
"weights": str(staged_output),
"providers": ["CPUExecutionProvider"],
"imgsz": args.imgsz,
},
expected_labels=definition.labels,
)
runtime.load()
runtime.close()
output_sha256 = _sha256(staged_output)
staged_manifest = staging / "manifest.json"
onnxslim = dependencies["onnxslim"]
manifest_payload = {
"schema_version": "cmvr.detection-model-manifest/v1",
"source_model_id": definition.source_model_id,
"model_id": definition.target_model_id,
"backend": "onnxruntime-yolov8",
"source": str(source),
"source_sha256": source_sha256,
"artifact": str(output),
"artifact_sha256": output_sha256,
"task": "detect",
"labels": list(definition.labels),
"input": {
"layout": "NCHW",
"shape": [1, 3, args.imgsz, args.imgsz],
"pixel_format": "RGB",
"normalization": "uint8 / 255",
"letterbox_color": [114, 114, 114],
},
"export": {
"format": "onnx",
"opset": args.opset,
"batch": 1,
"dynamic": False,
"nms": False,
"half": False,
"simplify": bool(args.simplify),
},
"tools": {
"torch": dependencies["torch"].__version__,
"ultralytics": dependencies["ultralytics"].__version__,
"onnx": dependencies["onnx"].__version__,
"onnxruntime": dependencies["onnxruntime"].__version__,
"onnxslim": (
None if onnxslim is None else onnxslim.__version__
),
},
}
_write_manifest(staged_manifest, manifest_payload)
# Each file is published with an atomic same-filesystem replacement.
# The manifest is published last and carries the artifact digest, so a
# process interrupted between replacements fails closed on hash check
# instead of accepting a mismatched artifact pair.
os.replace(staged_output, output)
os.replace(staged_manifest, manifest)
return output, manifest
def main(argv: list[str] | None = None) -> int:
output, manifest = export_model(parse_args(argv))
print(f"exported ONNX artifact: {output}")
print(f"wrote model manifest: {manifest}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

99
server/detect/README.md Normal file
View File

@ -0,0 +1,99 @@
# Detect Server
本目录归属“被动调用”的视觉检测 HTTP 服务。服务端负责模型注册、模型生命周期、
`category -> model` 映射、图像解码和推理资源;调用方只使用稳定的业务
`category`,不能指定 `model_id`、权重路径或 pipeline。
阶段一固定协议入口:
- `GET /v1/models`:列出 `active_push``passive_invoke` 两组能力;
- `POST /v1/inference`:接收 `InferenceRequest/v1` JSON返回
`InferenceResponse/v1`
本顶层目录保存部署边界和可编译示例HTTP API 与 pipeline 边界组件位于
`cmvr_edge_ai.server`。公共 wire DTO 以 `cmvr_edge_ai.contracts` 中的版本化类型为准,
路由层不得复制一套形状相近但不兼容的字典协议。
每个被动检测 route 使用一条完整且独立的请求链:
```text
server.request_source@1
-> media.image_decoder.pillow@1
-> detection.model@1
-> server.detection_response@1
-> server.response_sink@1
```
链路使用 `request/block` QoS不配置 `max_fps` 或丢帧策略,因此每个进入 broker 的
请求都会产生一个相关响应或明确失败。Detector 配置 `attach_frame: true`,只有调用方
`requested_artifact_roles` 中请求 `annotated``original`response 节点才编码
对应 JPEG artifact。
## 配置归属
服务端配置拥有监听地址、category、模型 ID、权重、provider、并发和服务端默认参数。
客户端配置只拥有服务 URL、认证/超时以及每次调用允许覆盖的业务参数。
见 [configs/server_detect.yaml](../../configs/server_detect.yaml)。它沿用根 `AppConfig`
`api_version: cmvr.edge.ai/v1`,并通过 `server.routes` 把公开 category 映射到同一
文件内声明的 pipelines。
示例只做配置加载和 DAG 编译时不会加载模型权重;真正启动 pipeline 时才会初始化
Pillow、ONNX Runtime、manifest 和静态 ONNX graph。
CPU ONNX 服务需要 `server``onnx-cpu` extras同一环境还要运行 Detect Client 时加入
`http`
```bash
uv sync --locked --extra server --extra http --extra onnx-cpu
```
只校验配置和两条 DAG
```bash
uv run --no-sync cmvr-edge-ai validate \
--config configs/server_detect.yaml
```
启动两条被动检测 Pipeline 和 HTTP API
```bash
uv run --no-sync cmvr-edge-ai serve \
--config configs/server_detect.yaml \
--log-level INFO \
--log-format text
```
`serve` 会启动配置中全部 `enabled: true` 的 Pipeline不接受 `--pipeline`。启动后可在
另一终端检查健康状态、能力目录并发起一次检测:
```bash
curl -sS http://127.0.0.1:8081/health/live
curl -sS http://127.0.0.1:8081/health/ready
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
```
最后一个命令调用 `POST /v1/inference`;也可把 category 改为
`detect.mobile_phone`。客户端始终不携带 `model_id`、Pipeline ID 或权重路径。
若把 `server.http.bind` 改为局域网地址,配置会强制要求 `bearer_token`,并默认要求
`tls_certfile``tls_keyfile`。此时调用示例还需设置
`CMVR_DETECT_BEARER_TOKEN`base URL 使用 `https://`。只有受信隔离网络才能显式配置
`allow_insecure_remote: true` 跳过 TLS存活和就绪探针始终不要求 token。
## OpenCV 实时画框 Viewer
Viewer 是直接订阅 cmvr-es 相机并同时显示 PPE 与手机检测结果的本地调试入口,不经过
被动 HTTP API
```bash
uv run --no-sync python server/detect/show_detections.py \
--config configs/detection_viewer.yaml \
--pipeline detection_show \
--log-level INFO
```
运行前配置远端 endpoint、`device_id` 和两份权重;按 `q``Esc` 退出。

View File

@ -40,9 +40,8 @@ from cmvr_edge_ai.workers import run_blocking
_LOGGER = logging.getLogger("cmvr_edge_ai.demo.detection_viewer") _LOGGER = logging.getLogger("cmvr_edge_ai.demo.detection_viewer")
_DEFAULT_CONFIG = ( _DEFAULT_CONFIG = (
Path(__file__).resolve().parents[1] Path(__file__).resolve().parents[2]
/ "configs" / "configs"
/ "debug"
/ "detection_viewer.yaml" / "detection_viewer.yaml"
) )
_VIEWER_PLUGIN_ID = "demo.opencv_detection_viewer@1" _VIEWER_PLUGIN_ID = "demo.opencv_detection_viewer@1"

92
server/gauge/README.md Normal file
View File

@ -0,0 +1,92 @@
# Analog Gauge Server
本目录把 [ethz-asl/analog_gauge_reader](https://github.com/ethz-asl/analog_gauge_reader)
作为被动调用模型接入统一 URL `POST /v1/inference`
- category`gauge.analog`
- model ID`ethz-analog-gauge-reader@1`
- 输入:一张 JPEG 或 PNG
- 输出:`scalar` 读数与可选单位
- artifact`original`、`annotated`、`diagnostics`
客户端只选择 categoryPython 版本、项目路径、三个模型路径和超时全部由
[configs/server_gauge.yaml](../../configs/server_gauge.yaml) 的服务端配置控制。
服务端默认限制输入图像不超过 25,000,000 像素。worker 直接从 JPEG/PNG 头读取宽高,
在 Pillow/OpenCV 解码或模型推理之前拒绝超限图像;部署时可通过 `max_pixels`
1 到 250,000,000 之间调整。该限制与 HTTP 请求字节限制分别控制解压后尺寸和传输体积。
## 一次性准备
1. 确认上游项目和 Git LFS 模型文件已经完整:
```bash
git -C /home/xtkuang/Projects/cmvr/changan_robot/analog_gauge_reader lfs pull
```
2. 创建隔离的 Python 3.8 worker 环境:
```bash
bash server/gauge/worker/bootstrap.sh
```
3. 若仓库位置不同,修改配置中的 `python_executable``project_root`。三个模型路径可用
相对于 `project_root` 的路径。
## 校验与启动
根项目 HTTP 服务仍运行在 Python 3.10 环境;不要把 MMOCR/MMCV 安装到根 `.venv`
```bash
uv sync --locked --extra server --group dev
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 \
--log-format text
```
默认队列容量为 1同一个 worker 串行处理请求,不让上游模型的可变状态并发执行。
请求超时为 300 秒HTTP 超时为 360 秒,给首次加载 DINO/MMOCR 权重留出空间。
若服务监听非 loopback 地址,必须配置 Bearer token并默认使用 TLS只有受信隔离网络
可显式设置 `allow_insecure_remote: true`。远程请求需要携带
`Authorization: Bearer <token>`,健康探针不需要认证。
没有检测到仪表、刻度数字或指针时返回 `no_result`;模型 checkpoint、依赖或部署故障
返回 `failed`,不会伪装成“没有识别结果”。
## 请求示例
下面的脚本只把图像和稳定业务 category 发给统一入口:
```bash
python - <<'PY'
import base64
import json
from pathlib import Path
from urllib.request import Request, urlopen
path = Path("/path/to/gauge.jpg")
payload = {
"schema_version": "cmvr.inference-request/v1",
"category": "gauge.analog",
"inputs": [{
"kind": "image",
"name": "image",
"media_type": "image/jpeg",
"encoding": "base64",
"data": base64.b64encode(path.read_bytes()).decode("ascii"),
}],
"requested_artifact_roles": ["annotated", "diagnostics"],
}
request = Request(
"http://127.0.0.1:8081/v1/inference",
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
)
print(urlopen(request, timeout=360).read().decode("utf-8"))
PY
```
真实模型首次请求可能较慢;轻量协议测试使用 fake upstream project不加载约 1.2 GB 的
模型权重。

View File

@ -0,0 +1,39 @@
# Analog Gauge 隔离运行环境
`ethz-asl/analog_gauge_reader` 依赖 Python 3.8、Torch 2.0 和为 CPython 3.8
预编译的 MMCV 2.0 wheel不能安全地安装进本项目的 Python 3.10 主环境。本目录是一个
独立的 uv project只服务 `gauge.analog_reader@1` 子进程。
## 创建环境
运行:
```bash
bash server/gauge/worker/bootstrap.sh
```
脚本安装 uv 管理的 Python 3.8,并按照本目录的 `uv.lock` 创建 `.venv`。不会改动根目录
`.venv`。MMCV URL 是 Linux x86_64、CPython 3.8、Torch 2.0 CPU 的专用 wheel因此
当前部署配置不支持其他平台。
## 进程协议
主进程由 `gauge.analog_reader@1` 自动启动 worker无需手工常驻。worker 使用
`cmvr.external-worker/v1`4 字节大端 JSON header 长度、JSON header随后按
`blob_lengths` 顺序传输 raw blob。图像不做 base64 二次复制。
worker 启动后首先返回 `ready`;每个请求使用一个 JPEG/PNG blob响应包含标量读数、
单位、耗时、诊断和可选 `annotated` blob。Ultralytics/MMOCR 和上游项目的 stdout 已被
重定向到 stderr协议流单独持有启动时复制的 stdout FD日志不会污染二进制帧。
worker 会先解析图片头并按 `--max-pixels` 检查 `width * height`,超限时不会创建临时
输入文件,也不会进入 Pillow/OpenCV 或上游 pipeline。
明确的“未检测到仪表、OCR 数字不足、椭圆拟合失败、未找到指针”属于业务无结果,返回
`no_result`。checkpoint 不兼容、依赖缺失和其他推理/部署异常返回相关联的 `failed`
response帧本身损坏时无法继续可靠解析边界进程会退出并由主框架在下一次请求时重建。
版本基线来自上游项目Torch 2.0.0、Torchvision 0.15.1、MMCV 2.0.0、MMEngine
0.7.2、MMDetection 3.0.0、MMOCR 1.0.0、Ultralytics 8.0.66 和
scikit-learn 1.2.2。NumPy/OpenCV/SciPy 额外固定在仍支持 Python 3.8 且已知兼容的版本,
避免未来解析得到 NumPy 2 或停止支持 Python 3.8 的 wheel。Setuptools 固定为
75.3.2,因为旧版 Ultralytics 启动时仍会导入其中的 `pkg_resources`

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
GAUGE_WORKER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ "$(uname -s)" != "Linux" || "$(uname -m)" != "x86_64" ]]; then
echo "Analog Gauge worker currently requires Linux x86_64." >&2
exit 2
fi
uv python install 3.8.20
uv sync \
--project "${GAUGE_WORKER_DIR}" \
--python 3.8.20 \
--managed-python \
--locked \
--no-dev
"${GAUGE_WORKER_DIR}/.venv/bin/python" -c \
"import cv2, mmcv, mmengine, mmdet, mmocr, numpy, scipy, sklearn, torch, torchvision, ultralytics; print('gauge worker dependencies: OK')"

View File

@ -0,0 +1,37 @@
[project]
name = "cmvr-analog-gauge-worker"
version = "0.1.0"
description = "Isolated Python 3.8 runtime for ethz-asl/analog_gauge_reader"
requires-python = ">=3.8,<3.9"
dependencies = [
"mmcv @ https://download.openmmlab.com/mmcv/dist/cpu/torch2.0.0/mmcv-2.0.0-cp38-cp38-manylinux1_x86_64.whl",
"mmengine==0.7.2",
"mmdet==3.0.0",
"mmocr==1.0.0",
"numpy==1.24.3",
"opencv-python==4.8.1.78",
"scikit-learn==1.2.2",
# Ultralytics 8.0.66 imports pkg_resources during module initialization.
# 75.3.2 is the final setuptools release line that supports Python 3.8.
"setuptools==75.3.2",
"scipy==1.10.1",
"torch==2.0.0",
"torchvision==0.15.1",
"ultralytics==8.0.66",
]
[tool.uv]
package = false
required-version = ">=0.11.16"
required-environments = [
"sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'",
]
[tool.uv.sources]
torch = { index = "pytorch-cpu" }
torchvision = { index = "pytorch-cpu" }
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true

1114
server/gauge/worker/uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

11
server/talk/README.md Normal file
View File

@ -0,0 +1,11 @@
# Talk Server
本目录预留给对话服务端。Talk Server 将拥有语音/文本 provider、模型凭据、会话状态、
资源限制以及服务端路由;边缘 pipeline 与 Detect Client 都不应承担这些职责。
当前仅保留目录与配置归属说明,不实现 Talk HTTP API也不把 Talk 配置混入 Detect 或
Gauge。公共协议落地后应复用 `cmvr_edge_ai.contracts` 中的版本化 DTO。
仓库不再保留模拟音频或禁用 passthrough 组成的占位 Pipeline因为它们不能提供真实
Talk 能力。实现音频 proto、VAD/ASR/LLM/TTS 节点和服务端 route 后,再在根目录
`configs/` 新增 `server_talk.yaml`

Some files were not shown because too many files have changed in this diff Show More