Files
Video-Tree-TRM5/scripts/build_video_split.sh
T

150 lines
6.8 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ============================================================================
# 结果驱动视频级切分 —— capstone 可复现实验(离线管线最后一步)
# ----------------------------------------------------------------------------
# 把整条离线管线串成零参可复现实验:诊断信号 → 贪心联合约束选择 → 冻结 pools.json。
# 全部旋钮写死在 config/video_split.yaml,零参即可复现(GPU 卡号除外)。
#
# 用法:
# CUDA_VISIBLE_DEVICES=0 bash scripts/build_video_split.sh
#
# ============================================================================
# 两阶段流程
# ----------------------------------------------------------------------------
# ① 离线诊断(Task 4run_baseline_diagnosis)——【LLM 重活,需单独跑一次】
# 对 baseline runinfer_adhocworkspaces/default/harness.db 内 236 道错题)
# 逐题跑诊断瀑布,把 defect/lapse/infra/degraded 投影为 tier 信号,逐行 upsert
# 到 harness.db 的 baseline_diagnosis 表(主键含 diag_fingerprint,断点续跑幂等)。
# 诊断依赖完整 harness 装配(GovernedLLMClient / 树数据 / 诊断 prompt),
# 不在本脚本内联,由诊断编排单独执行;本脚本前置校验诊断信号已落库,缺失即 fail loud。
#
# ② 冻结切分(Task 11build_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) ==="