Improve speed

This commit is contained in:
Kevin
2026-05-22 11:15:22 +08:00
parent 87b6a7b804
commit 941c71e991
14 changed files with 132 additions and 49 deletions

View File

@@ -12,14 +12,17 @@ RUN sed -i \
-e 's|http://deb.debian.org/debian|https://mirrors.aliyun.com/debian|g' \ -e 's|http://deb.debian.org/debian|https://mirrors.aliyun.com/debian|g' \
/etc/apt/sources.list.d/debian.sources /etc/apt/sources.list.d/debian.sources
# OpenCV (pulled in by ultralytics) links against X11 client libs; slim images omit them. # OpenCV / MediaPipe (doctor pose) need GL + GLES in slim images; omit X11/GUI stack.
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
docker.io \ docker.io \
ffmpeg \ ffmpeg \
fontconfig \ fontconfig \
fonts-noto-cjk \ fonts-noto-cjk \
fonts-wqy-microhei \ fonts-wqy-microhei \
libegl1 \
libgbm1 \
libgl1 \ libgl1 \
libgles2 \
libglib2.0-0 \ libglib2.0-0 \
libgomp1 \ libgomp1 \
libxcb1 \ libxcb1 \

View File

@@ -43,6 +43,23 @@ POSE_LITE_URL = (
POSE_LITE_NAME = "pose_landmarker_lite.task" POSE_LITE_NAME = "pose_landmarker_lite.task"
def build_pose_landmarker(
model_path: Path,
*,
min_pose_detection_confidence: float = 0.3,
) -> PoseLandmarker:
"""Headless/Docker-safe: force CPU delegate (avoids libGLESv2 GPU path)."""
opts = PoseLandmarkerOptions(
base_options=BaseOptions(
model_asset_path=str(model_path),
delegate=BaseOptions.Delegate.CPU,
),
running_mode=VisionRunningMode.IMAGE,
min_pose_detection_confidence=min_pose_detection_confidence,
)
return PoseLandmarker.create_from_options(opts)
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Input mp4 -> middle 10s pose crop -> doctor identity", description="Input mp4 -> middle 10s pose crop -> doctor identity",
@@ -282,12 +299,7 @@ def main() -> int:
try: try:
model_path = _ensure_pose_lite_model(THIS_DIR / ".mediapipe_models") model_path = _ensure_pose_lite_model(THIS_DIR / ".mediapipe_models")
opts = PoseLandmarkerOptions( landmarker = build_pose_landmarker(model_path, min_pose_detection_confidence=0.3)
base_options=BaseOptions(model_asset_path=str(model_path)),
running_mode=VisionRunningMode.IMAGE,
min_pose_detection_confidence=0.3,
)
landmarker = PoseLandmarker.create_from_options(opts)
try: try:
best_crop = pick_best_person_crop( best_crop = pick_best_person_crop(
video_path=args.video, video_path=args.video,

View File

@@ -57,14 +57,12 @@ def _infer_doctor_text(args: Namespace, video_path: Path) -> str:
try: try:
doctor_mod = _load_doctor_module(script_path) doctor_mod = _load_doctor_module(script_path)
model_path = doctor_mod._ensure_pose_lite_model(script_path.parent / ".mediapipe_models") model_path = doctor_mod._ensure_pose_lite_model(script_path.parent / ".mediapipe_models")
opts = doctor_mod.PoseLandmarkerOptions( landmarker = doctor_mod.build_pose_landmarker(
base_options=doctor_mod.BaseOptions(model_asset_path=str(model_path)), model_path,
running_mode=doctor_mod.VisionRunningMode.IMAGE,
min_pose_detection_confidence=float( min_pose_detection_confidence=float(
args.doctor_identity_pose_min_detection_confidence args.doctor_identity_pose_min_detection_confidence
), ),
) )
landmarker = doctor_mod.PoseLandmarker.create_from_options(opts)
try: try:
best_crop = doctor_mod.pick_best_person_crop( best_crop = doctor_mod.pick_best_person_crop(
video_path=video_path, video_path=video_path,

View File

@@ -20,9 +20,9 @@ from app.algo_host.result_adapter import (
) )
from app.algo_host.subprocess_runner import run_batch_main, run_visualization_script from app.algo_host.subprocess_runner import run_batch_main, run_visualization_script
from app.algo_host.transcode import ( from app.algo_host.transcode import (
ensure_batch_pipeline_input_video,
is_browser_compatible_mp4, is_browser_compatible_mp4,
is_readable_mp4, is_readable_mp4,
stage_batch_pipeline_input,
transcode_visualization_for_browser, transcode_visualization_for_browser,
) )
from app.domain.consumption import SurgeryConsumptionStored from app.domain.consumption import SurgeryConsumptionStored
@@ -189,19 +189,23 @@ class BatchAlgorithmService:
candidates = resolve_reference_candidates(candidate_consumables) candidates = resolve_reference_candidates(candidate_consumables)
candidate_key = candidate_cache_key(candidates) candidate_key = candidate_cache_key(candidates)
surgery_input_dir = self._root_dir / surgery_id / "input" pipeline_video = (
surgery_input_dir.mkdir(parents=True, exist_ok=True) self._root_dir
surgery_input = surgery_input_dir / f"{digest[:12]}.mp4" / "cache"
ensure_batch_pipeline_input_video( / digest
/ "input"
/ f"pipeline{uploaded_video_path.suffix or '.mp4'}"
)
stage_batch_pipeline_input(
source_path=uploaded_video_path, source_path=uploaded_video_path,
dest_path=surgery_input, dest_path=pipeline_video,
) )
cache_dir = self._root_dir / "cache" / digest / candidate_key cache_dir = self._root_dir / "cache" / digest / candidate_key
job = prepare_batch_job( job = prepare_batch_job(
bundle_dir=self._bundle_dir_override, bundle_dir=self._bundle_dir_override,
cache_dir=cache_dir, cache_dir=cache_dir,
uploaded_video_path=uploaded_video_path, pipeline_video_path=pipeline_video,
candidate_consumables=candidates, candidate_consumables=candidates,
) )
@@ -240,7 +244,7 @@ class BatchAlgorithmService:
return BatchRunResult( return BatchRunResult(
video_sha256=digest, video_sha256=digest,
candidate_cache_key=candidate_key, candidate_cache_key=candidate_key,
input_path=surgery_input, input_path=pipeline_video,
work_dir=job.work_dir, work_dir=job.work_dir,
output_path=job.output_path, output_path=job.output_path,
details=details, details=details,

View File

@@ -10,7 +10,6 @@ from pathlib import Path
import yaml import yaml
from app.algo_host.bundle import load_reference_default_config, resolve_reference_bundle_dir from app.algo_host.bundle import load_reference_default_config, resolve_reference_bundle_dir
from app.algo_host.transcode import ensure_batch_pipeline_input_video
from app.consumable_catalog import build_name_mapping from app.consumable_catalog import build_name_mapping
@@ -75,22 +74,17 @@ def prepare_batch_job(
*, *,
bundle_dir: Path | None, bundle_dir: Path | None,
cache_dir: Path, cache_dir: Path,
uploaded_video_path: Path, pipeline_video_path: Path,
candidate_consumables: list[str], candidate_consumables: list[str],
) -> BatchJobFiles: ) -> BatchJobFiles:
root = resolve_reference_bundle_dir(bundle_dir) root = resolve_reference_bundle_dir(bundle_dir)
cache_input_dir = cache_dir / "input"
cache_output_dir = cache_dir / "output" cache_output_dir = cache_dir / "output"
cache_work_dir = cache_dir / "work" cache_work_dir = cache_dir / "work"
cache_config_dir = cache_dir / "config" cache_config_dir = cache_dir / "config"
for d in (cache_input_dir, cache_output_dir, cache_work_dir, cache_config_dir): for d in (cache_output_dir, cache_work_dir, cache_config_dir):
d.mkdir(parents=True, exist_ok=True) d.mkdir(parents=True, exist_ok=True)
cache_input = cache_input_dir / "input.mp4" pipeline_video = pipeline_video_path.resolve()
ensure_batch_pipeline_input_video(
source_path=uploaded_video_path,
dest_path=cache_input,
)
output_path = cache_output_dir / "result.tsv" output_path = cache_output_dir / "result.tsv"
excel_path = cache_config_dir / "商品信息表.xlsx" excel_path = cache_config_dir / "商品信息表.xlsx"
whitelist_path = cache_config_dir / "whitelist.json" whitelist_path = cache_config_dir / "whitelist.json"
@@ -100,7 +94,7 @@ def prepare_batch_job(
write_reference_whitelist_json(whitelist_path, candidate_consumables=candidate_consumables) write_reference_whitelist_json(whitelist_path, candidate_consumables=candidate_consumables)
config = build_job_config( config = build_job_config(
bundle_dir=root, bundle_dir=root,
video_path=cache_input.resolve(), video_path=pipeline_video,
output_path=output_path.resolve(), output_path=output_path.resolve(),
work_dir=cache_work_dir.resolve(), work_dir=cache_work_dir.resolve(),
excel_path=excel_path.resolve(), excel_path=excel_path.resolve(),
@@ -116,5 +110,5 @@ def prepare_batch_job(
whitelist_path=whitelist_path, whitelist_path=whitelist_path,
output_path=output_path, output_path=output_path,
work_dir=cache_work_dir, work_dir=cache_work_dir,
input_video_path=cache_input, input_video_path=pipeline_video,
) )

View File

@@ -19,6 +19,9 @@ def build_reference_env() -> dict[str, str]:
env = os.environ.copy() env = os.environ.copy()
env["PYTHONFAULTHANDLER"] = "1" env["PYTHONFAULTHANDLER"] = "1"
env["PYTHONUNBUFFERED"] = "1" env["PYTHONUNBUFFERED"] = "1"
# Headless containers: avoid OpenGL/EGL init noise from CV/MediaPipe defaults.
env.setdefault("OPENCV_OPENCL_RUNTIME", "")
env.setdefault("QT_QPA_PLATFORM", "offscreen")
return env return env

View File

@@ -190,8 +190,17 @@ def normalize_batch_input_video(source_path: Path, output_path: Path) -> bool:
return True return True
def stage_batch_pipeline_input(*, source_path: Path, dest_path: Path) -> None:
"""Copy upload to digest-level pipeline input without browser normalize/transcode."""
dest_path.parent.mkdir(parents=True, exist_ok=True)
if dest_path.is_file() and dest_path.stat().st_size > 0:
return
shutil.copy2(source_path, dest_path)
def ensure_batch_pipeline_input_video(*, source_path: Path, dest_path: Path) -> None: def ensure_batch_pipeline_input_video(*, source_path: Path, dest_path: Path) -> None:
import shutil as sh """Browser-compatible normalize/copy; use only for visualization inputs, not batch inference."""
dest_path.parent.mkdir(parents=True, exist_ok=True) dest_path.parent.mkdir(parents=True, exist_ok=True)
if dest_path.is_file() and dest_path.stat().st_size > 0 and not batch_input_needs_normalize(dest_path): if dest_path.is_file() and dest_path.stat().st_size > 0 and not batch_input_needs_normalize(dest_path):
@@ -205,7 +214,7 @@ def ensure_batch_pipeline_input_video(*, source_path: Path, dest_path: Path) ->
dest_path, dest_path,
) )
if not dest_path.is_file(): if not dest_path.is_file():
sh.copy2(source_path, dest_path) shutil.copy2(source_path, dest_path)
def transcode_visualization_for_browser(source_path: Path, output_path: Path) -> bool: def transcode_visualization_for_browser(source_path: Path, output_path: Path) -> bool:

View File

@@ -173,12 +173,12 @@ async def offline_batch(
len(result.details), len(result.details),
) )
cache_input = result.output_path.parent.parent / "input" / "input.mp4" cache_input = result.input_path
if include_visualization: if include_visualization:
stage_visualization_pending( stage_visualization_pending(
runner.root_dir, runner.root_dir,
surgery_id, surgery_id,
source_mp4=cache_input if cache_input.is_file() else result.input_path, source_mp4=cache_input,
result_tsv=result.output_path, result_tsv=result.output_path,
) )
background_tasks.add_task(_background_finalize_visualization, runner, surgery_id) background_tasks.add_task(_background_finalize_visualization, runner, surgery_id)

View File

@@ -35,6 +35,7 @@ from app.algo_host.transcode import (
browser_transcode_tmp_path, browser_transcode_tmp_path,
ensure_batch_pipeline_input_video, ensure_batch_pipeline_input_video,
is_browser_compatible_mp4, is_browser_compatible_mp4,
stage_batch_pipeline_input,
transcode_visualization_for_browser, transcode_visualization_for_browser,
) )
from app.api import router as api_router from app.api import router as api_router
@@ -169,6 +170,42 @@ def test_browser_transcode_tmp_path_keeps_mp4_extension() -> None:
assert str(tmp).endswith(".mp4") assert str(tmp).endswith(".mp4")
@pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not installed")
def test_stage_batch_pipeline_input_copies_without_normalize(tmp_path: Path) -> None:
ffmpeg = shutil.which("ffmpeg")
assert ffmpeg is not None
source = tmp_path / "upload.mp4"
dest = tmp_path / "pipeline.mp4"
proc = subprocess.run(
[
ffmpeg,
"-y",
"-f",
"lavfi",
"-i",
"testsrc=size=640x360:rate=10",
"-t",
"0.5",
"-c:v",
"mpeg4",
"-pix_fmt",
"yuv420p",
str(source),
],
check=False,
capture_output=True,
text=True,
)
assert proc.returncode == 0, proc.stderr
assert batch_input_needs_normalize(source)
stage_batch_pipeline_input(source_path=source, dest_path=dest)
assert dest.is_file()
assert dest.read_bytes() == source.read_bytes()
assert batch_input_needs_normalize(dest)
stage_batch_pipeline_input(source_path=tmp_path / "other.mp4", dest_path=dest)
assert dest.read_bytes() == source.read_bytes()
@pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not installed") @pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not installed")
def test_ensure_batch_pipeline_input_video_normalizes_non_h264(tmp_path: Path) -> None: def test_ensure_batch_pipeline_input_video_normalizes_non_h264(tmp_path: Path) -> None:
ffmpeg = shutil.which("ffmpeg") ffmpeg = shutil.which("ffmpeg")
@@ -342,6 +379,10 @@ def test_batch_service_respects_reference_bundle_relative_env(
assert runner.bundle_dir == bundle.resolve() assert runner.bundle_dir == bundle.resolve()
assert calls[0][5] == str(bundle.resolve() / "main.py") assert calls[0][5] == str(bundle.resolve() / "main.py")
assert result.details[0].item_name == "耗材1" assert result.details[0].item_name == "耗材1"
config = yaml.safe_load(Path(calls[0][calls[0].index("--config") + 1]).read_text(encoding="utf-8"))
assert Path(config["io"]["video"]).name == "pipeline.mp4"
assert "/cache/" in config["io"]["video"]
assert "/input/" in config["io"]["video"]
def test_batch_service_reuses_cache_on_repeat_run( def test_batch_service_reuses_cache_on_repeat_run(
@@ -449,6 +490,8 @@ def test_batch_service_reuses_cache_on_repeat_run(
config_path = Path(calls[0][calls[0].index("--config") + 1]) config_path = Path(calls[0][calls[0].index("--config") + 1])
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) config = yaml.safe_load(config_path.read_text(encoding="utf-8"))
assert Path(config["io"]["video"]).is_file() assert Path(config["io"]["video"]).is_file()
assert Path(config["io"]["video"]).name == "pipeline.mp4"
assert "/input/" in str(Path(config["io"]["video"]))
assert Path(config["io"]["excel"]).is_file() assert Path(config["io"]["excel"]).is_file()
whitelist = json.loads(Path(config["io"]["whitelist_json"]).read_text(encoding="utf-8")) whitelist = json.loads(Path(config["io"]["whitelist_json"]).read_text(encoding="utf-8"))
assert whitelist == {"allowed_names": ["耗材1"]} assert whitelist == {"allowed_names": ["耗材1"]}
@@ -563,16 +606,16 @@ def test_demo_video_batch_endpoint_writes_queryable_result(
assert kwargs["candidate_consumables"] == ["耗材1"] assert kwargs["candidate_consumables"] == ["耗材1"]
assert kwargs.get("include_visualization") is False assert kwargs.get("include_visualization") is False
cache_dir = root_dir / "cache" / ("a" * 64) / "c1" cache_dir = root_dir / "cache" / ("a" * 64) / "c1"
cache_input = cache_dir / "input" / "input.mp4" pipeline_input = root_dir / "cache" / ("a" * 64) / "input" / "pipeline.mp4"
cache_input.parent.mkdir(parents=True) pipeline_input.parent.mkdir(parents=True)
cache_input.write_bytes(b"pipeline-input") pipeline_input.write_bytes(b"pipeline-input")
output_path = cache_dir / "output" / "result.tsv" output_path = cache_dir / "output" / "result.tsv"
output_path.parent.mkdir(parents=True) output_path.parent.mkdir(parents=True)
output_path.write_text(complete_result_tsv_body(), encoding="utf-8") output_path.write_text(complete_result_tsv_body(), encoding="utf-8")
return BatchRunResult( return BatchRunResult(
video_sha256="a" * 64, video_sha256="a" * 64,
candidate_cache_key="c1", candidate_cache_key="c1",
input_path=root_dir / "100001" / "input" / "saved.mp4", input_path=pipeline_input,
work_dir=cache_dir / "work", work_dir=cache_dir / "work",
output_path=output_path, output_path=output_path,
details=[detail], details=[detail],
@@ -630,7 +673,7 @@ def test_demo_video_batch_endpoint_writes_queryable_result(
assert body["status"] == "accepted" assert body["status"] == "accepted"
assert body["visualization_url"] is None assert body["visualization_url"] is None
assert vis_calls == [] assert vis_calls == []
assert not (root_dir / "cache" / ("a" * 64)).exists() assert not (root_dir / "cache" / ("a" * 64) / "c1").exists()
assert not (root_dir / "100001").exists() assert not (root_dir / "100001").exists()
got = client.get("/client/surgeries/100001/result") got = client.get("/client/surgeries/100001/result")
@@ -663,16 +706,16 @@ def test_demo_video_batch_endpoint_stages_vis_and_purges_cache_when_requested(
def run(self, **kwargs: Any) -> BatchRunResult: def run(self, **kwargs: Any) -> BatchRunResult:
cache_dir = root_dir / "cache" / ("b" * 64) / "c1" cache_dir = root_dir / "cache" / ("b" * 64) / "c1"
cache_input = cache_dir / "input" / "input.mp4" pipeline_input = root_dir / "cache" / ("b" * 64) / "input" / "pipeline.mp4"
cache_input.parent.mkdir(parents=True) pipeline_input.parent.mkdir(parents=True)
cache_input.write_bytes(b"pipeline-input") pipeline_input.write_bytes(b"pipeline-input")
output_path = cache_dir / "output" / "result.tsv" output_path = cache_dir / "output" / "result.tsv"
output_path.parent.mkdir(parents=True) output_path.parent.mkdir(parents=True)
output_path.write_text(complete_result_tsv_body(), encoding="utf-8") output_path.write_text(complete_result_tsv_body(), encoding="utf-8")
return BatchRunResult( return BatchRunResult(
video_sha256="b" * 64, video_sha256="b" * 64,
candidate_cache_key="c1", candidate_cache_key="c1",
input_path=root_dir / "100001" / "input" / "saved.mp4", input_path=pipeline_input,
work_dir=cache_dir / "work", work_dir=cache_dir / "work",
output_path=output_path, output_path=output_path,
details=[detail], details=[detail],
@@ -709,7 +752,8 @@ def test_demo_video_batch_endpoint_stages_vis_and_purges_cache_when_requested(
body = res.json() body = res.json()
assert body["visualization_url"] == "/internal/demo/offline-batch/100001/visualization" assert body["visualization_url"] == "/internal/demo/offline-batch/100001/visualization"
assert vis_calls == ["100001"] assert vis_calls == ["100001"]
assert not (root_dir / "cache" / ("b" * 64)).exists() assert not (root_dir / "cache" / ("b" * 64) / "c1").exists()
assert (root_dir / "cache" / ("b" * 64) / "input" / "pipeline.mp4").is_file()
pending_input = root_dir / "vis_pending" / "100001" / "input.mp4" pending_input = root_dir / "vis_pending" / "100001" / "input.mp4"
pending_tsv = root_dir / "vis_pending" / "100001" / "result.tsv" pending_tsv = root_dir / "vis_pending" / "100001" / "result.tsv"
assert pending_input.read_bytes() == b"pipeline-input" assert pending_input.read_bytes() == b"pipeline-input"

View File

@@ -117,7 +117,8 @@ def test_video_batch_endpoint_invokes_reference_bundle_subprocess(
assert kwargs.get("env", {}).get("PYTHONFAULTHANDLER") == "1" assert kwargs.get("env", {}).get("PYTHONFAULTHANDLER") == "1"
config = call["config"] config = call["config"]
assert Path(config["io"]["video"]).name == "input.mp4" assert Path(config["io"]["video"]).name == "pipeline.mp4"
assert "/input/" in config["io"]["video"]
assert str(config["io"]["excel"]).endswith("商品信息表.xlsx") assert str(config["io"]["excel"]).endswith("商品信息表.xlsx")
assert str(config["io"]["whitelist_json"]).endswith("whitelist.json") assert str(config["io"]["whitelist_json"]).endswith("whitelist.json")
assert config["runtime"]["keep_work_dir"] is False assert config["runtime"]["keep_work_dir"] is False

View File

@@ -37,8 +37,9 @@ def test_purge_batch_artifacts_removes_cache_and_uploads(tmp_path: Path) -> None
digest = "d" * 64 digest = "d" * 64
candidate_key = "c1" candidate_key = "c1"
cache_entry = root / "cache" / digest / candidate_key cache_entry = root / "cache" / digest / candidate_key
(cache_entry / "input").mkdir(parents=True) pipeline_input = root / "cache" / digest / "input" / "pipeline.mp4"
(cache_entry / "input" / "input.mp4").write_bytes(b"x" * 100) pipeline_input.parent.mkdir(parents=True)
pipeline_input.write_bytes(b"x" * 100)
(cache_entry / "output").mkdir(parents=True) (cache_entry / "output").mkdir(parents=True)
(cache_entry / "output" / "result.tsv").write_text("ok\n", encoding="utf-8") (cache_entry / "output" / "result.tsv").write_text("ok\n", encoding="utf-8")
(cache_entry / "work").mkdir(parents=True) (cache_entry / "work").mkdir(parents=True)
@@ -53,6 +54,7 @@ def test_purge_batch_artifacts_removes_cache_and_uploads(tmp_path: Path) -> None
purge_surgery_batch_tree(root, surgery_id) purge_surgery_batch_tree(root, surgery_id)
assert not cache_entry.exists() assert not cache_entry.exists()
assert pipeline_input.is_file()
assert not (root / surgery_id).exists() assert not (root / surgery_id).exists()

View File

@@ -458,6 +458,13 @@
} }
} }
function pickDoctorBannerText(details, fallbackDisplay) {
const rowDoctor = (details[0]?.doctor_id || "").trim();
const apiDisplay = (fallbackDisplay || "").trim();
if (apiDisplay && (!rowDoctor || rowDoctor === "vision")) return apiDisplay;
return rowDoctor || apiDisplay;
}
function showVideoBatchDoctorInfo(displayText) { function showVideoBatchDoctorInfo(displayText) {
const el = $("video-batch-doctor-info"); const el = $("video-batch-doctor-info");
if (!el) return; if (!el) return;
@@ -754,7 +761,7 @@
} }
const { details = [], summary = [] } = body; const { details = [], summary = [] } = body;
if (getRunMode() === "offline-batch") { if (getRunMode() === "offline-batch") {
showVideoBatchDoctorInfo(details[0]?.doctor_id || lastVideoBatchDoctorDisplay); showVideoBatchDoctorInfo(pickDoctorBannerText(details, lastVideoBatchDoctorDisplay));
} }
const renderTable = (title, rows, cols) => { const renderTable = (title, rows, cols) => {
const h = document.createElement("h3"); const h = document.createElement("h3");

View File

@@ -88,6 +88,12 @@ class DemoHandler(SimpleHTTPRequestHandler):
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, directory=str(SCRIPT_DIR), **kwargs) super().__init__(*args, directory=str(SCRIPT_DIR), **kwargs)
def end_headers(self) -> None:
path = self.path.split("?", 1)[0]
if path == "/labels.json" or path.endswith((".html", ".js", ".css")):
self.send_header("Cache-Control", "no-store")
super().end_headers()
def do_GET(self) -> None: # noqa: N802 (stdlib override) def do_GET(self) -> None: # noqa: N802 (stdlib override)
if self.path.split("?", 1)[0] == "/labels.json": if self.path.split("?", 1)[0] == "/labels.json":
self._send_labels() self._send_labels()
@@ -99,7 +105,6 @@ class DemoHandler(SimpleHTTPRequestHandler):
self.send_response(HTTPStatus.OK) self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body))) self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers() self.end_headers()
self.wfile.write(body) self.wfile.write(body)

View File

@@ -27,6 +27,7 @@ operation-room-monitor/
- 复制 `backend/.env.example``backend/.env` 并填写 - 复制 `backend/.env.example``backend/.env` 并填写
- 算法子进程包:`backend/algorithm_subprocesses/5.15/`(含 `main.py``weights/`;镜像构建时会 `COPY` 进容器,勿在 `.dockerignore` 中整目录排除) - 算法子进程包:`backend/algorithm_subprocesses/5.15/`(含 `main.py``weights/`;镜像构建时会 `COPY` 进容器,勿在 `.dockerignore` 中整目录排除)
- 标注视频中文字体:镜像内已安装 `fonts-noto-cjk``fonts-wqy-microhei`(供 `visualize_result_video.py` 绘制耗材标签) - 标注视频中文字体:镜像内已安装 `fonts-noto-cjk``fonts-wqy-microhei`(供 `visualize_result_video.py` 绘制耗材标签)
- 医生识别MediaPipe Pose镜像内已安装 `libgles2``libegl1`;子进程强制 CPU delegate避免无 GPU 图形栈时出现 `libGLESv2.so.2` 错误
- 可选备用权重:`backend/app/resources/actionformer_epoch_045.pth.tar` - 可选备用权重:`backend/app/resources/actionformer_epoch_045.pth.tar`
--- ---