- 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
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,
|
||
)
|