docs: 更新 AGENTS.md(批量处理/断点存档/环境变量)

This commit is contained in:
2026-08-23 16:25:22 +08:00
parent 39f309094d
commit ec8c8d7dcd
+54 -5
View File
@@ -20,8 +20,8 @@ ASS)在**同一个进程**内运行,不再启动子进程、不再走节点
vrsub/
├── src/wov_sdk/ # 协议数据模型(NodeManifest/InvokeRequest/InvokeResponse/
│ # WorkflowDefinition 等),与分布式版保持一致
├── src/wov_app/ # 应用层:main/config/db/registry/scheduler/seed/routers
│ └── routers/ # apps.py(用户端)、workflows.py(管理端)
├── src/wov_app/ # 应用层:main/config/db/registry/scheduler/batch/seed/routers
│ └── routers/ # apps.py(用户端)、workflows.py(管理端)、batch.py(批量处理)
├── nodes/ # 进程内节点实现:echo/ffmpeg/whisper/llm/ass
├── manifests/ # 各节点清单 JSONecho.json/ffmpeg.json/...
├── workflows/ # 默认工作流定义 JSON(模型/链路均为数据,改模型不改代码)
@@ -40,7 +40,7 @@ vrsub/
任务,按工作流 DAG 拓扑顺序调用节点,产物按
`data/storage/runs/<run_id>/steps/<node_id>/` 落盘并登记到 artifacts 表。
- **前端**:由 FastAPI 静态挂载 `web/`,节点注册/实例管理页面已移除,
仅保留应用中心、任务管理、管理后台(工作流)与工作流编排。
仅保留应用中心、任务管理、批量处理、管理后台(工作流)与工作流编排。
- **工作流编排页(web/workflow.html)**:支持新建工作流(空表单预填演示模板),
从列表"编辑"加载任一工作流的最新定义(ID 锁定,保存即追加新版本);"版本"
查看全部历史版本并可"加载到编辑器"(对比/回滚后另存新版本);管理后台
@@ -58,9 +58,9 @@ vrsub/
| `faster-whisper` | `audio_uri`16kHz 单声道) | `srt_uri` | 参数:`language``task``model_path``device``compute_type``beam_size``vad_filter`(默认开)、`condition_on_previous_text``chunk_seconds` |
| `llm-translate` | `srt_uri` | `cn_srt_uri` | 参数:`target_language``model` |
| `vlm-ocr` | `image_uri` | `text``text_uri` | 直接调本地 Ollama 多模态模型(glm-ocr)的 `/api/chat` 做视频帧 OCR(流式 + 5s 上限),参数:`model``ollama_host``prompt``timeout_seconds``keep_alive``num_predict``temperature``repeat_penalty` |
| `frame-extract` | `video_uri` | `frames_manifest``frame_count` | 按**帧间隔**抽帧(解析 fps → step=round(间隔秒×fps)ffmpeg select 按帧号精确取帧,帧时间=帧号/fps 无累计偏差)并 crop 裁切字幕区域,参数:`interval_seconds`(默认 0.5)、`crop`[x,y,w,h] 0~1)。**帧文件必须按帧号数值排序读取**(`_sorted_frame_files`):ffmpeg `%04d` 编号超过 9999 帧后扩为 5 位,字典序 `sorted()` 会把 5 位编号排在 4 位之前导致时间与图像错位(真实发生于 run_339ec7ee437f 的 14236 帧任务,回归测试见 `test_frame_files_read_order_matches_frame_number` |
| `frame-extract` | `video_uri` | `frames_manifest``frame_count` | 按**帧间隔**抽帧(解析 fps → step=round(间隔秒×fps)ffmpeg select 按帧号精确取帧,帧时间=帧号/fps 无累计偏差)并 crop 裁切字幕区域,参数:`interval_seconds`(默认 0.5)、`crop`[x,y,w,h] 0~1**默认画面底部 1/4** `[0,0.75,1,0.25]`——字幕很少出现在画面上半部分,2026-08 调整)。**帧文件必须按帧号数值排序读取**(`_sorted_frame_files`):ffmpeg `%04d` 编号超过 9999 帧后扩为 5 位,字典序 `sorted()` 会把 5 位编号排在 4 位之前导致时间与图像错位(真实发生于 run_339ec7ee437f 的 14236 帧任务,回归测试见 `test_frame_files_read_order_matches_frame_number` |
| `subtitle-ocr` | `frames_manifest` | `srt_uri``count` | 自适应线程池并发逐帧调 vlm-ocr → 垃圾过滤(无文字帧)→ 相同字幕合并(记录最后可见帧)→ 组装 SRT,消失时间=最后可见帧+采样间隔(间隔从帧清单推导),参数:`min_chars``min_alnum_ratio``garbage_tokens``pool_min_workers`/`pool_max_workers`/`pool_window_seconds`/`pool_fast_threshold`/`pool_slow_threshold` |
| `llm-filter` | `srt_uri` | `srt_uri``kept``removed` | 两级过滤:①**规则层**(不调 LLM)正则确定性删除——横线装饰、URL/邮箱/**裸网址域名**(含中文夹杂的注册地址)、**HTML/水印模式**html code/标签/javascript 等)、overlay tokenhtml/marketing 等)、单双 ASCII 字符;②**LLM 五类分类**garbage/overlay/noise 删,repeat/dialogue 留,未识别回退保留)每条连同前后各 `context_size`(默认 10)条纯文本分批判断——**上下文净化**:喂给 LLM 的是**过滤后的字幕**,规则层确定性垃圾从上下文中剔除(原文不进 LLM),避免覆盖层垃圾污染场景判断误删真实对话(回归:run_011d01f19999 曾 190 条含 ≥4 汉字对话被误删);**长文本保护**:≥`min_keep_len`(默认 12)时 noise 不构成删除依据——LLM 判定不稳定,长度是必要兜底(实测移除保护后新增误删 124 条真实长对话)。**限流自适应**:LLM 调用 429/5xx 指数退避重试(最多 3 次,1s/2s/4s),worker 捕获限流错误时调用线程池 `report_failure()` **内存中临时降低最大线程数并缩容**(连续无错误窗口后逐步回升),失败条目在收紧后的并发下**重试一轮**,二次仍失败才整体失败——20 并发一拥而上触发 429 时自动收敛到配额内而不打挂任务。**按文本去重**(忽略空白/大小写,相同文本只调一次 LLM,上下文取首次出现)保证判定一致并省调用。参数:`context_size``min_keep_len``overlay_tokens`JSON 数组)、`dedupe`(默认开)、`model``pool_min_workers`/`pool_max_workers`/`pool_window_seconds`/`pool_fast_threshold`/`pool_slow_threshold`。回归数据:testdata/ocr_srt_run_ac7f480a3ccb.srt(真实任务 1666 条 OCR 输出) |
| `llm-filter` | `srt_uri` | `srt_uri``kept``removed` | 两级过滤:①**规则层**(不调 LLM)正则确定性删除——横线装饰、URL/邮箱/**裸网址域名**(含中文夹杂的注册地址)、**HTML/水印模式**html code/标签/javascript 等)、overlay tokenhtml/marketing 等)、单双 ASCII 字符;②**LLM 五类分类**garbage/overlay/noise 删,repeat/dialogue 留,未识别回退保留)每条连同前后各 `context_size`(默认 10)条纯文本分批判断——**上下文净化**:喂给 LLM 的是**过滤后的字幕**,规则层确定性垃圾从上下文中剔除(原文不进 LLM),避免覆盖层垃圾污染场景判断误删真实对话(回归:run_011d01f19999 曾 190 条含 ≥4 汉字对话被误删);**长文本保护**:≥`min_keep_len`(默认 12)时 noise 不构成删除依据——LLM 判定不稳定,长度是必要兜底(实测移除保护后新增误删 124 条真实长对话)。**限流自适应**:LLM 调用 429/5xx 指数退避重试(最多 3 次,1s/2s/4s),worker 捕获限流错误时调用线程池 `report_failure()` **内存中临时降低最大线程数并缩容**(连续无错误窗口后逐步回升),失败条目在收紧后的并发下**重试一轮**,二次仍失败才整体失败——20 并发一拥而上触发 429 时自动收敛到配额内而不打挂任务。**节点级断点存档**(2026-08):每条判定成功立即追加 `filter_partial.jsonl``{"index","category"}`,多线程加锁串行化),失败/中断后重跑只重判未判定条目,已判定结果复用(与 OCR 存档同机制)。**按文本去重**(忽略空白/大小写,相同文本只调一次 LLM,上下文取首次出现)保证判定一致并省调用。参数:`context_size``min_keep_len``overlay_tokens`JSON 数组)、`dedupe`(默认开)、`model``pool_min_workers`/`pool_max_workers`/`pool_window_seconds`/`pool_fast_threshold`/`pool_slow_threshold`。回归数据:testdata/ocr_srt_run_ac7f480a3ccb.srt(真实任务 1666 条 OCR 输出) |
| `srt-to-dual-eye-ass` | `cn_srt_uri` | `ass_uri` | 参数:`resolution`,如 `3840x1920` |
### 模型权重解析(本地优先)
@@ -190,6 +190,8 @@ subtitle-ocr 逐帧调 vlm-ocr 时使用 `nodes/adaptive_pool.py` 的自适应
| `WOV_CLEANUP_ENABLED` | `1` | 开启孤儿数据定时清理 |
| `WOV_CLEANUP_INTERVAL_SECONDS` | `3600` | 孤儿清理扫描周期(秒) |
| `WOV_CLEANUP_GRACE_SECONDS` | `3600` | 孤儿清理宽限期(秒) |
| `WOV_BATCH_ENABLED` | `1` | 开启文件夹批量处理引擎(处理 source=batch 任务) |
| `WOV_BATCH_INTERVAL_SECONDS` | `1.0` | 批量引擎轮询间隔 |
| `WHISPER_MODEL_PATH` | 见上 | 显式指定 whisper 模型路径 |
| `WHISPER_DEVICE` | `auto` | 转写设备 |
| `LLM_API_BASE` | `https://api.siliconflow.cn/v1/chat/completions` | LLM 兼容接口 |
@@ -274,6 +276,53 @@ 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
本地版核心能力:**不把视频上传到工作目录**,直接读取用户所选文件夹下的全部
视频,逐个执行所选流水线。入口为批量处理页(`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 拓扑执行、产物表登记与**断点续跑**逻辑。
- **暂停/继续**`POST /api/batch/jobs/{id}/pause` 把任务置 PAUSED 并暂停当前
run(写 `paused.flag`whisper **分块间**检查、OCR 逐帧检查后中止,当前节点
执行完才停);`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)用红色徽章醒目标示。
- **产物下载**`GET /api/batch/jobs/{id}/videos/{vid}/download?alias=result`
从完成标记解析产物文件并返回(只读同名文件夹根目录)。
- **孤儿清理保护**`source=batch` 的运行**跳过**自动清理——产物不在主存储
目录下,普通孤儿逻辑会误删记录并连带删除 `input_uri` 的父目录(用户的整个
视频文件夹)。
- **删除任务**`DELETE /api/batch/jobs/{id}` 只清理数据库记录(含关联 run),
磁盘上的同名文件夹与产物属于用户数据,保留不删。
- **环境变量**`WOV_BATCH_ENABLED`(默认 1)、`WOV_BATCH_INTERVAL_SECONDS`
(默认 1.0)。
## 测试与覆盖率