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:
56
app/services/video/rtsp_capture.py
Normal file
56
app/services/video/rtsp_capture.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@dataclass
|
||||
class RtspCapture:
|
||||
"""Thin OpenCV RTSP wrapper (blocking). Use from asyncio via to_thread."""
|
||||
|
||||
url: str
|
||||
open_timeout_sec: float
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self._cap: cv2.VideoCapture | None = None
|
||||
|
||||
def open(self) -> None:
|
||||
self._cap = cv2.VideoCapture(self.url, cv2.CAP_FFMPEG)
|
||||
if not self._cap.isOpened():
|
||||
raise RuntimeError(f"RTSP open failed (isOpened=False): {self.url!r}")
|
||||
# Reduce internal buffering where supported
|
||||
try:
|
||||
self._cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
except Exception:
|
||||
pass
|
||||
deadline = time.monotonic() + self.open_timeout_sec
|
||||
while time.monotonic() < deadline:
|
||||
ok, frame = self._cap.read()
|
||||
if ok and frame is not None:
|
||||
return
|
||||
time.sleep(0.05)
|
||||
raise TimeoutError(
|
||||
f"RTSP first frame timeout after {self.open_timeout_sec}s: {self.url!r}"
|
||||
)
|
||||
|
||||
def read(self) -> tuple[bool, np.ndarray | None]:
|
||||
if self._cap is None:
|
||||
return False, None
|
||||
return self._cap.read()
|
||||
|
||||
def release(self) -> None:
|
||||
if self._cap is not None:
|
||||
try:
|
||||
self._cap.release()
|
||||
except Exception as exc:
|
||||
logger.debug("VideoCapture.release: {}", exc)
|
||||
self._cap = None
|
||||
|
||||
@property
|
||||
def cap(self) -> Any:
|
||||
return self._cap
|
||||
Reference in New Issue
Block a user