A safety layer that measures itself before it judges a robot.
Structured safety events, a policy engine for PPE, zones and proximity, and an operator review API. Measured on Jetson AGX Thor for what it costs to run, then pushed through a real vision-language model to find out what it gets wrong.
Frames in, reviewable events out.
An edge worker samples frames from a source, asks a model adapter for detections, evaluates them against the safety policy, and posts structured safety events to a FastAPI backend with an operator review queue. Every event carries the rule, severity, confidence, a human-review flag and its evidence.
The adapter boundary is the point of the design. A deterministic mock adapter measures the pipeline alone. An OpenAI-compatible adapter puts a real vision-language model behind the same interface, so the same run artifact records both the runtime cost and what the model returned.
Every run writes one JSON artifact with device provenance, per-frame timings, token usage, event and detection counts, peak memory and a 1 Hz tegrastats summary. Nothing on this page is typed by hand.
What was built.
- Safety event schema and policy engine: PPE missing, zone intrusion, human-robot proximity, with severity, confidence gating and human-review flags.
- Edge worker with mock, OpenAI-compatible and Cosmos-Reason2 adapters; batched event posting; run reports with provenance.
- FastAPI backend with SQLite store, single and batch event endpoints, operator review, and Alembic migrations.
- Run recorder and tegrastats sampler that turn each run into a committed artifact; an evidence page generated from those artifacts.
The pipeline keeps camera rate; the backend does not.
With the mock adapter at 30 fps pacing, the worker sustained 28.588 frames/s over 1800 frames with rule evaluation at 0.9877 ms p95 and board power at the idle level, 24,604 mW p50 (`reports/thor/mock_30fps_no_post.json`). Posting each event to the backend synchronously dropped throughput to 4.357 frames/s at 160.9 ms per frame; batching a frame's events into one request lifted it to 7.075 frames/s at 68.2 ms (`mock_30fps_backend.json`, `mock_30fps_backend_batched.json`).
Cosmos-Reason2-2B served by vLLM on the same device cost 4.35 s p50 and 12.85 s p95 per frame with its reasoning block, at 66.6 W board power (`cosmos2b_video_60.json`). Reasoning length drove the tail: the slowest frames produced near 1000 completion tokens at about 61 tokens/s.
The grammar fixed the vocabulary and exposed the real problem.
Four runs on the same 60 frames. Disabling the think block halved the p95 to 6.58 s. Adding a JSON-schema grammar to the decoder removed every out-of-vocabulary label, 59 before, 0 after, and brought the p50 to 2.69 s and p95 to 3.36 s (`cosmos2b_video_60_schema.json`). It also made the 2B report six persons on footage that has none, and five of those tripped PPE and proximity rules: seven false safety events.
Cosmos-Reason2-8B under the same grammar and prompt reported zero persons and zero events on the same frames at 3.20 s p50 (`cosmos8b_video_60_schema.json`), decoding at roughly 13 tokens/s against 60 for the 2B. Sixty frames of one video is a signal, not a rate. A server detail is recorded too: with vLLM's `--reasoning-parser qwen3` enabled, `response_format` JSON schema was accepted but not enforced, so the constrained runs used a second container without the parser.
What went wrong and what was corrected.
The first real-model run failed at frame 26 on a 60 s adapter timeout; the log is kept (`cosmos2b_video_60_attempt1_timeout.log`), the timeout became a command-line option, and a failed run now writes a partial artifact with its error. With the grammar and the original prompt, which still asked for an `<answer>` wrapper the grammar forbids, the model emitted an empty list on every frame; that run is kept as `cosmos2b_video_60_schema_v1prompt.json` and the prompt was rewritten for JSON-only output.
The unconstrained 2B echoed the schema string itself as a label on 12 detections. That is in the artifact, not edited out.
What is not established.
- No precision or recall for any rule. There is no labelled ground truth, so the false events above are counted, not rated.
- No real camera input; the footage is an Isaac Sim recording with no people in it.
- Runs are 60 to 390 seconds, not sustained operation.
- No operator dashboard; review is through the API.
- Zone geometry is configured, not derived from calibration.
Next work is authored simulation scenes with known PPE and zone states so both model sizes get a precision and recall, asynchronous event posting, and a live camera source.
Evidence page and raw records.
The evidence page is generated from the committed run artifacts by the repository's own script and served by GitHub Pages; every row links to its JSON.
Sources and links.
Raw records: mock_30fps_no_post.json, mock_30fps_backend.json, mock_30fps_backend_batched.json, cosmos2b_video_60.json, cosmos2b_video_60_nothink.json, cosmos2b_video_60_schema.json, cosmos8b_video_60_schema.json, cosmos8b_video_60_nothink.json. Each has a `_tegrastats.jsonl` sidecar and a run log beside it.