update cam id
This commit is contained in:
@@ -141,10 +141,21 @@ async def health() -> HealthResponse | JSONResponse:
|
|||||||
summary="Demo 离线 batch(链路 3)是否可用",
|
summary="Demo 离线 batch(链路 3)是否可用",
|
||||||
description="供 demo 页探测;始终注册,不依赖 DEMO_ORCHESTRATOR_ENABLED。",
|
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()
|
f = (settings.or_site_config_json_file or "").strip()
|
||||||
enabled = bool(settings.demo_orchestrator_enabled)
|
enabled = bool(settings.demo_orchestrator_enabled)
|
||||||
return {
|
body: dict = {
|
||||||
"demo_recording_modes_enabled": enabled,
|
"demo_recording_modes_enabled": enabled,
|
||||||
"orchestrator_enabled": enabled,
|
"orchestrator_enabled": enabled,
|
||||||
"offline_batch_method": "POST",
|
"offline_batch_method": "POST",
|
||||||
@@ -167,6 +178,12 @@ async def recording_modes_status() -> dict:
|
|||||||
"rtsp_segment_ttl_hours": float(settings.rtsp_segment_ttl_hours),
|
"rtsp_segment_ttl_hours": float(settings.rtsp_segment_ttl_hours),
|
||||||
"video_batch_vis_ttl_hours": float(bp.VIDEO_BATCH_VIS_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):
|
class HlsPreviewEnsureRequest(BaseModel):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"video_rtsp_urls": {
|
"video_rtsp_urls": {
|
||||||
"or-cam-01": "rtsp://admin:Aa183137@192.168.3.2:554/Streaming/Channels/101",
|
"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.3:554/Streaming/Channels/101",
|
||||||
"or-cam-03": "rtsp://admin:Aa183137@192.168.3.4: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"
|
"or-cam-04": "rtsp://admin:Aa183137@192.168.3.5:554/Streaming/Channels/101"
|
||||||
},
|
},
|
||||||
"voice_or_room_bindings": [
|
"voice_or_room_bindings": [
|
||||||
|
|||||||
@@ -148,6 +148,21 @@ async def offline_batch(
|
|||||||
)
|
)
|
||||||
candidates = normalize_candidate_consumables_raw(candidates)
|
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()
|
raw = await video1.read()
|
||||||
if not raw:
|
if not raw:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||||
|
|
||||||
@@ -84,6 +85,10 @@ class SurgeryPipeline:
|
|||||||
except SurgeryPipelineError:
|
except SurgeryPipelineError:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def live_recording_phase(self, surgery_id: str) -> Literal["starting", "recording"] | None:
|
||||||
|
"""链路 1 实时会话是否占用该 surgery_id(starting=开录中,recording=已开录)。"""
|
||||||
|
return self._sessions.active_recording_phase(surgery_id)
|
||||||
|
|
||||||
async def save_video_batch_result(
|
async def save_video_batch_result(
|
||||||
self,
|
self,
|
||||||
surgery_id: str,
|
surgery_id: str,
|
||||||
|
|||||||
@@ -626,6 +626,10 @@ def test_demo_video_batch_endpoint_writes_queryable_result(
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.rows: dict[str, list[SurgeryConsumptionStored]] = {}
|
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(
|
async def save_video_batch_result(
|
||||||
self,
|
self,
|
||||||
surgery_id: str,
|
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))
|
vis_calls.append((surgery_id, video_path, result_path))
|
||||||
|
|
||||||
class _FakePipeline:
|
class _FakePipeline:
|
||||||
|
def live_recording_phase(self, surgery_id: str) -> None:
|
||||||
|
del surgery_id
|
||||||
|
return None
|
||||||
|
|
||||||
async def save_video_batch_result(
|
async def save_video_batch_result(
|
||||||
self,
|
self,
|
||||||
surgery_id: str,
|
surgery_id: str,
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
"""GET /internal/demo/recording-modes-status 契约。"""
|
"""GET /internal/demo/recording-modes-status 契约。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app.api import router as api_router
|
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:
|
def test_recording_modes_status_paths() -> None:
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.include_router(api_router)
|
app.include_router(api_router)
|
||||||
|
app.dependency_overrides[get_surgery_pipeline] = lambda: _FakePipeline()
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
res = client.get("/internal/demo/recording-modes-status")
|
res = client.get("/internal/demo/recording-modes-status")
|
||||||
assert res.status_code == 200
|
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_ttl_hours"] == 24.0
|
||||||
assert body["rtsp_segment_duration_sec"] == 120.0
|
assert body["rtsp_segment_duration_sec"] == 120.0
|
||||||
assert body["video_batch_vis_ttl_hours"] == 24.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"
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
## 界面说明
|
## 界面说明
|
||||||
|
|
||||||
- **链路 1**:只需填 `camera_ids` 与耗材候选,点「开始手术」;**无需上传视频**(服务端自动 RTSP 拉流切片)。
|
- **链路 1**:只需填 `camera_ids` 与耗材候选,点「开始手术」;**无需上传视频**(服务端自动 RTSP 拉流切片)。
|
||||||
- **链路 1 / 3 共用「步骤 2 · 耗材候选」**:标签区显示「编号 + 名称」;提交时以 `{"消耗品编号","名称"}` 对象数组发送(与医院导出表一致)。
|
- **链路 3**:独立 MP4 上传区;与链路 1 **互斥**——同一手术号若仍有实时开录,离线上传会被拒绝;页面会提示先「结束手术」或换号。
|
||||||
- 误把 `.mp4` 路径填进耗材候选时,页面会提示并阻止提交。
|
- **模式记忆**:上次选择的链路会保存在浏览器 localStorage;默认首次打开为链路 3。
|
||||||
|
|
||||||
详见 [`docs/video-backends.md`](../../docs/video-backends.md)。
|
详见 [`docs/video-backends.md`](../../docs/video-backends.md)。
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
let videoVisToken = 0;
|
let videoVisToken = 0;
|
||||||
const hlsPlayers = {};
|
const hlsPlayers = {};
|
||||||
let serverRecordingConfig = null;
|
let serverRecordingConfig = null;
|
||||||
|
let liveRecordingConflict = null;
|
||||||
|
|
||||||
|
const LS_RUN_MODE = "orm_demo_run_mode";
|
||||||
|
const VALID_RUN_MODES = new Set(["live-rtsp", "offline-batch"]);
|
||||||
|
|
||||||
const baseUrl = () => $("base-url").value.trim().replace(/\/+$/, "");
|
const baseUrl = () => $("base-url").value.trim().replace(/\/+$/, "");
|
||||||
const surgeryId = () => $("surgery-id").value.trim();
|
const surgeryId = () => $("surgery-id").value.trim();
|
||||||
@@ -51,6 +55,76 @@
|
|||||||
return activeMode;
|
return activeMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadSavedRunMode() {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(LS_RUN_MODE);
|
||||||
|
if (saved && VALID_RUN_MODES.has(saved)) return saved;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return "offline-batch";
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistRunMode(mode) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LS_RUN_MODE, mode);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function liveRecordingConflictMessage(body) {
|
||||||
|
const phase = body?.live_recording_phase;
|
||||||
|
const phaseText = phase === "starting" ? "开录启动中" : phase === "recording" ? "录制中" : "进行中";
|
||||||
|
return (
|
||||||
|
`手术号 ${body?.surgery_id || surgeryId()} 仍有链路 1 实时会话(${phaseText},会 RTSP 切片)。` +
|
||||||
|
"请先点「结束手术」或更换手术号,再跑离线精确。"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLiveRecordingConflictBanner() {
|
||||||
|
const el = $("live-recording-conflict");
|
||||||
|
if (!el) return;
|
||||||
|
if (activeMode !== "offline-batch" || !liveRecordingConflict?.live_recording_active) {
|
||||||
|
el.textContent = "";
|
||||||
|
el.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.textContent = liveRecordingConflictMessage(liveRecordingConflict);
|
||||||
|
el.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshLiveRecordingConflict(sid) {
|
||||||
|
if (!sid || !/^\d{6}$/.test(sid)) {
|
||||||
|
liveRecordingConflict = null;
|
||||||
|
updateLiveRecordingConflictBanner();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const url = baseUrl() + `/internal/demo/recording-modes-status?surgery_id=${encodeURIComponent(sid)}`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(url);
|
||||||
|
const data = await res.json();
|
||||||
|
addLog("GET", url, res.status, data, { error: !res.ok });
|
||||||
|
if (res.ok) {
|
||||||
|
liveRecordingConflict = data;
|
||||||
|
updateLiveRecordingConflictBanner();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
addLog("GET", url, "NETWORK", String(e), { error: true });
|
||||||
|
}
|
||||||
|
liveRecordingConflict = null;
|
||||||
|
updateLiveRecordingConflictBanner();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureOfflineNotBlockedByLiveSession(sid) {
|
||||||
|
const status = await refreshLiveRecordingConflict(sid);
|
||||||
|
if (status?.live_recording_active) {
|
||||||
|
throw new Error(liveRecordingConflictMessage(status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showBanner(msg, type) {
|
function showBanner(msg, type) {
|
||||||
const el = $("banner");
|
const el = $("banner");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -536,7 +610,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setActiveMode(mode) {
|
function setActiveMode(mode) {
|
||||||
|
if (!VALID_RUN_MODES.has(mode)) mode = "offline-batch";
|
||||||
activeMode = mode;
|
activeMode = mode;
|
||||||
|
persistRunMode(mode);
|
||||||
document.querySelectorAll(".mode-card").forEach((card) => {
|
document.querySelectorAll(".mode-card").forEach((card) => {
|
||||||
card.classList.toggle("active", card.dataset.mode === mode);
|
card.classList.toggle("active", card.dataset.mode === mode);
|
||||||
});
|
});
|
||||||
@@ -555,7 +631,11 @@
|
|||||||
$("pill-mode").textContent =
|
$("pill-mode").textContent =
|
||||||
mode === "live-rtsp" ? "链路 1 · 真摄像头" : "链路 3 · 离线精确";
|
mode === "live-rtsp" ? "链路 1 · 真摄像头" : "链路 3 · 离线精确";
|
||||||
refreshModeHints();
|
refreshModeHints();
|
||||||
if (mode !== "offline-batch") hideVideoBatchVisualization();
|
if (mode !== "offline-batch") {
|
||||||
|
hideVideoBatchVisualization();
|
||||||
|
liveRecordingConflict = null;
|
||||||
|
updateLiveRecordingConflictBanner();
|
||||||
|
}
|
||||||
refreshRecordingModesStatus();
|
refreshRecordingModesStatus();
|
||||||
if (mode === "live-rtsp") {
|
if (mode === "live-rtsp") {
|
||||||
rebuildPreviewGrid();
|
rebuildPreviewGrid();
|
||||||
@@ -770,13 +850,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshRecordingModesStatus() {
|
async function refreshRecordingModesStatus() {
|
||||||
const url = baseUrl() + "/internal/demo/recording-modes-status";
|
const sid = surgeryId();
|
||||||
|
const query =
|
||||||
|
activeMode === "offline-batch" && /^\d{6}$/.test(sid)
|
||||||
|
? `?surgery_id=${encodeURIComponent(sid)}`
|
||||||
|
: "";
|
||||||
|
const url = baseUrl() + "/internal/demo/recording-modes-status" + query;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
addLog("GET", url, res.status, data, { error: !res.ok });
|
addLog("GET", url, res.status, data, { error: !res.ok });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
applyRecordingConfigHints(data);
|
applyRecordingConfigHints(data);
|
||||||
|
if (data.surgery_id && Object.prototype.hasOwnProperty.call(data, "live_recording_active")) {
|
||||||
|
liveRecordingConflict = data;
|
||||||
|
updateLiveRecordingConflictBanner();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updateDemoModesPill(data, res.ok);
|
updateDemoModesPill(data, res.ok);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1054,6 +1143,12 @@
|
|||||||
showBanner("请先选择完整 MP4 文件", "err");
|
showBanner("请先选择完整 MP4 文件", "err");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await ensureOfflineNotBlockedByLiveSession(sid);
|
||||||
|
} catch (e) {
|
||||||
|
showBanner(e.message || String(e), "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("surgery_id", sid);
|
fd.append("surgery_id", sid);
|
||||||
fd.append("video1", f, f.name);
|
fd.append("video1", f, f.name);
|
||||||
@@ -1064,7 +1159,14 @@
|
|||||||
);
|
);
|
||||||
const { res, body } = await apiOfflineBatch(fd);
|
const { res, body } = await apiOfflineBatch(fd);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
showBanner("离线处理失败:" + formatDetail(body), "err");
|
const liveConflict = body?.detail?.code === "LIVE_RECORDING_ACTIVE";
|
||||||
|
showBanner(
|
||||||
|
liveConflict
|
||||||
|
? liveRecordingConflictMessage(body.detail)
|
||||||
|
: "离线处理失败:" + formatDetail(body),
|
||||||
|
"err",
|
||||||
|
);
|
||||||
|
if (liveConflict) void refreshLiveRecordingConflict(sid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastVideoBatchDoctorDisplay = body?.doctor_display || "";
|
lastVideoBatchDoctorDisplay = body?.doctor_display || "";
|
||||||
@@ -1194,7 +1296,7 @@
|
|||||||
document.querySelectorAll(".mode-card").forEach((card) => {
|
document.querySelectorAll(".mode-card").forEach((card) => {
|
||||||
card.addEventListener("click", () => setActiveMode(card.dataset.mode));
|
card.addEventListener("click", () => setActiveMode(card.dataset.mode));
|
||||||
});
|
});
|
||||||
setActiveMode("live-rtsp");
|
setActiveMode(loadSavedRunMode());
|
||||||
}
|
}
|
||||||
|
|
||||||
function initOfflineUpload() {
|
function initOfflineUpload() {
|
||||||
@@ -1256,6 +1358,12 @@
|
|||||||
if (logEl) logEl.innerHTML = "";
|
if (logEl) logEl.innerHTML = "";
|
||||||
});
|
});
|
||||||
$("base-url")?.addEventListener("change", refreshRecordingModesStatus);
|
$("base-url")?.addEventListener("change", refreshRecordingModesStatus);
|
||||||
|
$("surgery-id")?.addEventListener("change", () => {
|
||||||
|
if (activeMode === "offline-batch") void refreshLiveRecordingConflict(surgeryId());
|
||||||
|
});
|
||||||
|
$("surgery-id")?.addEventListener("blur", () => {
|
||||||
|
if (activeMode === "offline-batch") void refreshLiveRecordingConflict(surgeryId());
|
||||||
|
});
|
||||||
$("camera-ids")?.addEventListener("input", () => {
|
$("camera-ids")?.addEventListener("input", () => {
|
||||||
if (activeMode === "live-rtsp") startPreviewPolling();
|
if (activeMode === "live-rtsp") startPreviewPolling();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>选择运行模式</h2>
|
<h2>选择运行模式</h2>
|
||||||
<div class="mode-cards mode-cards--two">
|
<div class="mode-cards mode-cards--two">
|
||||||
<button type="button" class="mode-card active" data-mode="live-rtsp">
|
<button type="button" class="mode-card" data-mode="live-rtsp">
|
||||||
<div class="title">链路 1 · 真摄像头</div>
|
<div class="title">链路 1 · 真摄像头</div>
|
||||||
<div class="desc">填 camera_id 开录 · 服务端自动拉 RTSP · 无需上传视频</div>
|
<div class="desc">填 camera_id 开录 · 服务端自动拉 RTSP · 无需上传视频</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card mode-panel active" data-mode="live-rtsp">
|
<section class="card mode-panel" data-mode="live-rtsp">
|
||||||
<div class="mode-callout mode-callout--live">
|
<div class="mode-callout mode-callout--live">
|
||||||
<strong>正式对接链路。</strong>客户端只传 <code>camera_ids</code> 与耗材候选;视频由服务端从 RTSP 拉流并切片,<em>不要</em>在此页上传 MP4。
|
<strong>正式对接链路。</strong>客户端只传 <code>camera_ids</code> 与耗材候选;视频由服务端从 RTSP 拉流并切片,<em>不要</em>在此页上传 MP4。
|
||||||
</div>
|
</div>
|
||||||
@@ -73,6 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<h2>步骤 1 · 上传 MP4</h2>
|
<h2>步骤 1 · 上传 MP4</h2>
|
||||||
<p class="labels-meta">不启动实时会话,处理完成后直接查结果。</p>
|
<p class="labels-meta">不启动实时会话,处理完成后直接查结果。</p>
|
||||||
|
<div id="live-recording-conflict" class="consumables-warn hidden" role="alert"></div>
|
||||||
<div class="upload-zone" id="offline-zone" style="margin-top:12px">
|
<div class="upload-zone" id="offline-zone" style="margin-top:12px">
|
||||||
<div class="icon">MP4</div>
|
<div class="icon">MP4</div>
|
||||||
<div id="offline-fname">点击或拖放 MP4 到此处</div>
|
<div id="offline-fname">点击或拖放 MP4 到此处</div>
|
||||||
@@ -103,7 +104,7 @@
|
|||||||
<div id="rtsp-preview-grid" class="preview-grid preview-grid--rtsp"></div>
|
<div id="rtsp-preview-grid" class="preview-grid preview-grid--rtsp"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card mode-panel active" data-mode="live-rtsp offline-batch">
|
<section class="card mode-panel" data-mode="live-rtsp offline-batch">
|
||||||
<h2>步骤 2 · 耗材候选(AI 白名单)</h2>
|
<h2>步骤 2 · 耗材候选(AI 白名单)</h2>
|
||||||
<p id="consumables-mode-hint" class="field-hint">
|
<p id="consumables-mode-hint" class="field-hint">
|
||||||
选择本次手术可能用到的耗材名称或产品编码;留空表示使用全部标签。<strong>此处不是视频上传区,请勿填写 .mp4 路径。</strong>
|
选择本次手术可能用到的耗材名称或产品编码;留空表示使用全部标签。<strong>此处不是视频上传区,请勿填写 .mp4 路径。</strong>
|
||||||
|
|||||||
Reference in New Issue
Block a user