模型与管线切换后需要把媒体库里旧管线生成的字幕整批重跑。批量引擎按"视频旁 已有字幕即 SKIPPED"判定,重跑前必须先统计范围、再把旧字幕改名备份,否则建出来 的任务会把所有视频全部跳过。 - `scripts/plan_regenerate_subtitles.py`:扫描媒体库,按 CN 产物的最早 mtime 判定 生成时间,统计待重生成的数量/时长/体积并输出 JSON 计划;`--backup-old [--apply] [--select]` 把旧字幕原地改名为 `<原名>.old-<日期>` 供批量重跑。脚本不依赖仓库, 可拷到媒体库主机直接跑(CIFS 挂载下逐文件 ffprobe 太慢)。 - 测试覆盖真实 10 秒视频与真实字幕文件,含 ffprobe 时长探测、分类边界、 备份幂等与选择清单解析。
193 lines
14 KiB
Markdown
193 lines
14 KiB
Markdown
# 测试
|
||
|
||
测试代码位于 `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/ # 三组 API(apps / 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=80,V3=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` | 单帧 OCR(Ollama) | `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)
|