102 lines
3.6 KiB
Python
102 lines
3.6 KiB
Python
|
|
import pytest
|
|||
|
|
|
|||
|
|
from app.algorithm_ipc.schema import WhitelistSpec
|
|||
|
|
from app.algorithm_runner.actionformer_gated.runner import ActionFormerSegmentRecord
|
|||
|
|
from app.algorithm_runner.segment_policy import (
|
|||
|
|
events_for_tear_record,
|
|||
|
|
pad_ranked_candidates_for_voice,
|
|||
|
|
rank_topk_for_candidates,
|
|||
|
|
)
|
|||
|
|
from app.domain.vision_prediction import PredictionCandidate
|
|||
|
|
from app.services.voice_confirm import build_prompt_text
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_rank_preserves_model_order_within_whitelist() -> None:
|
|||
|
|
"""清单顺序可能与模型置信度顺序不同;播报「第n个」应与模型 top1/top2 一致。"""
|
|||
|
|
topk = [
|
|||
|
|
PredictionCandidate(label="缝线", confidence=0.9),
|
|||
|
|
PredictionCandidate(label="纱布", confidence=0.5),
|
|||
|
|
]
|
|||
|
|
ordered = ["纱布", "缝线"]
|
|||
|
|
ranked = rank_topk_for_candidates(topk, ordered)
|
|||
|
|
assert [c.label for c in ranked] == ["缝线", "纱布"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_rank_without_candidates_keeps_model_order() -> None:
|
|||
|
|
topk = [
|
|||
|
|
PredictionCandidate(label="a", confidence=0.9),
|
|||
|
|
PredictionCandidate(label="b", confidence=0.5),
|
|||
|
|
]
|
|||
|
|
ranked = rank_topk_for_candidates(topk, [])
|
|||
|
|
assert [c.label for c in ranked] == ["a", "b"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_rank_drops_labels_not_on_surgery_whitelist() -> None:
|
|||
|
|
topk = [
|
|||
|
|
PredictionCandidate(label="缝线", confidence=0.9),
|
|||
|
|
PredictionCandidate(label="陌生类", confidence=0.5),
|
|||
|
|
PredictionCandidate(label="纱布", confidence=0.1),
|
|||
|
|
]
|
|||
|
|
ordered = ["纱布", "缝线"]
|
|||
|
|
ranked = rank_topk_for_candidates(topk, ordered)
|
|||
|
|
assert [c.label for c in ranked] == ["缝线", "纱布"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_pad_voice_options_fills_from_surgery_list() -> None:
|
|||
|
|
ranked = [PredictionCandidate(label="手套", confidence=0.88)]
|
|||
|
|
order = ["手套", "纱布", "缝线", "钳子"]
|
|||
|
|
out = pad_ranked_candidates_for_voice(ranked, order)
|
|||
|
|
assert [c.label for c in out] == ["手套", "纱布", "缝线"]
|
|||
|
|
assert out[0].confidence == pytest.approx(0.88)
|
|||
|
|
assert out[1].confidence == 0.0 and out[2].confidence == 0.0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_pad_voice_respects_max_options() -> None:
|
|||
|
|
out = pad_ranked_candidates_for_voice([], ["a", "b", "c"], max_options=2)
|
|||
|
|
assert [c.label for c in out] == ["a", "b"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_events_for_tear_record_voice_has_three_options(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
|
|
from app.baked import pipeline as p
|
|||
|
|
|
|||
|
|
monkeypatch.setattr(p, "VOICE_CONFIRMATION_ENABLED", True, raising=False)
|
|||
|
|
monkeypatch.setattr(p, "VIDEO_AUTO_CONFIRM_CONFIDENCE", 0.99, raising=False)
|
|||
|
|
monkeypatch.setattr(p, "VIDEO_VOICE_CONFIRM_MIN_CONFIDENCE", 0.1, raising=False)
|
|||
|
|
|
|||
|
|
wl = WhitelistSpec(
|
|||
|
|
candidate_consumables=["手套", "纱布", "缝线"],
|
|||
|
|
name_to_id={"手套": "g1", "纱布": "g2", "缝线": "g3"},
|
|||
|
|
)
|
|||
|
|
rec = ActionFormerSegmentRecord(
|
|||
|
|
segment_index=1,
|
|||
|
|
start_sec=0.0,
|
|||
|
|
end_sec=2.0,
|
|||
|
|
mid_stream_sec=1.0,
|
|||
|
|
item_id="g1",
|
|||
|
|
item_name="手套",
|
|||
|
|
top1_conf=0.55,
|
|||
|
|
top2_name="",
|
|||
|
|
top2_conf=0.0,
|
|||
|
|
top3_name="",
|
|||
|
|
top3_conf=0.0,
|
|||
|
|
majority_ref="",
|
|||
|
|
)
|
|||
|
|
evs = events_for_tear_record(
|
|||
|
|
rec,
|
|||
|
|
wl=wl,
|
|||
|
|
camera_id="cam",
|
|||
|
|
wall_lo=100.0,
|
|||
|
|
wall_hi=102.0,
|
|||
|
|
cooldown_key="k",
|
|||
|
|
)
|
|||
|
|
assert len(evs) == 1
|
|||
|
|
assert evs[0]["type"] == "needs_voice_confirm"
|
|||
|
|
opts = evs[0]["options"]
|
|||
|
|
assert len(opts) == 3
|
|||
|
|
assert [o["label"] for o in opts] == ["手套", "纱布", "缝线"]
|
|||
|
|
text = build_prompt_text([(str(o["label"]), float(o["confidence"])) for o in opts])
|
|||
|
|
assert "第1个,手套。" in text
|
|||
|
|
assert "第2个,纱布。" in text
|
|||
|
|
assert "第3个,缝线。" in text
|
|||
|
|
|