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
45 changes: 33 additions & 12 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,26 +1,47 @@
# ── Eagle Environment Configuration ──────────────────────────────────────
# Copy this file to .env and fill in the values before running the project.
# All variables map 1:1 to fields in libs/config/settings.py (Settings class).

# Redis (used for temporal memory ring buffer)
REDIS_URL=redis://localhost:6379/0
# ── Redis ─────────────────────────────────────────────────────────────────
REDIS_URL=redis://localhost:6379

# Ollama (local VLM inference)
OLLAMA_HOST=http://localhost:11434
# ── Memory / ring-buffer ─────────────────────────────────────────────────
MAX_EVENTS_PER_TRACK=50
TRACK_TTL_SECONDS=86400

# ── Action classifier thresholds ─────────────────────────────────────────
LINGERING_THRESHOLD_SEC=5.0
MOVEMENT_THRESHOLD_PX=8.0
NEAR_KEYPAD_DIST_PX=80.0
KEYPAD_CENTER_X=600.0
KEYPAD_CENTER_Y=280.0

# Detection
# ── Detection ────────────────────────────────────────────────────────────
YOLO_MODEL=yolov8n.pt
DETECTION_CONFIDENCE=0.4
DETECTION_CONFIDENCE_THRESHOLD=0.45

# Policy
POLICY_PATH=policies/default.yaml
# ── Tracker ──────────────────────────────────────────────────────────────
TRACKER_MAX_AGE=30
TRACKER_N_INIT=3

# Backend
API_HOST=0.0.0.0
API_PORT=8000

# VLM / LLM provider
# ── VLM / LLM provider ──────────────────────────────────────────────────
# Use "mock" for CI, offline development, or contributors without a GPU.
# No Ollama installation required when set to "mock".
# Future values: "ollama", "openai", "gemini"
VLM_PROVIDER=mock
LLM_PROVIDER=mock
OLLAMA_HOST=http://localhost:11434

# ── Reasoning / alerts ───────────────────────────────────────────────────
REASONING_DWELL_THRESHOLD_SECONDS=5.0
REASONING_COOLDOWN_SECONDS=5.0
RING_BUFFER_MAX=50
ALERT_DEDUP_WINDOW=300

# ── Backend / API ────────────────────────────────────────────────────────
API_HOST=0.0.0.0
API_PORT=8000

# ── Policy ───────────────────────────────────────────────────────────────
POLICY_PATH=policies/default.yaml
67 changes: 35 additions & 32 deletions libs/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,64 @@


class Settings(BaseSettings):
# Environment-backed connection / API settings
"""Centralised configuration for the Eagle surveillance system.

Every field can be overridden via an environment variable of the same
(uppercased) name or via a ``.env`` file in the project root.
"""

# ── Redis ─────────────────────────────────────────────────────────────
redis_url: str = "redis://localhost:6379"
vlm_provider: str = "mock"
llm_provider: str = "mock"
ollama_host: str = "http://localhost:11434"

# YOLO / detection settings (kept for backward compatibility alongside existing names)
yolo_model: str = "yolov8n.pt"
detection_confidence: float = 0.4
api_host: str = "0.0.0.0"
api_port: int = 8000
# ── Memory / ring-buffer ──────────────────────────────────────────────
max_events_per_track: int = 50
track_ttl_seconds: int = 86_400 # 24 h

# Action classifier thresholds
# ── Action classifier thresholds ──────────────────────────────────────
lingering_threshold_sec: float = 5.0
movement_threshold_px: float = 10.0
movement_threshold_px: float = 8.0
near_keypad_dist_px: float = 80.0
keypad_center_x: int = 320
keypad_center_y: int = 240
policy_path: str = "policies/default.yaml"
keypad_center_x: float = 600.0
keypad_center_y: float = 280.0

# ── Detection ─────────────────────────────────────────────────────────
yolo_model: str = "yolov8n.pt"
detector_model: str = "yolov8n.pt"
detection_confidence: float = 0.4
detection_confidence_threshold: float = 0.45
detector_device: str = "cpu"
confidence_threshold: float = 0.45

# ── Tracker ───────────────────────────────────────────────────────────
tracker_fps: float = 30
tracker_max_age: int = 30
tracker_n_init: int = 3
tracker_max_cosine_distance: float = 0.4
camera_id: str = "cam_01"

