Files
vrsub/docs/代码审查问题跟踪.md
T
cat-shark dcdc5e8604 feat: 批量分块流水线、本地模型显存让渡与任务列表分工
批量引擎改为「分块流水线」:视频按 WOV_BATCH_STAGE_GROUP_SIZE(默认 8)分组,
组内按 DAG 拓扑序跑完全部视频(全部 extract → 全部 ASR → 全部翻译 → 全部 ASS)
再进入下一组,本地模型每组只加载一次、卸载一次,而不是每个视频来回加载卸载;
产物仍按组增量落到视频旁。调度器新增 execute_run(run_id, stop_after=节点):
该节点完成后任务保持 RUNNING 不收尾,下一次调用从产物表跳过已完成节点继续,
用于实现阶段边界。

- nodes/llm.py:翻译节点结束释放本机 Ollama 显存(node 参数 unload_after >
  LLM_UNLOAD_AFTER > 本机 loopback 端点默认卸载,云端端点不卸载;卸载失败只告警),
  新增 keep_model.flag 语义(阶段内保持常驻)与 release_local_model();
  新增节点内暂停(按批 20 行检查 paused.flag,抛 PauseRequested,调度器保持 PAUSED)。
- src/wov_app/batch.py:分组阶段执行与阶段末统一释放显存;失败视频只在它失败
  节点的那个阶段重试(避免 LLM 已常驻时重跑 ASR 抢显存);任务没有明细时保持
  QUEUED 等登记完成、仍有未完成视频时置回 QUEUED 自愈(原先留 RUNNING 会卡死:
  引擎只拾取 QUEUED,任务停在“运行中但没人推进”);无失败视频时删除任务级空目录;
  每个阶段开始前清理 paused.flag / keep_model.flag,避免强杀残留影响后续阶段。
- src/wov_app/config.py:新增 WOV_BATCH_STAGE_GROUP_SIZE(设为 1 即旧的每视频全链路)。
- 任务列表与批量页分工:GET /api/runs 默认排除 source=batch(一个批量任务会产生
  N 条单视频 run,会把 20 条窗口占满;且任务管理页的暂停/重试/删除对批量 run
  语义不成立),需要排查时用 include_batch=1;作为补偿批量页详情新增阶段列
  (阶段 i/N · 中文标签,由该视频 run 的 current_node_id 在 DAG 拓扑序中的位置
  推导,节点类型映射中文标签)。阶段只有节点边界粒度,句级进度不落库、只在日志。
- 顺带纳入此前未提交的批量僵尸状态恢复:recover_interrupted_batch_jobs 除 RUNNING
  外也把「COMPLETED 但仍含未结束视频」的任务置回 QUEUED;fix_zombie_batch_jobs.py
  改为按条件扫描并支持 --apply 预览;批量页明细只列本批真正处理过的视频。

测试新增/更新:分块流水线调用顺序(组内按节点跑完再下一组)、每组只释放一次模型、
阶段内保持常驻标志、翻译按批暂停、失败视频不跨阶段推进、任务无明细/中途登记视频时
置回 QUEUED、任务工作空间与残留信号清理、任务列表默认过滤批量 run、详情阶段字段、
前端阶段列渲染;全量 507 passed(唯一失败为既有素材缺失的 integration 用例)。
2026-09-18 10:31:52 +08:00

