Metadata-Version: 2.4
Name: cmvr-edge-ai
Version: 0.1.0
Summary: Configuration-driven AI pipeline runtime for CMVR robot edge devices
Requires-Python: <3.13,>=3.10
Description-Content-Type: text/markdown
Requires-Dist: pydantic<3,>=2.8
Requires-Dist: PyYAML<7,>=6
Provides-Extra: grpc
Requires-Dist: grpcio<2,>=1.76; extra == "grpc"
Requires-Dist: protobuf<7,>=6.31.1; extra == "grpc"
Provides-Extra: http
Requires-Dist: httpx<1,>=0.27; extra == "http"
Provides-Extra: video
Requires-Dist: av<17,>=12; extra == "video"
Provides-Extra: image
Requires-Dist: Pillow<13,>=10; extra == "image"
Provides-Extra: yolo
Requires-Dist: numpy<3,>=1.24; extra == "yolo"
Requires-Dist: ultralytics==8.4.31; extra == "yolo"
Provides-Extra: yolo-cpu
Requires-Dist: numpy<3,>=1.24; extra == "yolo-cpu"
Requires-Dist: torch==2.7.0; extra == "yolo-cpu"
Requires-Dist: torchvision==0.22.0; extra == "yolo-cpu"
Requires-Dist: ultralytics==8.4.31; extra == "yolo-cpu"

# cmvr-edge-ai

`cmvr-edge-ai` 是部署在机器人边缘端的、配置驱动的 AI 流水线运行时。它把上游传感器、AI 算法和下游机器人/平台接口拆成可组合节点，通过 YAML 把节点连接成有向无环图（DAG）。协议只出现在边界连接器中，算法节点使用统一的内部消息，因此以后增加 HTTP、gRPC、UDP 或 QUIC 时不需要改动算法本身。

当前版本已经包含可运行的检测链路和框架基础设施：

- 严格 YAML 配置校验、环境变量展开和 DAG 编译检查；
- `Source -> Operator -> Sink` 异步运行时；
- 每条边独立的有界队列和明确的溢出策略；
- 版本化插件注册表、检测模型注册表和 Python entry point 扩展机制；
- cmvr-es RGB 相机 gRPC Source（带退避上限的指数重连）、AGV gRPC Sink；
- PyAV H264/H265 有状态解码、通用检测模型节点和重复命中规则节点；
- Construction PPE YOLOv8 模型注册，以及按标签、置信度和最大 FPS 的部署配置；
- 平台 HTTP JSON Sink，支持有限重试并使用告警 `event_id` 作为幂等键；
- `RobotCommand -> ApprovedRobotCommand` 安全门和无重试的类型化 AGV 命令映射；
- 文本/JSON 日志、共享 gRPC Channel 与 HTTP Client；
- 可直接执行的 smoke、PPE 检测和对话占位配置。

`detect_server/pipeline.yaml` 已实现“相机编码流 -> PyAV 解码 -> 注册模型推理 -> 时间窗口规则 -> HTTP 告警”。VAD/ASR/LLM/TTS 尚未内置；`talk_server/pipeline.yaml` 仍使用模拟音频数据，等待 cmvr-es 音频双向流 proto 落地。

## 架构概览

```mermaid
flowchart LR
    A["cmvr-es / 平台<br/>gRPC、HTTP、未来 UDP/QUIC"] --> B["Source 连接器"]
    B --> C["每条边独立的有界队列"]
    C --> D["Operator DAG<br/>解码、检测、VAD、ASR、LLM、TTS、策略"]
    D --> E["Safety Gate"]
    D --> F["平台 Sink"]
    E --> G["机器人执行器 Sink"]
```

内部节点传递 `Envelope[T]`，其中包含载荷、`schema/version`、采集时间、序号、截止时间、`trace_id` 和 `session_id`。连接器负责 protobuf/HTTP JSON 与内部契约之间的转换，AI 插件不应直接依赖 cmvr-es protobuf。

完整设计和配置字段见 [docs/architecture.md](docs/architecture.md)。

## 目录

