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:
103
app/services/video/backend_resolver.py
Normal file
103
app/services/video/backend_resolver.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from app.config import Settings
|
||||
from app.services.video.hikvision_runtime import HikvisionRuntime
|
||||
from app.services.video.types import VideoBackendKind
|
||||
|
||||
|
||||
class BackendResolver:
|
||||
"""Resolve per-camera backend (RTSP vs Hikvision SDK) and RTSP URL."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
settings: Settings,
|
||||
*,
|
||||
hikvision_runtime: HikvisionRuntime | None,
|
||||
) -> None:
|
||||
self._s = settings
|
||||
self._hik = hikvision_runtime
|
||||
self._rtsp_urls_map = settings.video_rtsp_url_map()
|
||||
|
||||
def _parse_json_object(self, raw: str) -> dict[str, Any]:
|
||||
raw = (raw or "").strip()
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid JSON mapping: {exc}") from exc
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("JSON mapping must be an object")
|
||||
return {str(k): v for k, v in data.items()}
|
||||
|
||||
def backend_for_camera(self, camera_id: str) -> VideoBackendKind:
|
||||
overrides = self._parse_json_object(self._s.video_camera_backend_overrides_json)
|
||||
if camera_id in overrides:
|
||||
v = str(overrides[camera_id]).lower()
|
||||
if v in ("rtsp", "hikvision_sdk", "sdk"):
|
||||
return (
|
||||
VideoBackendKind.HIKVISION_SDK
|
||||
if v in ("hikvision_sdk", "sdk")
|
||||
else VideoBackendKind.RTSP
|
||||
)
|
||||
default = self._s.video_default_backend.strip().lower()
|
||||
if default in ("auto", ""):
|
||||
if self._hik is not None and self._s.hikvision_sdk_enabled:
|
||||
return VideoBackendKind.HIKVISION_SDK
|
||||
return VideoBackendKind.RTSP
|
||||
if default in ("hikvision_sdk", "sdk"):
|
||||
return VideoBackendKind.HIKVISION_SDK
|
||||
return VideoBackendKind.RTSP
|
||||
|
||||
def rtsp_url_for_camera(self, camera_id: str) -> str:
|
||||
if camera_id in self._rtsp_urls_map:
|
||||
return self._rtsp_urls_map[camera_id]
|
||||
tpl = (self._s.video_rtsp_url_template or "").strip()
|
||||
if tpl:
|
||||
try:
|
||||
return tpl.format(camera_id=camera_id)
|
||||
except KeyError as exc:
|
||||
raise ValueError(
|
||||
f"video_rtsp_url_template missing placeholder: {exc}"
|
||||
) from exc
|
||||
raise ValueError(
|
||||
f"No RTSP URL for camera_id={camera_id!r}: set VIDEO_RTSP_URLS_JSON_FILE, "
|
||||
f"VIDEO_RTSP_URLS_JSON, or VIDEO_RTSP_URL_TEMPLATE"
|
||||
)
|
||||
|
||||
def rtsp_url_after_hikvision_login(self, camera_id: str) -> str:
|
||||
"""RTSP URL used after SDK login (often same as device preview URL)."""
|
||||
urls = self._parse_json_object(self._s.hikvision_camera_rtsp_urls_json)
|
||||
if camera_id in urls:
|
||||
return str(urls[camera_id])
|
||||
tpl = (self._s.hikvision_preview_rtsp_template or "").strip()
|
||||
if not tpl:
|
||||
logger.warning(
|
||||
"Hikvision backend without HIKVISION_PREVIEW_RTSP_TEMPLATE / "
|
||||
"HIKVISION_CAMERA_RTSP_URLS_JSON — falling back to generic RTSP map"
|
||||
)
|
||||
return self.rtsp_url_for_camera(camera_id)
|
||||
return self._format_hikvision_rtsp(tpl, camera_id)
|
||||
|
||||
def _format_hikvision_rtsp(self, template: str, camera_id: str) -> str:
|
||||
ip = self._s.hikvision_device_ip.strip()
|
||||
user = self._s.hikvision_user.strip()
|
||||
password = self._s.hikvision_password.strip()
|
||||
channel = self._s.hikvision_channel
|
||||
try:
|
||||
return template.format(
|
||||
camera_id=camera_id,
|
||||
ip=ip,
|
||||
user=user,
|
||||
password=password,
|
||||
channel=channel,
|
||||
)
|
||||
except KeyError as exc:
|
||||
raise ValueError(
|
||||
f"hikvision_preview_rtsp_template missing key: {exc}"
|
||||
) from exc
|
||||
Reference in New Issue
Block a user