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
This commit is contained in:
Kevin
2026-04-21 18:33:54 +08:00
parent d1a3d029ec
commit 04866559db
56 changed files with 7196 additions and 43 deletions

View File

@@ -0,0 +1,116 @@
"""手术录制与实时算法流水线(待接入真实子系统)。"""
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,
)