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:
116
app/services/surgery_pipeline.py
Normal file
116
app/services/surgery_pipeline.py
Normal 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:
|
||||
"""上传医生语音 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,
|
||||
)
|
||||
Reference in New Issue
Block a user