Files
vrsub/docs/operations.md
T
cat-shark 171b088e7c feat: 批量流水线按 GPU 资源调度(非 GPU 阶段并行、GPU 阶段互斥)
此前组内阶段是串行的(先全部提音、再全部转写、再全部翻译),LLM 走线上端点时
翻译阶段不占显存、GPU 全程空转——实测占整轮挂钟约 40%(19.6W / 272MiB)。

- `_run_job` 改为按组启动在途流水线:每个视频独立推进自己的阶段,最多
  `WOV_BATCH_PIPELINE_WORKERS`(默认 4)个阶段在途。
- 派发只看资源:`stage_gpu_need_mb` 为 0 的阶段(提音、线上翻译、ASS)立刻派发,
  可与其它视频的转写并行;需要 GPU 的阶段由 `GpuGate` 互斥准入,并按"阶段索引
  最小者优先"派发,组内仍是先跑完全部转写再进翻译——本机 Ollama 模型每组只
  加载一次,不需要按"是否云端"写分支。
- 同一阶段只在途一份(派发即标记 running),单视频异常不带走整组;暂停沿用
  run 级 paused.flag,暂停后不再派发新阶段。
- 测试:远端翻译与其它视频转写重叠、本机端点下全部转写先于翻译且翻译互斥、
  提音与转写重叠,以及既有分组/暂停/失败隔离用例。
2026-09-18 22:40:53 +08:00

210 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.
# 运维
## 孤儿数据清理
应用内置后台清理器(`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)。任务行先以
`CREATING` 入库、**明细全部登记完才置 QUEUED**:否则引擎会在明细写一半时拾起
任务、收尾把任务误标 COMPLETED(理由见
[decisions.md](./decisions.md#批量任务先写明细再排队creating-状态))。**视频所在目录
(视频旁)若已存在文件名含视频名的字幕文件**(`.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`)。文件名**对齐媒体库既有约定**:日语转写
存为 `<视频名>.JA.srt`、中文字幕存为 `<视频名>.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 帧)
不落库、只在控制台日志里。
- **按资源调度的在途流水线(2026-09 起)**:批量引擎把待处理视频按
`WOV_BATCH_STAGE_GROUP_SIZE`(默认 8)分组,组内**每个视频独立推进自己的
阶段**,最多 `WOV_BATCH_PIPELINE_WORKERS`(默认 4)个阶段在途。判定只看资源:
阶段是否需要 GPU 由 `wov_app.resources.stage_gpu_need_mb` 给出(提音、**线上
端点**的翻译、ASS 都不需要),不需要 GPU 的阶段立刻派发,于是转写能与线上翻译
并行、提音能与转写并行(此前组内阶段是串行的,翻译时 GPU 全程空转)。需要 GPU
的阶段由进程内 `GpuGate` 串行准入并按"阶段索引最小者优先"派发,因此组内仍是
先跑完全部转写再进翻译——**本机 Ollama 模型每组只加载一次**,不需要按"是否
云端"写分支。每个视频的 run 在阶段边界保持 RUNNING`execute_run(stop_after=节点)`),
下一阶段从产物表跳过已完成节点继续。LLM 阶段执行时引擎在 run 根目录写
`keep_model.flag`,节点据此不在每次调用后卸载模型(`nodes/llm.py`),组末由
引擎调 `release_local_model()` 统一释放显存,让下一组的 ASR 拿到 GPU(否则
本机模型常驻显存会让 whisper 直接 CUDA OOM)。显存探测不到(无 `nvidia-smi`
时退化为"GPU 阶段互斥",行为与串行一致。设为 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)。
## 媒体库历史字幕重建(统计 → 备份 → 批量重跑)
模型或管线切换后,媒体库里由旧管线生成的字幕需要整批重跑。批量引擎按
“视频旁已有字幕即 SKIPPED”判定,所以重跑前必须先统计范围、再把旧字幕改名
备份,否则建出来的任务会把所有视频全部跳过。工具是 `scripts/plan_regenerate_subtitles.py`
(**不依赖仓库**,可拷到媒体库主机直接跑:CIFS 挂载下逐文件 ffprobe 与
目录扫描都慢得多,在 NAS 本地跑 524 个视频只要几秒)。
生成时间的判定口径:只看本流水线产出的 CN 产物(`<视频名>.CN.srt`、
`<视频名>.CN_dual_eye.ass`),取它们**最早**的 mtime——双目 `.ass` 可能被样式
统一脚本原地改写而“变新”,中文字幕的 mtime 才是真实生成时间。
```bash
# 1) 统计:数量 / 时长 / 体积 / 按月分布,并写出逐条明细 JSON
python3 scripts/plan_regenerate_subtitles.py /vol1/1000/123 \
--before 2026-09-01 --out data/experiments/regen_plan/plan_2026-09-01.json
# 2) 备份旧字幕(默认预览,--apply 才落盘):改名 <原名>.old-<当天日期>
python3 scripts/plan_regenerate_subtitles.py /vol1/1000/123 --backup-old
python3 scripts/plan_regenerate_subtitles.py /vol1/1000/123 --backup-old --apply \
--select sample10.txt # 只处理清单里的视频,支持 # 注释
# 3) 建批量任务(批量页或 POST /api/batch/jobs
```
- 备份文件名带 `.old-<日期>` 后缀,扩展名不再是字幕后缀,批量引擎不会再把它当
旁挂字幕;确认新字幕无误后删除备份,需要回退则去掉后缀改回原名。
- 未备份的视频仍有旁挂字幕 → 创建任务时记 SKIPPED,因此“整库建一个任务”即可,
实际只会处理被备份的那批;已重跑的视频产出新 CN 字幕(mtime 变新),全量重建
时自动归入“已是最新”,不会重复处理。
- 实测数据(380 个待重生成 / 207.88 小时;10 个样本 6.54 小时素材跑 77.7 分钟、
GPU 平均 289.6 W、约 0.375 kWh,全量推算约 41 小时 / 整机 15 kWh)见
`data/experiments/regen_plan/REPORT.md`。
## 相关文档
- 环境变量全表:[configuration.md](./configuration.md)
- 产物命名与 finalization 规则:[workflows.md](./workflows.md#最终产物命名)
- 历史缺陷与修复记录:[代码审查问题跟踪.md](./代码审查问题跟踪.md)