fix(v1): wire model confidence, explicit source mode, unique event ids
A: PoseAdapter.set_confidence_threshold is applied on start, so the
settings model-confidence field actually affects inference.
B: config source.mode ('stream'|'replay') is explicit; app no longer
guesses the source type from the URL prefix.
C: FallStateMachine takes a session_id and from_config generates a
unique one per run, so event ids never collide across restarts
(no screenshot overwrite or duplicate JSONL identity in a day).
51 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,40 @@ def test_load_config_rejects_embedded_source_address(tmp_path):
|
||||
load_config(config_file)
|
||||
|
||||
|
||||
def test_source_mode_defaults_to_stream(tmp_path, monkeypatch):
|
||||
config_file = tmp_path / "config.json"
|
||||
_write_config(
|
||||
config_file,
|
||||
{"id": "lobby-camera-01", "rtsp_url_env": "SILVER_POSE_RTSP_URL"},
|
||||
)
|
||||
monkeypatch.setenv("SILVER_POSE_RTSP_URL", "rtsp://demo.invalid/live")
|
||||
|
||||
assert load_config(config_file).source_mode == "stream"
|
||||
|
||||
|
||||
def test_source_mode_replay_is_parsed(tmp_path, monkeypatch):
|
||||
config_file = tmp_path / "config.json"
|
||||
_write_config(
|
||||
config_file,
|
||||
{"id": "lobby-camera-01", "rtsp_url_env": "SILVER_POSE_RTSP_URL", "mode": "replay"},
|
||||
)
|
||||
monkeypatch.setenv("SILVER_POSE_RTSP_URL", "rtsp://demo.invalid/live")
|
||||
|
||||
assert load_config(config_file).source_mode == "replay"
|
||||
|
||||
|
||||
def test_invalid_source_mode_is_rejected(tmp_path, monkeypatch):
|
||||
config_file = tmp_path / "config.json"
|
||||
_write_config(
|
||||
config_file,
|
||||
{"id": "lobby-camera-01", "rtsp_url_env": "SILVER_POSE_RTSP_URL", "mode": "loop"},
|
||||
)
|
||||
monkeypatch.setenv("SILVER_POSE_RTSP_URL", "rtsp://demo.invalid/live")
|
||||
|
||||
with pytest.raises(ConfigError, match="mode"):
|
||||
load_config(config_file)
|
||||
|
||||
|
||||
def test_runtime_config_version_is_stable_and_excludes_rtsp_address(tmp_path, monkeypatch):
|
||||
config_file = tmp_path / "config.json"
|
||||
_write_config(
|
||||
|
||||
@@ -82,6 +82,25 @@ def test_confirmation_window_must_remain_within_customer_target():
|
||||
)
|
||||
|
||||
|
||||
def test_session_id_makes_event_ids_unique_across_runs():
|
||||
run_a = FallStateMachine(
|
||||
confirm_window_seconds=1.0, recovery_window_seconds=2.0,
|
||||
config_version="cfg", session_id="run-a",
|
||||
)
|
||||
run_b = FallStateMachine(
|
||||
confirm_window_seconds=1.0, recovery_window_seconds=2.0,
|
||||
config_version="cfg", session_id="run-b",
|
||||
)
|
||||
run_a.update("P-0001", Evidence(True, True), now=0.0)
|
||||
event_a = run_a.update("P-0001", Evidence(True, True), now=1.0)
|
||||
run_b.update("P-0001", Evidence(True, True), now=0.0)
|
||||
event_b = run_b.update("P-0001", Evidence(True, True), now=1.0)
|
||||
|
||||
assert event_a[0].event_id != event_b[0].event_id
|
||||
assert "run-a" in event_a[0].event_id
|
||||
assert "run-b" in event_b[0].event_id
|
||||
|
||||
|
||||
def test_each_track_has_an_independent_confirmation_window():
|
||||
machine = FallStateMachine(
|
||||
confirm_window_seconds=1.0,
|
||||
|
||||
@@ -119,3 +119,18 @@ def test_pipeline_from_config_uses_runtime_version_for_confirmed_event():
|
||||
result = pipeline.process(_packet(1.1))
|
||||
|
||||
assert result.events[0].config_version == config.runtime_config_version
|
||||
|
||||
|
||||
def test_from_config_gives_each_run_a_unique_event_id():
|
||||
config = _config()
|
||||
|
||||
def confirm(pipeline):
|
||||
pipeline.process(_packet(0.0))
|
||||
pipeline.process(_packet(0.1))
|
||||
return pipeline.process(_packet(1.1)).events[0].event_id
|
||||
|
||||
frames = [(_pose(),), (_pose(horizontal=True),), (_pose(horizontal=True),)]
|
||||
first = confirm(FallPipeline.from_config(config, _SequencePoseAdapter(list(frames))))
|
||||
second = confirm(FallPipeline.from_config(config, _SequencePoseAdapter(list(frames))))
|
||||
|
||||
assert first != second
|
||||
|
||||
@@ -46,6 +46,61 @@ def test_from_results_extracts_person_box_confidence_and_seventeen_keypoints():
|
||||
assert poses[0].keypoints[5].confidence == 0.9
|
||||
|
||||
|
||||
class _RecordingPoseModel:
|
||||
task = "pose"
|
||||
names = {0: "person"}
|
||||
|
||||
class model:
|
||||
kpt_shape = (17, 3)
|
||||
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def __call__(self, image, conf, verbose):
|
||||
self.calls.append(conf)
|
||||
|
||||
class _Empty:
|
||||
names = {0: "person"}
|
||||
|
||||
class boxes:
|
||||
xyxy = []
|
||||
conf = []
|
||||
cls = []
|
||||
|
||||
class keypoints:
|
||||
data = []
|
||||
|
||||
return _Empty()
|
||||
|
||||
|
||||
def test_set_confidence_threshold_is_forwarded_to_inference(tmp_path):
|
||||
weights = tmp_path / "pose.pt"
|
||||
weights.write_bytes(b"fake-weights")
|
||||
expected_sha256 = hashlib.sha256(weights.read_bytes()).hexdigest()
|
||||
model = _RecordingPoseModel()
|
||||
adapter = PoseAdapter(
|
||||
weights, expected_sha256, confidence_threshold=0.25, model_factory=lambda _p: model
|
||||
)
|
||||
|
||||
adapter.set_confidence_threshold(0.6)
|
||||
adapter.infer(np.zeros((10, 10, 3), dtype=np.uint8))
|
||||
|
||||
assert adapter.confidence_threshold == 0.6
|
||||
assert model.calls == [0.6]
|
||||
|
||||
|
||||
def test_set_confidence_threshold_rejects_out_of_range(tmp_path):
|
||||
weights = tmp_path / "pose.pt"
|
||||
weights.write_bytes(b"fake-weights")
|
||||
expected_sha256 = hashlib.sha256(weights.read_bytes()).hexdigest()
|
||||
adapter = PoseAdapter(
|
||||
weights, expected_sha256, model_factory=lambda _p: _RecordingPoseModel()
|
||||
)
|
||||
|
||||
with pytest.raises(ModelValidationError):
|
||||
adapter.set_confidence_threshold(1.5)
|
||||
|
||||
|
||||
def test_pose_adapter_rejects_non_pose_model_after_hash_validation(tmp_path):
|
||||
model_path = tmp_path / "model.pt"
|
||||
model_path.write_bytes(b"model bytes")
|
||||
|
||||
Reference in New Issue
Block a user