feat: 批量分块流水线、本地模型显存让渡与任务列表分工
批量引擎改为「分块流水线」:视频按 WOV_BATCH_STAGE_GROUP_SIZE(默认 8)分组, 组内按 DAG 拓扑序跑完全部视频(全部 extract → 全部 ASR → 全部翻译 → 全部 ASS) 再进入下一组,本地模型每组只加载一次、卸载一次,而不是每个视频来回加载卸载; 产物仍按组增量落到视频旁。调度器新增 execute_run(run_id, stop_after=节点): 该节点完成后任务保持 RUNNING 不收尾,下一次调用从产物表跳过已完成节点继续, 用于实现阶段边界。 - nodes/llm.py:翻译节点结束释放本机 Ollama 显存(node 参数 unload_after > LLM_UNLOAD_AFTER > 本机 loopback 端点默认卸载,云端端点不卸载;卸载失败只告警), 新增 keep_model.flag 语义(阶段内保持常驻)与 release_local_model(); 新增节点内暂停(按批 20 行检查 paused.flag,抛 PauseRequested,调度器保持 PAUSED)。 - src/wov_app/batch.py:分组阶段执行与阶段末统一释放显存;失败视频只在它失败 节点的那个阶段重试(避免 LLM 已常驻时重跑 ASR 抢显存);任务没有明细时保持 QUEUED 等登记完成、仍有未完成视频时置回 QUEUED 自愈(原先留 RUNNING 会卡死: 引擎只拾取 QUEUED,任务停在“运行中但没人推进”);无失败视频时删除任务级空目录; 每个阶段开始前清理 paused.flag / keep_model.flag,避免强杀残留影响后续阶段。 - src/wov_app/config.py:新增 WOV_BATCH_STAGE_GROUP_SIZE(设为 1 即旧的每视频全链路)。 - 任务列表与批量页分工:GET /api/runs 默认排除 source=batch(一个批量任务会产生 N 条单视频 run,会把 20 条窗口占满;且任务管理页的暂停/重试/删除对批量 run 语义不成立),需要排查时用 include_batch=1;作为补偿批量页详情新增阶段列 (阶段 i/N · 中文标签,由该视频 run 的 current_node_id 在 DAG 拓扑序中的位置 推导,节点类型映射中文标签)。阶段只有节点边界粒度,句级进度不落库、只在日志。 - 顺带纳入此前未提交的批量僵尸状态恢复:recover_interrupted_batch_jobs 除 RUNNING 外也把「COMPLETED 但仍含未结束视频」的任务置回 QUEUED;fix_zombie_batch_jobs.py 改为按条件扫描并支持 --apply 预览;批量页明细只列本批真正处理过的视频。 测试新增/更新:分块流水线调用顺序(组内按节点跑完再下一组)、每组只释放一次模型、 阶段内保持常驻标志、翻译按批暂停、失败视频不跨阶段推进、任务无明细/中途登记视频时 置回 QUEUED、任务工作空间与残留信号清理、任务列表默认过滤批量 run、详情阶段字段、 前端阶段列渲染;全量 507 passed(唯一失败为既有素材缺失的 integration 用例)。
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""scripts/fix_zombie_batch_jobs.py 的模块级测试(数据 → 测试过程 → 验证结果)。
|
||||
|
||||
被测模块:僵尸批量任务修复脚本(把「COMPLETED 但仍有未结束视频」的历史脏数据
|
||||
置回 QUEUED),可独立调用。用例在临时目录构造真实 SQLite 库与真实明细记录,
|
||||
调用脚本的真实函数而不是重写扫描逻辑。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.fix_zombie_batch_jobs import find_zombie_jobs, main
|
||||
from wov_app.db import Database
|
||||
|
||||
|
||||
def _db_with_workflow(tmp_path: Path) -> Database:
|
||||
"""建好已发布工作流(版本)的临时库:批量明细无外键约束,任务表需要它。"""
|
||||
db = Database(tmp_path / "wov.db")
|
||||
db.upsert_workflow({"id": "wf", "name": "流程", "description": ""})
|
||||
db.create_workflow_version("wf", 1, {"nodes": []})
|
||||
return db
|
||||
|
||||
|
||||
def _job(db: Database, job_id: str, status: str) -> None:
|
||||
"""登记一条批量任务。"""
|
||||
db.create_batch_job({
|
||||
"id": job_id, "folder_path": "/videos", "workflow_id": "wf", "recursive": 0,
|
||||
"status": status, "created_at": "t1", "updated_at": "t1",
|
||||
})
|
||||
|
||||
|
||||
def _video(db: Database, job_id: str, video_id: str, status: str) -> None:
|
||||
"""登记一条批量视频明细。"""
|
||||
db.create_batch_video({
|
||||
"id": video_id, "job_id": job_id, "video_path": f"/videos/{video_id}.mp4",
|
||||
"work_dir": f"/tmp/{video_id}", "status": status,
|
||||
"created_at": "t1", "updated_at": "t1",
|
||||
})
|
||||
|
||||
|
||||
def test_find_zombie_jobs_selects_completed_with_unfinished_videos(tmp_path: Path) -> None:
|
||||
"""只挑出 COMPLETED 但仍有未结束视频的任务,已真正完成的跳过。"""
|
||||
# 数据:僵尸任务(COMPLETED + PENDING)与真正完成的任务(COMPLETED + SKIPPED)。
|
||||
db = _db_with_workflow(tmp_path)
|
||||
_job(db, "zombie", "COMPLETED")
|
||||
_video(db, "zombie", "z1", "PENDING")
|
||||
_video(db, "zombie", "z2", "SKIPPED")
|
||||
_job(db, "done", "COMPLETED")
|
||||
_video(db, "done", "d1", "COMPLETED")
|
||||
_video(db, "done", "d2", "SKIPPED")
|
||||
|
||||
# 测试过程
|
||||
zombies = find_zombie_jobs(db)
|
||||
|
||||
# 验证结果:只有僵尸任务入选,且带出未结束明细。
|
||||
assert [str(job["id"]) for job, _ in zombies] == ["zombie"]
|
||||
assert [v["id"] for v in zombies[0][1]] == ["z1"]
|
||||
|
||||
|
||||
def test_main_apply_requeues_zombie_and_keeps_done(tmp_path: Path) -> None:
|
||||
"""--apply 把僵尸任务置回 QUEUED 并清 progress,正常完成的任务不动。"""
|
||||
# 数据:僵尸任务(1 完成 + 1 PENDING)与正常完成任务。
|
||||
db_path = tmp_path / "wov.db"
|
||||
db = Database(db_path)
|
||||
db.upsert_workflow({"id": "wf", "name": "流程", "description": ""})
|
||||
db.create_workflow_version("wf", 1, {"nodes": []})
|
||||
_job(db, "zombie", "COMPLETED")
|
||||
db.update_batch_job("zombie", progress=1.0, total=1, done=0, updated_at="t1")
|
||||
_video(db, "zombie", "z1", "COMPLETED")
|
||||
_video(db, "zombie", "z2", "PENDING")
|
||||
_job(db, "done", "COMPLETED")
|
||||
_video(db, "done", "d1", "COMPLETED")
|
||||
|
||||
# 测试过程
|
||||
main(str(db_path), apply=True)
|
||||
|
||||
# 验证结果:僵尸任务可被引擎拾起,completed 明细与任务保持原状。
|
||||
zombie = db.get_batch_job("zombie")
|
||||
assert zombie["status"] == "QUEUED"
|
||||
assert zombie["progress"] == 0
|
||||
assert zombie["done"] == 1
|
||||
assert db.get_batch_job("done")["status"] == "COMPLETED"
|
||||
assert db.next_queued_batch_job()["id"] == "zombie"
|
||||
Reference in New Issue
Block a user