117 lines
4.2 KiB
Python
117 lines
4.2 KiB
Python
|
|
"""手术录制与实时算法流水线(待接入真实子系统)。"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from app.database import AsyncSessionLocal
|
|||
|
|
from app.repositories.surgery_results import SurgeryResultRepository
|
|||
|
|
from app.schemas import (
|
|||
|
|
PendingConfirmationOption,
|
|||
|
|
SurgeryConsumptionDetail,
|
|||
|
|
SurgeryPendingConfirmationResponse,
|
|||
|
|
)
|
|||
|
|
from app.services.video.session_manager import CameraSessionManager
|
|||
|
|
from app.services.voice_resolution import VoiceConfirmationService, VoiceResolveResult
|
|||
|
|
from app.surgery_errors import SurgeryPipelineError
|
|||
|
|
|
|||
|
|
|
|||
|
|
class SurgeryPipeline:
|
|||
|
|
"""协调开录、停录与算法产出。路由仅在子系统确认后返回 HTTP 200。"""
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
sessions: CameraSessionManager,
|
|||
|
|
*,
|
|||
|
|
result_repository: SurgeryResultRepository,
|
|||
|
|
voice_confirmation: VoiceConfirmationService,
|
|||
|
|
) -> None:
|
|||
|
|
self._sessions = sessions
|
|||
|
|
self._repo = result_repository
|
|||
|
|
self._voice = voice_confirmation
|
|||
|
|
|
|||
|
|
async def start_recording(
|
|||
|
|
self,
|
|||
|
|
surgery_id: str,
|
|||
|
|
camera_ids: list[str],
|
|||
|
|
candidate_consumables: list[str],
|
|||
|
|
) -> None:
|
|||
|
|
"""启动关联摄像头录制。仅在确认已开录时返回;否则抛出 SurgeryPipelineError。"""
|
|||
|
|
try:
|
|||
|
|
await self._sessions.start_surgery(
|
|||
|
|
surgery_id,
|
|||
|
|
camera_ids,
|
|||
|
|
candidate_consumables,
|
|||
|
|
)
|
|||
|
|
except SurgeryPipelineError:
|
|||
|
|
raise
|
|||
|
|
except ValueError as exc:
|
|||
|
|
raise SurgeryPipelineError(
|
|||
|
|
"RECORDING_CANNOT_START",
|
|||
|
|
f"开录未能确认:{exc}",
|
|||
|
|
) from exc
|
|||
|
|
except RuntimeError as exc:
|
|||
|
|
raise SurgeryPipelineError(
|
|||
|
|
"RECORDING_CANNOT_START",
|
|||
|
|
f"开录未能确认:{exc}",
|
|||
|
|
) from exc
|
|||
|
|
|
|||
|
|
async def stop_recording(self, surgery_id: str) -> None:
|
|||
|
|
"""停止该手术关联的摄像头录制。仅在确认已全部停录时返回。"""
|
|||
|
|
try:
|
|||
|
|
await self._sessions.stop_surgery(surgery_id, require_active=True)
|
|||
|
|
except SurgeryPipelineError:
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
async def get_consumption_details_for_client(
|
|||
|
|
self,
|
|||
|
|
surgery_id: str,
|
|||
|
|
) -> list[SurgeryConsumptionDetail] | None:
|
|||
|
|
"""进行中:返回内存明细;已结束:返回数据库最终结果;持久化失败时回退内存归档。"""
|
|||
|
|
live = self._sessions.live_consumption_if_active(surgery_id)
|
|||
|
|
if live is not None:
|
|||
|
|
return live
|
|||
|
|
async with AsyncSessionLocal() as session:
|
|||
|
|
async with session.begin():
|
|||
|
|
persisted = await self._repo.load_final_details(session, surgery_id)
|
|||
|
|
if persisted is not None:
|
|||
|
|
return persisted
|
|||
|
|
return self._sessions.archived_consumption_fallback(surgery_id)
|
|||
|
|
|
|||
|
|
def voice_status(self, surgery_id: str) -> dict[str, object] | None:
|
|||
|
|
return self._sessions.voice_status(surgery_id)
|
|||
|
|
|
|||
|
|
def get_pending_confirmation_for_client(
|
|||
|
|
self, surgery_id: str
|
|||
|
|
) -> SurgeryPendingConfirmationResponse | None:
|
|||
|
|
pending = self._sessions.next_pending_confirmation(surgery_id)
|
|||
|
|
if pending is None:
|
|||
|
|
return None
|
|||
|
|
return SurgeryPendingConfirmationResponse(
|
|||
|
|
surgery_id=surgery_id,
|
|||
|
|
confirmation_id=pending.id,
|
|||
|
|
prompt_text=pending.prompt_text,
|
|||
|
|
options=[
|
|||
|
|
PendingConfirmationOption(label=a, confidence=b)
|
|||
|
|
for a, b in pending.options
|
|||
|
|
],
|
|||
|
|
model_top1_label=pending.model_top1_label,
|
|||
|
|
model_top1_confidence=pending.model_top1_confidence,
|
|||
|
|
created_at=pending.created_at,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
async def resolve_pending_confirmation_from_audio(
|
|||
|
|
self,
|
|||
|
|
surgery_id: str,
|
|||
|
|
confirmation_id: str,
|
|||
|
|
wav_bytes: bytes,
|
|||
|
|
filename: str,
|
|||
|
|
content_type: str | None,
|
|||
|
|
) -> VoiceResolveResult:
|
|||
|
|
"""上传医生语音 WAV:MinIO 追溯 + 百度 ASR + 解析候选项并完成确认。"""
|
|||
|
|
return await self._voice.resolve_from_wav(
|
|||
|
|
surgery_id=surgery_id,
|
|||
|
|
confirmation_id=confirmation_id,
|
|||
|
|
wav_bytes=wav_bytes,
|
|||
|
|
filename=filename,
|
|||
|
|
content_type=content_type,
|
|||
|
|
)
|