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:
+29
-11
@@ -300,13 +300,19 @@ class Database:
|
||||
result["param_overrides"] = json.loads(raw) if raw else None
|
||||
return result
|
||||
|
||||
def list_runs(self, limit: int = 20) -> list[dict[str, Any]]:
|
||||
"""按创建时间倒序返回最近的运行记录。"""
|
||||
def list_runs(self, limit: int = 20, include_batch: bool = False) -> list[dict[str, Any]]:
|
||||
"""按创建时间倒序返回最近的运行记录。
|
||||
|
||||
默认排除 source=batch:批量 run 是批量任务的单视频明细(一个任务会产生
|
||||
N 条),把 20 条窗口占满会把用户自己提交的任务挤出列表;它们由批量页
|
||||
的 `/api/batch/jobs` 展示,需要排查时可显式 include_batch=True。
|
||||
"""
|
||||
sql = "SELECT * FROM workflow_runs"
|
||||
if not include_batch:
|
||||
sql += " WHERE source != 'batch'"
|
||||
sql += " ORDER BY created_at DESC LIMIT ?"
|
||||
with self._connect() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM workflow_runs ORDER BY created_at DESC LIMIT ?",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
rows = conn.execute(sql, (limit,)).fetchall()
|
||||
return [self._parse_overrides(row) for row in rows]
|
||||
|
||||
def list_run_ids(self) -> list[str]:
|
||||
@@ -385,16 +391,28 @@ class Database:
|
||||
return cur.rowcount
|
||||
|
||||
def recover_interrupted_batch_jobs(self, updated_at: str) -> int:
|
||||
"""重启恢复:把遗留 RUNNING 的批量任务恢复为 QUEUED,返回恢复数量。
|
||||
"""重启恢复:把没在运行、也永远不会被拾起的批量任务恢复为 QUEUED。
|
||||
|
||||
批量任务若停在 RUNNING,next_queued_batch_job 只拾取 QUEUED,
|
||||
永远不会重新驱动它,未处理完的 PENDING 视频会永久残留;恢复为
|
||||
QUEUED 后引擎从断点(剩余视频 + 已恢复的 run)继续。用户主动暂停的
|
||||
两类任务需要恢复:停在 RUNNING 的(进程被杀,next_queued_batch_job
|
||||
不拾起;不恢复则剩余 PENDING 视频永久残留),以及被提前标记 COMPLETED
|
||||
但仍有未结束视频的僵尸任务(完成标记先于视频收尾写出,用户看到“已完成”
|
||||
却还有视频没处理)。恢复为 QUEUED 后引擎从断点续跑,用户主动暂停的
|
||||
PAUSED 保持不变。
|
||||
"""
|
||||
with self._connect() as conn:
|
||||
cur = conn.execute(
|
||||
"UPDATE batch_jobs SET status = 'QUEUED', updated_at = ? WHERE status = 'RUNNING'",
|
||||
"""
|
||||
UPDATE batch_jobs SET status = 'QUEUED', updated_at = ?
|
||||
WHERE status = 'RUNNING'
|
||||
OR (
|
||||
status = 'COMPLETED'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM batch_videos
|
||||
WHERE batch_videos.job_id = batch_jobs.id
|
||||
AND batch_videos.status NOT IN ('COMPLETED', 'FAILED', 'SKIPPED')
|
||||
)
|
||||
)
|
||||
""",
|
||||
(updated_at,),
|
||||
)
|
||||
return cur.rowcount
|
||||
|
||||
Reference in New Issue
Block a user