feat: 批量处理重做——视频旁已有字幕即跳过、产物对齐 CN 命名并清理过程文件

- 创建批量任务时一次性定位视频:视频所在目录存在文件名含视频名的字幕文件
  (.srt/.ass/.ssa/.vtt)直接记 SKIPPED,不触发流水线;运行时只消费已定位
  的明细,不再重新扫描文件夹。
- 视频完成后把最终产物放到视频旁,命名对齐媒体库约定:中文字幕存为
  <视频名>.CN.srt、双目字幕存为 <视频名>.CN_dual_eye.ass;其余扩展名产物
  保留原文件名。
- 收尾删除 run 记录与过程工作空间;工作空间改到应用私有目录
  storage/batch/<job_id>/<bv_id>/,与用户媒体库隔离,防止媒体库把切片数据
  当视频入库。
- 批量 API 产物清单/下载改为解析视频旁字幕文件,旧版 batch.done.json 语义
  别名保持兼容;删除任务时清理私有工作空间。
- 前端说明与创建提示同步;测试按新语义重写并补覆盖(278 passed,100% 行覆盖率)。
This commit is contained in:
2026-09-03 08:22:42 +08:00
parent ec8c8d7dcd
commit d1557eca46
7 changed files with 754 additions and 438 deletions
+36 -27
View File
@@ -276,31 +276,38 @@ http://127.0.0.1:8000/docs API 文档
(线程池 `on_progress` 回调,每任务完成触发);
- whisper:分块转写打印"分块 X/Y 完成 offset=... 耗时 Zs (Nx 实时, 累计 ...s)"
- frame-extractffmpeg `-progress` 输出解析 `frame=N`,打印"抽帧进度 X/Y 帧 (Z 帧/s)"。
## 文件夹批量处理(2026-08
## 文件夹批量处理(2026-09 更新
本地版核心能力:**不把视频上传到工作目录**,直接读取用户所选文件夹下的全部
视频,逐个执行所选流水线。入口为批量处理页(`web/batch.html`,导航"批量处理"),
后端为 `src/wov_app/batch.py``BatchWorker`(单线程轮询线程,处理
`source=batch` 的运行,与主调度器互不抢占)与 `routers/batch.py`
- **路径选择2026-08 起不用手敲路径)**:批量页点击"选择文件夹…"按钮弹出
目录树选择器(懒加载),选完回填只读路径框。浏览器拿不到所选文件夹的绝对
路径,因此由**本地后端**提供目录浏览:`GET /api/batch/roots`Windows 盘符 /
POSIX 根 + 家目录)、`GET /api/batch/dirs?path=`(列直接子目录,隐藏目录
过滤;不存在/不可读返回空列表不报 500)。只暴露目录名,不返回文件内容。
- **数据落盘**:每个视频的中间态(`runs/<run_id>/steps/...`)与最终产物都放在
**视频所在目录的同名文件夹**`movie.mp4``movie/`,去掉扩展名,
`work_dir_for` 推导);最终产物在任务完成后从节点产物目录**复制**到同名
文件夹根目录,并写 `batch.done.json` 完成标记(记录 workflow_id 与产物文件名)。
- **已处理过的不再处理**:同名文件夹已有**同工作流**完成标记且产物文件齐全 →
直接 SKIPPED;不同工作流的标记不互相误判(换流水线会重新处理)。
- **任务参数**`POST /api/batch/jobs {folder, workflow_id, recursive}` 创建批量
任务(校验文件夹/已发布工作流/有版本/至少一个视频,失败 422);任务入
`batch_jobs` 表,每个视频一行 `batch_videos`PENDING/RUNNING/PAUSED/
COMPLETED/FAILED/SKIPPED)。可用任意已发布流水线(demo / zh-direct /
ocr-subtitle 等),前端下拉选择。
- **执行复用**:每个视频创建一个 `source=batch` 的 run`input_uri` 直接指向
本地视频路径),用 per-video 的 `WorkflowScheduler` 实例(storage=同名文件夹)
执行——完整复用 DAG 拓扑执行、产物表登记与**断点续跑**逻辑
- **路径选择**:批量页点击"选择文件夹…"按钮弹出目录树选择器(懒加载),选完
回填只读路径框。浏览器拿不到所选文件夹的绝对路径,因此由**本地后端**提供目录
浏览:`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` 映射,其余扩展名产物保留原文件名;同名目标直接覆盖)
文件名含视频主名,下次批量扫描会命中"已有字幕"规则直接跳过该视频。
- **过程文件清理(2026-09 起)**:视频收尾完成后删除该视频的整个工作空间与
run 记录(音频/分块/帧图/节点产物不留残),防止媒体库把切片数据当视频入库。
工作空间位于**应用私有目录** `data/storage/batch/<job_id>/<bv_id>/`
(不再放视频同名文件夹),与用户视频库天然隔离;暂停/失败的视频保留工作空间
以便断点续跑。per-video 的 `WorkflowScheduler` 实例以该目录为 storage——
完整复用 DAG 拓扑执行、产物表登记与**断点续跑**逻辑。
- **暂停/继续**`POST /api/batch/jobs/{id}/pause` 把任务置 PAUSED 并暂停当前
run(写 `paused.flag`whisper **分块间**检查、OCR 逐帧检查后中止,当前节点
执行完才停);`resume` 恢复 QUEUED,引擎从断点继续——PAUSED 视频的 run 显式
@@ -313,13 +320,15 @@ http://127.0.0.1:8000/docs API 文档
execute_run 从产物表跳过已完成节点、只重跑失败节点——extract/ocr 等长耗时
成果不浪费;配合 llm-filter/OCR 的节点级断点存档,失败节点自身也只重判未完成
条目。前端对"部分失败"COMPLETED 且 failed>0)用红色徽章醒目标示。
- **产物下载**`GET /api/batch/jobs/{id}/videos/{vid}/download?alias=result`
从完成标记解析产物文件并返回(只读同名文件夹根目录)。
- **孤儿清理保护**`source=batch` 的运行**跳过**自动清理——产物不在主存储
目录下,普通孤儿逻辑会误删记录并连带删除 `input_uri` 的父目录(用户的整个
视频文件夹)。
- **删除任务**`DELETE /api/batch/jobs/{id}` 只清理数据库记录(含关联 run),
磁盘上的同名文件夹与产物属于用户数据,保留不删
- **产物下载**`GET /api/batch/jobs/{id}/videos/{vid}/download?alias=<文件名>`
解析并返回视频旁的字幕文件;旧版 `batch.done.json` 完成标记里的语义别名
(位于旧 work_dir)仍兼容可下载。详情/创建响应里每个视频的 `finals` 合并上述
两处来源。
- **孤儿清理保护**`source=batch` 的运行**跳过**自动清理——其 run 位于私有
`storage/batch/...` 下,普通孤儿逻辑会误判删除,且 `_remove_run` 还会删除
`input_uri` 的父目录(用户的整个视频文件夹)
- **删除任务**`DELETE /api/batch/jobs/{id}` 清理数据库记录(含关联 run)与
应用私有工作空间残留;视频旁已放置的产物属于用户数据,保留不删。
- **环境变量**`WOV_BATCH_ENABLED`(默认 1)、`WOV_BATCH_INTERVAL_SECONDS`
(默认 1.0)。