feat: 任务断点续跑/暂停中断/LLM 过滤优化与调度容错

调度与状态机:
- 修复 PAUSED 任务被拾起后复活执行(点击暂停反而开始任务):next_queued_run
  只取 QUEUED,execute_run 以 PAUSED 进入直接返回,暂停必须显式 resume
- 重启恢复:启动时 recover_interrupted_runs 把遗留 RUNNING 置 QUEUED(保留产物)
- 暂停信号 paused.flag:暂停接口写、继续/重试清除,OCR 逐帧检查秒级中断,
  节点内被暂停保持 PAUSED 不误报 FAILED
- 调度轮询容错:_loop 异常不杀死线程(曾致任务永久停留 QUEUED)

subtitle-ocr 节点级断点:
- ocr_partial.jsonl 逐帧存档,重启/暂停后只处理未处理帧,产物与一次跑完一致
- 进度日志携带窗口平均耗时与线程数;取消后抑制进度日志井喷

llm-filter 过滤质量与限流自适应:
- 上下文净化:喂给 LLM 的是过滤后的字幕(规则层垃圾从上下文剔除)
- 正则确定性过滤:裸网址域名、HTML/水印模式直接删除
- 429/5xx 指数退避重试;worker 限流错误 report_failure 内存临时降最大线程数
  并缩容(无错误窗口回升),失败条目降并发后重试一轮
- 保留长文本保护(noise 不删 ≥min_keep_len 文本,LLM 判定不稳的必要兜底)

前端:
- 工作流编排页支持选择工作流编辑(加载最新/历史版本)、版本历史面板、
  新建/编辑双模式;管理后台编辑跳转 workflow.html?edit=<id>

工作流:ocr-subtitle v7(filter pool_max_workers=20、pool_fast_threshold=1)
This commit is contained in:
2026-08-19 01:27:41 +08:00
parent 4ebbfc5198
commit b16e0e9f3c
19 changed files with 1237 additions and 111 deletions
+33 -7
View File
@@ -41,6 +41,10 @@ vrsub/
`data/storage/runs/<run_id>/steps/<node_id>/` 落盘并登记到 artifacts 表。
- **前端**:由 FastAPI 静态挂载 `web/`,节点注册/实例管理页面已移除,
仅保留应用中心、任务管理、管理后台(工作流)与工作流编排。
- **工作流编排页(web/workflow.html)**:支持新建工作流(空表单预填演示模板),
从列表"编辑"加载任一工作流的最新定义(ID 锁定,保存即追加新版本);"版本"
查看全部历史版本并可"加载到编辑器"(对比/回滚后另存新版本);管理后台
admin.html)无编辑器,点"编辑"自动跳转 `workflow.html?edit=<id>` 加载。
## 节点输入/输出协议
@@ -56,7 +60,7 @@ vrsub/
| `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` |
| `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)直接删横线装饰/HTML 水印 token/URL/邮箱/单双 ASCII 字符;②**LLM 五类分类**garbage/overlay/noise 删,repeat/dialogue 留,未识别回退保留)每条连同前后各 `context_size`(默认 10)条纯文本分批判断,**按文本去重**(忽略空白/大小写,相同文本只调一次 LLM,上下文取首次出现)保证判定一致并省调用,**长文本保护**`min_keep_len` 默认 12 时 noise 不构成删除依据。参数:`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 时自动收敛到配额内而不打挂任务。**按文本去重**(忽略空白/大小写,相同文本只调一次 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` |
### 模型权重解析(本地优先)
@@ -238,14 +242,35 @@ http://127.0.0.1:8000/docs API 文档
- **状态机**`QUEUED / RUNNING / PAUSED / COMPLETED / FAILED`。排队中或运行中的
任务可暂停(`POST /api/runs/{run_id}/pause`),PAUSED 可继续
`POST /api/runs/{run_id}/resume` → 恢复 QUEUED)。
- **调度器语义**`next_queued_run` 同时取 QUEUED 与 PAUSED`execute_run` 在每个
节点边界检查状态,被暂停则停下保持 PAUSED(当前节点执行完后才停);
继续时从产物表(`restore_run_outputs`,剥去"节点ID."前缀还原输出名)重建已完成
节点的输出,**跳过已完成节点断点续跑**,最后补做 final_outputs 收尾。
- **调度器语义**`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)`llm-filter`字幕判定进度 X/Y 条 (Z 条/s)`
- 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)"。
@@ -340,7 +365,8 @@ http://127.0.0.1:8000/docs API 文档
- 存储、队列、调度器都要通过抽象边界隔离,方便从单机实现替换为分布式实现。
- 用户端永远只看到"输入 -> 进度 -> 结果",不暴露工作流细节。
- 单体对分布式版的三处降级:无子进程隔离、无空闲 TTL 回收(模型常驻,
仅懒加载)、慢任务无法强制中断(由节点自身超时兜底)。
仅懒加载)、非 OCR 慢任务无法强制中断(由节点自身超时兜底OCR 节点支持
暂停信号逐帧中断)。
## 单体化说明