fix(ceus): 降低长视频内存峰值,快照开跑即写,build 核对 CEUS 运行与交付包
- 内存:prepare_segmentation 改为流式解码,只留分屏和配准的抽样帧,绿框逐帧在两半上找; 分割那一半由 SegmentationInput.target_frames() 到用时再解码,segment_video 边读边写 jpg; MedSAM2 init_state 的读帧换成直接在存放设备上建张量(不改 MedSAM2 源码,结果逐位相同); 传播结果写进预分配数组。帧数最多的一段视频常驻内存峰值 11.2 GiB → 2.3 GiB,分辨率最大的 一段原代码被 OOM 杀掉、现为 4.2 GiB;两段的掩码和全部产物与改前逐字节相同,33 段视频的 第 1–2 步结果与改前一致,29 例配准与 review.csv 一致。 - 快照:ceus_quant 开跑时写 snapshot/(git、ceus.yaml、review.csv、uv.lock、segment.json), 配置和核对表从快照副本读;收尾由新函数 runs.finish_snapshot 只新建 finished.json。 build 与 ceus_align_review 同样改用 finish_snapshot 记结束时间。 - build:summary.json 加 video_sha256;装 CEUS 结果时核对快照里的 delivery_dir、逐例核对 视频 SHA-256 和推注时间,不同就整次停下;新字段做类型检查,不进 tic.json。 来源:主会话任务(问题 1–3 及补充的 delivery_dir 比对)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,7 @@ def _key_tree(node: Any) -> Any:
|
||||
def test_c15_skips_failed_and_missing_cases(tmp_path: Path) -> None:
|
||||
run = write_run(
|
||||
tmp_path / "runs" / RUN_ID,
|
||||
tmp_path / "delivery",
|
||||
{"U001": LOCATED, "U004": LOCATED},
|
||||
[("U002", 2, "两半都没有绿框")],
|
||||
)
|
||||
@@ -95,7 +96,11 @@ def test_c15_skips_failed_and_missing_cases(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
def test_tic_json_has_exactly_the_listed_fields(tmp_path: Path) -> None:
|
||||
result = _build(tmp_path, 1, write_run(tmp_path / "runs" / RUN_ID, {"U001": LOCATED}, []))
|
||||
result = _build(
|
||||
tmp_path,
|
||||
1,
|
||||
write_run(tmp_path / "runs" / RUN_ID, tmp_path / "delivery", {"U001": LOCATED}, []),
|
||||
)
|
||||
(alias,) = list_aliases(result.library_dir)
|
||||
tic = read_ultrasound(result.library_dir, alias).tic
|
||||
perfusion = {
|
||||
@@ -126,7 +131,7 @@ def test_tic_json_has_exactly_the_listed_fields(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
def test_keyframes_metadata_dropped_and_pixels_kept(tmp_path: Path) -> None:
|
||||
run = write_run(tmp_path / "runs" / RUN_ID, {"U001": LOCATED}, [])
|
||||
run = write_run(tmp_path / "runs" / RUN_ID, tmp_path / "delivery", {"U001": LOCATED}, [])
|
||||
result = _build(tmp_path, 1, run)
|
||||
(alias,) = list_aliases(result.library_dir)
|
||||
route = read_ultrasound(result.library_dir, alias)
|
||||
@@ -159,7 +164,9 @@ def test_null_ceus_run_has_no_ceus_files(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
def test_scan_finds_nothing_then_hits_video_id_in_tic_json(tmp_path: Path) -> None:
|
||||
run = write_run(tmp_path / "runs" / RUN_ID, {"U001": LOCATED, "U002": LOCATED}, [])
|
||||
run = write_run(
|
||||
tmp_path / "runs" / RUN_ID, tmp_path / "delivery", {"U001": LOCATED, "U002": LOCATED}, []
|
||||
)
|
||||
result = _build(tmp_path, 2, run)
|
||||
forbidden = ["P001", "P002", "U001", "U002", "U001_roi2.mp4", "U002_roi2.mp4", "U999"]
|
||||
assert scan_for_leaks(result.library_dir, forbidden, ("name",)) == []
|
||||
@@ -173,7 +180,7 @@ def test_scan_finds_nothing_then_hits_video_id_in_tic_json(tmp_path: Path) -> No
|
||||
|
||||
|
||||
def test_reason_with_video_id_stops_build(tmp_path: Path) -> None:
|
||||
run = write_run(tmp_path / "runs" / RUN_ID, {"U001": LOCATED}, [])
|
||||
run = write_run(tmp_path / "runs" / RUN_ID, tmp_path / "delivery", {"U001": LOCATED}, [])
|
||||
content = summary("U001", LOCATED)
|
||||
content["anchor_frames"]["D4"]["reason"] = "U001 的 D4 不在视频里"
|
||||
(run / "U001" / "summary.json").write_text(json.dumps(content), encoding="utf-8")
|
||||
@@ -183,7 +190,9 @@ def test_reason_with_video_id_stops_build(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
def test_keyframe_that_cannot_be_reencoded_skips_case_as_c14(tmp_path: Path) -> None:
|
||||
run = write_run(tmp_path / "runs" / RUN_ID, {"U001": LOCATED, "U002": LOCATED}, [])
|
||||
run = write_run(
|
||||
tmp_path / "runs" / RUN_ID, tmp_path / "delivery", {"U001": LOCATED, "U002": LOCATED}, []
|
||||
)
|
||||
Image.new("I;16", (4, 4)).save(run / "U002" / "keyframes" / "D2.png", format="PNG")
|
||||
result = _build(tmp_path, 2, run)
|
||||
assert [r.case_no for r in read_aliases(result.answers_dir)] == ["001"]
|
||||
@@ -199,22 +208,72 @@ def _assert_stops(tmp_path: Path, run: Path, error: type[Exception]) -> None:
|
||||
assert not (tmp_path / "library").exists() and not (tmp_path / "answers").exists()
|
||||
|
||||
|
||||
def test_video_changed_since_run_stops(tmp_path: Path) -> None:
|
||||
"""交付包里的视频内容和 CEUS 运行时记的 SHA-256 不同:运行不是在这份交付包上跑的,整次停下。"""
|
||||
run = write_run(tmp_path / "runs" / RUN_ID, tmp_path / "delivery", {"U001": LOCATED}, [])
|
||||
root = make_delivery(1).write(tmp_path / "delivery")
|
||||
(root / "CEUS_agent" / "001" / "U001_roi2.mp4").write_bytes(b"another video")
|
||||
with pytest.raises(ValueError, match=r"U001_roi2\.mp4 的 SHA-256"):
|
||||
run_build(write_config(tmp_path, root, 1, None, run), REPO_ROOT, NOW)
|
||||
assert not (tmp_path / "library").exists() and not (tmp_path / "answers").exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("delivery_bolus", "summary_offset", "message"),
|
||||
[
|
||||
(7, 5.0, "推注时间 7 秒"), # 交付包的推注时间改了
|
||||
(5, 5, "应为 float"), # summary 里的推注时间不是浮点数
|
||||
],
|
||||
)
|
||||
def test_injection_offset_mismatch_stops(
|
||||
tmp_path: Path, delivery_bolus: int, summary_offset: object, message: str
|
||||
) -> None:
|
||||
run = write_run(tmp_path / "runs" / RUN_ID, tmp_path / "delivery", {"U001": LOCATED}, [])
|
||||
content = summary("U001", LOCATED)
|
||||
content["injection_offset_s"] = summary_offset
|
||||
(run / "U001" / "summary.json").write_text(json.dumps(content), encoding="utf-8")
|
||||
delivery = make_delivery(1)
|
||||
delivery.case(1).ultrasound["推注开始时间s"] = delivery_bolus
|
||||
root = delivery.write(tmp_path / "delivery")
|
||||
with pytest.raises(ValueError, match=message):
|
||||
run_build(write_config(tmp_path, root, 1, None, run), REPO_ROOT, NOW)
|
||||
assert not (tmp_path / "library").exists() and not (tmp_path / "answers").exists()
|
||||
|
||||
|
||||
def test_run_on_other_delivery_dir_stops(tmp_path: Path) -> None:
|
||||
"""快照里记的交付包路径与 build 的不同(规范化后比):整次停下;写法不同但同一路径照常装。"""
|
||||
root = make_delivery(1).write(tmp_path / "delivery")
|
||||
same = write_run(
|
||||
tmp_path / "runs" / RUN_ID, tmp_path / "x" / ".." / "delivery", {"U001": LOCATED}, []
|
||||
)
|
||||
result = run_build(write_config(tmp_path, root, 1, None, same), REPO_ROOT, NOW)
|
||||
assert len(list_aliases(result.library_dir)) == 1
|
||||
other = write_run(tmp_path / "runs" / "other", tmp_path / "delivery_old", {"U001": LOCATED}, [])
|
||||
build_dir = tmp_path / "second"
|
||||
build_dir.mkdir()
|
||||
with pytest.raises(ValueError, match=r"paths\.delivery_dir"):
|
||||
run_build(write_config(build_dir, root, 1, None, other), REPO_ROOT, NOW)
|
||||
assert not (build_dir / "library").exists() and not (build_dir / "answers").exists()
|
||||
|
||||
|
||||
def test_run_dir_missing_stops(tmp_path: Path) -> None:
|
||||
_assert_stops(tmp_path, tmp_path / "runs" / RUN_ID, FileNotFoundError)
|
||||
|
||||
|
||||
def test_partial_run_dir_stops(tmp_path: Path) -> None:
|
||||
run = write_run(tmp_path / "runs" / f"{RUN_ID}.partial", {"U001": LOCATED}, [])
|
||||
run = write_run(
|
||||
tmp_path / "runs" / f"{RUN_ID}.partial", tmp_path / "delivery", {"U001": LOCATED}, []
|
||||
)
|
||||
_assert_stops(tmp_path, run, ValueError)
|
||||
|
||||
|
||||
def test_run_dir_without_failed_csv_stops(tmp_path: Path) -> None:
|
||||
run = write_run(tmp_path / "runs" / RUN_ID, {"U001": LOCATED}, [])
|
||||
run = write_run(tmp_path / "runs" / RUN_ID, tmp_path / "delivery", {"U001": LOCATED}, [])
|
||||
(run / "failed.csv").unlink()
|
||||
_assert_stops(tmp_path, run, FileNotFoundError)
|
||||
|
||||
|
||||
def test_run_dir_without_snapshot_stops(tmp_path: Path) -> None:
|
||||
run = write_run(tmp_path / "runs" / RUN_ID, {"U001": LOCATED}, [])
|
||||
run = write_run(tmp_path / "runs" / RUN_ID, tmp_path / "delivery", {"U001": LOCATED}, [])
|
||||
(run / "snapshot" / "snapshot.json").unlink()
|
||||
_assert_stops(tmp_path, run, FileNotFoundError)
|
||||
|
||||
Reference in New Issue
Block a user