Files
vrsub/docs/operations.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

168 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.
# 运维
## 孤儿数据清理
应用内置后台清理器(`src/wov_app/maintenance.py`),按周期自动清理死数据:
- **自动删除**:无任务记录的上传/步骤残留目录;COMPLETED 且产物文件全部丢失、
超过宽限期(默认 1 小时)的任务记录(下载已全部 404)。
- **绝不自动删除**:FAILED 任务(可重试)、QUEUED/RUNNING 任务、宽限期内的任务、
仍有产物文件的任务。
- 手动删除任务仅通过删除接口(`DELETE /api/runs/{run_id}`)或管理界面进行。
## 任务暂停/继续(2026-08
- **状态机**`QUEUED / RUNNING / PAUSED / COMPLETED / FAILED`。排队中或运行中的
任务可暂停(`POST /api/runs/{run_id}/pause`),PAUSED 可继续
`POST /api/runs/{run_id}/resume` → 恢复 QUEUED)。
- **调度器语义**`next_queued_run` **只取 QUEUED**——PAUSED 任务不会被调度器
自动拾起(修复回归:此前 PAUSED 被拾起后 `execute_run` 先置 RUNNING 再检查,
节点循环读到的是刚改的 RUNNING,"暂停检查"永远不成立 → 任务被复活继续跑,
表现为"点击暂停反而开始任务")。`execute_run` 以 PAUSED 进入时直接返回保持暂停,
必须用户显式 resumePAUSED → QUEUED)后才真正执行;运行中被暂停的任务在每个
节点边界检查状态停下保持 PAUSED(当前节点执行完后才停);继续时从产物表
`restore_run_outputs`,剥去"节点ID."前缀还原输出名)重建已完成节点的输出,
**跳过已完成节点断点续跑**,最后补做 final_outputs 收尾。
- **重启恢复**:进程被杀/重启时遗留的 RUNNING 任务在启动时被
`recover_interrupted_runs` 恢复为 QUEUED(保留产物),调度器自动断点续跑;
PAUSED 任务保持不变,等待显式 resume。
- **节点级断点(subtitle-ocr**OCR 每帧完成后立即把 `{frame, text}` 追加到
`steps/ocr/ocr_partial.jsonl`(多线程下加锁串行化)。invoke 启动时读取存档,
只对未处理帧调用 vlm-ocr,存档文本与新增结果合并后组装 SRT——2 小时视频级
OCR 任务中断/重启后不重复已处理帧,产物与一次跑完逐字节一致。
- **节点内暂停响应(subtitle-ocr)**:暂停接口(`POST /api/runs/{run_id}/pause`
除置 PAUSED 外还向 run 根目录写入 `paused.flag`OCR 工作线程**逐帧检查**该
信号,存在即立即中止(不 OCR、不入存档,恢复时重跑该帧),invoke 返回
failed;调度器捕获节点异常时若任务已是 PAUSED 则**保持 PAUSED 不标 FAILED**
resume 时清除信号并从断点存档继续——点击暂停后 OCR 秒级停下,不再等整个
节点跑完。继续/重试接口与调度器执行前都会清理残留信号。
- **前端**:任务管理页为 QUEUED/RUNNING 提供"暂停"、PAUSED 提供"继续"按钮。
## 进度日志(数据处理速度)
- 调度器:每节点完成打印"任务 X 进度 i/N 节点: Y 耗时 Zs, 运行累计 Ws"
- subtitle-ocr`OCR 进度 X/Y 帧 (Z 帧/s, 平均 Ws/帧, 线程 N/M)`
llm-filter`字幕判定进度 X/Y 条 (Z 条/s, 平均 Ws/条, 线程 N/M)`
——`W` 为最近窗口平均单任务耗时(窗口未满时回退累计平均),`N` 为当前目标
线程数、`M``pool_max_workers` 上限,用于判断多线程是否因单次处理过慢
(窗口平均 ≥ `pool_fast_threshold`)而未扩容(线程池 `on_progress` 回调,
每任务完成触发);
- whisper:分块转写打印"分块 X/Y 完成 offset=... 耗时 Zs (Nx 实时, 累计 ...s)"
- frame-extractffmpeg `-progress` 输出解析 `frame=N`,打印"抽帧进度 X/Y 帧 (Z 帧/s)"。
## 文件夹批量处理
本地版核心能力:**不把视频上传到工作目录**,直接读取用户所选文件夹下的全部
视频,逐个执行所选流水线。入口为批量处理页(`web/batch.html`,导航"批量处理"),
后端为 `src/wov_app/batch.py``BatchWorker`(单线程轮询线程,处理
`source=batch` 的运行,与主调度器互不抢占)与 `routers/batch.py`
- **路径选择**:批量页点击"选择文件夹…"按钮弹出目录树选择器(懒加载),选完
回填只读路径框。浏览器拿不到所选文件夹的绝对路径,因此由**本地后端**提供目录
浏览:`GET /api/batch/roots`Windows 盘符 / POSIX 根 + 家目录)、
`GET /api/batch/dirs?path=`(列直接子目录,隐藏目录过滤;不存在/不可读返回
空列表不报 500)。只暴露目录名,不返回文件内容。
- **创建任务时一次性定位(2026-09 起)**`POST /api/batch/jobs {folder,
workflow_id, recursive}` 只扫描一次文件夹并把每个视频登记为 `batch_videos`
明细(PENDING/RUNNING/PAUSED/COMPLETED/FAILED/SKIPPED)。**视频所在目录
(视频旁)若已存在文件名含视频名的字幕文件**(`.srt/.ass/.ssa/.vtt`
`list_sidecar_subtitles` 判定,如 `movie.CN.srt`、`movie.CN_dual_eye.ass`),
说明该视频已有字幕,创建即记 **SKIPPED**——不为它触发任何流水线。运行时
`BatchWorker` **只消费这批已定位的明细,不再重新扫描文件夹**(运行期间新增/
删除的视频不会改变本次任务的范围)。校验失败(文件夹不存在/未发布工作流/
无版本/一个视频都没有)返回 422。
- **产物放在视频旁**:每个视频处理完成后,把工作流 `final_outputs` 对应的最终
产物文件(字幕流水线即中文 `.srt` 与双目 `.ass`**复制一份到视频所在目录**
与 .mp4 放在一起(`_place_products`)。文件名**对齐媒体库既有约定**:中文字幕
存为 `<视频名>.CN.srt`、双目字幕存为 `<视频名>.CN_dual_eye.ass`(稳定无时间戳,
`_sidecar_product_name` 映射,其余扩展名产物保留原文件名;同名目标直接覆盖)。
文件名含视频主名,下次批量扫描会命中"已有字幕"规则直接跳过该视频。
- **成品放置校验(审查 R03)**:先预检全部 `final_outputs` 对应的记录与文件,
缺任一项即失败,不开始覆盖视频旁成品;全部齐备后逐文件原子替换。复制失败时
视频记 FAILED,保留 run、工作空间和已放置的完整成品,供修复后幂等重试。
原子替换保证单文件完整,不代表多个成品的跨文件事务或断电持久性。详见
[代码审查问题跟踪.md](./代码审查问题跟踪.md#r03-修复记录)。
- **过程文件清理(2026-09 起)**:视频收尾完成后删除该视频的整个工作空间与
run 记录(音频/分块/帧图/节点产物不留残),防止媒体库把切片数据当视频入库。
工作空间位于**应用私有目录** `data/storage/batch/<job_id>/<bv_id>/`
(不再放视频同名文件夹),与用户视频库天然隔离;暂停/失败的视频保留工作空间
以便断点续跑。per-video 的 `WorkflowScheduler` 实例以该目录为 storage——
完整复用 DAG 拓扑执行、产物表登记与**断点续跑**逻辑。
- **任务级工作空间清理**:任务全部完成且**无失败视频**时,连任务级目录
`storage/batch/<job_id>/` 一并删除(每个视频的工作空间已在收尾时各自删完,
任务级目录只剩空壳);有失败视频时保留(它们的中间产物供断点重试)。
`paused.flag`/`keep_model.flag` 在每个阶段开始前清理,避免进程被强杀后的
残留影响后续阶段。
- **任务管理与批量页的分工**:批量 run(`source=batch`**默认不出现在任务管理页**
`GET /api/runs` 默认排除,排查时用 `?include_batch=1`)——一个批量任务会产生
N 条单视频 run,混进 20 条窗口会把用户自己提交的任务挤出去,且任务管理页的
暂停/继续/重试/删除对批量 run 语义不成立(暂停会被引擎下一次断点续跑静默复位,
删除被 422 拒绝)。作为补偿,批量页详情表补**阶段**列:`阶段 2/4 · 转写`,由该视频
run 的 `current_node_id` 在 DAG 拓扑序中的位置推导、节点类型映射中文标签。
粒度限制:`progress` 只有**节点边界**粒度,句级进度(转写分块、翻译批次、OCR 帧)
不落库、只在控制台日志里。
- **分块流水线执行(本地模型只加载一次)**:批量引擎把待处理视频按
`WOV_BATCH_STAGE_GROUP_SIZE`(默认 8)分组,**组内按节点顺序跑完全部视频**
(先全部 extract、再全部 ASR、再全部 LLM 翻译、最后 ASS)再进入下一组。每个
视频的 run 在阶段边界保持 RUNNING`execute_run(stop_after=节点)`),下一阶段
从产物表跳过已完成节点继续,因此本地模型每组只加载一次、卸载一次,而不是每个
视频来回加载卸载;产物仍按组增量落地。LLM 阶段执行时引擎在 run 根目录写
`keep_model.flag`,节点据此不在每次调用后卸载模型(`nodes/llm.py`),阶段
结束由引擎调 `release_local_model()` 统一释放显存,让下一组的 ASR 拿到 GPU
(否则本地模型常驻显存会让 whisper 直接 CUDA OOM)。设为 1 即回到「每个视频
跑完整链路」的旧行为。
- **失败视频不跨阶段推进**:某阶段失败的视频只在**它失败节点的那个阶段**重试
(下一次引擎循环从断点续跑),不会在后续阶段里重跑前序节点——避免本地 LLM 已
常驻时重跑 ASR 抢显存;视频仍按既有语义记 FAILED,任务在没有其他待处理视频时
以 failed>0 收尾。
- **暂停/继续**`POST /api/batch/jobs/{id}/pause` 把任务置 PAUSED 并暂停当前
run(写 `paused.flag`whisper **分块间**检查、OCR 逐帧检查、llm-translate
**按批(20 行)**检查后中止,当前节点执行完才停);`resume` 恢复 QUEUED,引擎
从断点继续——PAUSED 视频的 run 显式 resume 后从产物表续跑,未开始的视频接着
处理。重启进程后 RUNNING 残留 run 由 `recover_interrupted_runs` 恢复,暂停的
继续处理。
- **失败容错**:单个视频失败(节点失败/文件缺失)记为 FAILED,批量任务继续
处理后续视频,结束后统计 done/failed;DAG 解析/任务级异常把任务置 FAILED。
**重跑保留产物**2026-08,修复 run_e2b74e89e232 实测):FAILED 视频重新处理
时不再 reset_run 清空产物记录,而是保留已完成节点的 artifacts 恢复 QUEUED
execute_run 从产物表跳过已完成节点、只重跑失败节点——extract/ocr 等长耗时
成果不浪费;配合 llm-filter/OCR 的节点级断点存档,失败节点自身也只重判未完成
条目。前端对"部分失败"COMPLETED 且 failed>0)用红色徽章醒目标示。
- **完成任务判定**`_run_job` 置 COMPLETED 前**校验全部非 SKIPPED 视频都已
结束**(无 PENDING/PAUSED 残留),否则**把任务置回 QUEUED** 交引擎下一轮续跑:
留 RUNNING 是错的——`next_queued_batch_job` 只拾取 QUEUED,任务会停在“运行中
但没人推进”。这条路径主要出现在**任务创建与明细写入的竞态**:任务行先于视频
明细写入(`create_job` 逐条插入),引擎可能在登记完成前就拾起任务,本轮只看到
已写入的那部分视频(实测 batch_959e510259f6524 条明细中只看到最初 4 个非
SKIPPED 视频),剩下的留到下一轮;已登记明细全部尚未写入时(一条明细都没有)
同样保持 QUEUED,不能按空任务收尾。旧行为留下“N 个 PENDING 待处理却已完成”
的假完成(batch_969fabe74b83 等 3 个任务实测),或停在运行中无人推进。
**崩溃恢复**:重启时除 `recover_interrupted_runs` 外,
`recover_interrupted_batch_jobs` 把 RUNNING 的批量任务恢复为 QUEUED
(否则停在 RUNNING 的批量任务永远不会被 `next_queued_batch_job` 再次拾起,
未处理完的 PENDING 永久残留);**同一恢复也会把被提前标记 COMPLETED 但仍有
未结束视频的僵尸任务置回 QUEUED**(完成标记先于视频收尾写出的旧数据,
batch_351833b7d446 实测:COMPLETED/done=0 却仍有 1 个 PENDING),
否则只靠 `fix_zombie_batch_jobs.py` 手动修数据,重启也不会自动诊好。
- **产物下载**`GET /api/batch/jobs/{id}/videos/{vid}/download?alias=<文件名>`
解析并返回视频旁的字幕文件;旧版 `batch.done.json` 完成标记里的语义别名
(位于旧 work_dir)仍兼容可下载。详情/创建响应里每个视频的 `finals` 合并上述
两处来源。
- **详情明细展示**:任务列表的“详情”只列出本批实际处理过的视频行,SKIPPED
(视频旁已有字幕、创建时即被跳过)不出现在明细表里;整批都已被跳过时提示
“无待处理视频”。
- **孤儿清理保护**`source=batch` 的运行**跳过**自动清理——其 run 位于私有
`storage/batch/...` 下,普通孤儿逻辑会误判删除,且 `_remove_run` 还会删除
`input_uri` 的父目录(用户的整个视频文件夹)。详见
[代码审查问题跟踪.md](./代码审查问题跟踪.md#r01-修复记录)。
- **删除任务**`DELETE /api/batch/jobs/{id}` 清理数据库记录(含关联 run)与
应用私有工作空间残留;视频旁已放置的产物属于用户数据,保留不删。
- **环境变量**`WOV_BATCH_ENABLED`(默认 1)、`WOV_BATCH_INTERVAL_SECONDS`
(默认 1.0),完整列表见 [configuration.md](./configuration.md)。
## 相关文档
- 环境变量全表:[configuration.md](./configuration.md)
- 产物命名与 finalization 规则:[workflows.md](./workflows.md#最终产物命名)
- 历史缺陷与修复记录:[代码审查问题跟踪.md](./代码审查问题跟踪.md)