Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,8 @@ CODEX.md
.copilot/
graphify-out/
*.pt

# Local working docs and benchmark artifacts (kept out of the repo)
*.local.md
*.local.py
sweep-results/
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ Server settings in `configs/server.yaml` or via `CLOCKD_` environment variables:
|---------|---------|-------------|
| `host` | `0.0.0.0` | Bind address |
| `port` | `8000` | Bind port |
| `detection_backend` | `local` | `"local"`, `"roboflow"`, `"localai"`, or `"codeproject_ai"` |
| `detection_backend` | `local` | `"local"`, `"roboflow"`, `"localai"`, `"codeproject_ai"`, or `"coralapi"` |
| `detection_fallback` | `none` | `"local"` to fall back to local CPU inference when a remote backend is unreachable |
| `model` | `yolo26n.pt` | Default YOLO model (local backend only) |
| `confidence` | `0.3` | Detection confidence threshold |
| `default_unit` | `mph` | Speed unit (`mph` or `kmh`) |
Expand Down Expand Up @@ -286,11 +287,13 @@ All remote backends offload detection to an external server. Clockd sends each f
| **[Roboflow Inference](https://github.com/roboflow/inference)** | YOLO v8/v10/v11/26, RF-DETR, YOLO-NAS | CUDA + TensorRT | No | Linux, macOS | Very active (daily commits) | GPU server, best model selection |
| **[LocalAI](https://github.com/mudler/localai)** | RF-DETR | CUDA, ROCm, Vulkan | No | Linux, macOS, Windows | Very active (monthly releases) | Already running LocalAI for LLMs |
| **[CodeProject.AI](https://github.com/codeproject/CodeProject.AI-Server)** | YOLOv5, v8, v11 | CUDA | Yes (YOLOv5 TFLite) | Windows, Linux, macOS | Slow (last release Dec 2024) | Raspberry Pi + Coral TPU |
| **[coralapi](https://github.com/nathan-v/coralapi)** | Edge TPU `.tflite` (SSD MobileNet, EfficientDet) | No (TPU only) | Yes (native) | Linux | Active | Dedicated Coral TPU, lowest power |

**Notes:**
- **Roboflow Inference** is the recommended remote backend. Broadest model support, actively maintained, no API key needed for pre-trained COCO models, and supports TensorRT quantization for maximum GPU throughput. Runs on Linux and macOS only (Docker-based). For GPU use, Linux with the NVIDIA Container Toolkit is recommended.
- **LocalAI** only supports RF-DETR for object detection (not YOLO). It's a good choice if you already run LocalAI for other AI tasks. Supports the widest range of GPU vendors (NVIDIA, AMD, Intel).
- **CodeProject.AI** is the only backend with Coral TPU support, via its YOLOv5 TFLite module. This makes it a viable option for low-power deployments like a Raspberry Pi with a Coral accelerator. The project has slowed (volunteer-maintained, 18+ months since last release) but remains functional.
- **CodeProject.AI** supports Coral TPUs via its YOLOv5 TFLite module. The project has slowed (volunteer-maintained, 18+ months since last release) but remains functional.
- **coralapi** is a purpose-built Edge TPU inference server (sync/async API, per-request model selection, automatic model download, no GPU required). It serves quantized Edge TPU `.tflite` models — expect SSD-MobileNet-class accuracy, below the larger YOLO models, at single-digit-watt power draw. Detections return normalized boxes, so `coralapi.resize_max_px` costs no accuracy on this backend.
- For most users, **local + `yolo26n.pt` on CPU** is fast enough. Remote backends are worth it for larger models on GPU, high-volume processing, or shared inference across multiple services.

##### Backend Configuration
Expand Down Expand Up @@ -329,6 +332,20 @@ codeproject_ai:
timeout: 30
```

**coralapi:**

```yaml
detection_backend: "coralapi"

coralapi:
url: "http://localhost:8000"
model: "ssd_mobilenet_v2_coco_quant_postprocess_edgetpu"
timeout: 30
resize_max_px: 640 # optional; Edge TPU models run at ~300px anyway
```

Start the server (Coral Edge TPU required): see the [coralapi quickstart](https://github.com/nathan-v/coralapi#quickstart-docker).

## Event Sources (NVR Integration)

Clockd can automatically poll your NVR for vehicle detection events, download clips, and process them — no external automation needed.
Expand Down
17 changes: 17 additions & 0 deletions configs/server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ host: "0.0.0.0"
port: 8000
verbose: false
detection_backend: "local" # "local", "roboflow", "localai", or "codeproject_ai"
# Optional: fall back to local CPU inference (using `model`) when a remote
# detection backend is unreachable. "none" (default) keeps the old behavior
# of logging and yielding empty detections.
# detection_fallback: "local"
model: "yolo26n.pt"
confidence: 0.3
default_unit: "mph"
Expand All @@ -26,6 +30,19 @@ roboflow:
url: "http://localhost:9001"
model_id: "yolo11n-640"
timeout: 30
# Optional: downscale frames to this longest-side size before sending.
# The server resizes to the model input anyway; enabling this trades a
# little local CPU for much smaller uploads. Omit to send full frames.
# resize_max_px: 640

# coralapi Edge TPU server (https://github.com/nathan-v/coralapi)
coralapi:
url: "http://localhost:8000"
model: "ssd_mobilenet_v2_coco_quant_postprocess_edgetpu"
timeout: 30
# Edge TPU SSD models run at 300x300; downscaling before upload saves
# bandwidth with no accuracy cost (boxes are returned normalized).
# resize_max_px: 640

localai:
url: "http://localhost:8080"
Expand Down
34 changes: 32 additions & 2 deletions src/clockd/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import re

import yaml
from pydantic import BaseModel, field_validator
from pydantic import BaseModel, Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

SAFE_ID_RE = re.compile(r"^[a-zA-Z0-9_-]+$")
Expand Down Expand Up @@ -148,6 +148,11 @@ class RoboflowInferenceConfig(BaseModel):
url: str = "http://localhost:9001"
model_id: str = "yolo11n-640"
timeout: int = 30
# Downscale frames so their longest side is at most this many pixels before
# sending to the inference server. The server resizes to the model input size
# anyway, so this trades local resize CPU for much smaller encode/transfer
# payloads. None sends full-resolution frames (pushes all work to the server).
resize_max_px: Optional[int] = Field(default=None, ge=64, le=8192)


class LocalAIConfig(BaseModel):
Expand All @@ -156,6 +161,18 @@ class LocalAIConfig(BaseModel):
timeout: int = 30


class CoralAPIConfig(BaseModel):
"""coralapi Edge TPU inference server (https://github.com/nathan-v/coralapi)."""

url: str = "http://localhost:8000"
model: str = "ssd_mobilenet_v2_coco_quant_postprocess_edgetpu"
timeout: int = 30
# Same semantics as roboflow.resize_max_px: downscale before upload.
# Edge TPU SSD models run at 300x300, so full-res frames are pure
# transfer overhead; boxes come back normalized, so no accuracy cost.
resize_max_px: Optional[int] = Field(default=None, ge=64, le=8192)


class UnifiProtectConfig(BaseModel):
host: str = ""
username: str = ""
Expand Down Expand Up @@ -232,7 +249,19 @@ def settings_customise_sources(
host: str = "0.0.0.0"
port: int = 8000
verbose: bool = False # enable detailed logging of processing results
detection_backend: str = "local" # "local", "roboflow", "localai", or "codeproject_ai"
detection_backend: str = "local" # "local", "roboflow", "localai", "codeproject_ai", "coralapi"
# "local": if a remote detection_backend is unreachable, fall back to
# local CPU inference (using `model`) for the rest of the job. "none":
# unreachable backends yield empty detections (logged) as before.
detection_fallback: str = "none"

@field_validator("detection_fallback")
@classmethod
def _validate_detection_fallback(cls, v: str) -> str:
if v not in ("none", "local"):
raise ValueError("detection_fallback must be 'none' or 'local'")
return v

model: str = "yolo26n.pt" # validated at startup, must be in ALLOWED_MODELS

@field_validator("model")
Expand All @@ -253,6 +282,7 @@ def validate_model(cls, v: str) -> str:
codeproject_ai: CodeProjectAIConfig = CodeProjectAIConfig()
roboflow: RoboflowInferenceConfig = RoboflowInferenceConfig()
localai: LocalAIConfig = LocalAIConfig()
coralapi: CoralAPIConfig = CoralAPIConfig()
metrics: MetricsConfig = MetricsConfig()
event_sources: dict[str, UnifiEventSourceConfig] = {}

Expand Down
4 changes: 4 additions & 0 deletions src/clockd/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ class ProcessingResult(BaseModel):
vehicles_filtered: int = 0 # tracks excluded by min_detections or speed_range
processing_time_s: float
warnings: list[str] = []
# what actually performed detection (reflects fallback, not just config)
detection_backend: str = ""
detection_model: str = ""
detection_fallback: bool = False


class JobStatus(str, Enum):
Expand Down
Loading
Loading