feat: 站点 JSON、语音终端 WebSocket 指派与客户端联调
- 用 OR_SITE_CONFIG_JSON_FILE 统一术间配置(video_rtsp_urls + voice_or_room_bindings) - VoiceTerminalHub:assignment、WS 推送与 HTTP 查询;开录/停录后 notify - 一键联调 orchestrate-and-start 与 /client/surgeries/start 共用指派逻辑,修复 demo 路径不发 WS - 语音桌面端:SIGINT 退出、shutdown 清理、仅 WS 指派、固定 pending 轮询间隔、界面仅保留录音时长 - 新增/调整契约与绑定测试,文档与示例配置同步 Made-with: Cursor
This commit is contained in:
84
app/services/voice_terminal_binding.py
Normal file
84
app/services/voice_terminal_binding.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""术间配置:camera_ids 集合与语音桌面终端 ID 绑定。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OrRoomBinding:
|
||||
or_room_id: str
|
||||
camera_ids: frozenset[str]
|
||||
voice_terminal_id: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class VoiceTerminalBindingIndex:
|
||||
"""由 ``or_site_config.voice_or_room_bindings`` 数组构建。"""
|
||||
|
||||
rooms: tuple[OrRoomBinding, ...]
|
||||
|
||||
def resolve_terminal(self, camera_ids: list[str]) -> str | None:
|
||||
"""精确匹配 camera 集合;否则开录路数为术间子集时匹配最小超集术间。"""
|
||||
key = frozenset(str(x).strip() for x in camera_ids if str(x).strip())
|
||||
if not key:
|
||||
return None
|
||||
for r in self.rooms:
|
||||
if r.camera_ids == key:
|
||||
return r.voice_terminal_id
|
||||
candidates = [r for r in self.rooms if key <= r.camera_ids]
|
||||
if not candidates:
|
||||
return None
|
||||
if len(candidates) == 1:
|
||||
return candidates[0].voice_terminal_id
|
||||
candidates.sort(key=lambda r: (len(r.camera_ids), r.or_room_id, r.voice_terminal_id))
|
||||
return candidates[0].voice_terminal_id
|
||||
|
||||
@staticmethod
|
||||
def from_binding_list(data: list[Any]) -> VoiceTerminalBindingIndex | None:
|
||||
rows: list[OrRoomBinding] = []
|
||||
seen_terminals: set[str] = set()
|
||||
seen_camera_sets: set[frozenset[str]] = set()
|
||||
for i, item in enumerate(data):
|
||||
if not isinstance(item, dict):
|
||||
logger.warning("voice_or_room_bindings[{}] must be an object", i)
|
||||
return None
|
||||
rid = str(item.get("or_room_id") or "").strip()
|
||||
tid = str(item.get("voice_terminal_id") or "").strip()
|
||||
cams = item.get("camera_ids")
|
||||
if not rid or not tid or not isinstance(cams, list):
|
||||
logger.warning(
|
||||
"voice_or_room_bindings[{}] missing or invalid fields", i
|
||||
)
|
||||
return None
|
||||
cam_set = frozenset(str(x).strip() for x in cams if str(x).strip())
|
||||
if not cam_set:
|
||||
logger.warning(
|
||||
"voice_or_room_bindings[{}] camera_ids must be non-empty", i
|
||||
)
|
||||
return None
|
||||
if tid in seen_terminals:
|
||||
logger.warning(
|
||||
"voice_or_room_bindings: duplicate voice_terminal_id {!r}",
|
||||
tid,
|
||||
)
|
||||
return None
|
||||
if cam_set in seen_camera_sets:
|
||||
logger.warning(
|
||||
"voice_or_room_bindings: duplicate camera_ids set for room {!r}",
|
||||
rid,
|
||||
)
|
||||
return None
|
||||
seen_terminals.add(tid)
|
||||
seen_camera_sets.add(cam_set)
|
||||
rows.append(
|
||||
OrRoomBinding(
|
||||
or_room_id=rid,
|
||||
camera_ids=cam_set,
|
||||
voice_terminal_id=tid,
|
||||
)
|
||||
)
|
||||
return VoiceTerminalBindingIndex(rooms=tuple(rows))
|
||||
Reference in New Issue
Block a user