```text
cmvr_edge_ai/
├── .python-version                # uv 默认 Python 3.10
├── uv.lock                        # 所有 profile 的可复现依赖锁
├── configs/
│   └── smoke.yaml                 # 不依赖外部服务的最小运行验证
├── detect_server/
│   └── pipeline.yaml              # 相机 -> 解码 -> PPE 检测 -> 重复规则 -> HTTP
├── talk_server/
│   ├── nodes/                     # 对话插件预留目录
│   └── pipeline.yaml              # 模拟音频 -> 对话占位 -> 日志
├── scripts/
│   ├── bootstrap.sh               # 一键创建 uv 环境、生成 bindings 并自检
│   └── generate_cmvr_stubs.py     # 从 cmvr-es proto 生成 Python bindings
├── src/cmvr_edge_ai/
│   ├── config/                    # 配置模型、加载与环境变量展开
│   ├── core/                      # Envelope、组件接口、队列和 DAG 运行时
│   ├── contracts/                 # 协议无关的图像、音频、AI 和控制契约
│   ├── detection/                 # 模型注册、视频解码、推理与时间窗口规则
│   ├── plugins/                   # 插件注册、发现和内置基础插件
│   ├── connectors/                # cmvr-es 与平台边界连接器
│   ├── transports/                # gRPC/HTTP 连接池；UDP/QUIC 扩展位置
│   ├── workers/                   # 显式线程 offload 与常驻进程 Worker 工具
│   ├── observability/             # 低开销文本/JSON 日志
│   ├── application.py             # 多 Pipeline 与共享网络客户端的所有者
│   ├── compiler.py                # 配置到可执行 DAG 的编译器
│   └── cli.py                     # validate/run/plugins/models
└── tests/
```

## 快速开始

项目使用 `uv` 管理 Python、`.venv` 和锁定依赖。`.python-version` 默认选择
Python 3.10，支持范围是 3.10～3.12；首次执行时，uv 会在本机没有合适解释器时
自动安装。先确认已经安装 uv：

```bash
uv --version
```

未安装时可使用 uv 官方安装器：

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

只验证框架和模拟对话链路时，一条命令创建最小环境并自检：

```bash
cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
bash scripts/bootstrap.sh --profile core
```

`uv.lock` 是安装的唯一版本来源；bootstrap 使用 `uv sync --locked`，不会在用户
机器上重新选择依赖版本。无需 `source .venv/bin/activate`，统一通过
`uv run --no-sync` 使用已经安装好的环境：

```bash
uv run --no-sync cmvr-edge-ai validate --config configs/smoke.yaml
uv run --no-sync cmvr-edge-ai plugins
uv run --no-sync cmvr-edge-ai run \
  --config configs/smoke.yaml \
  --log-level INFO \
  --log-format text
```

预期会看到两条日志，内容分别包含 `framework-ready` 和 `bounded-dag-running`。该 Source 是有限数据源，数据处理完后进程会自然退出。

bootstrap 支持以下环境：

| Profile | 安装内容 | 命令 |
|---|---|---|
| `core` | 框架核心和模拟 smoke/talk 链路 | `bash scripts/bootstrap.sh --profile core` |
| `detection-cpu` | gRPC、HTTP、PyAV、Pillow 告警图片和固定版本 CPU YOLO；默认值 | `bash scripts/bootstrap.sh` |
| `dev` | `detection-cpu` 加测试和 protobuf codegen 工具，并运行完整测试 | `bash scripts/bootstrap.sh --profile dev` |

如果只希望安装、不执行自检，可加 `--skip-check`。完整参数通过以下命令查看：

```bash
bash scripts/bootstrap.sh --help
```

不要使用 `uv sync --all-extras`：`yolo` 与 `yolo-cpu` 是为不同 PyTorch 来源准备的
互斥环境。请使用 bootstrap profile，或显式只选择其中一个 extra。
告警图片的画框和 JPEG 编码由独立的 `image` extra 提供；默认的
`detection-cpu`/`dev` profile 已安装它，手动组合检测环境时也必须选择
`--extra image`。

CLI 的四个子命令如下：

| 命令 | 用途 |
|---|---|
| `validate -c FILE [--pipeline ID]` | 加载配置、展开环境变量、构造插件并校验 DAG；不启动 Pipeline |
| `run -c FILE [--pipeline ID]` | 启动选中的 Pipeline；未指定时启动所有 `enabled: true` 的 Pipeline |
| `plugins` | 列出内置插件和已安装 entry point 插件 |
| `models` | 列出检测模型 ID、名称、backend 和注册的全部标签 |

