Files
vrsub/tests/test_translate_model_bench.py
T
cat-shark 8963afa771 feat: 新增翻译模型横向评测工具链与窗口级评测集
为评估"更便宜的 LLM 能否替换 llm-translate 默认模型"新增真实数据驱动的
评测工具(不 mock 模型,直接调用生产节点实现,模型仅作为 params 变量):

- scripts/build_translate_eval.py:把 ocr-subtitle 的中文烧录字幕产物与
  learn-translate 的日语 whisper ASR 按时间配对,产出候选池;
- scripts/build_segment_eval.py:改为**窗口级**配对(一条中文基准字幕 +
  其时间窗内 1-3 条日语 cue)。逐条配对不可用——烧录字幕是按屏幕合并的
  整行,与 whisper 的 cue 切分不同,直接逐条对照会被基准错位污染;
- scripts/bench_translate_models.py:对每个模型跑完整片、记录单次调用耗时
  与 token、输出窗口级多模型对照表供人工 review;VL 模型需剔除
  enable_thinking(Qwen3-VL 不接受该参数,生产代码固定携带);
- scripts/run_translate_bench_queue.py:批量评测队列;
- tests/test_translate_model_bench.py:评测数据契约与对照输出一致性测试
  (含先红后绿修复:stdout 与 markdown 两套输出格式漂移);
- testdata/translate_eval/:候选池、窗口池与 124 窗口人工评测集资产。

评测结论见 data/experiments/translate_models/REPORT.md(gitignored)。
2026-09-13 10:15:23 +08:00

132 lines
6.4 KiB
Python
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.
"""翻译模型横向评测的数据契约与对照逻辑测试(真实数据,不 mock 模型)。
背景
----
为评估"更便宜的 LLM 能否替代当前 Qwen/Qwen3.6-35B-A3B 做 llm-translate"
新增了三件评测工具(scripts/ 下,都是真实数据驱动):
- `scripts/build_translate_eval.py`:把 run_479b411f299docr-subtitle)的
烧录字幕中文产物与 run_d386ccf124f7learn-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("")