Files
vrsub/tests/test_integration_alignment.py
T
cat-shark 078170c12a feat: 真实数据时间对齐集成测试框架
- tests/realdata_contract.py:数据契约底座(SRT 解析/清洗、时间对齐量化
  指标 align_report、幻觉词/专名判定、提示词规则拼接)
- tests/test_integration_alignment.py:流水线产物 vs 硬字幕参考的时间对齐
  (真实数据复现"字幕过早/过晚",红→绿闭环)
- tests/test_integration_prompt_rules.py:寒暄幻觉/专名提示词规则测试
- scripts/extract_reference_srt.py:从烧录字幕视频自动提取参考时间轴
- testdata/REALDATA_README.md + alignment/*.reference.srt:真实参考字幕
  (视频素材较大,gitignore 不入库)
2026-09-05 19:36:23 +08:00

192 lines
7.7 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.
"""视频字幕生成流水线 → 时间对齐集成测试(真实数据复现"字幕时间不吻合")。
背景:用户反馈 demo"视频字幕生成"产物的字幕与人物说话时间不吻合——存在
字幕**过早**或**过晚**展示。本测试**不自己跑 whisper**(长视频转写慢、低
显存易 OOM),而是:
1. 直接用现有"视频字幕生成"流水线跑一遍真实视频(该流水线已产出
transcript.srt 中文翻译字幕,时间轴来自 whisper + 提示词翻译);
2. 测试读取流水线**产物文件夹**里的最终字幕(.srt/.ass);
3. 与人工校对的**参考字幕**testdata/alignment/<name>.reference.srt,即
硬字幕 OCR 提取、时间轴为"说话真实发生的时间")做时间对齐量化评估,
复现"过早/过晚"问题(红),为修复提供绿标准。
产物来源(用户告知):流水线结果文件夹,含最终字幕文件。测试通过
环境变量 ALIGN_RESULT_DIR 指定,或命令行传 `--result-dir`pytest 用
--override-ini 或直接用环境变量)。
数据契约:
- 参考字幕:``testdata/alignment/<name>.reference.srt``(硬字幕/人工校对)
- 流水线产物:``$ALIGN_RESULT_DIR/*.srt|*.ass``(最终字幕,时间轴为产物)
素材/参考/产物任一缺失时测试整体跳过(不污染 100% 覆盖率门禁)。
"""
from __future__ import annotations
import os
from pathlib import Path
import pytest
from tests.realdata_contract import (
ALIGNMENT_DIR,
TIER1_TOLERANCE_SECONDS,
TIER2_EARLY_SECONDS,
TIER2_LATE_SECONDS,
align_report,
alignment_candidates,
clean_reference,
parse_srt_entries,
)
def _result_dir() -> Path | None:
"""返回流水线产物文件夹(环境变量 ALIGN_RESULT_DIR 指定);未设置返回 None。"""
raw = os.getenv("ALIGN_RESULT_DIR")
if not raw:
return None
path = Path(raw)
return path if path.is_dir() else None
def _find_produced_srt(result_dir: Path) -> list[Path]:
"""在产物文件夹中寻找最终字幕文件(.srt / .ass),按名称排序。"""
files = []
for ext in (".srt", ".ass"):
files.extend(result_dir.glob(f"*{ext}"))
# 排除参考字幕与临时文件。
files = [f for f in files if ".reference" not in f.name and not f.name.startswith("._")]
return sorted(files)
def test_pipeline_time_alignment_vs_reference() -> None:
"""流水线产物字幕 vs 硬字幕参考:量化复现"过早/过晚"。
做法(真实数据,不 mock):
- 取 testdata/alignment/<name>.reference.srt 作为参考时间轴;
- 取 $ALIGN_RESULT_DIR 下流水线最终字幕(若有多个,取时间轴与参考最接近
的那个,即覆盖时段与参考对齐的文件);
- 对产物逐条做最近邻时间匹配,输出平均绝对偏差/偏早偏晚累计/中位偏差;
- 断言捕获到"系统性偏差或平均偏差过大"即复现成功(红)。
修复后(vad 策略/对齐策略/翻译时间戳策略改善)偏差应回落,测试转绿。
"""
candidates = alignment_candidates()
if not candidates:
pytest.skip(
f"缺少参考数据({ALIGNMENT_DIR}/<name>.<ext> + <name>.reference.srt),"
"跳过时间对齐集成测试"
)
result_dir = _result_dir()
if result_dir is None:
pytest.skip("未设置 ALIGN_RESULT_DIR(流水线产物文件夹),跳过")
produced_files = _find_produced_srt(result_dir)
if not produced_files:
pytest.skip(
f"{result_dir} 下没有 .srt/.ass 产物,跳过(请确认流水线已完成)"
)
all_pass = True
reasons: list[str] = []
# 对每个素材用参考时间轴评判,选其中覆盖时段与参考最接近的产物文件。
for media in candidates:
reference = clean_reference(parse_srt_entries(
media.with_suffix(".reference.srt").read_text(encoding="utf-8")
))
if not reference:
reasons.append(f"{media.stem}: 参考字幕为空")
all_pass = False
continue
# 择优:产物文件与参考起始时间差最小的作为该素材的评判对象。
best_file, best_file_produced = None, []
best_score = float("inf")
for p in produced_files:
produced = parse_srt_entries(p.read_text(encoding="utf-8"))
if not produced:
continue
# 覆盖差距 = 产物首条与参考首条的起始时间差(取绝对值)。
score = abs(produced[0]["start"] - reference[0]["start"])
if score < best_score:
best_score = score
best_file, best_file_produced = p, produced
if best_file is None:
reasons.append(f"{media.stem}: 产物文件均无法与参考匹配")
all_pass = False
continue
report = align_report(media.stem, True, best_file_produced, reference)
print("\n" + report.format_summary())
print(f" 产物文件: {best_file.name}")
reason = None
if report.mean_abs_error > TIER1_TOLERANCE_SECONDS:
reason = (
f"{media.stem} 平均绝对偏差 {report.mean_abs_error:.2f}s > "
f"容差 {TIER1_TOLERANCE_SECONDS}s(过早/过晚普遍存在)"
)
elif report.consistently_early:
reason = (
f"{media.stem} 系统性偏早(中位 {report.bias:+.2f}s < "
f"-{TIER2_EARLY_SECONDS}s"
)
elif report.consistently_late:
reason = (
f"{media.stem} 系统性偏晚(中位 {report.bias:+.2f}s > "
f"+{TIER2_LATE_SECONDS}s"
)
if reason:
reasons.append(reason)
all_pass = False
else:
print(f" 对齐正常: {media.stem}")
# 结论性断言:至少量化捕获到一个"过早/过晚"信号才是"复现成功"。
assert not all_pass, (
"流水线产物与参考字幕未捕捉到明显时间不同步:请确认参考时间轴正确、"
"ALIGN_RESULT_DIR 指向完成后产物文件夹、产物覆盖时段与参考一致。"
)
if reasons:
raise AssertionError(
"已量化复现字幕时间与说话时间不吻合:\n- " + "\n- ".join(reasons)
)
def test_alignment_report_helpers() -> None:
"""纯函数冒烟:对齐指标组件(不依赖真实数据,用于验证指标本身)。"""
produced = [
{"start": 1.0, "end": 2.0, "text": "a"},
{"start": 4.0, "end": 5.0, "text": "b"},
]
reference = [
{"start": 1.0, "end": 2.0, "text": "A"},
{"start": 4.0, "end": 5.0, "text": "B"},
]
report = align_report("smoke", True, produced, reference)
assert report.mean_abs_error == 0.0 # 完美对齐时平均绝对偏差为 0
assert not report.consistently_early
assert not report.consistently_late
assert report.bias == 0.0
# 系统性偏晚:产物起始全部比参考晚 1.5s。
produced_late = [
{"start": 2.5, "end": 3.5, "text": "a"},
{"start": 5.5, "end": 6.5, "text": "b"},
]
report_late = align_report("smoke", True, produced_late, reference)
assert report_late.consistently_late
assert report_late.bias > TIER2_LATE_SECONDS
# 系统性偏早:产物起始全部比参考早 1.5s。
ref_early = [
{"start": 2.0, "end": 3.0, "text": "X"},
]
produced_early = [
{"start": 0.5, "end": 1.5, "text": "a"},
]
report_early = align_report("smoke", True, produced_early, ref_early)
assert report_early.consistently_early
assert report_early.bias < -TIER2_EARLY_SECONDS