`--pipeline` 可以重复传入。`run` 还支持 `--log-level` 和 `--log-format text|json`。配置错误退出码为 `2`，运行错误为 `1`，键盘中断为 `130`。

## 运行 PPE 检测链路

默认 bootstrap 就是当前 YAML 使用的 CPU 检测环境。它会从相邻的
`../cmvr-es` 读取 proto、用锁定的 `grpcio-tools` 生成 bindings，然后安装完整
检测依赖并校验 smoke 和 PPE 配置：

```bash
cd /home/xtkuang/Projects/cmvr/cmvr_edge_ai
bash scripts/bootstrap.sh
```

如果 cmvr-es 不在相邻目录，显式指定路径：

```bash
bash scripts/bootstrap.sh \
  --cmvr-es-root /home/xtkuang/Projects/cmvr/cmvr-es
```

bootstrap 默认使用 portable codegen，所以不要求 cmvr-es 已经编译出 `protoc`。
要跳过生成（例如部署包已经包含匹配版本的 bindings），使用
`--skip-codegen`。手动生成时可以执行：

```bash
uv sync --locked --only-group codegen
.venv/bin/python scripts/generate_cmvr_stubs.py \
  --cmvr-es-root /home/xtkuang/Projects/cmvr/cmvr-es \
  --portable
```

生成文件默认写入 `src/cmvr/...`，使 `cmvr.api.*_pb2` 可以被连接器导入。
生成后应再次执行目标 profile 的 `uv sync --locked`，让可编辑安装识别新包；bootstrap
已经按这个顺序处理。

在 `detect_server/pipeline.yaml` 中配置部署参数：

```yaml
endpoints:
  cmvr_es:
    target: 127.0.0.1:50052
  platform:
    base_url: http://127.0.0.1:8081

pipelines:
  detection:
    nodes:
      detector:
        with:
          attach_frame: true
          model_options:
            weights: /home/xtkuang/Projects/cmvr/changan_robot/construction-ppe-yolov8/best.pt
            device: cpu
      repeat_gate:
        with:
          alert_image:
            enabled: true
            jpeg_quality: 85
```

然后验证并运行检测链路，不再要求预先导出环境变量：

```bash
uv run --no-sync cmvr-edge-ai models
uv run --no-sync cmvr-edge-ai validate \
  --config detect_server/pipeline.yaml \
  --pipeline detection
uv run --no-sync cmvr-edge-ai run --config detect_server/pipeline.yaml \
  --pipeline detection \
  --log-level INFO \
  --log-format json
```

运行前需要确认权重文件存在、cmvr-es 已启用 `right_hand_cam`，并且平台的 `/v1/detection-alerts` 可访问。默认 `yolo-cpu` profile 将 PyTorch 2.7.0 和 torchvision 0.22.0 绑定到官方 CPU wheel，并固定 checkpoint 记录的 Ultralytics 8.4.31。`model_options.device` 在该环境中应保持 `cpu`，`half` 应保持 `false`。

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` 为个别标签覆盖。内置模型 `construction-ppe-yolov8@1` 的名称和 19 个标签可以通过 `cmvr-edge-ai models` 查看。`attach_frame: true` 让检测结果临时携带对应的解码帧，供后续告警节点使用；因此 detector 到 repeat gate 的队列应保持较小，避免堆积未压缩图像。

`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`：

```json
{
  "image": {
    "media_type": "image/jpeg",
    "width": 1280,
    "height": 720,
    "encoding": "base64",
    "data": "..."
  }
}
```

这里的外层对象是 `DetectionAlert` payload 的片段；HTTP JSON Sink 会把内部 JPEG
`bytes` 转成上述扁平 Base64 图片对象。未启用图片，或运行时因第三方结果未附带
帧、坏帧等原因渲染失败时，告警仍会发送且 `payload.image` 为 `null`。
同一阈值帧若同时触发多条规则或多个 track，只编码一次相关框的并集，并让这些
告警共享同一个不可变 JPEG 对象，以限制边缘端瞬时 CPU 和内存开销。

对话占位链路不依赖音频 proto：

```bash
uv run --no-sync cmvr-edge-ai validate --config talk_server/pipeline.yaml
uv run --no-sync cmvr-edge-ai run --config talk_server/pipeline.yaml
```

## 配置最小示例

```yaml
api_version: cmvr.edge.ai/v1

