`create_job` 先把任务行以 QUEUED 入库(引擎立刻可见),再逐条登记明细(扫描 媒体库时 500+ 条要数秒);引擎轮询到的快照可能还没包含剩余明细,收尾时"无未结束 明细"检查也看不到它们,于是把任务误标 COMPLETED,剩余视频永远不再被处理。 - 任务行改以 `CREATING` 入库,明细全部登记完才置 QUEUED(引擎只取 QUEUED, 看不到半成品);登记中途异常置 FAILED 并把异常交给路由层。 - 启动时把上一进程遗留的 CREATING 统一置 FAILED(`fail_creating_batch_jobs`), 避免明细写一半被热重载/强杀后留下看不见的残留任务。 - 回归测试:明细每写一条就问一次引擎队列(写入过程中取不到任务);中途失败记 FAILED;启动清理遗留 CREATING。
205 lines
16 KiB
Markdown
205 lines
16 KiB
Markdown
# 运维
|
||
|
||
## 孤儿数据清理
|
||
|
||
应用内置后台清理器(`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 进入时直接返回保持暂停,
|
||
必须用户显式 resume(PAUSED → 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-extract:ffmpeg `-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`)。文件名**对齐媒体库既有约定**:中文字幕
|
||
存为 `<视频名>.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_959e510259f6:524 条明细中只看到最初 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)
|