# Action classifier settings
lingering_threshold_sec: float = 5.0
movement_threshold_px: float = 15.0
near_keypad_dist_px: float = 75.0
keypad_center_x: float = 500.0
keypad_center_y: float = 500.0
# ── VLM / LLM providers ──────────────────────────────────────────────
vlm_provider: str = "mock"
llm_provider: str = "mock"
ollama_host: str = "http://localhost:11434"

# Reasoning trigger settings
# ── Reasoning / alerts ────────────────────────────────────────────────
reasoning_dwell_threshold_seconds: float = 5.0
reasoning_cooldown_seconds: float = 5.0

# New reasoning / alert settings
reasoning_trigger_sec: float = 5.0
ring_buffer_max: int = 50
alert_dedup_window: int = 300
snapshot_dir: str = "/tmp/eagle_snapshots"

# ── Backend / API ─────────────────────────────────────────────────────
api_host: str = "0.0.0.0"
api_port: int = 8000
cors_origins: list[str] = ["http://localhost:5173"] # Vite dev
max_alerts_page: int = 50
snapshot_dir: str = "/tmp/eagle_snapshots"

# Action classifier settings
lingering_threshold_sec: float = 10.0
movement_threshold_px: float = 5.0
near_keypad_dist_px: float = 80.0
keypad_center_x: float = 640.0
keypad_center_y: float = 360.0
# ── Policy ────────────────────────────────────────────────────────────
policy_path: str = "policies/default.yaml"
camera_id: str = "cam_01"

# Kafka Settings
# ── Kafka ─────────────────────────────────────────────────────────────
use_kafka: bool = False
kafka_bootstrap_servers: str = "localhost:9092"
kafka_topic: str = "track-events"
Expand Down
3 changes: 2 additions & 1 deletion services/memory/action_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@

from libs.schemas.action_recognition import ActionFrameResult, ActionPrediction
from libs.schemas.memory import ActionHint, TrackEvent
from libs.config.settings import settings

if TYPE_CHECKING:
from services.memory.memory import MemoryService

logger = logging.getLogger(__name__)

TRACK_TTL_SECONDS = 86_400
TRACK_TTL_SECONDS = settings.track_ttl_seconds


def _prediction_to_hint(pred: ActionPrediction) -> ActionHint:
Expand Down
11 changes: 7 additions & 4 deletions services/memory/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@
from libs.observability.metrics import redis_write_latency
from libs.schemas.tracking import TrackLifecycleEvent, TrackState
from libs.schemas.memory import TrackEvent, TrackSequence, ActionHint
from libs.config.settings import settings
from services.tracking.cross_camera_reid import CrossCameraReID

logger = logging.getLogger(__name__)

# ── Redis TTLs ────────────────────────────────────────────────────────────────
TRACK_TTL_SECONDS = 86_400 # 24 h — keep per-track state for a full day
EVENT_TTL_SECONDS = 86_400
# ── Redis TTLs (sourced from centralised settings) ───────────────────────────
TRACK_TTL_SECONDS = settings.track_ttl_seconds
EVENT_TTL_SECONDS = settings.track_ttl_seconds

# ── MemoryStore constants ─────────────────────────────────────────────────────
MAX_EVENTS_PER_TRACK = 50 # ring-buffer cap per track_id
Expand Down Expand Up @@ -274,7 +275,9 @@ def _append_event(
)


# ── MemoryStore ───────────────────────────────────────────────────────────────
# Compatibility layer: lightweight event store used by tests and the pipeline.
MAX_EVENTS_PER_TRACK = settings.max_events_per_track


class MemoryStore:
"""
Expand Down
3 changes: 2 additions & 1 deletion services/memory/ring_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from typing import Optional

from libs.schemas.memory import TrackEvent, TrackSequence
from libs.config.settings import settings

MAX_EVENTS_PER_TRACK = 50
MAX_EVENTS_PER_TRACK = settings.max_events_per_track


class MemoryStore:
Expand Down
152 changes: 152 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""
Unit tests for libs/config/settings.py (issue #32).

Verifies:
- Default values match the documented contract.
- Environment variables override defaults correctly.
- The singleton ``settings`` object is importable and valid.
"""
from __future__ import annotations

import os
import pytest

from libs.config.settings import Settings


# ── Default values ────────────────────────────────────────────────────────────

