"""翻译模型横向评测的数据契约与对照逻辑测试(真实数据,不 mock 模型)。 背景 ---- 为评估"更便宜的 LLM 能否替代当前 Qwen/Qwen3.6-35B-A3B 做 llm-translate", 新增了三件评测工具(scripts/ 下,都是真实数据驱动): - `scripts/build_translate_eval.py`:把 run_479b411f299d(ocr-subtitle)的 烧录字幕中文产物与 run_d386ccf124f7(learn-translate)的日语 whisper ASR 按时间配对,产出候选池; - `scripts/build_segment_eval.py`:把配对改成**窗口级**(一条中文基准字幕 + 其时间窗内覆盖的 1-3 条日语 cue),产出可用窗口; - `scripts/bench_translate_models.py`:调用**生产翻译实现**(nodes/llm.py) 对每个模型跑完整片,并在窗口级打印各模型译文对照供人工 review。 本测试验证上述工具的**数据契约与对照逻辑**:评测集必须真实可用(时间轴自洽、 基准非空、可定位到真实 ASR 条目),对照输出必须包含全部模型各自的行。真实 产物缺失时整组跳过(与 tests/realdata_contract.py 同一约定)。 """ from __future__ import annotations import importlib.util import json import subprocess import sys from pathlib import Path import pytest # 仓库根(tests/ 的上一级)。 WORKSPACE = Path(__file__).resolve().parent.parent EVAL_SET = WORKSPACE / "testdata/translate_eval/eval_set.jsonl" WINDOWS = WORKSPACE / "testdata/translate_eval/windows.jsonl" JA_SRT = WORKSPACE / "data/storage/runs/run_d386ccf124f7/steps/asr/transcript.srt" BENCH = WORKSPACE / "scripts/bench_translate_models.py" # 评测中间产物目录(gitignored),各模型跑完后才存在。 EXPERIMENT_ROOT = WORKSPACE / "data/experiments/translate_models" def _load(name: str, path: Path): """按文件路径加载 scripts 下的模块(scripts 不是包,无法 import)。""" spec = importlib.util.spec_from_file_location(name, path) module = importlib.util.module_from_spec(spec) assert spec is not None and spec.loader is not None spec.loader.exec_module(module) return module def _seconds(ts: str) -> float: """SRT 时间戳 -> 秒。""" hours, minutes, rest = ts.split(":") secs, millis = rest.split(",") return int(hours) * 3600 + int(minutes) * 60 + int(secs) + int(millis) / 1000 def test_eval_set_is_real_and_self_consistent() -> None: """评测集必须来自真实产物:条数充足、基准非空、时间轴可定位真实日语 cue。 这是"人工 review 的样本必须真实可核对"的硬约束——如果窗口的时间区间里 找不到对应日语 cue,说明窗口是用假数据拼的,或时间轴与 ASR 不同源。 """ if not EVAL_SET.is_file() or not JA_SRT.is_file(): pytest.skip("评测集或日语 ASR 产物缺失(先跑 build_segment_eval.py)") from nodes.srt import parse_srt windows = [json.loads(line) for line in EVAL_SET.read_text(encoding="utf-8").splitlines()] assert len(windows) >= 100, "评测集窗口过少,不足以支撑人工质量结论" ja_cues = parse_srt(JA_SRT.read_text(encoding="utf-8")) for window in windows: assert window["ja_lines"], f"窗口 {window.get('id')} 没有日语原文" assert window["zh_ref"].strip(), f"窗口 {window.get('id')} 基准中文为空" # 基准中文必须位于窗口时间区间内(真实配对),且窗口跨度合理。 assert _seconds(window["zh_start"]) < _seconds(window["zh_end"]) assert _seconds(window["zh_end"]) - _seconds(window["zh_start"]) <= 15.0 # 每条日语 cue 都能在真实 ASR 里按时间戳找到(同源校验)。 for line in window["ja_lines"]: assert any(cue.text == line for cue in ja_cues), f"日语行不在真实 ASR 中: {line!r}" def test_window_builder_matches_production_parser() -> None: """窗口构建器与生产 SRT 解析器结果一致(不另写一套 SRT 规则)。 用 window.jsonl 的候选池与当场重算的窗口对比:同一输入必须得到同一数量与 同一序列,避免"导出一次、之后代码改动悄悄漂移"。 """ if not WINDOWS.is_file() or not JA_SRT.is_file(): pytest.skip("候选池缺失(先跑 build_segment_eval.py --export)") module = _load("build_segment_eval", WORKSPACE / "scripts/build_segment_eval.py") zh_srt = WORKSPACE / "data/storage/runs/run_479b411f299d/steps/filter/filtered.srt" if not zh_srt.is_file(): pytest.skip("中文基准产物缺失(ocr-subtitle 任务未产出 filtered.srt)") rebuilt = module.build_windows(JA_SRT, zh_srt) exported = [json.loads(line) for line in WINDOWS.read_text(encoding="utf-8").splitlines()] assert len(rebuilt) == len(exported) assert [(w["zh_start"], tuple(w["ja_lines"])) for w in rebuilt] == [ (w["zh_start"], tuple(w["ja_lines"])) for w in exported ] def test_bench_compare_prints_every_model_for_shared_window() -> None: """对照表必须为每个模型打印同一窗口的译文(人工 review 的数据来源)。 用两个真实已跑完的模型产物(基线 + 本地 30B)生成对照表,断言:任意窗口 区块里两个模型都出现且行数一致——否则 review 会因缺行而漏判。 """ baseline = EXPERIMENT_ROOT / "_baseline/steps/cn.srt" other = EXPERIMENT_ROOT / "local-qwen3-30b-a3b/steps/cn.srt" if not EVAL_SET.is_file() or not baseline.is_file() or not other.is_file(): pytest.skip("评测集或某个模型产物缺失(先跑 bench_translate_models.py run)") result = subprocess.run( [sys.executable, str(BENCH), "compare", "--tags", "_baseline", "local-qwen3-30b-a3b", "--limit", "5"], cwd=WORKSPACE, capture_output=True, text=True, check=True, ) output = result.stdout assert "基准中文(OCR)" in output blocks = [b for b in output.split("\n### ") if b.strip()] assert blocks, "对照表没有任何窗口区块" for block in blocks: assert "- _baseline:" in block assert "- local-qwen3-30b-a3b:" in block # 两个模型的行数(以 | 分隔的译文数)必须一致。 base_line = next(l for l in block.splitlines() if l.startswith("- _baseline:")) other_line = next(l for l in block.splitlines() if l.startswith("- local-qwen3-30b-a3b:")) assert base_line.count("|") == other_line.count("|")