runtime:
  thread_workers: 2
  shutdown_timeout_s: 5

pipelines:
  example:
    enabled: true
    nodes:
      source:
        uses: core.sequence_source@1
        with:
          items: [hello]
          schema_name: TextEvent
          schema_version: 1
      sink:
        uses: core.log_sink@1
    edges:
      - from: source.output
        to: sink.input
        qos:
          profile: request
          capacity: 4
          overflow: block
```

配置模型是严格的，多余字段会报错。字符串支持以下环境变量形式：

- `${NAME}`：变量必须存在；
- `${NAME:-default}`：未设置或为空时使用默认值；
- `env://NAME`：整个字符串取自必填环境变量。

v1 支持五个 `qos.profile`，并在编译期约束其溢出策略：编码 H264/H265 在解码前必须使用 `video_contiguous`，只能阻塞或拒绝；解码后的完整图像可使用 `realtime_latest` 丢旧帧控制延迟；`audio_contiguous` 和 `request` 只能阻塞或拒绝；`telemetry` 支持全部策略。未填写 QoS 时使用无损的 `request + block + capacity=1`。`reject` 会抛异常，`error` 是它的兼容别名。`max_age_ms`、`put_timeout_ms` 已预留，设置后 `validate` 会拒绝配置。完整矩阵见架构文档。

## 注册新的检测模型

检测模型和 DAG 插件是两层注册：流水线固定使用通用的 `detection.model@1`，具体模型通过 `DetectionModelRegistry` 注册 `DetectionModelSpec`。每个 spec 必须给出版本化 `model_id`、面向运维的 `name`、有序且唯一的 `supported_labels`、`backend` 和 factory。factory 返回实现 `load/predict/close` 的 `DetectionModel`；部署 YAML 中的 `model_options` 原样交给它。第三方模型包可以使用 `cmvr_edge_ai.detection_models` entry point 发布 spec 或注册回调。安装后先执行 `cmvr-edge-ai models`，再让配置引用其中的精确模型 ID。

模型实际输出的标签仍会在通用 Operator 边界二次校验和过滤；模型返回未注册标签会让节点失败。直接相连的重复规则若引用了 detector 没有选择的标签，也会在 `validate` 阶段被编译器拒绝。

## 开发插件

插件必须使用带版本的稳定 ID，例如 `example.text_upper@1`，并声明节点种类、输入/输出端口及 schema。工厂签名固定为 `(node_id, params)`；组件分别继承 `Source`、`Operator` 或 `Sink`。

```python
from collections.abc import Mapping
from typing import Any

from cmvr_edge_ai.contracts import TextEvent
from cmvr_edge_ai.core import Emission, Envelope, Operator
from cmvr_edge_ai.plugins import PluginKind, PluginRegistry, PluginSpec


class UppercaseOperator(Operator):
    def __init__(self, node_id: str, params: Mapping[str, Any]) -> None:
        self.node_id = node_id

    async def process(
        self, envelope: Envelope[Any], input_port: str = "input"
    ) -> Emission:
        event = envelope.payload
        if not isinstance(event, TextEvent):
            raise TypeError("expected TextEvent")
        result = TextEvent(text=event.text.upper(), role=event.role, final=event.final)
        return Emission(
            "output",
            envelope.with_payload(result, schema_name="TextEvent", schema_version=1),
        )


def register_plugins(registry: PluginRegistry) -> None:
    registry.register(
        PluginSpec(
            plugin_id="example.text_upper@1",
            kind=PluginKind.OPERATOR,
            factory=UppercaseOperator,
            inputs={"input": "TextEvent/v1"},
            outputs={"output": "TextEvent/v1"},
            description="Uppercase text events",
        )
    )
```

在插件包的 `pyproject.toml` 中注册：

```toml
[project.entry-points."cmvr_edge_ai.plugins"]
example = "my_cmvr_plugin.plugins:register_plugins"
```

安装插件包后，用下面的命令确认发现成功：

```bash
uv run --no-sync cmvr-edge-ai plugins
```

