update cam id

This commit is contained in:
op
2026-05-25 16:39:08 +08:00
parent caf62f232c
commit 5fd0851a44
9 changed files with 214 additions and 13 deletions

View File

@@ -141,10 +141,21 @@ async def health() -> HealthResponse | JSONResponse:
summary="Demo 离线 batch链路 3是否可用",
description="供 demo 页探测;始终注册,不依赖 DEMO_ORCHESTRATOR_ENABLED。",
)
async def recording_modes_status() -> dict:
async def recording_modes_status(
surgery_id: Annotated[
str | None,
Query(
min_length=6,
max_length=6,
pattern=r"^\d{6}$",
description="可选:查询该手术号是否仍有链路 1 实时开录占用。",
),
] = None,
pipeline: Annotated[SurgeryPipeline, Depends(get_surgery_pipeline)] = ...,
) -> dict:
f = (settings.or_site_config_json_file or "").strip()
enabled = bool(settings.demo_orchestrator_enabled)
return {
body: dict = {
"demo_recording_modes_enabled": enabled,
"orchestrator_enabled": enabled,
"offline_batch_method": "POST",
@@ -167,6 +178,12 @@ async def recording_modes_status() -> dict:
"rtsp_segment_ttl_hours": float(settings.rtsp_segment_ttl_hours),
"video_batch_vis_ttl_hours": float(bp.VIDEO_BATCH_VIS_TTL_HOURS),
}
if surgery_id is not None:
phase = pipeline.live_recording_phase(surgery_id)
body["surgery_id"] = surgery_id
body["live_recording_phase"] = phase
body["live_recording_active"] = phase is not None
return body
class HlsPreviewEnsureRequest(BaseModel):

View File

@@ -1,8 +1,8 @@
{
"video_rtsp_urls": {
"or-cam-01": "rtsp://admin:Aa183137@192.168.3.2:554/Streaming/Channels/101",
"or-cam-02": "rtsp://admin:Aa183137@192.168.3.3:554/Streaming/Channels/101",
"or-cam-03": "rtsp://admin:Aa183137@192.168.3.4:554/Streaming/Channels/101",
"or-cam-03": "rtsp://admin:Aa183137@192.168.3.3:554/Streaming/Channels/101",
"or-cam-02": "rtsp://admin:Aa183137@192.168.3.4:554/Streaming/Channels/101",
"or-cam-04": "rtsp://admin:Aa183137@192.168.3.5:554/Streaming/Channels/101"
},
"voice_or_room_bindings": [

View File

@@ -148,6 +148,21 @@ async def offline_batch(
)
candidates = normalize_candidate_consumables_raw(candidates)
live_phase = pipeline.live_recording_phase(surgery_id)
if live_phase is not None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={
"code": "LIVE_RECORDING_ACTIVE",
"message": (
"该手术号仍有链路 1 实时开录RTSP 切片)进行中,与离线精确互斥。"
"请先 POST /client/surgeries/end 结束实时会话,或更换 surgery_id 后再上传。"
),
"surgery_id": surgery_id,
"live_recording_phase": live_phase,
},
)
raw = await video1.read()
if not raw:
raise HTTPException(

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import base64
from typing import Literal
from sqlalchemy.ext.asyncio import async_sessionmaker
@@ -84,6 +85,10 @@ class SurgeryPipeline:
except SurgeryPipelineError:
raise
def live_recording_phase(self, surgery_id: str) -> Literal["starting", "recording"] | None:
"""链路 1 实时会话是否占用该 surgery_idstarting=开录中recording=已开录)。"""
return self._sessions.active_recording_phase(surgery_id)
async def save_video_batch_result(
self,
surgery_id: str,

View File

@@ -626,6 +626,10 @@ def test_demo_video_batch_endpoint_writes_queryable_result(
def __init__(self) -> None:
self.rows: dict[str, list[SurgeryConsumptionStored]] = {}
def live_recording_phase(self, surgery_id: str) -> None:
del surgery_id
return None
async def save_video_batch_result(
self,
surgery_id: str,
@@ -729,6 +733,10 @@ def test_demo_video_batch_endpoint_stages_vis_and_purges_cache_when_requested(
vis_calls.append((surgery_id, video_path, result_path))
class _FakePipeline:
def live_recording_phase(self, surgery_id: str) -> None:
del surgery_id
return None
async def save_video_batch_result(
self,
surgery_id: str,

View File

@@ -1,14 +1,30 @@
"""GET /internal/demo/recording-modes-status 契约。"""
from __future__ import annotations
from typing import Literal
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.api import router as api_router
from app.dependencies import get_surgery_pipeline
from app.routers import recording_demo
class _FakePipeline:
def __init__(self, *, phase: Literal["starting", "recording"] | None = None) -> None:
self._phase = phase
def live_recording_phase(self, surgery_id: str) -> Literal["starting", "recording"] | None:
del surgery_id
return self._phase
def test_recording_modes_status_paths() -> None:
app = FastAPI()
app.include_router(api_router)
app.dependency_overrides[get_surgery_pipeline] = lambda: _FakePipeline()
client = TestClient(app)
res = client.get("/internal/demo/recording-modes-status")
assert res.status_code == 200
@@ -19,3 +35,34 @@ def test_recording_modes_status_paths() -> None:
assert body["rtsp_segment_ttl_hours"] == 24.0
assert body["rtsp_segment_duration_sec"] == 120.0
assert body["video_batch_vis_ttl_hours"] == 24.0
assert "live_recording_active" not in body
def test_recording_modes_status_with_surgery_id() -> None:
app = FastAPI()
app.include_router(api_router)
app.dependency_overrides[get_surgery_pipeline] = lambda: _FakePipeline(phase="recording")
client = TestClient(app)
res = client.get("/internal/demo/recording-modes-status?surgery_id=100101")
assert res.status_code == 200
body = res.json()
assert body["surgery_id"] == "100101"
assert body["live_recording_phase"] == "recording"
assert body["live_recording_active"] is True
def test_offline_batch_rejects_active_live_session(monkeypatch) -> None:
monkeypatch.setattr(recording_demo.settings, "demo_orchestrator_enabled", True)
app = FastAPI()
app.include_router(recording_demo.router)
app.dependency_overrides[get_surgery_pipeline] = lambda: _FakePipeline(phase="recording")
client = TestClient(app)
res = client.post(
"/internal/demo/offline-batch",
data={"surgery_id": "100101", "candidate_consumables_json": "[]"},
files={"video1": ("case.mp4", b"video-bytes", "video/mp4")},
)
assert res.status_code == 409, res.text
assert res.json()["detail"]["code"] == "LIVE_RECORDING_ACTIVE"