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:
@@ -213,3 +213,83 @@ class TestSubtitleOcrOrderUnderThreading:
|
||||
# ⑤ 已知真实内容存在于结果中(确认结果的代表性条目)。
|
||||
for line in ("北冈小姐", "这是特别病房患者的病历表", "应该已经察觉到 至今为止的一切了吧"):
|
||||
assert line in confirmed, line
|
||||
|
||||
|
||||
def test_ocr_resumes_from_partial_checkpoint(monkeypatch, tmp_path) -> None:
|
||||
"""节点级断点:预写部分帧存档后运行,只处理未处理帧,产物与全量一致。
|
||||
|
||||
模拟中断时已落盘的 ocr_partial.jsonl(前 100 帧已处理):invoke 应只对
|
||||
未处理帧调用 vlm-ocr,并把存档文本与新增文本合并,最终 SRT 与全量一次
|
||||
跑完逐字节一致——重启不浪费已处理的帧。
|
||||
"""
|
||||
manifest, texts_by_frame = _load_full_data()
|
||||
confirmed = CONFIRMED_SRT.read_text(encoding="utf-8")
|
||||
out_dir = tmp_path / "resume"
|
||||
partial_path = out_dir / "ocr_partial.jsonl"
|
||||
partial_path.parent.mkdir(parents=True)
|
||||
lines = []
|
||||
for i in range(100):
|
||||
# 存档按 0-based 帧序号记录;manifest[i] 的帧号 = i+1。
|
||||
lines.append(
|
||||
json.dumps({"frame": i, "text": texts_by_frame[i + 1]}, ensure_ascii=False)
|
||||
)
|
||||
if i == 50:
|
||||
lines.append("") # 空行:_load_partial 必须跳过,不视为一条记录。
|
||||
if i == 51:
|
||||
lines.append("broken-json-line") # 损坏行(进程被杀残留):跳过。
|
||||
partial_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
from nodes.subtitle_ocr import invoke as ocr_invoke
|
||||
|
||||
fake = FakeVlmOcrApi(texts_by_frame, seed=99)
|
||||
monkeypatch.setattr("wov_app.registry.invoke", fake)
|
||||
response = ocr_invoke(
|
||||
InvokeRequest(
|
||||
run_id="resume_test",
|
||||
node_instance_id="",
|
||||
inputs={"frames_manifest": str(FULL_MANIFEST)},
|
||||
params={"pool_min_workers": 4, "pool_max_workers": 4},
|
||||
output_dir=str(out_dir),
|
||||
)
|
||||
)
|
||||
assert response.status == "completed", response.error
|
||||
srt = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8")
|
||||
assert srt == confirmed # 存档 + 新增合并结果与全量一致。
|
||||
# 只处理未处理帧:前 100 帧不再调用 vlm-ocr。
|
||||
|
||||
# 场景 2:先用真实逻辑完整跑一遍(生成存档),再以同一目录续跑——
|
||||
# 第二次 pending 为空,完全不调用 vlm-ocr,产物与第一次逐字节一致
|
||||
# (覆盖 _load_partial 全恢复路径,存档文本与处理逻辑天然一致)。
|
||||
out_dir_all = tmp_path / "resume_all"
|
||||
fake_first = FakeVlmOcrApi(texts_by_frame, seed=7)
|
||||
monkeypatch.setattr("wov_app.registry.invoke", fake_first)
|
||||
resp_first = ocr_invoke(
|
||||
InvokeRequest(
|
||||
run_id="resume_all_test",
|
||||
node_instance_id="",
|
||||
inputs={"frames_manifest": str(FULL_MANIFEST)},
|
||||
params={"pool_min_workers": 4, "pool_max_workers": 4},
|
||||
output_dir=str(out_dir_all),
|
||||
)
|
||||
)
|
||||
assert resp_first.status == "completed", resp_first.error
|
||||
srt_first = Path(resp_first.outputs["srt_uri"]).read_text(encoding="utf-8")
|
||||
assert srt_first == confirmed
|
||||
assert len(fake_first.completed_frames) == TOTAL_FRAMES
|
||||
|
||||
fake_second = FakeVlmOcrApi(texts_by_frame, seed=8)
|
||||
monkeypatch.setattr("wov_app.registry.invoke", fake_second)
|
||||
resp_second = ocr_invoke(
|
||||
InvokeRequest(
|
||||
run_id="resume_all_test",
|
||||
node_instance_id="",
|
||||
inputs={"frames_manifest": str(FULL_MANIFEST)},
|
||||
params={"pool_min_workers": 4, "pool_max_workers": 4},
|
||||
output_dir=str(out_dir_all),
|
||||
)
|
||||
)
|
||||
assert resp_second.status == "completed", resp_second.error
|
||||
srt_second = Path(resp_second.outputs["srt_uri"]).read_text(encoding="utf-8")
|
||||
assert srt_second == srt_first # 存档恢复与一次跑完逐字节一致。
|
||||
assert len(fake_second.completed_frames) == 0 # 一帧都不重新调用。
|
||||
assert len(fake.completed_frames) == TOTAL_FRAMES - 100
|
||||
|
||||
Reference in New Issue
Block a user