Files
operating-room-monitor-server/app/services/video/classification_handler.py

212 lines
6.9 KiB
Python
Raw Normal View History

"""视觉分类结果处理:把 ``PredictionResult`` 转成自动记账 or 待人工确认。
``CameraSessionManager`` 抽出保持原先行为
- 置信度低于 ``video_voice_confirm_min_confidence`` 丢弃
- 会话状态中候选清单为空 丢弃开录时通常会由空请求解析为全量目录/模型类名
- 置信度 ``video_auto_confirm_confidence`` Top1 在候选内 自动追加 vision 明细并写消耗 TSV记具体耗材
- 置信度 自动阈值但 Top1 不在候选内 voice_confirmation_enabled pending
- 中等置信度 pending若有可展示候选项
需医生确认时消耗 TSV / 内存明细记待确认不写模型 top1 商品名语音确认后再落最终耗材并更新汇总
"""
from __future__ import annotations
from loguru import logger
from app.config import Settings
from app.services.consumable_vision_algorithm import (
PredictionCandidate,
PredictionResult,
)
from app.services.consumption_tsv_log import (
append_consumption_pending_window,
append_consumption_window,
resolve_consumption_item_id,
)
from app.services.video.inference_aggregator import WindowInferenceReady
from app.services.video.session_registry import (
SurgerySessionRegistry,
SurgerySessionState,
)
def rank_topk_for_candidates(
topk: list[PredictionCandidate],
ordered_candidates: list[str],
*,
limit: int = 5,
) -> list[PredictionCandidate]:
if not topk:
return []
stripped_order = [c.strip() for c in ordered_candidates if c.strip()]
if not stripped_order:
return topk[:limit]
order_index = {name: i for i, name in enumerate(stripped_order)}
picked = [c for c in topk if c.label.strip() in order_index]
picked.sort(key=lambda c: order_index[c.label.strip()])
return picked[:limit]
class VisionClassificationHandler:
"""把分类结果转化为 registry 上的状态变更(追加明细 / 入队待确认)。"""
def __init__(
self,
*,
settings: Settings,
registry: SurgerySessionRegistry,
) -> None:
self._s = settings
self._registry = registry
def _append_vision_consumption_window_if_ready(
self,
state: SurgerySessionState,
ready: WindowInferenceReady | None,
surgery_id: str,
camera_id: str,
) -> None:
if (
ready is None
or not surgery_id
or not camera_id
or (
not self._s.consumption_tsv_log_enabled
and not self._s.consumption_log_markdown_terminal
)
):
return
append_consumption_window(
surgery_id=surgery_id,
name_to_code=state.name_to_code,
best=ready.best,
doctor_id=self._s.video_result_doctor_id,
camera_id=camera_id,
wall_start_epoch=ready.wall_lo,
wall_end_epoch=ready.wall_hi,
running_totals=state.consumption_log_totals,
)
async def handle(
self,
*,
state: SurgerySessionState,
cls_res: PredictionResult,
ready: WindowInferenceReady | None = None,
surgery_id: str = "",
camera_id: str = "",
) -> None:
conf = cls_res.confidence
label = (cls_res.label or "").strip()
item_id = resolve_consumption_item_id(label, "", state.name_to_code)
voice_floor = self._s.video_voice_confirm_min_confidence
if conf < voice_floor:
return
cand_order = [c.strip() for c in state.candidate_consumables if c.strip()]
if not cand_order:
return
cand_set = set(cand_order)
ranked = rank_topk_for_candidates(cls_res.topk, cand_order)
auto_th = self._s.video_auto_confirm_confidence
def in_allowed(name: str) -> bool:
return name in cand_set
if conf >= auto_th and in_allowed(label):
self._append_vision_consumption_window_if_ready(
state, ready, surgery_id, camera_id
)
await self._registry.append_confirmed_detail(
state=state,
item_id=item_id or "unknown",
item_name=label or "unknown",
doctor_id=self._s.video_result_doctor_id,
source="vision",
)
return
if conf >= auto_th and not in_allowed(label):
if ranked and self._s.voice_confirmation_enabled:
await self._enqueue(
state,
ranked,
label,
conf,
ready=ready,
surgery_id=surgery_id,
camera_id=camera_id,
)
return
if not self._s.voice_confirmation_enabled:
return
if ranked:
await self._enqueue(
state,
ranked,
label,
conf,
ready=ready,
surgery_id=surgery_id,
camera_id=camera_id,
)
elif in_allowed(label):
await self._enqueue(
state,
[PredictionCandidate(label=label, confidence=conf)],
label,
conf,
ready=ready,
surgery_id=surgery_id,
camera_id=camera_id,
)
async def _enqueue(
self,
state: SurgerySessionState,
ranked: list[PredictionCandidate],
top_key: str,
top_confidence: float,
*,
ready: WindowInferenceReady | None = None,
surgery_id: str = "",
camera_id: str = "",
) -> None:
cid = await self._registry.enqueue_pending_confirmation(
state,
ranked,
top_key=top_key,
top_confidence=top_confidence,
)
if cid is None:
return
logger.info(
"Enqueued pending consumable confirmation id={} top_key={}",
cid,
top_key,
)
if ready is not None and surgery_id and camera_id and (
self._s.consumption_tsv_log_enabled
or self._s.consumption_log_markdown_terminal
):
append_consumption_pending_window(
surgery_id=surgery_id,
confirmation_id=cid,
model_snap=ready.best,
doctor_id=self._s.video_result_doctor_id,
camera_id=camera_id,
wall_start_epoch=ready.wall_lo,
wall_end_epoch=ready.wall_hi,
tsv_enabled=self._s.consumption_tsv_log_enabled,
markdown_terminal=self._s.consumption_log_markdown_terminal,
)
await self._registry.append_pending_consumption_detail(
state=state,
confirmation_id=cid,
doctor_id=self._s.video_result_doctor_id,
)