class TestDefaults:
"""Verify all defaults match the values specified in the issue."""

def test_redis_url(self):
s = Settings()
assert s.redis_url == "redis://localhost:6379"

def test_max_events_per_track(self):
s = Settings()
assert s.max_events_per_track == 50

def test_track_ttl_seconds(self):
s = Settings()
assert s.track_ttl_seconds == 86_400

def test_lingering_threshold_sec(self):
s = Settings()
assert s.lingering_threshold_sec == 5.0

def test_movement_threshold_px(self):
s = Settings()
assert s.movement_threshold_px == 8.0

def test_near_keypad_dist_px(self):
s = Settings()
assert s.near_keypad_dist_px == 80.0

def test_keypad_center_x(self):
s = Settings()
assert s.keypad_center_x == 600.0

def test_keypad_center_y(self):
s = Settings()
assert s.keypad_center_y == 280.0

def test_yolo_model(self):
s = Settings()
assert s.yolo_model == "yolov8n.pt"

def test_confidence_threshold(self):
s = Settings()
assert s.confidence_threshold == 0.45

def test_tracker_max_age(self):
s = Settings()
assert s.tracker_max_age == 30

def test_tracker_n_init(self):
s = Settings()
assert s.tracker_n_init == 3

def test_vlm_provider_default_is_mock(self):
s = Settings()
assert s.vlm_provider == "mock"


# ── Environment overrides ────────────────────────────────────────────────────

class TestEnvOverrides:
"""Verify that environment variables correctly override defaults."""

def test_redis_url_override(self, monkeypatch):
monkeypatch.setenv("REDIS_URL", "redis://prod:6380/1")
s = Settings()
assert s.redis_url == "redis://prod:6380/1"

def test_max_events_per_track_override(self, monkeypatch):
monkeypatch.setenv("MAX_EVENTS_PER_TRACK", "100")
s = Settings()
assert s.max_events_per_track == 100

def test_track_ttl_seconds_override(self, monkeypatch):
monkeypatch.setenv("TRACK_TTL_SECONDS", "3600")
s = Settings()
assert s.track_ttl_seconds == 3600

def test_lingering_threshold_override(self, monkeypatch):
monkeypatch.setenv("LINGERING_THRESHOLD_SEC", "10.0")
s = Settings()
assert s.lingering_threshold_sec == 10.0

def test_movement_threshold_override(self, monkeypatch):
monkeypatch.setenv("MOVEMENT_THRESHOLD_PX", "20.0")
s = Settings()
assert s.movement_threshold_px == 20.0

def test_keypad_center_override(self, monkeypatch):
monkeypatch.setenv("KEYPAD_CENTER_X", "400.0")
monkeypatch.setenv("KEYPAD_CENTER_Y", "300.0")
s = Settings()
assert s.keypad_center_x == 400.0
assert s.keypad_center_y == 300.0

def test_yolo_model_override(self, monkeypatch):
monkeypatch.setenv("YOLO_MODEL", "yolov8s.pt")
s = Settings()
assert s.yolo_model == "yolov8s.pt"

def test_confidence_threshold_override(self, monkeypatch):
monkeypatch.setenv("CONFIDENCE_THRESHOLD", "0.60")
s = Settings()
assert s.confidence_threshold == 0.60

def test_tracker_max_age_override(self, monkeypatch):
monkeypatch.setenv("TRACKER_MAX_AGE", "60")
s = Settings()
assert s.tracker_max_age == 60

def test_vlm_provider_override(self, monkeypatch):
monkeypatch.setenv("VLM_PROVIDER", "ollama")
s = Settings()
assert s.vlm_provider == "ollama"

def test_api_port_override(self, monkeypatch):
monkeypatch.setenv("API_PORT", "9000")
s = Settings()
assert s.api_port == 9000


# ── Singleton import ─────────────────────────────────────────────────────────

class TestSingleton:
"""Verify the module-level ``settings`` instance is usable."""

def test_settings_singleton_importable(self):
from libs.config.settings import settings
assert isinstance(settings, Settings)

def test_settings_singleton_has_expected_type(self):
from libs.config.settings import settings
assert hasattr(settings, "redis_url")
assert hasattr(settings, "max_events_per_track")
assert hasattr(settings, "track_ttl_seconds")
assert hasattr(settings, "lingering_threshold_sec")
Loading