various fix

This commit is contained in:
Kevin
2026-05-22 10:45:47 +08:00
parent 7600c5da2f
commit 87b6a7b804
8 changed files with 178 additions and 25 deletions

View File

@@ -16,10 +16,14 @@ RUN sed -i \
RUN apt-get update && apt-get install -y --no-install-recommends \
docker.io \
ffmpeg \
fontconfig \
fonts-noto-cjk \
fonts-wqy-microhei \
libgl1 \
libglib2.0-0 \
libgomp1 \
libxcb1 \
&& fc-cache -fv \
&& rm -rf /var/lib/apt/lists/*
# ghcr.io「增加前缀」形式与 kindest/node 示例一致)

View File

@@ -43,7 +43,9 @@ class SegmentRow:
_FONT_CANDIDATES = [
Path("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"),
Path("/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc"),
Path("/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc"),
Path("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"),
Path("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"),
]

View File

@@ -0,0 +1,21 @@
"""Resolve a CJK-capable font for offline visualization subprocesses."""
from __future__ import annotations
from pathlib import Path
# Keep in sync with algorithm_subprocesses/5.15/visualize_result_video.py
CJK_FONT_CANDIDATES: tuple[Path, ...] = (
Path("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"),
Path("/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc"),
Path("/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc"),
Path("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"),
Path("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"),
)
def resolve_cjk_font_path() -> Path | None:
for candidate in CJK_FONT_CANDIDATES:
if candidate.is_file():
return candidate.resolve()
return None

View File

@@ -11,6 +11,7 @@ from pathlib import Path
from loguru import logger
from app.algo_host.bundle import load_reference_default_config, resolve_bundle_relative_path
from app.algo_host.cjk_font import resolve_cjk_font_path
from app.algo_host.transcode import VISUALIZATION_MAX_WIDTH
@@ -46,7 +47,7 @@ def build_visualization_command(
device_cfg = cfg.get("device") if isinstance(cfg.get("device"), dict) else {}
hand_raw = str((weights or {}).get("hand") or "weights/hand_detect.pt").strip()
hand_model = resolve_bundle_relative_path(bundle_dir, hand_raw)
return [
cmd = [
sys.executable,
"-X",
"faulthandler",
@@ -64,6 +65,10 @@ def build_visualization_command(
"--max-width",
str(VISUALIZATION_MAX_WIDTH),
]
font_path = resolve_cjk_font_path()
if font_path is not None:
cmd.extend(["--font-path", str(font_path)])
return cmd
def _signal_name(signum: int) -> str:

View File

@@ -266,6 +266,32 @@ def test_build_visualization_command_uses_hand_model_and_result_tsv(
assert cmd[cmd.index("--max-width") + 1] == str(VISUALIZATION_MAX_WIDTH)
def test_build_visualization_command_passes_font_path_when_available(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
bundle = tmp_path / "bundle"
write_minimal_reference_bundle(bundle)
(bundle / "weights").mkdir()
(bundle / "weights" / "hand_detect.pt").write_bytes(b"fake")
(bundle / "visualize_result_video.py").write_text("# fake\n", encoding="utf-8")
font = tmp_path / "NotoSansCJK-Regular.ttc"
font.write_bytes(b"fake-font")
monkeypatch.setattr(
"app.algo_host.subprocess_runner.resolve_cjk_font_path",
lambda: font,
)
cmd = build_visualization_command(
bundle_dir=bundle,
video_path=tmp_path / "input.mp4",
result_path=tmp_path / "result.tsv",
output_video_path=tmp_path / "result_vis.mp4",
)
assert cmd[cmd.index("--font-path") + 1] == str(font)
def test_build_batch_main_command_uses_5_15_main_py(tmp_path: Path) -> None:
cmd = build_batch_main_command(
bundle_dir=tmp_path / "algorithm_subprocesses" / "5.15",

View File

@@ -12,6 +12,7 @@
let allLabels = [];
let selectedConsumables = new Set();
let lastVideoBatchDoctorDisplay = "";
let videoVisToken = 0;
const webcamSlotState = {};
const baseUrl = () => $("base-url").value.trim().replace(/\/+$/, "");
@@ -299,6 +300,7 @@
: mode === "live-simulated"
? "链路 2 · 模拟实时"
: "链路 3 · 离线精确";
if (mode !== "offline-batch") hideVideoBatchVisualization();
refreshRecordingModesStatus();
}
@@ -358,39 +360,119 @@
}
}
function hideVideoBatchVisualization() {
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function cancelVideoBatchVisualization() {
videoVisToken += 1;
const wrap = $("video-batch-vis");
const player = $("video-batch-vis-player");
const hint = $("video-batch-vis-hint");
if (wrap) wrap.classList.add("hidden");
if (player) {
player.onerror = null;
player.onloadeddata = null;
player.removeAttribute("src");
player.load();
}
if (hint) hint.textContent = "";
}
function showVideoBatchVisualization(sid, urlPath, doctorDisplay) {
function hideVideoBatchVisualization() {
cancelVideoBatchVisualization();
}
function revealVideoBatchVisualization() {
const wrap = $("video-batch-vis");
if (wrap) wrap.classList.remove("hidden");
}
function loadVideoWhenReady(player, src, token, timeoutMs = 15000) {
return new Promise((resolve) => {
if (token !== videoVisToken) {
resolve(false);
return;
}
let settled = false;
const finish = (ok) => {
if (settled) return;
settled = true;
clearTimeout(timer);
player.onerror = null;
player.onloadeddata = null;
if (!ok) {
player.removeAttribute("src");
player.load();
}
resolve(ok);
};
const timer = setTimeout(() => finish(false), timeoutMs);
player.onerror = () => finish(false);
player.onloadeddata = () => finish(true);
player.src = src;
player.load();
});
}
async function waitForVideoBatchVisualization(sid, urlPath, doctorDisplay) {
const wrap = $("video-batch-vis");
const player = $("video-batch-vis-player");
const hint = $("video-batch-vis-hint");
if (!wrap || !player) return;
const token = ++videoVisToken;
const path = urlPath || `/internal/demo/offline-batch/${sid}/visualization`;
const src = baseUrl() + path + "?t=" + Date.now();
wrap.classList.remove("hidden");
const docEl = $("video-batch-doctor-info");
const text = (doctorDisplay || lastVideoBatchDoctorDisplay || "").trim();
if (docEl) {
docEl.textContent = text ? "识别医生:" + text : "";
docEl.style.display = text ? "block" : "none";
const base = baseUrl() + path;
const pollIntervalMs = 3000;
const maxAttempts = 120;
wrap.classList.add("hidden");
showVideoBatchDoctorInfo(doctorDisplay || lastVideoBatchDoctorDisplay);
if (hint) hint.textContent = "";
for (let attempt = 0; attempt < maxAttempts; attempt++) {
if (token !== videoVisToken) return;
const src = base + "?t=" + Date.now();
const loaded = await loadVideoWhenReady(player, src, token);
if (token !== videoVisToken) return;
if (loaded) {
revealVideoBatchVisualization();
if (hint) hint.textContent = "标注视频已加载";
player.onerror = () => {
if (hint) {
hint.textContent = "视频加载失败,请稍后重试或新标签页打开链接。";
}
};
showBanner("标注视频已就绪", "ok");
return;
}
if (attempt === 0) {
showBanner("标注视频生成中,就绪后自动展示…", "info");
}
await sleep(pollIntervalMs);
}
player.onerror = () => {
if (hint) hint.textContent = "视频加载失败,请稍后重试或新标签页打开链接。";
};
player.onloadeddata = () => {
if (hint) hint.textContent = "标注视频已加载";
};
player.src = src;
player.load();
if (hint) hint.textContent = "正在加载…";
if (token === videoVisToken) {
showBanner("标注视频尚未就绪,请稍后点击「查询结果」重试", "warn");
}
}
function showVideoBatchDoctorInfo(displayText) {
const el = $("video-batch-doctor-info");
if (!el) return;
const text = (displayText || "").trim();
if (!text) {
el.textContent = "";
el.style.display = "none";
return;
}
el.textContent = "识别医生:" + text;
el.style.display = "block";
}
function showVideoBatchVisualization(sid, urlPath, doctorDisplay) {
void waitForVideoBatchVisualization(sid, urlPath, doctorDisplay);
}
function getDebugStreamCount() {
@@ -563,9 +645,12 @@
lastVideoBatchDoctorDisplay = body?.doctor_display || "";
if ($("offline-batch-include-vis")?.checked && body?.visualization_url) {
showVideoBatchVisualization(sid, body.visualization_url, lastVideoBatchDoctorDisplay);
} else hideVideoBatchVisualization();
} else {
hideVideoBatchVisualization();
showVideoBatchDoctorInfo(lastVideoBatchDoctorDisplay);
}
showBanner("离线处理完成,正在查询结果…", "ok");
await handleResult();
await handleResult({ skipVisualization: true });
return;
}
@@ -647,10 +732,14 @@
}
}
async function handleResult() {
async function handleResult(options = {}) {
const sid = ensureSurgeryId();
if (!sid) return;
if (getRunMode() === "offline-batch" && $("offline-batch-include-vis")?.checked) {
if (
!options.skipVisualization &&
getRunMode() === "offline-batch" &&
$("offline-batch-include-vis")?.checked
) {
showVideoBatchVisualization(sid, null);
} else if (getRunMode() !== "offline-batch") {
hideVideoBatchVisualization();
@@ -664,6 +753,9 @@
return;
}
const { details = [], summary = [] } = body;
if (getRunMode() === "offline-batch") {
showVideoBatchDoctorInfo(details[0]?.doctor_id || lastVideoBatchDoctorDisplay);
}
const renderTable = (title, rows, cols) => {
const h = document.createElement("h3");
h.textContent = title;
@@ -686,11 +778,13 @@
};
renderTable("消耗明细", details, [
{ key: "timestamp", label: "时间" },
{ key: "item_id", label: "耗材 ID" },
{ key: "item_name", label: "耗材" },
{ key: "qty", label: "数量" },
{ key: "doctor_id", label: "医生" },
{ key: "doctor_id", label: "医生(姓名+ID" },
]);
renderTable("汇总", summary, [
{ key: "item_id", label: "耗材 ID" },
{ key: "item_name", label: "耗材" },
{ key: "total_quantity", label: "合计" },
]);

View File

@@ -191,8 +191,8 @@
<section class="card">
<h2>结果</h2>
<p id="video-batch-doctor-info" class="labels-meta"></p>
<div id="video-batch-vis" class="vis-block hidden">
<p id="video-batch-doctor-info" class="labels-meta"></p>
<video id="video-batch-vis-player" controls playsinline></video>
<p id="video-batch-vis-hint" class="stream-hint"></p>
</div>

View File

@@ -26,6 +26,7 @@ operation-room-monitor/
- Docker Compose V2、NVIDIA 驱动、NVIDIA Container Toolkit
- 复制 `backend/.env.example``backend/.env` 并填写
- 算法子进程包:`backend/algorithm_subprocesses/5.15/`(含 `main.py``weights/`;镜像构建时会 `COPY` 进容器,勿在 `.dockerignore` 中整目录排除)
- 标注视频中文字体:镜像内已安装 `fonts-noto-cjk``fonts-wqy-microhei`(供 `visualize_result_video.py` 绘制耗材标签)
- 可选备用权重:`backend/app/resources/actionformer_epoch_045.pth.tar`
---