配置文件不能通过 `module:Class` 任意导入代码；只会使用内置或已安装 entry point 注册的插件。插件完整生命周期、返回值规范和连接器开发约定见 [docs/architecture.md](docs/architecture.md#5-插件开发约定)。

## 机器人控制安全边界

控制链固定为：

```text
Policy -> RobotCommand/v1
       -> safety.robot_command_gate@1
       -> ApprovedRobotCommand/v1
       -> cmvr.grpc.agv_command_sink@1
```

AGV Sink 只接受 `ApprovedRobotCommand`，原始 `RobotCommand` 会在运行时被拒绝。配置编译器还要求安全门是执行器的直接前驱；安全门与 actuator 之间不能插入其他节点，也不能增加绕过安全门的输入边。内置安全门负责过期检查和默认 5 秒的最大 TTL、动作/设备白名单、参数范围、最小发送间隔及默认开启的单调序号检查；不合格命令会被丢弃并记录 warning，不会因为一条业务拒绝停止整条 Pipeline。

AGV Sink 必须绑定固定的非空 `device_id`。`set_velocity` 默认禁用；`unsafe_allow_unleased_velocity: true` 只是面向隔离测试场景的显式逃生开关，并且仍要求 Sink 配置 `vx/vy/wz` 的 `velocity_limits`。它只能限制当前客户端发出的数值，并在正常关闭时尽力调用 `stopVelocityControl`；进程崩溃、`SIGKILL`、断电或网络分区时无法保证停车。

真实机器人要启用持续速度控制，必须先在 cmvr-es 服务端实现带过期时间的 lease/deadman：客户端停止续租后，由 cmvr-es 在独立于 edge-ai 进程的安全路径中自动清零速度并停车。客户端的安全门、TTL 和 shutdown hook 不能替代这项服务端保护。

## 当前实现边界

- 音频内部契约 `AudioChunk/v1` 已定义，但 cmvr-es 麦克风/扬声器双向流 proto 和连接器尚未落地。
- UDP 与 QUIC 目录目前是扩展占位，没有可用传输实现。
- 运行时 v1 只支持 `execution.mode: async|inline`，两者当前都是单 task、单并发执行。`thread`、`process`、`model_worker` 是保留值；`concurrency != 1`、非空 `max_in_flight/timeout_s`、`ordered: false` 也是保留配置，都会在编译期被拒绝。插件仍可在组件内部显式使用 `workers.run_blocking()`、`PersistentProcessWorker` 或自有模型 Worker，但不能把 `execution` 声明误当作自动调度。
- v1 对未实现的声明采取 fail-closed：显式设置 `runtime.max_processes/process_start_method/health_bind/reserved_memory_mb`、非默认 pipeline `priority` 或任何非空 `resources` 都会在编译期被拒绝。健康状态和队列统计目前只能通过 Python API 获取。
- 当前没有配置热更新、配置 overlay、持久化 outbox/spool 或共享内存图像池。HTTP Sink 只有当前进程内的有限重试，重试耗尽会终止 Pipeline，进程退出或断电仍可能丢失未上报告警。
- 相机 Source 可以重连，但 cmvr-es 当前服务端通过 `getLatestEncodedFrame` 获取最新编码数据；如果上游在 edge-ai 收到之前已跳过 H264/H265 参考包，`video_contiguous` 无法补回数据，解码器只能在错误后重置并等待关键帧。正式部署应验证 cmvr-es 输出的是连续 access unit 流，或改为对 AI 友好的原始/JPEG/可检测不连续性的接口。
- 内置 YOLO adapter 不运行 tracker，所有检测的 `track_id` 都为空；当前示例因此使用 `scope: source`。
- PPE 权重仓库对权重的许可说明与 Ultralytics runtime/checkpoint 中的 AGPL 信息需要在商业交付前核对，并同时确认训练数据和权重分发许可。
- 端口 schema 在编译期按字符串匹配；运行时不会自动验证 Python payload 类型，插件必须在边界处主动检查。
- AGV 执行器不会自动重试。安全门已提供动作/设备白名单、TTL、参数范围、最小间隔和单调序号检查，但设备状态、command ID 去重、来源仲裁、服务端 lease/deadman 和急停优先级仍需按机器人型号补齐。服务端 deadman 未完成前，禁止把 `set_velocity` 用于真实机器人持续运动。

这些限制及推荐演进顺序在 [docs/architecture.md](docs/architecture.md#9-当前限制与演进顺序) 中有更详细说明。
