- 新模块 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>
13 KiB
医生核对包
医生核对包是一个文件夹,交给临床方的医生,请他们核对我们对交付资料的处理结果。医生不用写代码就能打开:表格是 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/<出包号>/,取出包那一刻的时间。
- 构建号:一次构建,即
一、什么时候跑
医生每交一版新数据,按顺序做完:
- CEUS 定量
scripts/ceus_quant.py,得到新的runs/ceus/<CEUS运行号>/; - 把
config/build.yaml的ceus_run指向这个目录,跑scripts/build.py构建两个库,得到新的构建号; - 再跑本入口出核对包。
核对包只读已经构建好的两个库和那次 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 结果的包。
跑
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「两档标准」)。
四、打包交给医生
-
只打包
医生核对包_<构建号>/这一个目录,不带snapshot/:快照里是仓库的 git 状态和配置,对医生没用。例如:cd runs/review_pack/<出包号> zip -r 医生核对包_<构建号>.zip 医生核对包_<构建号>打包后最好在另一台电脑上解开看一眼,中文文件名是否正常显示(医生多半用 Windows)。
-
核对包里有患者资料:病例号、报告原文、诊断、超声画面和视频(
AGENTS.md硬性约束 3)。只在临床方和我们之间传递,由负责人用认可的渠道(如 U 盘)转交。不贴进文档、提交说明、Linear,不发给外部模型服务。 -
交出去的包以后不改。医生改好交付资料后,按第一节重新跑一遍,得到新的构建号和新的核对包。
五、常见报错
| 报错 | 原因 | 怎么办 |
|---|---|---|
资料库与答案库不是同一次构建: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 里补上中文名,同时补说明模板 |