feat(build): 任务书 02 第四阶段代码——build 装入 CEUS 结果

- build.yaml 加必填字段 ceus_run(CEUS 运行目录或 null)。
- 不为 null 时按视频号找结果;在 failed.csv 里或缺结果目录的整例跳过,记 C15;关键帧图沿用 C14 检查。
- 资料库超声一路加 keyframes/<槽位>.png(去附加数据重编码)和 tic.json(白名单逐字段构造:
  四条曲线的拟合成败与 R²、灌注指标、紫圈曲线是否可用、7 个槽位的定位与视频时间或原因)。
- manifest 的 CEUS 来源写运行号;泄露扫描已覆盖 tic.json,补命中测试。
- 真实构建等配准改完、全量 CEUS 运行之后再做。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 05:55:57 -04:00
parent e78d1b50b8
commit c582fe426f
15 changed files with 683 additions and 35 deletions
+15 -8
View File
@@ -153,14 +153,20 @@ runs/ceus/<运行号>/ ────► ② 装配两个库 config/bu
```
library/<构建号>/
manifest.json 构建清单:每个文件的 SHA-256、构建时的 git 提交、配置哈希、所用 CEUS 结果的来源
manifest.json 构建清单:每个文件的 SHA-256、构建时的 git 提交、配置哈希、所用 CEUS 运行号(没装为 null)
cases/<化名>/
clinical/clinical.json 临床那一路
ultrasound/ 超声那一路:report.txt、key.png、keyframes/*.png、tic.json
(后两项等 CEUS 定量落地后由 build 加入,目前只有前两项)
ultrasound/ 超声那一路:report.txt、key.png;装了 CEUS 结果时另有
keyframes/<槽位>.png 和 tic.json
mr/report.txt MR 那一路
```
`build.yaml` 的 `ceus_run` 指向某次 CEUS 定量的运行目录时,build 才往超声一路装 CEUS 结果,为 null 时不装。装的时候:
- `keyframes/<槽位>.png`:下文关键帧表里定位到的那几张(槽位即 B0、D1 等),和 `key.png` 一样去掉附加数据重新编码;每例最多 8 张图(7 张关键帧加 `key.png`)。
- `tic.json`:从这一例的 `summary.json` 里逐字段挑出,此外什么都不写:四条曲线(`roi_all` 病灶整体、`roi_perf` 病灶去暗区、`roi_pan` 紫圈即医生圈的一块正常胰腺、`background_circle` 背景圆)各自的拟合是否成功(`success`)和拟合优度 R²(`r2`);病灶两条曲线的灌注指标(`perfusion_metrics`,拟合没过质量门为 null);紫圈曲线是否可用(`pan_curve_usable`);7 个槽位各自是否定位到(`located`)、定位到的视频时间(`video_time_s`,秒)、没定位的原因(`reason`)。视频号、文件路径、配置、帧号、拟合参数都不写。挑哪些字段只在 `src/cadx/build/_ceus.py` 里列。
- CEUS 在这一例上失败(在 `failed.csv` 里)或运行目录里没有这一例的结果,这一例整例跳过,记 C15。失败的病例要不要改成「超声不可评价、仍参与会诊」等 `../待定问题/03-病例排除标准.md` 定。
每一路能看见什么:
| 内容 | 临床 | 超声 | MR |
@@ -245,7 +251,7 @@ X 是超声报告工作簿的「推注开始时间s」,含义是视频第 0
| 文件 | 给谁用 | 内容 |
| --- | --- | --- |
| `build.yaml` | `scripts/build.py` | `delivery_dir`(交付包位置)、`library_root`、`answers_root`(两个库的上级目录)、`alias_seed`(生成化名的随机种子)、`decode_codes`(编码解码开关)、`code_maps`(映射表位置或 null);字段定义和校验在 `src/cadx/build/_config.py`,逐项说明见 `../guides/装配两个库.md` |
| `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` | CEUS 定量的全部参数,每个参数用注释写明出处 |
| `consult.yaml` | `scripts/consult.py` | 会诊的实验配置与模型用途 |
@@ -339,13 +345,14 @@ Python 环境用 uv;安装与检查命令见 `../guides/开发环境.md`。
### library(`src/cadx/library/`)
- `write_library(build_dir, build_id, git_commit, git_clean, config_sha256, codes_decoded, uncompared, ceus_source, cases)`:把一次构建的各例三路内容写进一个已存在的空目录,关键图去掉附加数据重新编码,最后写 `manifest.json`。关键图不能保真重编码时报错停下(说明 build 漏了 C14)。
- `write_library(build_dir, build_id, git_commit, git_clean, config_sha256, codes_decoded, uncompared, ceus_source, cases)`:把一次构建的各例三路内容写进一个已存在的空目录,关键图和关键帧去掉附加数据重新编码,有 CEUS 结果时写 `tic.json`,最后写 `manifest.json`(`ceus_source` 是 CEUS 运行号或 None,与每例的 `ceus` 是否为 None 必须配套)。图不能保真重编码时报错停下(说明 build 漏了 C14)。
- `check_key_image(path)`:关键图能逐像素保真重编码时返回 None,不能时(不是 PNG、位深或颜色类型不行、带 tRNS 透明色)返回原因。
- `LibraryCase`:要写进资料库的一例:化名、临床一路的字段、超声描述、关键图源文件、MR 描述。
- `LibraryCase`:要写进资料库的一例:化名、临床一路的字段、超声描述、关键图源文件、MR 描述、CEUS 部分(`LibraryCeus` 或 None)。
- `LibraryCeus`:一例超声一路的 CEUS 部分:build 挑好的 tic.json 内容、槽位 → 关键帧源图。
- `list_aliases(build_dir)`:一次构建的全部化名(按化名排序)。
- `read_clinical(build_dir, alias)`、`read_ultrasound(build_dir, alias)`、`read_mr(build_dir, alias)`:按路读出一例,读时核对 SHA-256,拒绝 `.partial` 目录。
- `ClinicalRoute`:临床一路:是否已解码、22 列字段。
- `UltrasoundRoute`:超声一路:报告「所见」原文、关键图路径。
- `UltrasoundRoute`:超声一路:报告「所见」原文、关键图路径、tic.json 的内容(没装 CEUS 时为 None)、槽位 → 关键帧路径(没装时为空)。
- `MrRoute`:MR 一路:报告「所见」原文。
- `scan_for_leaks(build_dir, forbidden_strings, forbidden_keys)`:在构建目录的全部 `.txt`、`.json` 里找禁止的字符串(NFKC 归一、不分大小写、按子串)和禁止的 JSON 键名,返回命中列表。
- `LeakHit`:一处命中:文件(相对构建目录)、类别(「字符串」或「键名」)、命中的词。
@@ -390,7 +397,7 @@ Python 环境用 uv;安装与检查命令见 `../guides/开发环境.md`。
### build(`src/cadx/build/`)
- `run_build(config_path, repo_root, now)`:完整做一次构建:读配置和映射表、读交付包、对每例做 C14(`check_key_image` 返回原因的整例跳过,记进跳过清单)、取化名、先写进 `.partial` 目录再改名,交出 `BuildSummary`。
- `run_build(config_path, repo_root, now)`:完整做一次构建:读配置和映射表、读交付包、对每例做 C14(`check_key_image` 返回原因的整例跳过,记进跳过清单);`ceus_run` 不为 null 时核对运行目录(不合规整次停下),对每例做 C15(CEUS 失败或没有结果的整例跳过)、关键帧也过 C14,按白名单构造 tic.json;取化名、先写进 `.partial` 目录再改名,交出 `BuildSummary`。
- `BuildSummary`:供终端打印的计数:构建号、两个库的路径、病例总数、进库数、跳过数、认不出编号的行数、按检查编号分的跳过数、复核清单条数、跳过清单说明句。
- `BuildConfig`:`build.yaml` 的字段定义(pydantic,字段必填、不许多写、读入后不可改)。
- `load_build_config(path, repo_root)`:读并校验 `build.yaml`,相对路径按仓库根目录解析。
+21 -10
View File
@@ -6,7 +6,8 @@
- **四类胰腺囊性肿瘤**:IPMN(导管内乳头状黏液性肿瘤)、SPN(实性假乳头状肿瘤)、MCN(黏液性囊性肿瘤)、SCN(浆液性囊性肿瘤)。项目要专家判断病灶属于哪一类,所以资料库里出现这些名字就等于泄露答案。
- **金标准**:术后或穿刺的病理结论,交付表里叫「病理类型」或 `Diagnosis`,用 1–4 的数字记。
- **CEUS、MR**:CEUS 是超声造影视频(往静脉打造影剂后录的超声);MR 是磁共振检查,本项目只有它的文字报告。这一步不处理 CEUS 视频,只检查它在不在。
- **CEUS、MR**:CEUS 是超声造影视频(往静脉打造影剂后录的超声);MR 是磁共振检查,本项目只有它的文字报告。这一步不处理 CEUS 视频,只检查它在不在;CEUS 定量(`scripts/ceus_quant.py`,见 `../任务书/02-CEUS定量.md`)另外先跑,这一步按 `ceus_run` 读它的结果。
- **TIC、关键帧、槽位**:TIC(时间-强度曲线)是病灶区域平均亮度随时间的变化;关键帧是按 TIC 挑出的 7 个时刻的画面,每个时刻叫一个槽位(B0、D1–D4、F1、F2,含义见 `../explanation/结构.md` 第四部分的关键帧表)。
- **交付包**:临床方交来的一批原始资料,目前是 `data/交付包_20260908_40例/`:三张汇总表(临床工作簿、超声报告、MR 报告)和每例一个病例目录(关键图、CEUS 视频、单例报告表)。
- **单例报告表**:每个病例目录里的一份 Excel,只有一行,列与超声汇总表相同(7 列)。它和汇总表重复,用来核对两份是否一致。
- **所见、结论段**:一份影像报告通常分「所见」(描述看到了什么)和「印象 / 结论」(医生的诊断倾向)。交付包的报告文字只有所见,这一步检查里面没有混进结论段(C11)。
@@ -32,6 +33,7 @@
| `alias_seed` | 整数 | 生成化名的随机种子。同一个种子、同一批通过的病例,得到同样的化名 |
| `decode_codes` | true / false | 编码列是否按映射表解码 |
| `code_maps` | 路径或 null | 映射表位置 |
| `ceus_run` | 路径或 null | 某次 CEUS 定量的运行目录(`runs/ceus/<运行号>/`);null 表示不装 CEUS 结果,超声一路只有报告和关键图 |
相对路径**相对仓库根目录**解析,不相对配置文件所在目录,也不相对你敲命令时所在的目录。路径不能写空串(空串会变成当前目录)。
@@ -43,6 +45,10 @@
为什么做成开关:映射表还没从临床方拿到,不能编造含义;先用 false 跑通,映射表到位后改成 true 重新构建一次(负责人 2026-10-16:解码做成可选开关,映射表是独立的配置文件)。诊断编码和其他编码列一样归这个开关管;诊断不进资料库,所以开关对诊断只影响答案库。
### ceus_run
指向一次**跑完的** CEUS 运行目录。目录不存在、名字以 `.partial` 结尾(那次运行没跑完)、缺 `failed.csv` 或 `snapshot/snapshot.json`,整次构建在建目录之前停下。目录合规时,按视频号找每例的结果,找不到或 CEUS 在这一例上失败的按 C15 跳过(第四节)。
### 映射表的格式
映射表目前**还没有**(`config/code_maps.yaml` 尚未建)。拿到后按下面的格式写:每个编码列一项,列名映射到「数字 → 含义」。
@@ -73,7 +79,7 @@ uv run python scripts/build.py --config config/build.yaml
uv run python scripts/scan_library.py --library library/<构建号> --answers answers/<构建号>
```
它从答案库读出这次构建的真实病例号和视频号,加上脚本里独立写的亚型名、诊断列名、交付文件名词表,扫资料库的全部 `.txt`、`.json`(不扫 PNG:PNG 的附加数据在写入时已经去掉,像素上烧录的信息栏按 `AGENTS.md` 硬性约束 3 不遮蔽,文字扫描也查不了像素),并检查 JSON 里有没有 `diagnosis`、`label`、`gold`、`pathology`、`name`、`row` 这类键名。只输出命中的条数和类别,不输出命中的内容;全部为 0 才说明资料库里没有这些东西。两个目录的构建号不同时直接报错。词表在脚本里另写一份、不取自 delivery 的常量,是为了让扫描不依赖被检查的代码本身:delivery 漏了一个词,扫描还能查出来。这个脚本同时读两个库,所以只能放在 `scripts/`,不能进 `src/cadx`(会诊代码不许碰答案库)。
它从答案库读出这次构建的真实病例号和视频号,加上脚本里独立写的亚型名、诊断列名、交付文件名词表,扫资料库的全部 `.txt`、`.json`(含 `tic.json`;不扫 PNG:PNG 的附加数据在写入时已经去掉,像素上烧录的信息栏按 `AGENTS.md` 硬性约束 3 不遮蔽,文字扫描也查不了像素),并检查 JSON 里有没有 `diagnosis`、`label`、`gold`、`pathology`、`name`、`row` 这类键名。只输出命中的条数和类别,不输出命中的内容;全部为 0 才说明资料库里没有这些东西。两个目录的构建号不同时直接报错。词表在脚本里另写一份、不取自 delivery 的常量,是为了让扫描不依赖被检查的代码本身:delivery 漏了一个词,扫描还能查出来。这个脚本同时读两个库,所以只能放在 `scripts/`,不能进 `src/cadx`(会诊代码不许碰答案库)。
## 三、产物在哪
@@ -89,18 +95,22 @@ cases/<化名>/
clinical/clinical.json
ultrasound/report.txt
ultrasound/key.png
ultrasound/keyframes/<槽位>.png (ceus_run 不为 null 时;只有定位到的槽位)
ultrasound/tic.json (ceus_run 不为 null 时)
mr/report.txt
```
| 文件 | 内容 |
| --- | --- |
| `manifest.json` | 构建号、git 提交号、工作区是否干净、配置文件(解码时含映射表)的 SHA-256、`codes_decoded`、`uncompared`(没比对的项)、`ceus_source`(所用 CEUS 结果的来源,这一步是 null)、化名列表(按化名排序)、每个文件的相对路径和 SHA-256。不含真实病例号、交付文件名和诊断 |
| `manifest.json` | 构建号、git 提交号、工作区是否干净、配置文件(解码时含映射表)的 SHA-256、`codes_decoded`、`uncompared`(没比对的项)、`ceus_source`(所用 CEUS 运行号,`ceus_run` 为 null 时是 null)、化名列表(按化名排序)、每个文件的相对路径和 SHA-256。不含真实病例号、交付文件名和诊断 |
| `clinical.json` | `{"codes_decoded": ..., "fields": {列名: 值}}`。字段是临床表去掉编号和诊断后的 22 列:性别、年龄、5 个症状、5 项既往史、5 项化验的原值和档位。没测的化验写 null |
| `ultrasound/report.txt` | 超声报告的「所见」原文,UTF-8,不加标题 |
| `ultrasound/key.png` | 医生截的超声关键图,只保留像素和颜色模式重新编码,不带文本块、EXIF、ICC 等附加数据(附加数据里可能有文件名、时间等线索)。超声帧上烧录的信息栏不遮蔽(`AGENTS.md` 硬性约束 3) |
| `ultrasound/keyframes/<槽位>.png` | CEUS 运行里定位到的关键帧整幅画面,和 `key.png` 一样去掉附加数据重新编码 |
| `ultrasound/tic.json` | 四条 TIC 曲线的拟合是否成功和 R²、病灶两条曲线的灌注指标、紫圈曲线是否可用、7 个槽位是否定位到及视频时间或未定位原因。字段逐个从 CEUS 的 `summary.json` 挑出,不含视频号、路径、配置、帧号(`结构.md` 第四部分) |
| `mr/report.txt` | MR 报告的「所见」原文 |
性别、年龄只在临床一路:两份报告表里医院也复制了这两列,但影像两路先看到会被人口学先验带偏(`结构.md` 第四部分)。以后 CEUS 定量落地,超声一路再加关键帧和灌注指标。
性别、年龄只在临床一路:两份报告表里医院也复制了这两列,但影像两路先看到会被人口学先验带偏(`结构.md` 第四部分)。
读资料库要经 `cadx.library` 的 `read_clinical`、`read_ultrasound`、`read_mr`:读时逐个核对文件的 SHA-256 与清单一致,被改过就报错。
@@ -147,18 +157,18 @@ CSV 用带 BOM 的 UTF-8(文件开头有一个不可见的标记),医生
交付资料里的问题分两种(`AGENTS.md`「错误怎么处理」):
- **整张表的问题**(T1–T5):一例也读不出来,整次构建停下,报错信息以检查编号开头,写明哪个文件哪一项不符。
- **只落在某一个病例上的问题**(C01–C14):跳过这一整例,接着处理下一例,在 `skipped.csv` 里记一行。这一例不进资料库,也不进答案库的 `aliases.csv`、`diagnoses.csv`。
- **只落在某一个病例上的问题**(C01–C15):跳过这一整例,接着处理下一例,在 `skipped.csv` 里记一行。这一例不进资料库,也不进答案库的 `aliases.csv`、`diagnoses.csv`。
先对三张表的每一行和每个病例目录做 C01(编号认不认得出);认得出的,再逐例按 C02 到 C13 的顺序检查,**遇到第一项没过就停下**,清单上每例只记这一项(C01 记的是认不出的那一行,不算某一例,见下文)。后面的检查没做过,不代表没问题:医生改好这一项后,重新构建时它可能卡在后面某一项。这句说明写在答案库 `manifest.json` 的 `skipped_note` 里,终端也会打印;CSV 文件开头不能加注释行,所以没写在 `skipped.csv` 里。为什么整例跳过而不是停下整次构建:负责人 2026-10-16「都是遇到整个病例跳过,让医生核对去吧」。评价时要报告跳过了几例、各因为什么。
先对三张表的每一行和每个病例目录做 C01(编号认不认得出);认得出的,再逐例按 C02 到 C13、C14(关键图)、C15(CEUS 结果)、C14(关键帧图)的顺序检查,**遇到第一项没过就停下**,清单上每例只记这一项(C01 记的是认不出的那一行,不算某一例,见下文)。后面的检查没做过,不代表没问题:医生改好这一项后,重新构建时它可能卡在后面某一项。这句说明写在答案库 `manifest.json` 的 `skipped_note` 里,终端也会打印;CSV 文件开头不能加注释行,所以没写在 `skipped.csv` 里。为什么整例跳过而不是停下整次构建:负责人 2026-10-16「都是遇到整个病例跳过,让医生核对去吧」。评价时要报告跳过了几例、各因为什么。
### skipped.csv 的列
| 列 | 含义 |
| --- | --- |
| 病例 | 三位病例号。C01(编号认不出)的行不知道是哪个病人,写成「表的路径 第 N 行」(N 是 Excel 行号,第 1 行是表头);病例目录名不在 1–999 之间时写成「病例目录 目录名」 |
| 检查编号 | C01–C14 |
| 检查编号 | C01–C15 |
| 检查说明 | 这项检查的一句话说明 |
| 文件 | 涉及的文件,相对交付包根目录,多个用「; 」隔开 |
| 文件 | 涉及的文件,相对交付包根目录,多个用「; 」隔开;C15 和关键帧图的 C14 相对 CEUS 运行的上级目录(以运行号开头) |
| 列 | 涉及的列,多个用「; 」隔开;不涉及具体列(如目录里的文件不对)时为空 |
| 涉及值 | JSON 字符串:不合规的原值、对不上的各表的值、命中的词和所在句子等,供医生核对 |
@@ -166,7 +176,7 @@ C01 的行不计入病例数,也不计入终端的「跳过病例」,单独
### 检查编号速查
规则细节以代码为准:T1–T4 在 `src/cadx/delivery/_tables.py`,T5 在 `src/cadx/build/_config.py`,C01–C10 在 `src/cadx/delivery/_cases.py`,C11–C13 的词表和正则在 `src/cadx/delivery/_text.py`,C14 由 build 调 library 的 `check_key_image`(`src/cadx/library/_write.py`)在写库前做,表名、工作表名、表头在 `src/cadx/delivery/_shape.py`。每条规则为什么这样定见任务书 `../任务书/01-装配两个库.md` 第二部分。
规则细节以代码为准:T1–T4 在 `src/cadx/delivery/_tables.py`,T5 在 `src/cadx/build/_config.py`,C01–C10 在 `src/cadx/delivery/_cases.py`,C11–C13 的词表和正则在 `src/cadx/delivery/_text.py`,C14 由 build 调 library 的 `check_key_image`(`src/cadx/library/_write.py`)在写库前做,C15 在 `src/cadx/build/_ceus.py`,表名、工作表名、表头在 `src/cadx/delivery/_shape.py`。每条规则为什么这样定见任务书 `../任务书/01-装配两个库.md` 第二部分。
| 编号 | 查什么 | 不过时 |
| --- | --- | --- |
@@ -188,7 +198,8 @@ C01 的行不计入病例数,也不计入终端的「跳过病例」,单独
| C11 | 报告文字里没有结论段标题:段首(全文开头或换行之后)出现「印象」「结论」「提示」等关键词加冒号 | 跳过这一例 |
| C12 | 报告文字里没有亚型名(缩写、中文名及其常见变体) | 跳过这一例 |
| C13 | 报告文字里没有人口学信息:没有「数字岁」,没有「男」「女」两个字 | 跳过这一例 |
| C14 | 关键图能原样重编码:是 PNG,位深和颜色类型能逐像素保真(8 位灰度、RGB、灰度加透明通道、RGBA,或 1 位灰度),不带 tRNS 透明色;否则重编码会悄悄降位或丢透明。涉及值写不过的原因 | 跳过这一例 |
| C14 | 关键图能原样重编码:是 PNG,位深和颜色类型能逐像素保真(8 位灰度、RGB、灰度加透明通道、RGBA,或 1 位灰度),不带 tRNS 透明色;否则重编码会悄悄降位或丢透明。装 CEUS 结果时,每张关键帧图也查这一条(规则相同,「文件」一栏是哪张关键帧)。涉及值写不过的原因 | 跳过这一例 |
| C15 | CEUS 定量结果齐全(只在 `ceus_run` 不为 null 时查):这一例的视频号不在那次运行的 `failed.csv` 里,且运行目录里有这一例的结果目录。涉及值写失败在第几步和原因,或「没有这一例的结果目录」。失败的病例以后要不要改成「超声不可评价、仍参与会诊」等 `../待定问题/03-病例排除标准.md` 定 | 跳过这一例 |
几点说明: