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

17 KiB
Raw Blame History

代码审查问题跟踪

本文件跟踪本次项目审查发现的问题、修复范围与验证结果。状态:待处理 / 处理中 / 已修复。只有完成实现与相关验证后才标记已修复;代码修改不代表已部署。

全部审查问题的修复统一在 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/ 下的真实视频与字幕副本、临时数据库及私有存储;覆盖批量任务拒绝删除、正常上传清理、历史外部输入路径保护和自动清理路径保护。
  • 实现:普通删除接口 在删除前拒绝批量 run自动清理器 和普通接口均按私有目录布局定位清理范围。
  • 回归测试: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)。
  • TDD 红:uv run pytest tests/test_run_deletion_safety.py -q --tb=short7 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 -q73 passed7.73 秒;仅有既存 Starlette/httpx 弃用警告。
  • 状态:修复及验证完成,纳入 fix/review-improvements 分支;未部署。运行中的旧进程需加载新代码后才能获得保护。
  • 后续边界:普通列表仍会显示批量 run,但点击删除会收到批量入口提示;运行中删除的统一取消/状态协调属于 O08,未在本次展开。

R02 修复记录

  • 根因:report_failure() 用降低后的最大值直接调用扩缩容,导致当前 1 并发扩为 19;缩容哨兵追加在 FIFO 积压队列尾部,存量任务仍按旧并发执行。
  • 实现: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=short85 passed51.60 秒;包含真实 14236 帧顺序数据、OCR 暂停、过滤限流重试。
  • 补充重试额度用例后:uv run pytest tests/test_adaptive_pool.py -q22 passed0.27 秒。连续限流后的第二轮 map 保持 1 并发,进度序号重新从 1 开始。git diff --check 通过。
  • 状态:修复及验证完成,纳入 fix/review-improvements 分支;未部署。尚未对真实服务配额下的吞吐做性能结论;已发出请求的终止依赖节点自身超时。

R03 修复记录

  • 根因:最终产物原地改名后,节点输出 URI 未同步,恢复时旧路径覆盖有效最终记录;批量放置对缺失产物直接跳过,随后仍清理并标记完成。
  • 实现:scheduler.py 保留节点原文件,把命名成品复制至 run 的 finals 别名目录;时间戳固定取 run 创建时间,重复收尾路径稳定。缺少最终引用或文件时进入 FAILED。
  • 兼容:旧版本已经改名但最终别名记录仍有效时复用该路径,不再覆盖为失效节点 URI;原始与最终文件均丢失时明确报错。
  • 文件操作:storage.py 提供同目录临时文件复制和原子替换,失败清理临时文件、保留源及原目标。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=short97 passed3.22 秒。
  • 补充批量复制故障及安全回归:uv run pytest tests/test_finalization.py tests/test_maintenance.py tests/test_run_deletion_safety.py -q --tb=short22 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=no71 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_validate_definition 在校验后调用 topological_sort,环形 DAG(含自环)以 "workflow contains a cycle" 转 422;创建/校验/发布三个入口共用该函数,publish 额外重新校验最新版本定义,历史遗留的环形版本无法进入应用中心。校验先于任何写库,被拒请求不产生工作流或版本记录。
  • 修复 2(历史无效任务):scheduler.pyfrom_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 -q29 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 -q95 passed2.59 秒。
  • 全量回归:uv run pytest -q402 passed、5 skipped84.03 秒;仅既存 Starlette/httpx 弃用警告。git diff --check 通过。
  • 状态:修复及验证完成,纳入 fix/review-improvements 分支工作区(与 R05/R06 未提交改动共存);未部署。历史遗留的环形已发布工作流不会被自动取消发布,仍可创建任务,但任务会立即 FAILED 且不堵塞队列;如需清理线上遗留数据需另行确认后执行。

审查基线

  • 修复前全套测试:uv run pytest369 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.pyrecover_interrupted_batch_jobs 在恢复 RUNNING 任务之外,同时把"COMPLETED 且存在 PENDING/RUNNING/PAUSED 明细"的任务置回 QUEUED;启动时即执行,引擎随后从断点续跑。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 -q57 passed。
  • 状态:修复及验证完成;实际数据由运行中的服务在改动落盘后重启、启动恢复时自动纠正,视频已重新进入 asr 节点处理。