Fix minor issues: enum types, backup path, JS URLs, status field, timestamp docs

- CameraConfig.location now uses CameraLocation enum (Pydantic v2 coerces TOML strings)
- Wildlife classifier returns ThreatLevel enum values with correct return type annotation
- Model backup path fixed: pet_id_backup.pt instead of pet_id.backup.pt
- Dashboard submitLabel JS now posts to /pets/<sighting_id>/label matching Flask route
- Pet status API computes status field (safe/unknown) based on last-seen recency
- digest.py comment explains timestamp unit difference between tables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee 2026-04-03 13:49:20 -04:00
parent 9858738e82
commit 0b82105179
6 changed files with 24 additions and 11 deletions

View File

@ -20,6 +20,7 @@ from vigilar.constants import (
DEFAULT_RETENTION_DAYS,
DEFAULT_UPS_POLL_INTERVAL_S,
DEFAULT_WEB_PORT,
CameraLocation,
)
# --- Camera Config ---
@ -42,7 +43,7 @@ class CameraConfig(BaseModel):
resolution_capture: list[int] = Field(default_factory=lambda: [1920, 1080])
resolution_motion: list[int] = Field(default_factory=lambda: [640, 360])
zones: list["CameraZone"] = Field(default_factory=list)
location: str = "INTERIOR" # EXTERIOR | INTERIOR | TRANSITION
location: CameraLocation = CameraLocation.INTERIOR
# --- Sensor Config ---

View File

@ -122,7 +122,9 @@ class PetTrainer:
epoch + 1, epochs, running_loss / len(loader), accuracy)
if self._model_output_path.exists():
backup_path = self._model_output_path.with_suffix(".backup.pt")
backup_path = self._model_output_path.with_name(
self._model_output_path.stem + "_backup.pt"
)
shutil.copy2(self._model_output_path, backup_path)
log.info("Backed up previous model to %s", backup_path)

View File

@ -1,6 +1,7 @@
"""Wildlife threat level classification."""
from vigilar.config import WildlifeConfig
from vigilar.constants import ThreatLevel
from vigilar.detection.person import Detection
@ -8,7 +9,7 @@ def classify_wildlife_threat(
detection: Detection,
config: WildlifeConfig,
frame_area: int,
) -> tuple[str, str]:
) -> tuple[ThreatLevel, str]:
"""Classify a wildlife detection into threat level and species.
Returns (threat_level, species_name).
@ -18,11 +19,11 @@ def classify_wildlife_threat(
# Direct COCO class mapping first
if species in threat_map.predator:
return "PREDATOR", species
return ThreatLevel.PREDATOR, species
if species in threat_map.nuisance:
return "NUISANCE", species
return ThreatLevel.NUISANCE, species
if species in threat_map.passive:
return "PASSIVE", species
return ThreatLevel.PASSIVE, species
# Fallback to size heuristics for unknown species
_, _, w, h = detection.bbox
@ -31,8 +32,8 @@ def classify_wildlife_threat(
heuristics = config.size_heuristics
if area_ratio < heuristics.small:
return "NUISANCE", species
return ThreatLevel.NUISANCE, species
elif area_ratio < heuristics.medium:
return "PREDATOR", species
return ThreatLevel.PREDATOR, species
else:
return "PASSIVE", species
return ThreatLevel.PASSIVE, species

View File

@ -36,6 +36,8 @@ def build_digest(engine: Engine, data_dir: str, since_hours: int = 12) -> dict:
.where(recordings.c.started_at >= since_ts // 1000)
).scalar() or 0
# pet_sightings/wildlife_sightings use float seconds (time.time()),
# while events table uses integer milliseconds (time.time() * 1000)
pet_count = conn.execute(
select(func.count()).select_from(pet_sightings)
.where(pet_sightings.c.ts >= since_ts / 1000)

View File

@ -58,13 +58,20 @@ def pet_status():
from vigilar.storage.queries import get_all_pets, get_pet_last_location
pets_list = get_all_pets(engine)
now = time.time()
result = []
for pet in pets_list:
last = get_pet_last_location(engine, pet["id"])
if last:
last_seen_ago = now - last["ts"]
status = "safe" if last_seen_ago < 300 else "unknown" # seen in last 5 min
else:
status = "unknown"
result.append({
**pet,
"last_seen_ts": last["ts"] if last else None,
"last_camera": last["camera_id"] if last else None,
"status": status,
})
return jsonify({"pets": result})

View File

@ -392,10 +392,10 @@
function submitLabel(petId) {
if (!pendingLabelCropId) return;
fetch('/pets/api/label', {
fetch(`/pets/${pendingLabelCropId}/label`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ crop_id: pendingLabelCropId, pet_id: petId }),
body: JSON.stringify({ sighting_id: pendingLabelCropId, pet_id: petId }),
}).finally(() => {
bootstrap.Modal.getOrCreateInstance(document.getElementById('labelModal')).hide();
loadUnlabeled();