feat(review): 医生核对包入口;CEUS 运行目录的读取收进 runs

- 新模块 review(组织层,只依赖 library、answers、runs):把一次构建和它装入的
  CEUS 运行整理成医生可读的文件夹(CSV 带 BOM、说明与报告转 PDF、图片视频逐字节拷贝),
  入口 scripts/review_pack.py + config/review_pack.yaml。任务书 03。
- 读 CEUS 运行目录(failed.csv、结果目录、关键帧文件)从 build 挪进 runs,build 与
  review 都经它读;failed.csv 读取在边界上收严(步号须为整数、视频号不重复、列数固定)。
- library、answers 补只读函数;import-linter 增 review 契约。
- 文档:guides/医生核对包.md(过一轮硕士生阅读),结构.md 第三至八部分同步。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 06:04:19 -04:00
parent 14d6f8e153
commit c7373efde2
23 changed files with 1718 additions and 83 deletions
+37 -14
View File
@@ -98,10 +98,11 @@ runs/ceus/<运行号>/ ────► ② 装配两个库 config/bu
| 层 | 模块 | 藏住的决定 | 旧项目里的对应内容 |
| --- | --- | --- | --- |
| 入口 | `scripts/`(不在包内) | 人怎么操作:每段一个脚本(`ceus_quant`、`build`、`consult`、`evaluate`),另有一个按顺序调用四段的总入口;读配置文件,把模块装配起来 | `debugs/t1_local_debug.py`、`debugs/tic_lognormal_replay.py` |
| 入口 | `scripts/`(不在包内) | 人怎么操作:每段一个脚本(`ceus_quant`、`build`、`consult`、`evaluate`),另有一个按顺序调用四段的总入口,以及出医生核对包的 `review_pack`;读配置文件,把模块装配起来 | `debugs/t1_local_debug.py`、`debugs/tic_lognormal_replay.py` |
| 组织 | build | 两个库怎么装配:每一路放哪些内容(可见性的唯一决定处)、化名怎么取(化名随机,资料库按化名排序,顺序就与交付顺序无关)、清单记什么。全项目只有它同时碰得到原始资料和答案 | `app/domain/input_readers.py`、`ingestion*.py` 的装配部分 |
| 组织 | consult | 会诊怎么进行:几个专家、谁先说、后发言的看得到前面的什么、什么时候结束;「调一次模型」的统一入口(先存请求、记开始、调用、存回复、记结束)也在这里 | 没有 |
| 组织 | evaluate | 评价口径:指标、统计方法、字段级对照怎么计分 | 没有(`core/eval` 已删) |
| 组织 | review | 给医生的核对包长什么样:目录布局、每个文件的格式(表格 CSV、文字 PDF、图和视频原样拷贝)、问题清单的来源与写法、说明模板。只读已构建好的两个库和那次 CEUS 运行,不写它们、不碰交付包 | 没有 |
| 格式 | library | 给专家的资料库的目录布局、文件格式和读取;按路分别读出,一路一个对象 | `app/domain/input_packages.py` 的数据形状 |
| 格式 | answers | 标准答案库的目录布局、文件格式和读取 | 旧库里的金标准表 |
| 格式 | verdicts | 会诊产物(各路意见、会诊结论)的文件格式和读取;consult 写、evaluate 读 | 没有 |
@@ -109,7 +110,7 @@ runs/ceus/<运行号>/ ────► ② 装配两个库 config/bu
| 外部 | segment | 用哪个分割模型、权重在哪、怎么在显卡上把提示传播成逐帧掩码 | `debugs/medsam2_local_track.py`、`roi_inference/` 的后端 |
| 外部 | llm | 经 PolyGateway(实验室的模型调用库,管外部错误的重试和超时)调模型;交出服务商返回的精确版本号、响应编号、用量、结束原因。只管调用,不写账本 | `app/providers/model_gateway_config.py`(只借思路);ClinDB 的 `llm/` |
| 外部 | ledger | 账本怎么存:SQLite 只追加、大块内容按哈希存成文件、同一目录同时只许一个进程写、找出开始了没结束的调用 | 没有;ClinDB 的 `ledger/` |
| 外部 | runs | 运行目录怎么命名、运行快照记什么(git 提交号、配置、所用构建、提示词哈希、模型版本、随机种子) | 没有 |
| 外部 | runs | 运行目录怎么命名、运行快照记什么(git 提交号、配置、所用构建、提示词哈希、模型版本、随机种子);一次 CEUS 定量运行目录(`runs/ceus/<运行号>/`)的布局和读取,build 和 review 都经它读 | 没有 |
| 纯计算 | ceus | CEUS 定量的全部算法和参数:分屏、绿框、坐标映射、多边形、暗区、TIC 采样、Lognormal 拟合、关键帧、灌注指标、出图;视频解码也在这里 | `app/domain/ceus_roi/`(除 `service.py`)、`app/providers/video_decoder.py` |
暂不设共用词汇模块。一样东西要被两个互相不能引用的模块用到,才另立一处放它;目前跨模块的类型各有归属(资料库格式归 library、答案格式归 answers、会诊产物格式归 verdicts)。配置的字段定义和校验归使用它的模块:CEUS 参数归 ceus(旧 `research_config.py`),会诊的实验配置归 consult;每个字段必填,不补默认值(`AGENTS.md`「不设默认值」)。
@@ -118,7 +119,7 @@ runs/ceus/<运行号>/ ────► ② 装配两个库 config/bu
```
入口 scripts/
组织 build consult evaluate
组织 build consult evaluate review
格式 library answers verdicts
外部 delivery segment llm ledger runs
纯计算 ceus
@@ -133,6 +134,7 @@ runs/ceus/<运行号>/ ────► ② 装配两个库 config/bu
| build | delivery、ceus(读 CEUS 结果的类型)、library、answers、runs |
| consult | library、verdicts、llm、ledger、runs |
| evaluate | answers、verdicts、runs |
| review | library、answers、runs |
| scripts/ | 任何模块(不在导入检查范围内,见第五部分) |
### 几条边界
@@ -142,6 +144,8 @@ runs/ceus/<运行号>/ ────► ② 装配两个库 config/bu
- **三路互相隔开。** 每个专家的提示词只用自己那一路的对象拼。consult 里拼某一路提示词的函数只接收那一路的对象,不接收整个病例。这是研究设计本身:模拟各科医生各看各的检查结果,再在会诊时交换意见。会诊时一路能看到另一路的什么,由 consult 的会诊流程决定,在会诊的任务书里定。
- **我们自己调模型只有一个入口:consult 的「调一次模型」。** 它按固定顺序做五步:把完整请求存进账本的大块存储、写「调用开始」、经 llm 调用、把完整回复存进大块存储、写「调用结束」。llm 只管调用,不写账本,所以任何一次模型调用都绕不开登记。中断后重启时,从账本找出开始了没结束的调用,记为「状态未知」,不重做已有结果的调用。做重复运行(同一病例、同一专家调多次,测一致性)时,每一次都恰好登记一次。
- **CEUS 算法不知道分割模型是谁,也不管中间文件放哪。** ceus 接收视频路径、参数和分割结果,交出曲线、拟合、关键帧和指标;中间文件写在哪、按什么复用,由 `scripts/ceus_quant.py` 决定(目前只有它一个使用者,出现第二个使用者再收进模块)。segment 只管「给提示、出逐帧掩码」,不认识分屏和绿框。
- **CEUS 运行目录只有 runs 认得。** `failed.csv`、`summary.json`、关键帧、TIC 曲线图、回放视频各在运行目录的哪里,由 runs 读出;build 和 review 都经它读,不各自拼路径(任务书 03:出现第二个使用者,就把读取收进模块)。`summary.json` 里挑哪些字段给专家仍只在 build 决定。
- **核对包只读不写。** review 读某次构建的 `library/`、`answers/` 和资料库记的那次 CEUS 运行,输出只写进 `runs/review_pack/<运行号>/`;不写两个库、不读交付包。核对包里有答案和真实病例号,是给医生的,不是给会诊的。
- **医生的判断不进 CEUS 定量。** 医生标注工作簿里的时刻(如造影剂到达时间 AT)属于标准答案库,ceus 和 build 都不读它;关键帧全部由程序自己算或按推注时间取(design 0002 决定 6)。交付视频里医生画的绿框是 CEUS 定量的输入,它能否作为输入、在最终病灶轮廓里占多大分量,记在 `../待定问题/01-医生画的绿框能否作为输入.md`。
- **评价只读文件,不重新调模型。** evaluate 读 `runs/` 里的会诊产物(经 verdicts)和 `answers/`,改指标或统计方法不需要重跑会诊。
@@ -192,6 +196,10 @@ library/<构建号>/
- 跳过清单:没进资料库的病例、没过的那一项检查、涉及的文件和列,以及对不上的各表原值,供医生核对。40 例里已知有 6 例临床表与超声表对不上(`../待定问题/04-交付数据的跨表不一致.md`),会出现在这份清单里;
- 将来加医生标注工作簿。
### 医生核对包(`runs/review_pack/<运行号>/`)
把一次构建的两个库和它装入的那次 CEUS 运行,整理成医生不用写代码就能看的一个文件夹(表格 CSV、文字 PDF、图和视频逐字节拷贝),由负责人打包交给临床方核对我们的处理结果。病例目录用医生的病例号,问题清单只写程序观察到的现象;包里有真实病例号、报告原文和诊断,只在临床方与我们之间传递。它不是流水线的一段:不产出任何后续段要读的东西,医生每交一版数据、跑完 CEUS 定量和装配之后出一次。包内布局和每个文件的格式见 `../任务书/03-医生核对包.md` 和包里的 `说明.pdf`,怎么跑见 `../guides/医生核对包.md`。
### CEUS 定量的产物(`runs/ceus/` 与 `work/`)
每次运行的产物写进 `runs/ceus/<运行号>/`,只追加;全量运行之前的配准目视核对写进 `runs/ceus_align/<运行号>/`(图、`key.csv`、判读、`review.csv`),ceus_quant 按 `review.csv` 拦下核对没通过的病例;第 3 步的分割结果按下表的键缓存在 `work/`,可以删掉重算。具体布局见 `../任务书/02-CEUS定量.md`。
@@ -240,7 +248,7 @@ X 是超声报告工作簿的「推注开始时间s」,含义是视频第 0
| `data/` | 医院交付的原始资料 | 否 |
| `library/`、`answers/` | 两个库,每次构建一个目录 | 否 |
| `work/` | 可以删掉重算的中间文件,目前只有分割缓存 | 否 |
| `runs/` | CEUS 定量、会诊与评价的运行,只追加不删 | 否 |
| `runs/` | CEUS 定量、医生核对包、会诊与评价的运行,只追加不删 | 否 |
| `models/` | 模型权重 | 否 |
| `reference/`、`reference-pdf/` | 旧项目副本、外部代码、文献原文 | 否 |
@@ -255,6 +263,7 @@ X 是超声报告工作簿的「推注开始时间s」,含义是视频第 0
| `build.yaml` | `scripts/build.py` | `delivery_dir`(交付包位置)、`library_root`、`answers_root`(两个库的上级目录)、`alias_seed`(生成化名的随机种子)、`decode_codes`(编码解码开关)、`code_maps`(映射表位置或 null)、`ceus_run`(CEUS 运行目录或 null);字段定义和校验在 `src/cadx/build/_config.py`,逐项说明见 `../guides/装配两个库.md` |
| `code_maps.yaml` | build(经 `build.yaml` 引用) | 编码映射表:交付包里每个用数字编码的列,每个数字代表什么(如性别的 1、2)。还没向临床方拿到,尚未建 |
| `ceus.yaml` | `scripts/ceus_quant.py`、`scripts/ceus_align_review.py` | CEUS 定量的全部参数和配准目视核对的出图设置,每个参数用注释写明出处 |
| `review_pack.yaml` | `scripts/review_pack.py` | `library_dir`、`answers_dir`(同一次构建的两个库,构建号必须相同)、`output_root`(核对包运行目录的上级);用哪次 CEUS 运行从资料库清单读,不另配;字段定义和校验在 `src/cadx/review/_config.py`,逐项说明见 `../guides/医生核对包.md` |
| `consult.yaml` | `scripts/consult.py` | 会诊的实验配置与模型用途 |
| `evaluate.yaml` | `scripts/evaluate.py` | 评价的指标与统计设置 |
@@ -270,16 +279,17 @@ X 是超声报告工作簿的「推注开始时间s」,含义是视频第 0
文件格式:数组用 npz(读取时 `allow_pickle=False`,pickle 读取时会执行任意代码),每例的标量和摘要用 json,跨病例的表用 csv,图用 png。缺失值写 null,不写 NaN 或 0:0 是合法的测量值,NaN 在 json 里不合法。
导入边界用 import-linter 检查,写进 `make check`。契约在 `pyproject.toml`,检查范围是 `src/cadx`,**不含 `scripts/` 和 `tests/`**。下面只列契约实际强制的(共 8 条),没有的保护不写:
导入边界用 import-linter 检查,写进 `make check`。契约在 `pyproject.toml`,检查范围是 `src/cadx`,**不含 `scripts/` 和 `tests/`**。下面只列契约实际强制的(共 9 条),没有的保护不写:
- `cadx` 里只有 segment 引用 `torch`、`sam2`(契约逐个列出其余已落地的模块,新模块落地时加进去)。
- segment 不引用其他 `cadx` 模块。
- delivery、library、answers、runs 互不引用。
- delivery、library、answers、runs 不引用其他 `cadx` 模块(build 和规划中的各模块)。
- delivery、library、answers、runs 不引用其他 `cadx` 模块(build、review 和规划中的各模块)。
- ceus 不引用其他 `cadx` 模块(含 segment:分割结果由 `scripts/ceus_quant.py` 传进来)。
- build 不引用 delivery、library、answers、runs 以外的 `cadx` 模块(ceus 落地时放行)。
- review 不引用 library、answers、runs 以外的 `cadx` 模块(不碰交付包,不依赖 build 和 ceus)。
- 只有 delivery 引用 `openpyxl`(交付包的 Excel 只有它碰)。
- 只有 library 引用 `PIL`(写资料库的图片时去掉元数据)。
- 只有 library 引用 `PIL`(写资料库的图片时去掉元数据;review 拷图片只按字节拷,不打开)。
第三部分依赖表的其余部分和下面几条外部库独占,随模块落地逐条写成契约(import-linter 只能检查已经存在的模块):只有 llm 引用 `polygateway`;只有 ledger 引用 `sqlite3`。
@@ -302,16 +312,17 @@ Python 环境用 uv;安装与检查命令见 `../guides/开发环境.md`。
| ceus | 6000–7500(基本照搬旧算法,去掉只为产品层服务的部分) | 7103 | `ceus_roi/` 除 `service.py` 7330,`video_decoder.py` 135 |
| segment | 150–300 | 300 | `medsam2_local_track.py` 240 |
| delivery | 400–700 | 739 | `clinical.py` 266、`modalities.py` 522、`workbook.py` 99、`delivery.py` 134,共 1021,其中入库与数据库部分不迁 |
| library | 150–300 | 354 | `input_packages.py` 104 |
| answers | 100–200 | 201 | — |
| library | 150–300 | 377 | `input_packages.py` 104 |
| answers | 100–200 | 227 | — |
| verdicts | 100–200 | — | — |
| build | 300–500 | 564(含装入 CEUS 结果的 `_ceus.py`) | `input_readers.py` 329 的装配部分 |
| build | 300–500 | 535(含装入 CEUS 结果的 `_ceus.py`;读 CEUS 运行目录已挪进 runs) | `input_readers.py` 329 的装配部分 |
| llm | 200–450 | — | ClinDB `llm/` 415 |
| ledger | 200–300 | — | ClinDB `ledger/` 274 |
| runs | 100–250 | 109(只含快照部分) | — |
| runs | 100–250 | 202(快照 `_snapshot.py` 98、读 CEUS 运行的 `_ceus_run.py` 83、`__init__.py` 21) | — |
| consult | 500–1000(会诊口径定后重估) | — | — |
| evaluate | 300–600(评价口径定后重估) | — | 已删的 `core/eval` 622 |
| scripts/ 每个 | ≤ 300 | `build.py` 37、`scan_library.py` 88、`ceus_quant.py` 403(超出,含目视核对表的读取与核对、开跑即写快照)、`ceus_align_review.py` 243 | `t1_local_debug.py` 762、`tic_lognormal_replay.py` 627 |
| review | 300–500 | 587(`_pack.py` 395、`_pdf.py` 118、`_config.py` 64、`__init__.py` 10;不含说明模板 `readme_template.md` 202 行) | — |
| scripts/ 每个 | ≤ 300 | `build.py` 37、`scan_library.py` 88、`ceus_quant.py` 403(超出,含目视核对表的读取与核对、开跑即写快照)、`ceus_align_review.py` 243、`review_pack.py` 37 | `t1_local_debug.py` 762、`tic_lognormal_replay.py` 627 |
---
@@ -351,6 +362,7 @@ Python 环境用 uv;安装与检查命令见 `../guides/开发环境.md`。
- `LibraryCase`:要写进资料库的一例:化名、临床一路的字段、超声描述、关键图源文件、MR 描述、CEUS 部分(`LibraryCeus` 或 None)。
- `LibraryCeus`:一例超声一路的 CEUS 部分:build 挑好的 tic.json 内容、槽位 → 关键帧源图。
- `list_aliases(build_dir)`:一次构建的全部化名(按化名排序)。
- `read_build_info(build_dir)`:读构建清单的概况,交出 `BuildInfo`:构建号、编码列是否解码、装入的 CEUS 运行号(没装为 None)。
- `read_clinical(build_dir, alias)`、`read_ultrasound(build_dir, alias)`、`read_mr(build_dir, alias)`:按路读出一例,读时核对 SHA-256,拒绝 `.partial` 目录。
- `ClinicalRoute`:临床一路:是否已解码、22 列字段。
- `UltrasoundRoute`:超声一路:报告「所见」原文、关键图路径、tic.json 的内容(没装 CEUS 时为 None)、槽位 → 关键帧路径(没装时为空)。
@@ -365,15 +377,19 @@ Python 环境用 uv;安装与检查命令见 `../guides/开发环境.md`。
- `DiagnosisRow`:化名、病例号、三张表的诊断编码、诊断名(不解码时为 None)。
- `SkippedRow`:跳过清单的一行,字段同 `SkipRecord`。
- `ReviewRow`:复核清单的一行:化名、路(「超声」或「MR」)、命中的词、所在句子。
- `read_aliases(answers_dir)`、`read_diagnoses(answers_dir)`:评价用的读取,读时核对 SHA-256 和表头,拒绝 `.partial` 目录。
- `read_aliases(answers_dir)`、`read_diagnoses(answers_dir)`、`read_skipped(answers_dir)`:读化名对照、诊断、跳过清单,读时核对 SHA-256 和表头,拒绝 `.partial` 目录;跳过清单的文件和列两栏拆回元组。
- `verified_file(answers_dir, name)`:答案库里某个文件(如 `snapshot/build.yaml`)的路径,先核对它与清单的 SHA-256 一致;给需要原样拷贝或读快照的使用者(review)用。
### runs(`src/cadx/runs/`,目前只有快照部分)
### runs(`src/cadx/runs/`)
- `read_git_state(repo_root)`:读出完整和短提交号、工作区是否干净(含未跟踪文件)、`git diff HEAD` 输出的 SHA-256。
- `GitState`:上面这四项。
- `make_build_id(now, git)`:构建号 `YYYYMMDD-HHMMSS-<git 短哈希>`,工作区不干净时末尾加 `-dirty`。
- `write_snapshot(snapshot_dir, build_id, git, started_at, uv_lock, configs)`:开跑时调。新建快照目录,复制配置原文并写 `snapshot.json`(构建号、git 状态、Python 版本、`uv.lock` 哈希、开始时间、各配置的哈希)。运行要跑很久时(CEUS 定量约一小时)在开跑时写,期间改了配置文件,快照里仍是这次用的那份。
- `finish_snapshot(snapshot_dir, finished_at, record)`:收尾时调。在快照目录里新建 `finished.json`:结束时间加 `record`(运行结束才知道的值,如显存峰值、缓存命中数);已存在就报错,不改开跑时写的文件。
- `open_ceus_run(run_dir)`:打开一次已完成的 CEUS 定量运行:核对目录存在、不是 `.partial`、有 `snapshot/snapshot.json`,读快照里的 `ceus.yaml` 和 `failed.csv`(表头、步号、视频号不重复),交出 `CeusRun`。
- `CeusRun`:运行号、目录、快照里的整份 `ceus.yaml`、`failed`(视频号 → `CeusFailure`);按视频号取文件:`has_result`(有没有结果目录)、`summary`(`summary.json`,核对里面记的视频号与目录一致)、`keyframes`(槽位 → 关键帧图路径)、`tic_png`、`overlay`。
- `CeusFailure`:`failed.csv` 的一行:失败在第几步、原因原文。
### ceus(`src/cadx/ceus/`)
@@ -413,3 +429,10 @@ Python 环境用 uv;安装与检查命令见 `../guides/开发环境.md`。
- `load_build_config(path, repo_root)`:读并校验 `build.yaml`,相对路径按仓库根目录解析。
- `load_code_maps(path)`:读映射表并做 T5(恰好覆盖 17 项,性别映射到「男」「女」,诊断映射到四个亚型缩写)。
- `make_aliases(case_nos, seed)`:病例号排序后依次抽 4 位化名,同种子同病例得到同样的化名。
### review(`src/cadx/review/`)
- `run_review_pack(config_path, repo_root, now)`:出一次医生核对包:读配置,经 library、answers 读这次构建,经 runs 打开资料库记的那次 CEUS 运行(路径取自答案库快照里的 `build.yaml`,运行号须与资料库清单一致),先写进 `<运行号>.partial/` 再改名,交出 `PackSummary`。`now` 是本地时间,定运行号。
- `PackSummary`:供终端打印的计数:运行目录、核对包目录、病例数(进库、跳过)、按环节分的问题清单行数、PDF 份数、拷贝的视频总字节数;不含病例号。
- `ReviewPackConfig`:`review_pack.yaml` 的字段定义(pydantic,字段必填、不许多写、读入后不可改)。
- `load_review_config(path, repo_root)`:读并校验 `review_pack.yaml`,相对路径按仓库根目录解析;两个库的目录名(构建号)不同就报错。
+123
View File
@@ -0,0 +1,123 @@
# 医生核对包
医生核对包是一个文件夹,交给临床方的医生,请他们核对我们对交付资料的处理结果。医生不用写代码就能打开:表格是 CSV,文字是 PDF,图和视频原样拷贝。本文写给出包的人:什么时候跑、怎么跑、产物在哪、怎么交出去、报错了怎么办。为什么做、包里每个文件的格式,见 `../任务书/03-医生核对包.md`;它在代码里的位置见 `../explanation/结构.md` 第三、四部分。
## 先认识几个词
- **病例号**:交付包里每个病例的真实编号,核对包里写成 `P` 加三位数字(如 `P001`),和临床工作簿里的写法相同。
- **病灶**:病人胰腺上的那个囊性肿瘤,CEUS 定量要在视频里把它圈出来。
- **构建**:跑一次 `scripts/build.py`(`装配两个库.md`),产出同号的资料库 `library/<构建号>/` 和标准答案库 `answers/<构建号>/`。
- **资料库**:给 AI 专家看的资料,按临床、超声、MR 三路分开。
- **答案库**:放病理诊断(金标准,即术后或穿刺的病理结论)、化名与病例号的对照、跳过清单(没进资料库的病例和原因)、复核清单(报告里可能带诊断倾向的句子)。
- **资料库清单 `manifest.json`**:资料库根目录下的一个文件,记这次构建的构建号、每个文件的哈希,以及 `ceus_source`:这次构建装入的那次 CEUS 运行的运行号(没装为 null)。它只记运行号,不记运行目录的路径。
- **化名**:资料库里代替病例号的 4 位随机名字,只用在资料库里。交付表按诊断分组录入,病例号的先后本身带着答案。专家看到病例号,就可能从编号推出诊断(`AGENTS.md` 硬性约束 1)。
- **核对包为什么可以用病例号**:核对包是给医生看的,不给 AI 专家;里面本来就有诊断(答案库的诊断表原样在包里),不进专家能读到的任何资料。所以病例目录直接用病例号,化名只在表里出现,方便以后对回资料库。答案库原本「只给评价用」,核对包是另一类用途:把答案库连同资料库和 CEUS 结果一起交给临床方核对,范围仍限于我们和临床方之间(`AGENTS.md` 硬性约束 3)。
- **CEUS 定量运行**:跑一次 `scripts/ceus_quant.py`(`CEUS定量.md`),产物在 `runs/ceus/<CEUS运行号>/`。
- **`failed.csv`**:这次运行里记各病例在第几步失败、失败原因的表。
- **视频号**:每例 CEUS 视频的编号,`U` 加三位病例号(如 `U001`),CEUS 运行的结果目录按它命名。
- **TIC、拟合**:TIC(时间-强度曲线)是病灶区域平均亮度随时间的变化。拟合是用一条光滑的模型曲线(Lognormal,对数正态形状)去贴 TIC。拟合要过一道质量门才算成功;没成功时,关键帧 D1–D4 和灌注指标都取不到。
- **紫圈**:临床方在视频上画的紫色圈,圈一块正常胰腺作对照,程序也为它画一条 TIC。
- **关键帧、槽位**:从视频里挑出的 7 个时刻的画面,每个时刻叫一个槽位。B0 是刚开始变亮之前,D1–D4 是从拟合曲线上取的开始变亮、变亮最快、最亮、变暗最快,F1、F2 是推注造影剂后第 20、60 秒。细节见包里的 `说明.pdf`。
- **回放视频**:原视频逐帧叠加程序画的病灶轮廓等线条,医生用它看程序圈得对不对。
- **问题清单**:包里的 `问题清单.csv`,列出程序在每个病例上观察到的现象:某例没进资料库、CEUS 在第几步失败、拟合没完成、关键帧没取到、紫圈曲线不可用。每条都抄自某个已有文件里的值,只写现象,不写原因推测和建议。这是负责人 2026-10-17 定的(任务书 03「不做什么」):包只请医生核对事实,推测会把医生的注意力引向我们的猜测。
- **三种编号**:格式都是 `YYYYMMDD-HHMMSS-<git 短哈希>`,工作区有未提交改动时末尾加 `-dirty`(`装配两个库.md` 第三节「构建号」)。三者各指一样东西,本文用不同的名字区分:
- **构建号**:一次构建,即 `library/<构建号>/`、`answers/<构建号>/`;
- **CEUS 运行号**:一次 CEUS 定量运行,即 `runs/ceus/<CEUS运行号>/`;
- **出包号**:出一次核对包,即 `runs/review_pack/<出包号>/`,取出包那一刻的时间。
## 一、什么时候跑
医生每交一版新数据,按顺序做完:
1. CEUS 定量 `scripts/ceus_quant.py`,得到新的 `runs/ceus/<CEUS运行号>/`;
2. 把 `config/build.yaml` 的 `ceus_run` 指向这个目录,跑 `scripts/build.py` 构建两个库,得到新的构建号;
3. 再跑本入口出核对包。
核对包只读已经构建好的两个库和那次 CEUS 运行,不碰交付包,也不改两个库。所以交付数据或处理规则变了,要先重新构建,再出包。一次构建可以出多次包,每次一个新的出包号。
没装 CEUS 结果的构建(`build.yaml` 的 `ceus_run` 为 null)也能出包。包里没有关键帧、TIC 曲线、TIC 结果和回放视频,病例总表的 CEUS 各列留空,`说明.pdf` 里写明这次没有 CEUS 结果。
## 二、怎么跑
### 改配置
配置是 `config/review_pack.yaml`,进 git:出包用了哪两个库、产物写到哪,看提交历史就能查到,运行可以复现;每次出包时,快照里还会存一份配置原文。字段定义和校验在 `src/cadx/review/_config.py` 的 `ReviewPackConfig`:每个字段必填,没有默认值;多写字段、拼错字段名、同一层重复的键都直接报错;相对路径**相对仓库根目录**解析,不能写空串。当前取值看配置文件本身,这里不抄。
| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `library_dir` | 路径 | 某次构建的资料库目录(`library/<构建号>/`) |
| `answers_dir` | 路径 | 同一次构建的答案库目录(`answers/<构建号>/`)。两个目录名必须相同,不同就报错 |
| `output_root` | 路径 | 核对包运行目录的上级目录,每次出包在它下面新建 `<出包号>/` |
### 用哪次 CEUS 运行
用哪次 CEUS 运行不在这里配,从这次构建自己的记录里取:
- **CEUS 运行号**:取资料库 `manifest.json` 的 `ceus_source`。
- **运行目录的路径**:取答案库快照里的 `build.yaml`(`answers/<构建号>/snapshot/build.yaml`,构建时那份配置的原文副本)的 `ceus_run`。路径不从 `manifest.json` 拿,因为它只记了运行号、没记路径;任务书 03 又要求不在 `review_pack.yaml` 里另配。
- 两者必须一致:`ceus_run` 的目录名要等于 `ceus_source`,不一致就报错。这样包里的 TIC 曲线和回放视频,一定来自资料库实际装入的那次运行。
- `ceus_source` 为 null 时(构建时 `ceus_run` 为 null),程序不读快照里的 `build.yaml`,直接出不含 CEUS 结果的包。
### 跑
```bash
uv run python scripts/review_pack.py --config config/review_pack.yaml
```
脚本只接收 `--config` 一个参数(`../explanation/结构.md` 第五部分「配置」)。它调 `pandoc` 和 `soffice`(LibreOffice 的命令行程序),把说明和每例的两份报告转成 PDF。两个工具必须已装在 PATH 上(本机已装;中文字体用已装的 Noto CJK)。40 例的包要拷贝每例的回放视频、转几十份 PDF,耗时以实际运行为准;超过约一分钟时按 `AGENTS.md`「本机环境的坑」放进 tmux 跑。
终端只打印计数,不打印病例号,可以贴进汇报:
- 运行目录、核对包目录;
- 病例数(进库、跳过各几例);
- 问题清单行数(按「环节」分);
- PDF 份数;
- 拷贝的视频总大小(没装 CEUS 结果时显示 `0.0 MiB`);
- 耗时。
## 三、产物在哪
```
runs/review_pack/<出包号>/ (output_root 下)
snapshot/ 快照:不给医生
医生核对包_<构建号>/ 交给医生的就是这个目录
```
- `snapshot/`:记「这个包是怎么出来的」,给我们自己复现用。
- `snapshot.json`:git 状态、Python 版本、`uv.lock` 哈希、开始时间、配置哈希;
- `review_pack.yaml`:配置原文副本;
- `review.json`:构建号、CEUS 运行号、pandoc 与 soffice 的版本行;
- `finished.json`:结束时间、病例数、问题清单行数。
- `医生核对包_<构建号>/`:目录布局、每张 CSV 的列和每个文件的来源,见任务书 03「核对包的内容」。也可以直接打开包里的 `说明.pdf`:它是写给医生的完整说明,包括目录结构、每一列的意思、处理流程、关键帧和 TIC 曲线怎么看、编码含义。这里不重抄,免得两处说法不一致。`说明.pdf` 的源文件是 `src/cadx/review/readme_template.md`。
运行目录先写成 `<出包号>.partial/`,全部写完再改名成 `<出包号>/`。看到 `.partial`,说明那次出包中途出错。它不会被自动删除,留着查原因,查完由人决定删不删。已有的运行目录不删、不覆盖(`AGENTS.md`「两档标准」)。
## 四、打包交给医生
1. 只打包 `医生核对包_<构建号>/` 这一个目录,**不带 `snapshot/`**:快照里是仓库的 git 状态和配置,对医生没用。例如:
```bash
cd runs/review_pack/<出包号>
zip -r 医生核对包_<构建号>.zip 医生核对包_<构建号>
```
打包后最好在另一台电脑上解开看一眼,中文文件名是否正常显示(医生多半用 Windows)。
2. 核对包里有患者资料:病例号、报告原文、诊断、超声画面和视频(`AGENTS.md` 硬性约束 3)。只在临床方和我们之间传递,由负责人用认可的渠道(如 U 盘)转交。不贴进文档、提交说明、Linear,不发给外部模型服务。
3. 交出去的包以后不改。医生改好交付资料后,按第一节重新跑一遍,得到新的构建号和新的核对包。
## 五、常见报错
| 报错 | 原因 | 怎么办 |
| --- | --- | --- |
| `资料库与答案库不是同一次构建:A ≠ B` | `review_pack.yaml` 里两个目录名(构建号)不同 | 把两项改成同一个构建号 |
| `资料库清单里的构建号 … 与目录名 … 不同` | 资料库目录被改过名 | 用原目录名;构建目录不要改名 |
| `… 是没写完的构建(.partial),不能读` | 配置指向了没跑完的构建(名字以 `.partial` 结尾) | 改指向一次跑完的构建 |
| `… 的 SHA-256 与清单不一致` | 资料库或答案库里的文件在构建之后被改过 | 不要手改构建目录;重新构建一次 |
| `运行目录已存在,不覆盖:…` | `output_root` 下已有同名的 `<出包号>/` 或 `<出包号>.partial/`(出包号带到秒,通常是同一秒内跑了两次) | 隔一秒重跑;不要删已有的运行目录来腾位置 |
| `找不到 pandoc` / `找不到 soffice` | 转 PDF 的工具没装或不在 PATH 上 | 装上 pandoc 和 LibreOffice,再跑 |
| `pandoc 的参考样式里没有 …` | 中文 PDF 里有的字会显示成日文写法,程序为此改了 pandoc 自带的样式文件,把中文字体设成 Noto CJK 简体。这条报错说明 pandoc 版本变了,样式文件里要改的那段已经不在,程序设不了中文字体 | 先查 `pandoc --version` 是否被升级过。能退回原版本就退回;不能就打开 `src/cadx/review/_pdf.py` 顶部的注释和 `_STYLE_FONTS`,按新版样式文件改写要替换的内容 |
| `soffice 没有转出 …` | LibreOffice 转 PDF 失败 | 先单独用 `soffice --headless --convert-to pdf` 转一个 docx 试试,看 LibreOffice 本身是否正常 |
| `… 的 ceus_run 是 …,与资料库记的 CEUS 运行号 … 不符` | 答案库快照里 `build.yaml` 的 `ceus_run`(路径)与资料库 `manifest.json` 的 `ceus_source`(CEUS 运行号)对不上 | 两个库不是同一次构建写出的,检查配置是否指错 |
| `CEUS 运行目录不存在` / `是没跑完的 .partial` / `缺快照` | 构建所用的 CEUS 运行目录被移走或删了 | CEUS 运行目录只追加不删;找回原目录 |
| `CEUS 运行 failed.csv 里的视频号 … 对不上这次构建的任何病例` | 那次 CEUS 运行里失败的病例不在这次构建里,既没进库,也不在跳过清单上 | 检查 `build.yaml` 的 `ceus_run` 与 `delivery_dir`(交付包目录)是否配套,即那次 CEUS 运行是否在这份交付包上跑的 |
| `tic.json 里有 TIC结果.csv 没有中文名的字段` | build 往 `tic.json`(资料库里每例的 CEUS 结果摘要)里加了新字段,核对包还没给它起中文名 | 在 `src/cadx/review/_pack.py` 的 `TIC_ITEMS` 里补上中文名,同时补说明模板 |