#!/usr/bin/env bash # ============================================================================ # 结果驱动视频级切分 —— capstone 可复现实验(离线管线最后一步) # ---------------------------------------------------------------------------- # 把整条离线管线串成零参可复现实验:诊断信号 → 贪心联合约束选择 → 冻结 pools.json。 # 全部旋钮写死在 config/video_split.yaml,零参即可复现(GPU 卡号除外)。 # # 用法: # CUDA_VISIBLE_DEVICES=0 bash scripts/build_video_split.sh # # ============================================================================ # 两阶段流程 # ---------------------------------------------------------------------------- # ① 离线诊断(Task 4,run_baseline_diagnosis)——【LLM 重活,需单独跑一次】 # 对 baseline run(infer_adhoc,workspaces/default/harness.db 内 236 道错题) # 逐题跑诊断瀑布,把 defect/lapse/infra/degraded 投影为 tier 信号,逐行 upsert # 到 harness.db 的 baseline_diagnosis 表(主键含 diag_fingerprint,断点续跑幂等)。 # 诊断依赖完整 harness 装配(GovernedLLMClient / 树数据 / 诊断 prompt), # 不在本脚本内联,由诊断编排单独执行;本脚本前置校验诊断信号已落库,缺失即 fail loud。 # # ② 冻结切分(Task 11,build_split)——【本脚本执行】 # 读 canonical 基线预测 + 诊断信号 → 构建全视频画像 → 贪心选择 trainval/test → # 视频组原子切三池 → 原子冻结 pools.json + 溯源 manifest → 六条防御断言。 # # ============================================================================ # 标定程序(诊断跑完 → 用真实 T2 分布把 config/video_split.yaml 的占位值定死) # ---------------------------------------------------------------------------- # 1. floor_k : 读 baseline_diagnosis 各 task_type 的 T2(defect) 计数, # 每个高信号题型取克制值 floor = min(可用 defect 数, 3), # 避免把该类信号全抽进 trainval(会让 test 失去代表性)。 # 2. n_trainval : trainval 目标视频数,取 ~100(总 300 视频的约 1/3), # 给多样性阶段留足填充预算,同时保证 test ≥ 2/3 保代表性。 # 3. epsilon : test 相对全局的最大分布偏差,取 0.1(题型/难度逐桶容忍 10%)。 # 4. val_wrong_min : validation 池最少错题数,取 McNemar 检验功效阈 ≈ 20; # 切分后 val 错题不足此值 → 验证信号不可靠 → fail loud。 # 5. report_floor : per-type 报告门限,题数 ≥ 27 的 task_type 才入 ε 约束(滤长尾)。 # ============================================================================ set -euo pipefail cd "$(dirname "$0")/.." CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-0}" export CUDA_VISIBLE_DEVICES # 日志即时输出,不缓存,便于实时排错。 export PYTHONUNBUFFERED=1 # 工程配置(DB 路径、LLM 凭证)走 .env,不写死在脚本。 set -a source .env set +a PYTHON="$(conda run -n Video-Tree-TRM which python)" CONFIG="config/video_split.yaml" HARNESS_DB="workspaces/default/harness.db" QUESTIONS_DIR="store/questions/benchmarks/Video-MME" OUT_DIR="workspaces/video-split" echo "=== 结果驱动视频级切分:冻结 pools.json(读 ${CONFIG})===" # 诊断口径指纹的代码分量 = 当前 git 短 SHA(诊断代码变则指纹变,旧信号不被覆盖)。 CODE_VERSION="$(git rev-parse --short HEAD)" export CODE_VERSION HARNESS_DB QUESTIONS_DIR OUT_DIR CONFIG "${PYTHON}" -c " import datetime import os from pathlib import Path import yaml from loguru import logger from adapters.baseline_diagnosis_store import SqliteDiagnosisSignalStore from app.harness.build_split import SplitBuildConfig, build_split from app.harness.split_selection import diag_fingerprint config_path = Path(os.environ['CONFIG']) cfg = yaml.safe_load(config_path.read_text(encoding='utf-8')) vs = cfg['video_split'] dg = cfg['diag'] # 诊断口径指纹:诊断 prompt 版本 + 模型名 + 代码短 SHA(三分量任一变则换指纹)。 fingerprint = diag_fingerprint(dg['prompt_version'], dg['model'], os.environ['CODE_VERSION']) logger.info('诊断口径指纹 diag_fingerprint={} (prompt={} model={} code={})', fingerprint, dg['prompt_version'], dg['model'], os.environ['CODE_VERSION']) harness_db = Path(os.environ['HARNESS_DB']) questions_dir = Path(os.environ['QUESTIONS_DIR']) out_dir = Path(os.environ['OUT_DIR']) out_dir.mkdir(parents=True, exist_ok=True) store = SqliteDiagnosisSignalStore(str(harness_db)) try: # 前置校验(P5 fail loud):本指纹下必须已有诊断信号(Phase 1 诊断先跑完)。 signals = store.load(vs['baseline_run_id'], fingerprint) if not signals: raise SystemExit( f'诊断信号为空:baseline={vs[\"baseline_run_id\"]} fingerprint={fingerprint} ' f'在 {harness_db} 的 baseline_diagnosis 表无任何行。' '请先跑离线诊断(Phase 1 run_baseline_diagnosis)再执行本脚本。' ) logger.info('诊断信号就绪:{} 行,开始冻结切分。', len(signals)) result = build_split( db_path=harness_db, baseline_run_id=vs['baseline_run_id'], signal_store=store, diag_fingerprint=fingerprint, questions_dir=questions_dir, config=SplitBuildConfig( n_trainval=vs['n_trainval'], floor_k=vs['floor_k'], epsilon=vs['epsilon'], report_floor=vs['report_floor'], select_seed=vs['seed'], val_ratio=vs['val_ratio'], split_seed=vs['seed'], ), out_path=out_dir / 'pools.json', manifest_path=out_dir / 'split_manifest.json', generated_at=datetime.datetime.now(datetime.timezone.utc).isoformat(), ) finally: store.close() # McNemar 功效护栏(P5 fail loud):build_split 按 Task 11 契约不带 val_wrong_min, # 冻结后在 capstone 层校验 validation 池错题数是否达 McNemar 功效阈。 pools = result['pools'] val_wrong = sum(1 for q in pools.validation if not pools.correctness[q.question_id]) val_wrong_min = vs['val_wrong_min'] if val_wrong < val_wrong_min: raise SystemExit( f'validation 池错题数 {val_wrong} < val_wrong_min={val_wrong_min},' '验证信号不足以支撑可靠比较(McNemar 检验功效不够)。' '请放大 val_ratio / 调整旋钮后重跑,勿静默放行。' ) logger.info( '切分冻结完成:pools={} manifest={} val错题={}/{}(阈)', out_dir / 'pools.json', out_dir / 'split_manifest.json', val_wrong, val_wrong_min, ) " echo "=== 完成:冻结产物在 ${OUT_DIR}/ (pools.json + split_manifest.json) ==="