111 lines
17 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.
# 代码审查问题跟踪
本文件跟踪本次项目审查发现的问题、修复范围与验证结果。状态:待处理 / 处理中 / 已修复。只有完成实现与相关验证后才标记已修复;代码修改不代表已部署。
全部审查问题的修复统一在 `fix/review-improvements` 分支推进,验证完成后由用户合并到 `master`
## 缺陷清单
| ID | 优先级 | 问题 | 状态 | 验收标准 |
| --- | --- | --- | --- | --- |
| R01 | P0 | 普通删除接口可能删除批量源视频目录;清理逻辑从输入路径推导删除范围 | 已修复 | 普通接口拒绝单独删除批量 run;手动与自动清理只删除该上传任务的私有目录;视频、旁挂字幕及其他任务文件不受影响 |
| R02 | P1 | 自适应线程池限流后可能扩容;退出标记位于积压队列尾部,缩容不及时 | 已修复 | 限流不增加并发;降低目标后不再超额提交;真实积压队列验证 |
| R03 | P1 | 最后节点执行时暂停再继续会覆盖有效下载 URI;批量缺产物仍清理并完成 | 已修复 | 收尾幂等,恢复后全部必需产物可下载;缺产物不清理工作空间 |
| R04 | P1 | 环形 DAG 可通过保存校验,执行失败后仍在 QUEUED 堵塞队列 | 已修复 | 保存/发布拒绝无效 DAG;历史无效任务进入 FAILED,不阻塞后续任务 |
| R05 | P1 | 翻译按固定四行解析 SRT;补齐行数不能保证文本与时间轴对应 | 已修复 | 合法多行 cue 正确解析;按稳定 ID 回填译文并校验缺失、重复项 |
| R06 | P1 | OCR 跨空白帧合并相同字幕;临时请求失败被永久存为无文字 | 已修复 | 空白帧结束当前字幕段;无文字和可重试失败分开存档 |
| R07 | P2 | 抽帧/分块目录残留污染重跑;OCR/过滤存档无输入和参数指纹 | 待处理 | 输出减少时无旧文件混入;输入/有效参数变化时断点失效 |
| R08 | P2 | 批量任务创建与明细不原子;不区分工作流版本时的执行/收尾一致性 | 待处理(方案调整) | 用户决定不区分工作流版本,不实施固定版本方案;工作线程只看到完整任务,收尾使用本次执行实际产物 |
## 优化与维护清单
下列项目为优化建议,收益需按真实负载验证,尚未承诺实施顺序。
| ID | 项目 | 状态 | 验收方向 |
| --- | --- | --- | --- |
| O01 | Whisper 模型缓存及上传/批量共享 GPU 并发额度 | 待处理 | 相同配置复用模型;初始化线程安全;显存有界 |
| O02 | Whisper 分块、LLM 翻译批次断点 | 待处理 | 中断后只补未完成部分;输入与参数身份匹配 |
| O03 | VAD 流式读取、向量化 RMS | 待处理 | 与现有 RMS 结果一致;内存不随全片样本数增长;记录性能对比 |
| O04 | LLM 公共客户端、连接复用、统一限流重试 | 待处理 | 网络故障处理一致;提示词保留在各节点;避免重复计费与无界重试 |
| O05 | 字幕纠错批量化、减少重复上下文生成 | 待处理 | 保留目标 ID;固定输入上下文;真实字幕质量验证 |
| O06 | 合并媒体探测、减少 OCR 小文件,评估分段抽帧与 OCR 重叠执行 | 待处理 | 保持时间轴和落盘协议,测量 I/O 与吞吐变化 |
| O07 | 统一 SRT 解析/序列化/时间戳,移除生产代码对 tests 包的依赖 | 待处理 | 正式安装包可运行;多行 cue、空字幕、时间精度回归通过 |
| O08 | 统一运行状态、工作空间与 JSONL 存档公共能力 | 待处理 | 状态条件更新、文件归属明确、存档损坏可恢复 |
| O09 | 后台线程停止与窗口统计可靠性 | 待处理 | 停止等待可中断;不丢弃仍存活线程;并发统计一致 |
| O10 | 批量目录索引、聚合查询、数据库索引与轮询 | 待处理 | 避免每视频重复扫描目录;GET 不刷新状态更新时间;相关查询有界 |
| O11 | Whisper 解码耗时统计 | 待处理 | 消费 segments 生成器后计时,使用实际块时长计算实时倍率 |
| O12 | 校正测试断言与文档漂移 | 待处理 | 成功验证包括产物可读/可下载;对齐测试可在正确结果下通过;文档与模型/断点实现一致 |
## R01 修复记录
- 根因:普通任务删除接口未区分 `source=batch`,直接递归删除 `Path(input_uri).parent`;自动清理器也使用输入路径推导删除目录。
- 处理约定:普通删除接口拒绝批量 run(422),提示通过批量任务入口删除;上传任务只清理 `<storage>/uploads/<run_id>``<storage>/runs/<run_id>`。批量任务已有专用删除入口,保留源视频和视频旁成品。
- 测试要求:使用各模块 `data/` 下的真实视频与字幕副本、临时数据库及私有存储;覆盖批量任务拒绝删除、正常上传清理、历史外部输入路径保护和自动清理路径保护。
- 实现:[普通删除接口](../src/wov_app/routers/apps.py) 在删除前拒绝批量 run[自动清理器](../src/wov_app/maintenance.py) 和普通接口均按私有目录布局定位清理范围。
- 回归测试:`tests/app/test_batch/`(批量删除与旁挂字幕保护)、`tests/app/test_maintenance/`(自动清理路径保护)、`tests/app/test_routers/test_apps_api.py`(删除接口拒绝批量 run)。测试代码已按模块化重构,原平铺文件 `tests/test_run_deletion_safety.py` 已删除,用例覆盖范围不变(详见 [testing.md](./testing.md#重构历史已完成))。
- TDD 红:`uv run pytest tests/test_run_deletion_safety.py -q --tb=short`7 failed;批量 run 被错误删除,两个外部路径用例复现媒体文件丢失。
- TDD 绿及相关回归:`uv run pytest tests/test_run_deletion_safety.py tests/test_apps_api.py tests/test_batch.py tests/test_maintenance.py -q`73 passed7.73 秒;仅有既存 Starlette/httpx 弃用警告。
- 状态:修复及验证完成,纳入 `fix/review-improvements` 分支;未部署。运行中的旧进程需加载新代码后才能获得保护。
- 后续边界:普通列表仍会显示批量 run,但点击删除会收到批量入口提示;运行中删除的统一取消/状态协调属于 O08,未在本次展开。
## R02 修复记录
- 根因:`report_failure()` 用降低后的最大值直接调用扩缩容,导致当前 1 并发扩为 19;缩容哨兵追加在 FIFO 积压队列尾部,存量任务仍按旧并发执行。
- 实现:[adaptive_pool.py](../nodes/adaptive_pool.py) 改为标准 `ThreadPoolExecutor` 复用线程,map 按目标额度有界提交并串行收集结果。移除自建工作队列与退出哨兵,限流更新和提交共享锁。
- 限流语义:只保持或降低当前额度;已发出的请求允许完成,后续提交遵守新额度;错误窗口不扩容,干净窗口逐步恢复上限。有效上限跨重试 map 保留,每批重新统计窗口与真实单任务耗时。
- 兼容行为:map 按输入顺序返回结果,worker 异常仍作为结果返回;cancel 仍只抑制进度回调,OCR 自行检测暂停。线程执行器在 map 结束时回收。
- TDD 红:3 个新增复现测试全部失败,分别观测到限流后目标 19、缩容后后续实际并发 4、错误窗口仍增加额度。
- TDD 绿及调用方回归:`uv run pytest tests/test_adaptive_pool.py tests/test_llm_filter.py tests/test_ocr_flow.py tests/test_subtitle_ocr_order_threading.py -q --tb=short`85 passed51.60 秒;包含真实 14236 帧顺序数据、OCR 暂停、过滤限流重试。
- 补充重试额度用例后:`uv run pytest tests/test_adaptive_pool.py -q`22 passed0.27 秒。连续限流后的第二轮 map 保持 1 并发,进度序号重新从 1 开始。`git diff --check` 通过。
- 状态:修复及验证完成,纳入 `fix/review-improvements` 分支;未部署。尚未对真实服务配额下的吞吐做性能结论;已发出请求的终止依赖节点自身超时。
## R03 修复记录
- 根因:最终产物原地改名后,节点输出 URI 未同步,恢复时旧路径覆盖有效最终记录;批量放置对缺失产物直接跳过,随后仍清理并标记完成。
- 实现:[scheduler.py](../src/wov_app/scheduler.py) 保留节点原文件,把命名成品复制至 run 的 finals 别名目录;时间戳固定取 run 创建时间,重复收尾路径稳定。缺少最终引用或文件时进入 FAILED。
- 兼容:旧版本已经改名但最终别名记录仍有效时复用该路径,不再覆盖为失效节点 URI;原始与最终文件均丢失时明确报错。
- 文件操作:[storage.py](../src/wov_app/storage.py) 提供同目录临时文件复制和原子替换,失败清理临时文件、保留源及原目标。[batch.py](../src/wov_app/batch.py) 先预检全部必需成品,复制成功后才清理 run 和工作空间;中途失败保留断点供重试。
- TDD 红:新增/强化的 8 个场景失败,覆盖暂停恢复链接、旧改名记录、多别名同源、缺少输出引用/文件、批量缺产物清理、节点原始 URI 保留及复制故障。
- 相关回归:`uv run pytest tests/test_finalization.py tests/test_scheduler.py tests/test_batch.py tests/test_apps_api.py tests/test_db.py -q --tb=short`97 passed3.22 秒。
- 补充批量复制故障及安全回归:`uv run pytest tests/test_finalization.py tests/test_maintenance.py tests/test_run_deletion_safety.py -q --tb=short`22 passed,5.83 秒。真实视频/字幕验证写满故障后旧成品和工作空间完整,故障解除可成功放置并清理;API 下载验证原始文件与两个最终别名均可读取。仅有既存 Starlette/httpx 弃用警告。
- 状态:修复及验证完成,纳入 `fix/review-improvements` 分支;未部署。原子性为单文件级,多文件中途失败允许部分完整成品已经更新,重试会重新放置;不承诺断电持久性。
- 剩余边界:R08 按用户决定调整为不区分工作流版本,固定版本方案取消;版本机制调整及任务原子创建另行实施。此修复不会自动重建已经丢失的历史成品。
## R05 / R06 修复记录
- 顺序:按用户要求先完成 R05,再完成 R06;R03 已提交为 `3a61291`
- R05:新增 `nodes/srt.py` 按 cue 解析 BOM/CRLF、多行及空正文,坏字幕明确报错;翻译以全局位置 ID 的 JSON 条目请求,校验 ID 集合、类型、唯一性及正文。乱序按 ID 回填,结构错误最多尝试 3 次,耗尽 failed,取消末尾合并/补空策略。空 cue 保留时间轴且不请求模型,原有幻觉清洗继续执行。
- R05 TDD:12 个新回归用例先失败;相关节点/清洗/专名/翻译测试 93 passed、1 skipped、18 deselected(排除 Whisper)。最后调整提示词统一“条目”措辞后翻译对齐测试 13 passed(含真实短句 LLM 校准),0.76 秒。
- R06:空帧终止当前字幕段;失败/异常不写成功存档,失败帧在收紧的并发下重试一次,仍失败则节点 failed,已成功帧断点保留。新存档 status=completed 表示成功(含无文字),status=skipped 表示超长输出按既有规则跳过。
- 旧存档兼容:非空结果继续复用,无状态旧空串因无法区分超时与无文字而重新 OCR;不自动重跑已完成的历史任务。
- R06 TDD:4 个新回归先失败,覆盖跨空帧合并、failed 响应、抛异常和旧空串恢复。全量数据修正后 1942 条,旧 1666 条文件未修改;测试比较真实单线程、4/16 线程与断点结果,并逐采样点检查字幕不跨空白。基线必须走完整节点路径(包含超长过滤),不以原始 OCR 文本冒充成功存档。
- 最终组合验证:`uv run pytest tests/test_ocr_recovery.py tests/test_ocr_flow.py tests/test_subtitle_ocr_order_threading.py tests/test_integration_subtitle_ocr.py tests/test_translation_line_alignment.py tests/test_hallucination_mask.py tests/test_proper_nouns.py -q --tb=short --show-capture=no`71 passed、1 skipped58.69 秒;跳过项缺少历史专名素材,真实 LLM 与真实 OCR 集成通过。`git diff --check` 通过。
- 状态:R05/R06 工作区已修复,尚未提交、未部署。模型语义是否正确仍需内容质量评估,ID 校验保证程序不会因列表乱序或漏项贴错时间。
- R08 决策:用户计划不区分工作流版本,取消固定版本修复方案;版本机制调整、执行/收尾一致性及批量原子创建保留待办,本轮未实施数据库迁移。
## R04 修复记录
- 根因:①保存路径只做结构校验(`WorkflowDefinition.validate()`:名称/版本/节点 ID 唯一/边引用存在),环形依赖结构合法因而被写库并发布;②`execute_run` 的 DAG 解析、校验与拓扑排序在 `try` 块**之外**,环检测抛出的异常直接冒到 `_loop` 被吞掉,任务状态从未离开 QUEUED——`next_queued_run` 每轮拾起同一条队首记录,后续任务永久堵塞。
- 修复 1(保存/发布):[routers/workflows.py](../src/wov_app/routers/workflows.py) 的 `_validate_definition` 在校验后调用 `topological_sort`,环形 DAG(含自环)以 "workflow contains a cycle" 转 422;创建/校验/发布三个入口共用该函数,`publish` 额外重新校验最新版本定义,历史遗留的环形版本无法进入应用中心。校验先于任何写库,被拒请求不产生工作流或版本记录。
- 修复 2(历史无效任务):[scheduler.py](../src/wov_app/scheduler.py) 把 `from_dict` / `validate` / `topological_sort` 移入 try,任何预检异常都立刻把 run 置 FAILED 并记录错误后返回,队首随即前移,不再堵塞后续任务。
- 方案边界:环检测放在**入口校验**(保存/发布)而非 `WorkflowDefinition.validate()`,因此 `WorkflowDefinition.validate()` 保持只做结构校验、SDK 协议模型不变;批量任务在工作流级校验仍放行环形定义,由调度器把每个视频的 run 判 FAILED(保留既有"单视频失败不中断整批"语义,`test_batch_worker_cycle_fails_video_not_job` 未改动仍通过)。批量的 run 现在由调度器直接置 FAILED 并回填错误,不再依赖异常冒泡。
- TDD 红:新增 5 个回归用例先失败——创建带环工作流返回 200、校验接口返回 200、发布历史环形版本返回 200、环形任务执行后仍停在 QUEUED(队首堵塞)、缺 name 的非法定义同样卡住队列。
- TDD 绿及相关回归:`uv run pytest tests/test_workflow_api.py tests/test_scheduler.py -q`29 passed1.07 秒;`uv run pytest tests/test_batch.py tests/test_apps_api.py tests/test_seed.py tests/test_models.py tests/test_registry.py tests/test_api.py tests/test_finalization.py -q`95 passed2.59 秒。
- 全量回归:`uv run pytest -q`402 passed、5 skipped84.03 秒;仅既存 Starlette/httpx 弃用警告。`git diff --check` 通过。
- 状态:修复及验证完成,纳入 `fix/review-improvements` 分支工作区(与 R05/R06 未提交改动共存);未部署。历史遗留的环形已发布工作流不会被自动取消发布,仍可创建任务,但任务会立即 FAILED 且不堵塞队列;如需清理线上遗留数据需另行确认后执行。
## 审查基线
- 修复前全套测试:`uv run pytest`369 passed、6 skipped76.75 秒。
- 隔离复现已确认:源视频目录误删、限流后 1 → 19 并发、队列积压时缩容滞后、暂停恢复后成品 URI 失效、环形 DAG 阻塞队首、多行 SRT 损坏、OCR 跨空白合并、重用抽帧目录留下旧尾帧。
- 本文件中的“已修复”只表示当前工作区实现及验证完成;部署状态需另行记录。
## 僵尸批量任务自动恢复(2026-09)
- 现象:`batch_351833b7d446` 状态为 COMPLETEDdone=0/failed=0),明细里仍有 1 个 PENDING 视频未处理;用户看到"已完成"却什么都没做。
- 根因:完成标记先于视频收尾写出——旧版 `_run_job` 遍历结束后无条件把任务置 COMPLETED,而"置完成前校验无未结束明细"的修复(`leftovers` 检查)只对新记录生效;已落库的僵尸数据不会被自动纠正,因为 `next_queued_batch_job` 只拾取 QUEUED。
- 影响面:全库仅此 1 条;其余任务的非终态明细为空。
- 修复:[db.py](../src/wov_app/db.py) 的 `recover_interrupted_batch_jobs` 在恢复 RUNNING 任务之外,同时把"COMPLETED 且存在 PENDING/RUNNING/PAUSED 明细"的任务置回 QUEUED;启动时即执行,引擎随后从断点续跑。[fix_zombie_batch_jobs.py](../scripts/fix_zombie_batch_jobs.py) 从硬编码 job_id 列表改为动态扫描同类僵尸任务,供无需重启时手动修复。
- 验证:README 与运维文档已同步;TDD 红为 `tests/app/test_db/test_database.py::test_recover_interrupted_batch_jobs_requeues_zombie_completed`(恢复数 0)与 `tests/app/test_batch/test_batch.py::test_worker_processes_recovered_zombie_job`(任务停在 COMPLETED 且视频未处理);绿为 `uv run pytest tests/app/test_db tests/app/test_batch tests/scripts tests/web -q`57 passed。
- 状态:修复及验证完成;实际数据由运行中的服务在改动落盘后重启、启动恢复时自动纠正,视频已重新进入 asr 节点处理。