Files
operating-room-monitor-server/app/services/surgery_pipeline.py
Kevin 04866559db feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.

Made-with: Cursor
2026-04-21 18:33:54 +08:00

117 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""手术录制与实时算法流水线(待接入真实子系统)。"""
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:
"""上传医生语音 WAVMinIO 追溯 + 百度 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,
)