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:
parent
9858738e82
commit
0b82105179
@ -20,6 +20,7 @@ from vigilar.constants import (
|
|||||||
DEFAULT_RETENTION_DAYS,
|
DEFAULT_RETENTION_DAYS,
|
||||||
DEFAULT_UPS_POLL_INTERVAL_S,
|
DEFAULT_UPS_POLL_INTERVAL_S,
|
||||||
DEFAULT_WEB_PORT,
|
DEFAULT_WEB_PORT,
|
||||||
|
CameraLocation,
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Camera Config ---
|
# --- Camera Config ---
|
||||||
@ -42,7 +43,7 @@ class CameraConfig(BaseModel):
|
|||||||
resolution_capture: list[int] = Field(default_factory=lambda: [1920, 1080])
|
resolution_capture: list[int] = Field(default_factory=lambda: [1920, 1080])
|
||||||
resolution_motion: list[int] = Field(default_factory=lambda: [640, 360])
|
resolution_motion: list[int] = Field(default_factory=lambda: [640, 360])
|
||||||
zones: list["CameraZone"] = Field(default_factory=list)
|
zones: list["CameraZone"] = Field(default_factory=list)
|
||||||
location: str = "INTERIOR" # EXTERIOR | INTERIOR | TRANSITION
|
location: CameraLocation = CameraLocation.INTERIOR
|
||||||
|
|
||||||
|
|
||||||
# --- Sensor Config ---
|
# --- Sensor Config ---
|
||||||
|
|||||||
@ -122,7 +122,9 @@ class PetTrainer:
|
|||||||
epoch + 1, epochs, running_loss / len(loader), accuracy)
|
epoch + 1, epochs, running_loss / len(loader), accuracy)
|
||||||
|
|
||||||
if self._model_output_path.exists():
|
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)
|
shutil.copy2(self._model_output_path, backup_path)
|
||||||
log.info("Backed up previous model to %s", backup_path)
|
log.info("Backed up previous model to %s", backup_path)
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"""Wildlife threat level classification."""
|
"""Wildlife threat level classification."""
|
||||||
|
|
||||||
from vigilar.config import WildlifeConfig
|
from vigilar.config import WildlifeConfig
|
||||||
|
from vigilar.constants import ThreatLevel
|
||||||
from vigilar.detection.person import Detection
|
from vigilar.detection.person import Detection
|
||||||
|
|
||||||
|
|
||||||
@ -8,7 +9,7 @@ def classify_wildlife_threat(
|
|||||||
detection: Detection,
|
detection: Detection,
|
||||||
config: WildlifeConfig,
|
config: WildlifeConfig,
|
||||||
frame_area: int,
|
frame_area: int,
|
||||||
) -> tuple[str, str]:
|
) -> tuple[ThreatLevel, str]:
|
||||||
"""Classify a wildlife detection into threat level and species.
|
"""Classify a wildlife detection into threat level and species.
|
||||||
|
|
||||||
Returns (threat_level, species_name).
|
Returns (threat_level, species_name).
|
||||||
@ -18,11 +19,11 @@ def classify_wildlife_threat(
|
|||||||
|
|
||||||
# Direct COCO class mapping first
|
# Direct COCO class mapping first
|
||||||
if species in threat_map.predator:
|
if species in threat_map.predator:
|
||||||
return "PREDATOR", species
|
return ThreatLevel.PREDATOR, species
|
||||||
if species in threat_map.nuisance:
|
if species in threat_map.nuisance:
|
||||||
return "NUISANCE", species
|
return ThreatLevel.NUISANCE, species
|
||||||
if species in threat_map.passive:
|
if species in threat_map.passive:
|
||||||
return "PASSIVE", species
|
return ThreatLevel.PASSIVE, species
|
||||||
|
|
||||||
# Fallback to size heuristics for unknown species
|
# Fallback to size heuristics for unknown species
|
||||||
_, _, w, h = detection.bbox
|
_, _, w, h = detection.bbox
|
||||||
@ -31,8 +32,8 @@ def classify_wildlife_threat(
|
|||||||
|
|
||||||
heuristics = config.size_heuristics
|
heuristics = config.size_heuristics
|
||||||
if area_ratio < heuristics.small:
|
if area_ratio < heuristics.small:
|
||||||
return "NUISANCE", species
|
return ThreatLevel.NUISANCE, species
|
||||||
elif area_ratio < heuristics.medium:
|
elif area_ratio < heuristics.medium:
|
||||||
return "PREDATOR", species
|
return ThreatLevel.PREDATOR, species
|
||||||
else:
|
else:
|
||||||
return "PASSIVE", species
|
return ThreatLevel.PASSIVE, species
|
||||||
|
|||||||
@ -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)
|
.where(recordings.c.started_at >= since_ts // 1000)
|
||||||
).scalar() or 0
|
).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(
|
pet_count = conn.execute(
|
||||||
select(func.count()).select_from(pet_sightings)
|
select(func.count()).select_from(pet_sightings)
|
||||||
.where(pet_sightings.c.ts >= since_ts / 1000)
|
.where(pet_sightings.c.ts >= since_ts / 1000)
|
||||||
|
|||||||
@ -58,13 +58,20 @@ def pet_status():
|
|||||||
|
|
||||||
from vigilar.storage.queries import get_all_pets, get_pet_last_location
|
from vigilar.storage.queries import get_all_pets, get_pet_last_location
|
||||||
pets_list = get_all_pets(engine)
|
pets_list = get_all_pets(engine)
|
||||||
|
now = time.time()
|
||||||
result = []
|
result = []
|
||||||
for pet in pets_list:
|
for pet in pets_list:
|
||||||
last = get_pet_last_location(engine, pet["id"])
|
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({
|
result.append({
|
||||||
**pet,
|
**pet,
|
||||||
"last_seen_ts": last["ts"] if last else None,
|
"last_seen_ts": last["ts"] if last else None,
|
||||||
"last_camera": last["camera_id"] if last else None,
|
"last_camera": last["camera_id"] if last else None,
|
||||||
|
"status": status,
|
||||||
})
|
})
|
||||||
return jsonify({"pets": result})
|
return jsonify({"pets": result})
|
||||||
|
|
||||||
|
|||||||
@ -392,10 +392,10 @@
|
|||||||
|
|
||||||
function submitLabel(petId) {
|
function submitLabel(petId) {
|
||||||
if (!pendingLabelCropId) return;
|
if (!pendingLabelCropId) return;
|
||||||
fetch('/pets/api/label', {
|
fetch(`/pets/${pendingLabelCropId}/label`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ crop_id: pendingLabelCropId, pet_id: petId }),
|
body: JSON.stringify({ sighting_id: pendingLabelCropId, pet_id: petId }),
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('labelModal')).hide();
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('labelModal')).hide();
|
||||||
loadUnlabeled();
|
loadUnlabeled();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user