Files
operating-room-monitor-server/app/services/surgery_pipeline.py
Kevin 0c05463617 feat: 语音确认、联调与运维增强
- 语音:序数解析(第一个/第二个等)、解析失败计数与 API detail.retry_remaining;
  百度 ASR 固定 dev_pid 为普通话;SurgeryPipelineError 支持 extra 并入 HTTP detail。
- Demo:demo 路由与假 RTSP、客户端 index 与 README;BackendResolver 与配置调整。
- 可观测:消耗 TSV 日志、语音文件日志、终端 Markdown 辅助;相关测试与依赖更新。
- 注意:.env 仍被 gitignore,本地密钥不会进入本提交。

Made-with: Cursor
2026-04-23 14:24:20 +08:00

163 lines
5.8 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 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:
"""上传医生语音 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,
)
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,
)