"""手术录制与实时算法流水线(待接入真实子系统)。""" 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 fastapi.concurrency import run_in_threadpool 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) async def list_voice_audits( self, surgery_id: str, *, limit: int = 50, offset: int = 0, ): """持久化表 `voice_confirmation_audits` 分页,用于追溯/对账/报表。""" return await self._voice.list_voice_audits_for_surgery( surgery_id, limit=limit, offset=offset ) 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, ) async def resolve_pending_confirmation_from_client_text( self, surgery_id: str, confirmation_id: str, recognized_text: str, ) -> VoiceResolveResult: """浏览器等客户端本机识别后的文本,解析规则与 WAV 路径一致(无需 MinIO/百度)。""" return await self._voice.resolve_from_recognized_text( surgery_id=surgery_id, confirmation_id=confirmation_id, recognized_text=recognized_text, ) async def get_pending_prompt_audio_mp3( self, surgery_id: str, confirmation_id: str, ) -> bytes: """待确认 `prompt_text` 的百度 TTS MP3,供模拟客户端用 Audio 直放。""" pending = self._sessions.get_pending_confirmation_by_id( surgery_id, confirmation_id ) if pending is None or pending.status != "pending": raise SurgeryPipelineError( "CONFIRMATION_NOT_FOUND", "未找到该待确认项或已处理。", ) return await run_in_threadpool( self._voice.synthesize_prompt_to_mp3, pending.prompt_text, )