Add Apple Silicon ZMQ detector for Frigate (#206)

## Summary

- New `frigate_detector` ansible role deploys the [apple-silicon-detector](https://github.com/frigate-nvr/apple-silicon-detector) as a LaunchAgent on indri
- Switches Frigate from ONNX CPU detector (~117ms) to ZMQ detector backed by CoreML/Neural Engine (~15ms)
- Removes detect FPS cap (no longer needed with fast inference)
- Updates Frigate docs and adds changelog fragment

## Deployment

### Phase 1: Deploy detector on indri (one-time setup + ansible)
```fish
ssh indri 'git clone https://github.com/frigate-nvr/apple-silicon-detector.git ~/code/3rd/apple-silicon-detector'
ssh indri 'cd ~/code/3rd/apple-silicon-detector && make install'
mise run provision-indri -- --tags frigate_detector --check --diff  # dry run
mise run provision-indri -- --tags frigate_detector                 # apply
ssh indri 'launchctl list mcquack.eblume.frigate-detector'          # verify running
ssh indri 'tail ~/Library/Logs/mcquack.frigate-detector.out.log'    # verify bound
```

### Phase 2: Test connectivity
```fish
kubectl --context=minikube-indri -n frigate exec deploy/frigate -- nc -vz host.minikube.internal 5555
```

### Phase 3: Deploy Frigate config (branch workflow)
```fish
argocd app set frigate --revision feature/frigate-zmq-detector && argocd app sync frigate
```

### Phase 4: Post-deploy checks
- [ ] Pod starts, no config errors
- [ ] `/api/stats` shows detector type zmq, inference_speed ~15ms
- [ ] detect_fps uncapped
- [ ] Recordings and MQTT events flowing
- [ ] After merge: `argocd app set frigate --revision main && argocd app sync frigate`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/206
This commit is contained in:
Erich Blume 2026-02-17 19:03:28 -08:00
commit 5f9b024b4a
8 changed files with 137 additions and 14 deletions

View file

@ -175,3 +175,5 @@
tags: jellyfin_metrics
- role: caddy
tags: caddy
- role: frigate_detector
tags: frigate_detector

View file

@ -0,0 +1,8 @@
---
frigate_detector_repo: https://forge.ops.eblu.me/eblume/apple-silicon-detector.git
frigate_detector_dir: "{{ ansible_env.HOME }}/code/3rd/apple-silicon-detector"
frigate_detector_endpoint: "tcp://*:5555"
frigate_detector_model: AUTO
frigate_detector_log_dir: "{{ ansible_env.HOME }}/Library/Logs"
frigate_detector_uv_binary: "{{ ansible_env.HOME }}/.local/share/mise/installs/uv/latest/uv-aarch64-apple-darwin/uv"
frigate_detector_python: "3.12"

View file

@ -0,0 +1,6 @@
---
- name: Restart frigate-detector
ansible.builtin.shell: |
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.frigate-detector.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/mcquack.eblume.frigate-detector.plist
changed_when: true

View file

@ -0,0 +1,55 @@
---
# Apple Silicon ZMQ detector for Frigate
# Runs natively on macOS, using CoreML / Neural Engine for ~15ms inference.
# Communicates with Frigate via ZMQ over TCP.
# Dependencies managed inline by uv — no venv or make install needed.
#
# ONE-TIME SETUP (before running ansible):
#
# 1. Clone the repo (use localhost:3001 - hairpinning doesn't work):
# ssh indri 'git clone http://localhost:3001/eblume/apple-silicon-detector.git ~/code/3rd/apple-silicon-detector'
#
# 2. Run ansible to deploy LaunchAgent:
# mise run provision-indri -- --tags frigate_detector
- name: Verify apple-silicon-detector repo exists
ansible.builtin.stat:
path: "{{ frigate_detector_dir }}/detector/zmq_onnx_client.py"
register: frigate_detector_stat
- name: Fail if apple-silicon-detector not found
ansible.builtin.fail:
msg: |
apple-silicon-detector not found at {{ frigate_detector_dir }}.
Please clone first:
ssh indri 'git clone {{ frigate_detector_repo }} {{ frigate_detector_dir }}'
when: not frigate_detector_stat.stat.exists
- name: Verify uv binary exists
ansible.builtin.stat:
path: "{{ frigate_detector_uv_binary }}"
register: frigate_detector_uv_stat
- name: Fail if uv not found
ansible.builtin.fail:
msg: "uv not found at {{ frigate_detector_uv_binary }}. Install via mise: mise use uv@latest"
when: not frigate_detector_uv_stat.stat.exists
- name: Deploy frigate-detector LaunchAgent plist
ansible.builtin.template:
src: mcquack.eblume.frigate-detector.plist.j2
dest: ~/Library/LaunchAgents/mcquack.eblume.frigate-detector.plist
mode: '0644'
notify: Restart frigate-detector
- name: Check if frigate-detector LaunchAgent is loaded
ansible.builtin.command: launchctl list mcquack.eblume.frigate-detector
register: frigate_detector_launchctl_check
changed_when: false
failed_when: false
- name: Load frigate-detector LaunchAgent if not loaded
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.frigate-detector.plist
when: frigate_detector_launchctl_check.rc != 0
changed_when: true
failed_when: false

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- {{ ansible_managed }} -->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>mcquack.eblume.frigate-detector</string>
<key>ProgramArguments</key>
<array>
<string>{{ frigate_detector_uv_binary }}</string>
<string>run</string>
<string>--python</string>
<string>{{ frigate_detector_python }}</string>
<string>--with</string>
<string>numpy==1.26.*</string>
<string>--with</string>
<string>opencv-python-headless==4.11.0.*</string>
<string>--with</string>
<string>opencv-contrib-python==4.11.0.*</string>
<string>--with</string>
<string>onnxruntime==1.22.*</string>
<string>--with</string>
<string>pyzmq==26.2.*</string>
<string>--with</string>
<string>pydantic==2.10.*</string>
<string>detector/zmq_onnx_client.py</string>
<string>--model</string>
<string>{{ frigate_detector_model }}</string>
<string>--endpoint</string>
<string>{{ frigate_detector_endpoint }}</string>
</array>
<key>WorkingDirectory</key>
<string>{{ frigate_detector_dir }}</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>{{ frigate_detector_log_dir }}/mcquack.frigate-detector.out.log</string>
<key>StandardErrorPath</key>
<string>{{ frigate_detector_log_dir }}/mcquack.frigate-detector.err.log</string>
</dict>
</plist>

View file

@ -30,7 +30,6 @@ data:
roles: [detect]
detect:
enabled: true
fps: 2
stationary:
max_frames:
default: 1500
@ -42,25 +41,32 @@ data:
driveway_entrance:
coordinates: 0.85,0.366,0.735,0.344,0.681,0.2,0.795,0.255
objects: [car, dog, person]
driveway:
coordinates: 0.767,0.25,0.58,0.2,0.218,0.25,0.128,0.296,0.003,0.565,0.001,0.992,0.826,0.992,0.897,0.665,0.869,0.608,0.788,0.354
review:
alerts:
labels: [person, car]
required_zones:
- driveway_entrance
detections:
required_zones:
- driveway
- driveway_entrance
objects:
track: [person, car, dog, cat, bird]
detectors:
onnx:
type: onnx
apple_silicon:
type: zmq
endpoint: tcp://host.minikube.internal:5555
model:
model_type: yolonas
model_type: yolo-generic
width: 320
height: 320
input_tensor: nchw
input_pixel_format: bgr
path: /media/frigate/models/yolo_nas_s.onnx
input_dtype: float
path: /media/frigate/models/yolov9m.onnx
labelmap_path: /labelmap/coco-80.txt
record:

View file

@ -0,0 +1 @@
Add Apple Silicon ZMQ detector for Frigate — inference moves from in-pod ONNX CPU to CoreML on indri via ZMQ, using YOLOv9-m model

View file

@ -27,12 +27,14 @@ Open-source network video recorder (NVR) with object detection. Runs cloud-free
ReoLink Camera (GableCam)
│ RTSP
Frigate pod
├── go2rtc — RTSP restream proxy
├── FFmpeg — stream decoding
├── ONNX detector — object detection (YOLO-NAS-s, CPU)
├── /media/frigate — NFS recordings (sifaka)
└── /db — SQLite (local PVC)
Frigate pod (minikube)
├── go2rtc — RTSP restream proxy
├── FFmpeg — stream decoding
├── ZMQ detector ──tcp://host.minikube.internal:5555──→ apple-silicon-detector
│ ├── CoreML / Neural Engine
│ └── LaunchAgent (mcquack.eblume.frigate-detector)
├── /media/frigate — NFS recordings (sifaka)
└── /db — SQLite (local PVC)
└──→ MQTT (Mosquitto) → frigate-notify → ntfy → mobile
```
@ -47,9 +49,9 @@ Camera credentials are stored in 1Password and synced via [[external-secrets]] t
## Detection
Object detection uses ONNX with a YOLO-NAS-s model running on CPU (ARM64). The model file lives on the NFS recordings volume at `/media/frigate/models/yolo_nas_s.onnx`.
Object detection uses the [apple-silicon-detector](https://github.com/frigate-nvr/apple-silicon-detector) with a YOLOv9-m model (`yolo-generic`, 320x320), running natively on [[indri]] as a LaunchAgent (`mcquack.eblume.frigate-detector`). It communicates with Frigate via ZMQ over TCP (`tcp://host.minikube.internal:5555`), using CoreML with partial Neural Engine acceleration (~100-170ms inference). Model ONNX files are stored on the NFS volume at `/media/frigate/models/`.
A `driveway_entrance` zone is configured for alert filtering — only detections in this zone trigger review alerts.
Two zones are configured: `driveway_entrance` (triggers review alerts for person/car) and `driveway` (triggers review detections).
## Retention