feat: 装配两个库(不含 CEUS 结果)
按任务书 01 实现 delivery、library、answers、runs(快照部分)、build 五个模块, scripts/build.py、scripts/scan_library.py 与 config/build.yaml(decode_codes: false)。 交付包整表检查 T1–T4、映射表检查 T5、逐例检查 C01–C13,一例遇到第一项不过即整例跳过。 与任务书原文的出入: - C04 报告表名随视频名配对(负责人 2026-10-16 定)。 - 按两轮独立审阅加严 C11(更多换行符、比例号冒号)、C12(同义词与词根)、 C13(空格、周岁、中文数字、繁体);泄露扫描做 NFKC;关键图只接受能保真重编码的 PNG; YAML 配置拒绝重复键和空路径。 - import-linter 第 1 条拆成互不依赖与不引用其他 cadx 模块两条。 文档:结构.md 第五、六、八部分按实际更新;新增 guides/装配两个库.md;WORK_LOG 更新。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
"""装配两个库:读交付包,写出一对同号的资料库与答案库。
|
||||
|
||||
用法:uv run python scripts/build.py --config config/build.yaml
|
||||
终端只打印计数,不打印病例号和资料内容。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from cadx.build import run_build
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="装配给专家的资料库和标准答案库")
|
||||
parser.add_argument("--config", type=Path, required=True, help="build.yaml 的路径")
|
||||
config: Path = parser.parse_args().config
|
||||
|
||||
summary = run_build(config, REPO_ROOT, datetime.now().astimezone())
|
||||
|
||||
print(f"构建号:{summary.build_id}")
|
||||
print(f"资料库:{summary.library_dir}")
|
||||
print(f"答案库:{summary.answers_dir}")
|
||||
print(f"病例总数:{summary.total_cases}")
|
||||
print(f"进库病例:{summary.passed_cases}")
|
||||
print(f"跳过病例:{summary.skipped_cases}")
|
||||
print(f"认不出编号的行:{summary.unrecognized_rows}")
|
||||
by_check = ",".join(f"{check} {n}" for check, n in summary.skipped_by_check.items())
|
||||
print(f"按检查编号分的跳过数:{by_check or '无'}")
|
||||
print(f"复核清单条数:{summary.review_rows}")
|
||||
print(f"跳过清单说明:{summary.skipped_note}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,88 @@
|
||||
"""泄露扫描:查一次构建的资料库里有没有真实病例号、交付文件名、亚型名、诊断列名。
|
||||
|
||||
用法:uv run python scripts/scan_library.py --library library/<构建号> --answers answers/<构建号>
|
||||
病例号和视频号从答案库读;亚型名、列名、文件名的词表在本脚本里独立写,不取自 delivery。
|
||||
只输出命中的条数和类别,不输出命中的内容。这个脚本同时读两个库,只能放在 scripts/。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
from cadx.answers import read_aliases
|
||||
from cadx.library import scan_for_leaks
|
||||
|
||||
SUBTYPE_NAMES = (
|
||||
"IPMN",
|
||||
"IPMT",
|
||||
"SPN",
|
||||
"SPT",
|
||||
"SPTP",
|
||||
"MCN",
|
||||
"SCN",
|
||||
"导管内乳头状黏液性肿瘤",
|
||||
"导管内乳头状",
|
||||
"导管内乳头",
|
||||
"实性假乳头状",
|
||||
"实性假乳头",
|
||||
"黏液性囊性肿瘤",
|
||||
"黏液性囊",
|
||||
"粘液性囊",
|
||||
"黏液囊性",
|
||||
"粘液囊性",
|
||||
"浆液性囊性肿瘤",
|
||||
"浆液性囊",
|
||||
"浆液性微囊",
|
||||
"囊腺瘤",
|
||||
)
|
||||
COLUMN_NAMES = (
|
||||
"Case_ID",
|
||||
"Diagnosis",
|
||||
"病理类型",
|
||||
"病理类型(1 IPMN,2 SPN,3 MCN,4 SCN)",
|
||||
"患者ID",
|
||||
"CEUS视频ID",
|
||||
"推注开始时间",
|
||||
)
|
||||
DELIVERY_FILE_NAMES = (
|
||||
"临床工作簿_40例.xlsx",
|
||||
"超声报告文本_40例.xlsx",
|
||||
"MR报告文本_40例.xlsx",
|
||||
"Clinical_agent",
|
||||
"CEUS_agent",
|
||||
"MR_agent",
|
||||
)
|
||||
FORBIDDEN_KEYS = ("diagnosis", "label", "gold", "pathology", "name", "row")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="扫描资料库里的泄露")
|
||||
parser.add_argument("--library", type=Path, required=True, help="资料库的构建目录")
|
||||
parser.add_argument("--answers", type=Path, required=True, help="同一构建号的答案库目录")
|
||||
args = parser.parse_args()
|
||||
library: Path = args.library
|
||||
answers: Path = args.answers
|
||||
if library.name != answers.name:
|
||||
raise SystemExit(f"两个目录的构建号不同:{library.name} 与 {answers.name}")
|
||||
|
||||
categories: dict[str, str] = {}
|
||||
for row in read_aliases(answers):
|
||||
categories[f"P{row.case_no}"] = "病例号"
|
||||
categories[row.video_id] = "视频号"
|
||||
for suffix in ("_key.png", "_roi2.mp4", ".mp4", "_roi2_report.xlsx", "_report.xlsx"):
|
||||
categories[f"{row.video_id}{suffix}"] = "交付文件名"
|
||||
categories.update(dict.fromkeys(DELIVERY_FILE_NAMES, "交付文件名"))
|
||||
categories.update(dict.fromkeys(SUBTYPE_NAMES, "亚型名"))
|
||||
categories.update(dict.fromkeys(COLUMN_NAMES, "列名"))
|
||||
|
||||
hits = scan_for_leaks(library, list(categories), FORBIDDEN_KEYS)
|
||||
counts = Counter("键名" if hit.kind == "键名" else categories[hit.term] for hit in hits)
|
||||
print(f"扫描的病例数:{len(read_aliases(answers))}")
|
||||
print(f"禁止的字符串:{len(categories)} 个;禁止的键名:{len(FORBIDDEN_KEYS)} 个")
|
||||
print(f"命中总数:{len(hits)}")
|
||||
for category in ("病例号", "视频号", "交付文件名", "亚型名", "列名", "键名"):
|
||||
print(f" {category}:{counts[category]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user