test: 按模块重写测试代码,删除旧平铺结构

按"测试规则"重写 tests/:一个模块一个目录、用例按数据→过程→验证三段书写、
不保留全局 conftest.py、测试过程只调用真实生产代码。

结构(73 个文件、30 个模块目录、477 用例):
- tests/nodes/  15 个模块目录(srt/whisper/ass/ffmpeg/frame_extract/vlm/
  subtitle_ocr/llm/llm_filter/subtitle_cleanup/subtitle_correction/
  proper_nouns/adaptive_pool/vad_profiler/echo);
- tests/app/    11 个模块目录(db/scheduler/batch/maintenance/registry/seed/
  storage/config/logging/main/routers 三组 API);
- tests/sdk/test_models、tests/web/test_crop、tests/shared(公共设施)。

测试数据随模块目录入库(tests/**/data/),删除根级 testdata/;.gitignore
的 data/ 改为 /data/,否则会连带忽略 tests/**/data/ 导致测试数据无法入库。

顺带发现并修复三个真实缺陷:
- nodes/srt.py:相邻条目缺少空行时把下一条时间轴吞进正文(静默错位),
  改为正文行遇时间戳行即报错;
- src/wov_app/scheduler.py:_file_size 只捕获 OSError,含 \x00 的产物 URI
  抛 ValueError 导致任务误判失败,改为同时捕获;
- nodes/subtitle_correction.py:生产代码依赖测试包解析 SRT,
  改用生产模块 nodes/srt.py。

真实模型/服务集成测试按外部状态跳过:新增 tests/shared/gpu_memory.py
(运行时探测显存、CUDA OOM 转跳过)与 tests/shared/llm_service.py
(无 Key / 余额 / 限流转跳过)。全量 477 passed。
This commit is contained in:
2026-09-13 15:40:56 +08:00
parent 966f3e6b4b
commit 8a715a8064
139 changed files with 20810 additions and 10733 deletions
@@ -0,0 +1,127 @@
"""真实语音时间对齐集成测试(数据 → 测试过程 → 验证结果)。
覆盖模块:`nodes/whisper.py`(真实转写)+ `tests/shared/realdata_contract.py`
(对齐量化工具)。这是模块级直测的补充:用真实模型 + 真实音频 + 人工校对
参考字幕,量化字幕时间轴与"说话真实发生时间"的偏差。
数据契约:`tests/shared/data/alignment/` 下 `<name>.wav|.mp4` 与其同名
`<name>.reference.srt`(人工校对)。素材缺失或本地无模型时跳过。
"""
from __future__ import annotations
from pathlib import Path
import pytest
from nodes.whisper import _local_model_candidates, invoke
from tests.shared.realdata_contract import (
TIER1_TOLERANCE_SECONDS,
align_report,
alignment_candidates,
clean_reference,
)
from tests.shared.srt_entries import parse_srt_entries
from wov_sdk.models import InvokeRequest
def _model_available() -> Path | None:
"""返回可用的 V2 权重目录(缺失则返回 None 供跳过)。
只接受 V2 权重:V3 已全面停用(均改用 V2),留在盘上的 V3 目录不得被
测试使用,否则测的不是线上实际运行的模型。
"""
from tests.nodes.test_whisper.test_transcribe import _v2_model_candidates
for candidate in _v2_model_candidates():
if candidate.is_dir() and (candidate / "model.bin").is_file():
return candidate
return None
def _sample_dir() -> Path | None:
"""返回包含对齐素材的目录(共享数据目录;缺失时返回 None)。"""
directory = Path(__file__).resolve().parents[2] / "shared" / "data" / "alignment"
return directory if directory.is_dir() else None
@pytest.mark.integration
def test_real_alignment_candidates_discovered() -> None:
"""对齐素材探查:能找到成对的媒体与参考字幕(数据契约可用)。"""
# 数据:共享数据目录下的真实素材。
directory = _sample_dir()
if directory is None:
pytest.skip("缺少 tests/shared/data/alignment 目录")
# 测试过程
candidates = alignment_candidates(directory)
# 验证结果:至少一对,且每对都有同名 reference.srt。
assert candidates, "应至少有一对素材 + 参考字幕"
for media in candidates:
assert media.with_suffix(".reference.srt").is_file()
@pytest.mark.integration
def test_real_whisper_alignment_within_tolerance(tmp_path) -> None:
"""真实转写产物 vs 人工参考字幕:时间偏差在容差内且无系统性偏移。
这验证"字幕时间与说话时刻对齐"这一核心功能,使用真实模型与真实音频。
"""
# 数据:最小的真实对齐素材(避免长视频拖慢测试)。
directory = _sample_dir()
model_dir = _model_available()
if directory is None or model_dir is None:
pytest.skip("缺少对齐素材或本地 whisper 权重,跳过")
# 显存不足时跳过(转写整段真实音频需要显存,属外部环境状态)。
from tests.shared.gpu_memory import require_gpu_memory, require_node_result
require_gpu_memory(model_dir)
candidates = alignment_candidates(directory)
if not candidates:
pytest.skip("没有成对的对齐素材")
media = min(candidates, key=lambda p: p.stat().st_size)
reference = clean_reference(
parse_srt_entries(media.with_suffix(".reference.srt").read_text(encoding="utf-8"))
)
if not reference:
pytest.skip("参考字幕为空")
# 测试过程:走真实生产链路——先提音(视频/任意容器 → 16kHz 单声道 WAV),
# 再转写。whisper 只接受 WAV,直接喂 mp4 会解析失败(对齐素材多为 mp4)。
from nodes.ffmpeg import invoke as extract_audio
audio = extract_audio(InvokeRequest(
run_id="alignment", node_instance_id="ffmpeg-1", params={},
inputs={"video_uri": str(media)}, output_dir=str(tmp_path / "audio"),
))
require_node_result(audio, model_dir)
assert audio.status == "completed", audio.error
response = invoke(InvokeRequest(
run_id="alignment", node_instance_id="whisper-1",
params={
"language": "ja",
# 本用例目标是"时间轴对齐":整段单次解码(chunk_seconds=0)避免
# 分块上下文带来的额外显存与上下文损失;分块路径本身已由
# whisper 模块测试覆盖。实测 6GB 卡上该配置稳定完成。
"chunk_seconds": 0,
"vad_filter": True,
"model_path": str(model_dir),
},
inputs={"audio_uri": audio.outputs["audio_uri"]},
output_dir=str(tmp_path / "out"),
))
require_node_result(response, model_dir)
assert response.status == "completed", response.error
produced = parse_srt_entries(open(response.outputs["srt_uri"], encoding="utf-8").read())
report = align_report(media.stem, True, produced, reference)
# 验证结果(基线由 V2 模型实测标定,见 docs/testing.md):
# - 产出条数与参考规模同量级(不丢整段、不大量幻觉);
# - 平均绝对偏差在真实转写抖动范围内;
# - 中位偏差不构成系统性偏早/偏晚。
assert produced, "真实转写应产出字幕"
assert len(produced) >= len(reference) * 0.5, report.format_summary()
assert report.mean_abs_error <= 2.0, report.format_summary()
assert not report.consistently_early, report.format_summary()
assert not report.consistently_late, report.format_summary()