Files
vrsub/docs/testing.md
T
cat-shark f656ec98c5 feat: 媒体库字幕重生成统计脚本(数量/时长/旧字幕备份)
模型与管线切换后需要把媒体库里旧管线生成的字幕整批重跑。批量引擎按"视频旁
已有字幕即 SKIPPED"判定,重跑前必须先统计范围、再把旧字幕改名备份,否则建出来
的任务会把所有视频全部跳过。

- `scripts/plan_regenerate_subtitles.py`:扫描媒体库,按 CN 产物的最早 mtime 判定
  生成时间,统计待重生成的数量/时长/体积并输出 JSON 计划;`--backup-old [--apply]
  [--select]` 把旧字幕原地改名为 `<原名>.old-<日期>` 供批量重跑。脚本不依赖仓库,
  可拷到媒体库主机直接跑(CIFS 挂载下逐文件 ffprobe 太慢)。
- 测试覆盖真实 10 秒视频与真实字幕文件,含 ffprobe 时长探测、分类边界、
  备份幂等与选择清单解析。
2026-09-18 22:23:19 +08:00

193 lines
14 KiB
Markdown
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.
# 测试
测试代码位于 `tests/`,运行方式:
```bash
uv run pytest # 全部测试
uv run pytest tests/nodes/test_whisper # 只跑某个模块目录
uv run pytest tests/app/test_routers # 只跑某组 API 测试
uv run pytest --cov=src --cov=nodes # 需要覆盖率时手动追加
```
测试的**规则要求**写在 [../AGENTS.md](../AGENTS.md#测试规则):模块边界、
按模块组织、数据/过程/验证三段结构、功能覆盖优先、真实数据与真实生产代码。
## 目录结构与模块对应
测试按**模块**组织:被测代码的层级与模块名直接映射到 `tests/` 下的目录。
一个模块 = 一个模块目录;单文件过长时在模块目录**内部**按行为拆文件。
```
tests/
├── nodes/ # 对应 nodes/
│ ├── test_srt/ # SRT 解析/序列化(parsing + data/
│ ├── test_whisper/ # 转写:模型解析/分块/时间轴/幻觉清洗
│ ├── test_ass/ # SRT → VR 双目 ASS
│ ├── test_ffmpeg/ # ffmpeg 定位 + 提音(真实视频)
│ ├── test_frame_extract/ # 抽帧/crop/帧号排序(含 frames_boundary 回归)
│ ├── test_vlm/ # 单帧 OCR(真实图片 + Ollama 集成)
│ ├── test_subtitle_ocr/ # 逐帧 OCR 汇总、断点、暂停(14236 帧夹具)
│ ├── test_llm/ # 翻译:ID 对齐/重试/专名注入
│ ├── test_llm_filter/ # 规则层 + LLM 层(真实 OCR 回归数据)
│ ├── test_subtitle_cleanup/ # 幻觉删除 + 短呻吟过滤
│ ├── test_subtitle_correction/# 领域纠错(上下文/误听泛化)
│ ├── test_proper_nouns/ # 专名/隐语规则表
│ ├── test_adaptive_pool/ # 自适应并发额度
│ ├── test_vad_profiler/ # VAD 信号分析与评分
│ └── test_echo/ # 示例节点
├── app/ # 对应 src/wov_app/
│ ├── test_db/ # SQLite Repository
│ ├── test_scheduler/ # DAG 调度、断点、暂停
│ ├── test_batch/ # 批量扫描/放置/引擎
│ ├── test_maintenance/ # 孤儿清理
│ ├── test_registry/ # 节点注册表(唯一调用入口)
│ ├── test_seed/ # 内置工作流加载
│ ├── test_storage/ # 原子复制
│ ├── test_config/ # 环境变量与路径(子进程验证)
│ ├── test_logging/ # 日志配置
│ ├── test_main/ # 应用装配与生命周期
│ └── test_routers/ # 三组 APIapps / workflows / batch
├── sdk/test_models/ # 对应 src/wov_sdk/(协议数据模型)
├── web/test_crop/ # 对应 web/assets/(框选几何换算)
├── web/test_batch/ # 对应 web/assets/(批量页渲染)
├── scripts/test_fix_zombie_batch_jobs/ # 对应 scripts/(僵尸批量任务修复)
├── scripts/test_plan_regenerate_subtitles/ # 对应 scripts/(字幕重生成计划统计)
└── shared/ # 跨模块公共设施
├── realdata_contract.py # 真实数据契约与对齐量化
├── srt_entries.py # 按秒解析 SRT
├── env_isolation.py # 环境/临时目录隔离
├── data/alignment/ # 跨模块共享的真实素材 + 参考字幕
├── test_srt_entries/ # 公共设施自身的模块测试
└── test_alignment/ # 真实语音时间对齐集成测试
```
规则要点(完整表述见 [../AGENTS.md](../AGENTS.md#测试规则)):
- 每个模块目录含 `__init__.py`,因此不同模块下的同名测试文件可共存。
- 每个测试写成**数据 → 测试过程 → 验证结果**三段,可单独运行。
- **不保留全局 `conftest.py`**(已删除):隔离由模块自己的 fixture 完成
`tests/shared/env_isolation.py` 或模块内 `fixture`)。
- 测试过程调用真实生产代码(`nodes/``wov_app``wov_sdk`),
只自行构建测试数据并验证输出;mock 仅用于 I/O 边界与模型推理。
## 测试数据
测试媒体一次性准备后**随模块目录入库**,测试直接复用,禁止在执行时现场生成;
缺失时跳过而非现场生成。小体量数据(JSON 片段、期望输出)直接写在测试代码内。
| 素材 | 内容 | 归属模块 |
| --- | --- | --- |
| `data/speech_60s.wav` | 真实语音(合法 16kHz 单声道 WAV) | `tests/nodes/test_whisper/` |
| `data/clip_with_audio.mp4` | 10 秒真实视频 + 真实语音音轨(提音用例) | `tests/nodes/test_ffmpeg/` |
| `data/subtitle_10s.mp4` | 烧录 SUB 001/SUB 002 的 10s 视频(无音轨,验证失败路径) | `tests/nodes/test_ffmpeg/``tests/nodes/test_frame_extract/` |
| `data/frames_boundary/*.png` | 跨越 9999 帧边界的真实帧文件名(14236 帧事故回归) | `tests/nodes/test_frame_extract/` |
| `data/ocr_text.png` / `ocr_notext.png` | 有文字帧 / 无文字帧 | `tests/nodes/test_vlm/` |
| `data/test_real_hav_sub.png` | 真实视频字幕截图(期望识别出"还有没有什么困扰 或者奇怪的地方吗") | `tests/nodes/test_vlm/` |
| `data/ocr_srt_run_ac7f480a3ccb.srt` | 真实任务 1666 条 OCR 输出(规则层回归基线) | `tests/nodes/test_llm_filter/` |
| `data/frames_manifest_full.json` + `ocr_frames_full.json` | 真实任务 **全部 14236 帧**清单与逐帧 OCR 文本 | `tests/nodes/test_subtitle_ocr/` |
| `data/sample.reference.srt` | 真实参考字幕(5 条,含装饰行) | `tests/nodes/test_srt/` |
| `data/alignment/*.mp4` + `*.reference.srt` | 真实音视频 + 人工校对参考字幕(大体积 mp4 已 gitignored | `tests/shared/data/alignment/` |
| `scripts/data/translate_eval/*.jsonl` | 翻译模型评测集(评测脚本专用) | `scripts/` |
## 真实模型集成测试
使用真实模型/服务 + 真实素材验证端到端行为;本地缺模型或素材时跳过,
有则必须执行,作为对替身单测的校准。
**外部环境状态的两种处理**(都不算代码缺陷):
| 状态 | 处理 |
| --- | --- |
| 未配置密钥、账户余额/配额、限流(HTTP 401/402/403/429)、服务不可达 | `tests/shared/llm_service.py` 统一识别并跳过 |
| GPU 显存不足 / CUDA OOM | `tests/shared/gpu_memory.py` 运行时探测并跳过 |
`gpu_memory` 的两层防护:运行前按权重体量估算(实测 V2 权重 2.87GB → 峰值
增量约 4.1GB,系数 1.45)判断能否跑完;运行中若仍发生 CUDA OOM(临界卡上
的分配碎片化)则转为跳过。`fits_with_margin()` 进一步区分"显存充裕"与"临界"
充裕时走生产默认的分块路径,临界时退化为整段单次推理,避免整组跳过。
**权重版本**:测试只使用 **V2** 权重。V3 已全面停用(均改用 V2),即使留在
盘上也不得被测试使用;判据是 `preprocessor_config.json``feature_size`
V2=80V3=128),不依赖目录名。
| 模块目录 | 覆盖内容 |
| --- | --- |
| `tests/nodes/test_whisper/` | 真实 faster-whisper **V2** 模型端到端转写 |
| `tests/nodes/test_vlm/` | 真实 Ollama glm-ocr(有文字帧 + 无文字帧) |
| `tests/nodes/test_subtitle_ocr/` | 真实 14236 帧清单重组装一致性 |
| `tests/nodes/test_llm/` | 真实 LLM 翻译(专名不硬译) |
| `tests/nodes/test_llm_filter/` | 真实 OCR 输出规则层回归(保留 863 / 删除 803 基线) |
| `tests/nodes/test_subtitle_correction/` | 真实 LLM 误听泛化 |
| `tests/shared/test_alignment/` | 真实语音转写 vs 人工参考字幕的时间对齐量化 |
## 功能模块覆盖清单
规则要求**追求功能性代码覆盖率 100%、每个独立功能模块必须被测试覆盖**
(不追求行覆盖率 100%,大模块内辅助函数由主功能用例顺带覆盖即可,
完整表述见 [../AGENTS.md](../AGENTS.md#功能覆盖优先不追求代码覆盖率))。
本表是新增/修改模块时的核对清单:**新增独立功能模块必须同时补测试并更新本表**。
| 模块 | 职责 | 覆盖测试 | 状态 |
| --- | --- | --- | --- |
| `nodes/echo.py` | 示例回显节点 | `tests/nodes/test_echo/` | 已覆盖 |
| `nodes/ffmpeg.py` | 提音与 ffmpeg 定位 | `tests/nodes/test_ffmpeg/`(真实视频) | 已覆盖 |
| `nodes/whisper.py` | 转写、分块、模型解析、清洗入口 | `tests/nodes/test_whisper/`(含真实模型)+ `tests/shared/test_alignment/` | 已覆盖 |
| `nodes/llm.py` | LLM 翻译节点 | `tests/nodes/test_llm/`(含真实 LLM | 已覆盖 |
| `nodes/ass.py` | SRT → VR 双目 ASS | `tests/nodes/test_ass/` | 已覆盖 |
| `nodes/vlm.py` | 单帧 OCROllama | `tests/nodes/test_vlm/`(含真实模型) | 已覆盖 |
| `nodes/frame_extract.py` | 抽帧与 crop | `tests/nodes/test_frame_extract/`(真实视频 + 帧号边界) | 已覆盖 |
| `nodes/subtitle_ocr.py` | 逐帧 OCR → 汇总 SRT、断点/暂停 | `tests/nodes/test_subtitle_ocr/`14236 帧) | 已覆盖 |
| `nodes/adaptive_pool.py` | 自适应并发线程池 | `tests/nodes/test_adaptive_pool/` | 已覆盖 |
| `nodes/llm_filter.py` | 字幕规则/LLM 两级过滤 | `tests/nodes/test_llm_filter/` | 已覆盖 |
| `nodes/subtitle_cleanup.py` | 幻觉与短呻吟清洗 | `tests/nodes/test_subtitle_cleanup/` | 已覆盖 |
| `nodes/subtitle_correction.py` | 字幕领域纠错 | `tests/nodes/test_subtitle_correction/` | 已覆盖 |
| `nodes/proper_nouns.py` | 专名/隐语规则表 | `tests/nodes/test_proper_nouns/` | 已覆盖 |
| `nodes/srt.py` | SRT 解析/序列化/时间戳 | `tests/nodes/test_srt/` | 已覆盖 |
| `nodes/vad_profiler.py` | VAD 信号分析与评分 | `tests/nodes/test_vad_profiler/` | 已覆盖 |
| `wov_app/registry.py` | 节点注册表(唯一调用入口) | `tests/app/test_registry/` | 已覆盖 |
| `wov_app/scheduler.py` | DAG 调度与断点续跑 | `tests/app/test_scheduler/` | 已覆盖 |
| `wov_app/batch.py` | 文件夹批量引擎 | `tests/app/test_batch/` | 已覆盖 |
| `wov_app/db.py` | SQLite Repository | `tests/app/test_db/` | 已覆盖 |
| `wov_app/main.py` | 应用装配与生命周期 | `tests/app/test_main/` | 已覆盖 |
| `wov_app/routers/apps.py` | 用户端 API | `tests/app/test_routers/test_apps_api.py` | 已覆盖 |
| `wov_app/routers/workflows.py` | 管理端工作流 API | `tests/app/test_routers/test_workflows_api.py` | 已覆盖 |
| `wov_app/routers/batch.py` | 批量任务 API | `tests/app/test_routers/test_batch_api.py` | 已覆盖 |
| `wov_app/seed.py` | 内置工作流加载 | `tests/app/test_seed/` | 已覆盖 |
| `wov_app/maintenance.py` | 孤儿数据清理 | `tests/app/test_maintenance/` | 已覆盖 |
| `wov_app/storage.py` | 原子复制 | `tests/app/test_storage/` | 已覆盖 |
| `wov_app/config.py` | 路径与环境配置 | `tests/app/test_config/`(子进程验证真实解析) | 已覆盖 |
| `wov_app/logging.py` | 日志装配 | `tests/app/test_logging/` | 已覆盖 |
| `wov_app/schemas.py` | 请求模型(Pydantic | `tests/app/test_main/`schema 用例) | 已覆盖 |
| `wov_sdk/models.py` | 协议数据模型 | `tests/sdk/test_models/` | 已覆盖 |
| `web/assets/crop.js` | 框选几何换算 | `tests/web/test_crop/`(真实 node 执行) | 已覆盖 |
| `web/assets/batch.js` | 批量页明细/进度渲染 | `tests/web/test_batch/`(真实 node 执行) | 已覆盖 |
| `scripts/fix_zombie_batch_jobs.py` | 僵尸批量任务诊断与修复 | `tests/scripts/test_fix_zombie_batch_jobs/` | 已覆盖 |
| `scripts/plan_regenerate_subtitles.py` | 媒体库字幕重生成计划统计(数量/时长/分类) | `tests/scripts/test_plan_regenerate_subtitles/` | 已覆盖 |
| `tests/shared/srt_entries.py` | 按秒解析 SRT(测试公共设施) | `tests/shared/test_srt_entries/` | 已覆盖 |
| `tests/shared/realdata_contract.py` | 真实数据契约与对齐量化 | `tests/shared/test_alignment/` | 已覆盖 |
| `tests/shared/env_isolation.py` | 环境/临时目录隔离 | 被 `tests/app/test_config` 等间接覆盖 | 已覆盖(间接) |
核对方式(查看某模块是否真被执行):
```bash
uv run pytest tests/ -q --cov=wov_app --cov-report=term-missing
```
## 重构历史(已完成)
按新规则对 `tests/` 做了一次完整重写:旧平铺测试(40 个文件、约 10900 行)
已删除,旧内容仅作为**参考与测试数据来源**;现有测试全部按模块目录重写。
重构期间发现并修复的真实缺陷:
| 缺陷 | 发现方式 | 修复 |
| --- | --- | --- |
| `parse_srt` 在相邻条目缺少空行时把下一条时间轴吞进正文(静默错位) | `tests/nodes/test_srt/` 的红灯用例 | 正文行遇时间戳行即报错(`nodes/srt.py` |
| `scheduler._file_size` 只捕获 `OSError`,含 `\\x00` 的产物 URI 抛 `ValueError` 导致任务误判失败 | `tests/app/test_batch/` 端到端用例 | 同时捕获 `ValueError``src/wov_app/scheduler.py` |
| 生产节点 `subtitle_correction` 依赖测试包 `tests.realdata_contract` 解析 SRT | 模块化拆分时发现 | 改用生产模块 `nodes/srt.py` 的严格解析器 |
## 相关文档
- 测试规则(模块边界、三段结构、功能覆盖、TDD):[../AGENTS.md](../AGENTS.md#测试规则)
- 缺陷回归要求与验收标准:[代码审查问题跟踪.md](./代码审查问题跟踪.md)
- 模型/参数选择的实验依据:[decisions.md](./decisions.md)