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:
90
app/services/minio_audio_storage.py
Normal file
90
app/services/minio_audio_storage.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Upload voice confirmation WAV objects to MinIO (S3-compatible)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
from dataclasses import dataclass
|
||||
|
||||
from loguru import logger
|
||||
from minio import Minio
|
||||
from minio.error import S3Error
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StoredAudio:
|
||||
object_key: str
|
||||
sha256_hex: str
|
||||
size_bytes: int
|
||||
|
||||
|
||||
class MinioAudioStorageService:
|
||||
"""Stores raw doctor voice WAV for audit trail."""
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self._s = settings
|
||||
self._client: Minio | None = None
|
||||
if settings.minio_configured:
|
||||
endpoint = settings.minio_endpoint.strip()
|
||||
self._client = Minio(
|
||||
endpoint,
|
||||
access_key=settings.minio_access_key.strip(),
|
||||
secret_key=settings.minio_secret_key.strip(),
|
||||
secure=bool(settings.minio_secure),
|
||||
region=(settings.minio_region or "").strip() or None,
|
||||
)
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return self._client is not None
|
||||
|
||||
def ensure_bucket(self) -> None:
|
||||
if self._client is None:
|
||||
return
|
||||
name = self._s.minio_bucket.strip()
|
||||
try:
|
||||
if not self._client.bucket_exists(name):
|
||||
self._client.make_bucket(name)
|
||||
logger.info("MinIO bucket created: {}", name)
|
||||
except S3Error as exc:
|
||||
logger.warning("MinIO ensure_bucket failed: {}", exc)
|
||||
raise
|
||||
|
||||
def upload_voice_wav(
|
||||
self,
|
||||
*,
|
||||
surgery_id: str,
|
||||
confirmation_id: str,
|
||||
data: bytes,
|
||||
content_type: str | None,
|
||||
) -> StoredAudio:
|
||||
if self._client is None:
|
||||
raise RuntimeError("MinIO is not configured")
|
||||
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
suffix = digest[:12]
|
||||
object_key = (
|
||||
f"surgeries/{surgery_id}/confirmations/{confirmation_id}/{suffix}.wav"
|
||||
)
|
||||
bucket = self._s.minio_bucket.strip()
|
||||
ct = content_type or "audio/wav"
|
||||
stream = io.BytesIO(data)
|
||||
try:
|
||||
self._client.put_object(
|
||||
bucket,
|
||||
object_key,
|
||||
stream,
|
||||
length=len(data),
|
||||
content_type=ct,
|
||||
)
|
||||
except S3Error as exc:
|
||||
logger.warning("MinIO put_object failed: {}", exc)
|
||||
raise
|
||||
|
||||
return StoredAudio(
|
||||
object_key=object_key,
|
||||
sha256_hex=digest,
|
||||
size_bytes=len(data),
|
||||
)
|
||||
Reference in New Issue
Block a user