observe() held self._lock, called _check_anomaly, which called
get_usual_direction, which tried to acquire self._lock again → deadlock.
Split into _usual_direction_unlocked (no lock) for internal use.
This caused /scene and all other API endpoints to hang after the first
sound classification with spatial data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
spatial_scene.py: Builds a persistent map of where each sound category
usually comes from (30° angle bins, circular mean). Detects anomalies
when a sound appears from an unusual direction (90°+ deviation).
Scene map persists to ~/.vixy/scene_map.json across restarts.
headmic.py: Feed classified sounds + spatial position into scene tracker.
New endpoints:
/scene — learned scene summary + last anomaly
/scene/events — recent events with what+where+when
/scene/heatmap — per-category angular distribution (for visualization)
Example: after running for a day, /scene might show:
{"speech": {"usual_angle": 15.0, "observations": 847},
"music": {"usual_angle": 270.0, "observations": 312}}
And if speech comes from 270° (where music usually is): spatial anomaly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>