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:
+160
-90
@@ -11,6 +11,12 @@
|
||||
字幕文件(`.srt/.ass/.ssa/.vtt`),说明该视频已有字幕,直接记为 SKIPPED,
|
||||
不为它触发任何流水线。运行时(BatchWorker)只消费已定位好的明细列表,
|
||||
**不再重新扫描文件夹**(运行期间新增/删除的视频不会改变本次任务的范围)。
|
||||
- **分块流水线执行**:视频按 `WOV_BATCH_STAGE_GROUP_SIZE` 分组,组内按节点
|
||||
顺序跑完全部视频(先全部 extract、再全部 ASR、再全部 LLM 翻译、最后 ASS)
|
||||
再进入下一组——本地模型每组只加载一次、卸载一次,产物按组增量落地。
|
||||
阶段边界用 `execute_run(stop_after=节点)` 停在节点(任务保持 RUNNING),
|
||||
LLM 阶段靠 `keep_model.flag` 让节点保持模型常驻,阶段结束由引擎统一释放
|
||||
显存(详见 docs/operations.md#文件夹批量处理)。
|
||||
- **产物放在视频旁**:每个视频处理完成后,把工作流 `final_outputs` 对应的
|
||||
最终产物文件(字幕流水线即中文 `.srt` 与双目 `.ass`)**复制一份到视频的
|
||||
所在目录**,与 .mp4 放在一起;文件名**对齐媒体库既有约定**:中文字幕存为
|
||||
@@ -44,12 +50,14 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from wov_app import registry
|
||||
from wov_app.config import BATCH_INTERVAL_SECONDS, STORAGE_DIR
|
||||
from wov_app.config import BATCH_INTERVAL_SECONDS, BATCH_STAGE_GROUP_SIZE, STORAGE_DIR
|
||||
from wov_app.db import Database
|
||||
from wov_app.logging import get_logger
|
||||
from wov_app.scheduler import WorkflowScheduler
|
||||
from wov_app.scheduler import WorkflowScheduler, topological_sort
|
||||
from wov_app.storage import atomic_copy
|
||||
from wov_sdk.models import WorkflowDefinition
|
||||
from wov_sdk.models import WorkflowDefinition, WorkflowNode
|
||||
|
||||
from nodes.llm import release_local_model
|
||||
|
||||
# 批量引擎运行日志:任务进度、视频逐个处理与暂停/续跑等状态变化。
|
||||
logger = get_logger("batch")
|
||||
@@ -67,6 +75,13 @@ SUBTITLE_EXTENSIONS = {".srt", ".ass", ".ssa", ".vtt"}
|
||||
# 暂停信号文件名:与节点约定一致,位于 run 根目录(<work_dir>/runs/<run_id>/)。
|
||||
PAUSE_FLAG = "paused.flag"
|
||||
|
||||
# 保持模型常驻信号文件名:LLM 阶段执行期间由引擎写入 run 根目录,节点据此不在
|
||||
# 每次调用后卸载模型(同组视频共用一份已加载模型,减少加载/卸载次数)。
|
||||
KEEP_MODEL_FLAG = "keep_model.flag"
|
||||
|
||||
# LLM 节点类型前缀:这类节点加载本地大模型,阶段结束后由引擎统一释放显存。
|
||||
LLM_NODE_PREFIX = "llm"
|
||||
|
||||
# 兼容读取的历史完成标记文件名(旧任务用它记录产物路径)。当前逻辑不再
|
||||
# 写入,产物直接放视频旁;保留读取能力以便旧任务的详情/下载仍可用。
|
||||
MARKER_NAME = "batch.done.json"
|
||||
@@ -324,11 +339,12 @@ class BatchWorker:
|
||||
self.db.update_batch_job(job_id, status="FAILED", error=str(exc), updated_at=_now_iso())
|
||||
|
||||
def _run_job(self, job_id: str) -> None:
|
||||
"""批量任务主流程(内部实现,异常由 _process_job 统一处理)。
|
||||
"""批量任务主流程(分块流水线,异常由 _process_job 统一处理)。
|
||||
|
||||
只消费创建任务时已定位好的 batch_videos 明细:SKIPPED/COMPLETED 直接
|
||||
跳过,PENDING(含失败/暂停后恢复的)逐个交给 _process_video 处理,
|
||||
**不再扫描文件夹**补视频。
|
||||
视频按 WOV_BATCH_STAGE_GROUP_SIZE 分组,组内按 DAG 拓扑顺序逐节点跑完
|
||||
全部视频(先全部 extract、再全部 ASR、再全部 LLM 翻译、最后 ASS)再
|
||||
进入下一组:本地模型每组只加载一次、卸载一次,产物按组增量落地。
|
||||
只消费创建任务时已定位好的 batch_videos 明细,**不再扫描文件夹**。
|
||||
"""
|
||||
job = self.db.get_batch_job(job_id)
|
||||
if job is None:
|
||||
@@ -348,8 +364,21 @@ class BatchWorker:
|
||||
return
|
||||
definition = WorkflowDefinition.from_dict(version["definition"])
|
||||
definition.validate()
|
||||
order = topological_sort(definition)
|
||||
node_by_id = {node.id: node for node in definition.nodes}
|
||||
|
||||
items = self.db.list_batch_videos(job_id)
|
||||
# 任务行先于视频明细写入(create_job 逐条插入),引擎可能在登记完成前就拾起
|
||||
# 任务:此时没有任何明细,不能按“空任务”收尾,保持 QUEUED 等登记完成。
|
||||
if not self.db.list_batch_videos(job_id):
|
||||
logger.info("批量任务 %s 尚无视频明细(仍在登记),保持 QUEUED 稍后重试", job_id)
|
||||
self.db.update_batch_job(job_id, status="QUEUED", updated_at=_now_iso())
|
||||
return
|
||||
|
||||
# 待处理明细:SKIPPED 创建时已定(不参与 total/done),COMPLETED 无需重跑。
|
||||
items = [
|
||||
item for item in self.db.list_batch_videos(job_id)
|
||||
if item["status"] not in ("COMPLETED", "SKIPPED")
|
||||
]
|
||||
# total 创建时已固定为"无字幕需处理的视频数",此处不覆盖;历史任务
|
||||
# 的旧口径由 sync_batch_job_progress 在读取时修正为不含 SKIPPED。
|
||||
self.db.update_batch_job(
|
||||
@@ -359,49 +388,45 @@ class BatchWorker:
|
||||
# total 为进度条分母;为 0 表示整批跳过(创建即 COMPLETED)。
|
||||
total = int(job["total"] or 0)
|
||||
|
||||
for item in items:
|
||||
# 暂停检查:批量任务被暂停后停止处理后续视频,等待用户继续。
|
||||
current = self.db.get_batch_job(job_id)
|
||||
if current is None or current["status"] == "PAUSED":
|
||||
# 停下前先把已完成/失败项入账,让暂停中的前端看到真实进度。
|
||||
self.db.sync_batch_job_progress(job_id)
|
||||
logger.info("批量任务 %s 已暂停,停止在视频 %s", job_id, item["video_path"])
|
||||
return
|
||||
|
||||
# 已完成/已跳过的视频不再处理:COMPLETED 由断点续跑逻辑跳过,
|
||||
# SKIPPED 在创建任务时已定(不参与 total/done,故无需同步进度)。
|
||||
if item["status"] in ("COMPLETED", "SKIPPED"):
|
||||
continue
|
||||
|
||||
video = Path(item["video_path"])
|
||||
if not video.is_file():
|
||||
self.db.update_batch_video(item["id"], status="FAILED", error="video file not found", updated_at=_now_iso())
|
||||
self.db.sync_batch_job_progress(job_id)
|
||||
continue
|
||||
|
||||
work_dir = Path(item["work_dir"])
|
||||
self.db.update_batch_job(
|
||||
job_id, current_video=str(video), updated_at=_now_iso(),
|
||||
)
|
||||
try:
|
||||
self._process_video(job, item, version, definition, work_dir)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# 单视频兜底:不中断整个批量任务,记录错误后继续下一个视频。
|
||||
logger.exception("批量任务 %s 视频 %s 处理异常", job_id, video)
|
||||
self.db.update_batch_video(item["id"], status="FAILED", error=str(exc), updated_at=_now_iso())
|
||||
# 本视频处理完(成功/失败/暂停)后实时同步一次汇总,让进度尽快入账。
|
||||
self.db.sync_batch_job_progress(job_id)
|
||||
|
||||
# 重新读取视频明细:_process_video 可能刚创建 run 或已收尾清理
|
||||
# (快照里 run_id 可能是旧值),必须取最新记录判断暂停状态。
|
||||
item = self.db.get_batch_video(item["id"])
|
||||
# 视频处理中被暂停:批量任务整体保持 PAUSED,等待用户继续。
|
||||
run = self.db.get_run(item["run_id"]) if item and item.get("run_id") else None
|
||||
if run is not None and run["status"] == "PAUSED":
|
||||
self.db.update_batch_video(item["id"], status="PAUSED", updated_at=_now_iso())
|
||||
self.db.sync_batch_job_progress(job_id)
|
||||
self.db.update_batch_job(job_id, status="PAUSED", updated_at=_now_iso())
|
||||
return
|
||||
group_size = max(1, int(BATCH_STAGE_GROUP_SIZE))
|
||||
for start in range(0, len(items), group_size):
|
||||
group = items[start:start + group_size]
|
||||
for stage_index, node_id in enumerate(order):
|
||||
node_spec = node_by_id[node_id]
|
||||
# 末阶段不传 stop_after:让调度器收尾(final_outputs + COMPLETED)。
|
||||
is_last_stage = stage_index == len(order) - 1
|
||||
executed = False
|
||||
for item in group:
|
||||
# 暂停检查:批量任务被暂停后停止处理后续视频,等待用户继续。
|
||||
current = self.db.get_batch_job(job_id)
|
||||
if current is None or current["status"] == "PAUSED":
|
||||
# 停下前先把已完成/失败项入账,让暂停中的前端看到真实进度。
|
||||
self.db.sync_batch_job_progress(job_id)
|
||||
logger.info("批量任务 %s 已暂停,停止在视频 %s", job_id, item["video_path"])
|
||||
return
|
||||
self.db.update_batch_job(job_id, current_video=str(item["video_path"]), updated_at=_now_iso())
|
||||
try:
|
||||
outcome = self._run_stage(
|
||||
job, item, version, definition, node_spec, order, is_last_stage,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# 单视频兜底:不中断整个批量任务,记录错误后继续下一个视频。
|
||||
logger.exception(
|
||||
"批量任务 %s 视频 %s 阶段 %s 处理异常", job_id, item["video_path"], node_id,
|
||||
)
|
||||
self.db.update_batch_video(item["id"], status="FAILED", error=str(exc), updated_at=_now_iso())
|
||||
outcome = "FAILED"
|
||||
executed = executed or outcome is not None
|
||||
# 每个视频每个阶段后实时同步一次汇总,让进度尽快入账。
|
||||
self.db.sync_batch_job_progress(job_id)
|
||||
# 阶段内被暂停(节点内的 paused.flag):任务保持 PAUSED 等续跑。
|
||||
if outcome == "PAUSED":
|
||||
self.db.update_batch_job(job_id, status="PAUSED", updated_at=_now_iso())
|
||||
return
|
||||
# 阶段收尾:LLM 阶段结束时统一释放本地模型显存,让下一组的
|
||||
# whisper(ASR)拿到 GPU,否则下一个视频转写会 CUDA OOM。
|
||||
if executed and node_spec.node_type.startswith(LLM_NODE_PREFIX):
|
||||
self._release_llm_model(node_spec.params)
|
||||
|
||||
# 先按明细实时对齐汇总(done 不计 SKIPPED),再判断能否收尾。
|
||||
# 仍有未结束视频时不能标 COMPLETED,否则会出现“还有待处理视频却已完成”
|
||||
@@ -415,11 +440,15 @@ class BatchWorker:
|
||||
if v["status"] not in ("SKIPPED", "COMPLETED", "FAILED")
|
||||
]
|
||||
if leftovers:
|
||||
# 有未处理完的视频:保持 RUNNING,由引擎下一轮续跑。
|
||||
# 有未处理完的视频(常见于创建任务时明细还在逐条写入,本轮快照没包含
|
||||
# 它们):置回 QUEUED 自愈,让引擎下一轮按最新明细重新分组续跑。
|
||||
# 留在 RUNNING 不会被引擎再拾起(next_queued_batch_job 只取 QUEUED),
|
||||
# 任务会停在“运行中但没人推进”的状态。
|
||||
logger.warning(
|
||||
"批量任务 %s 仍有 %d 个视频未处理完(%s…),保持 RUNNING 待续跑,不置 COMPLETED",
|
||||
"批量任务 %s 仍有 %d 个视频未处理完(%s…),置回 QUEUED 待下一轮续跑",
|
||||
job_id, len(leftovers), Path(leftovers[0]["video_path"]).name,
|
||||
)
|
||||
self.db.update_batch_job(job_id, status="QUEUED", updated_at=_now_iso())
|
||||
return
|
||||
done = int(job["done"]) if job else 0
|
||||
failed = int(job["failed"]) if job else 0
|
||||
@@ -427,25 +456,84 @@ class BatchWorker:
|
||||
job_id, status="COMPLETED", progress=1.0,
|
||||
current_video=None, error=None, updated_at=_now_iso(),
|
||||
)
|
||||
# 无失败视频时每个视频的工作空间已在收尾时删除,任务目录只剩空壳;
|
||||
# 有失败视频则保留(它们的工作空间供断点重试)。
|
||||
if failed == 0:
|
||||
remove_job_workspace(job_id)
|
||||
logger.info(
|
||||
"批量任务 %s 完成: 待处理 %d 个视频, 完成 %d, 失败 %d",
|
||||
job_id, total, done, failed,
|
||||
)
|
||||
|
||||
def _process_video(
|
||||
def _run_stage(
|
||||
self,
|
||||
job: dict,
|
||||
item: dict,
|
||||
version: dict,
|
||||
definition: WorkflowDefinition,
|
||||
work_dir: Path,
|
||||
) -> None:
|
||||
"""处理单个视频:建 run(复用现有调度器)执行,成功后收尾清理。
|
||||
node_spec: WorkflowNode,
|
||||
order: list[str],
|
||||
is_last_stage: bool,
|
||||
) -> str | None:
|
||||
"""执行一个视频在一个阶段节点上的工作,返回执行后的视频状态。
|
||||
|
||||
per-video 的 WorkflowScheduler 以该视频的私有工作空间为 storage,
|
||||
中间态落在 <work_dir>/runs/<run_id>/steps/ 下;产物表记录全部节点
|
||||
输出,暂停后续跑从产物表重建已完成节点(断点续跑)。视频成功后
|
||||
把最终产物复制到视频旁并删除工作空间(见 _finalize_video)。
|
||||
返回 None 表示本阶段无需执行(视频已完成/已跳过)。per-video 的
|
||||
WorkflowScheduler 以该视频的私有工作空间为 storage,产物表记录各节点
|
||||
输出,所以同一视频的后续阶段直接从断点继续(不重跑已完成节点)。
|
||||
LLM 阶段会写 keep_model.flag:阶段内保持模型常驻,阶段结束由引擎统一
|
||||
释放(见 _release_llm_model),避免每个视频重新加载/卸载模型。
|
||||
"""
|
||||
fresh = self.db.get_batch_video(item["id"])
|
||||
if fresh is None or fresh["status"] in ("COMPLETED", "SKIPPED"):
|
||||
return None
|
||||
video = Path(fresh["video_path"])
|
||||
if not video.is_file():
|
||||
self.db.update_batch_video(fresh["id"], status="FAILED", error="video file not found", updated_at=_now_iso())
|
||||
return "FAILED"
|
||||
work_dir = Path(fresh["work_dir"])
|
||||
# 失败节点在当前阶段之前:本轮不再推进(否则会在本地 LLM 已常驻时重跑
|
||||
# ASR 抢显存),留待下一次引擎循环从其失败节点重试。必须在复位 run 状态
|
||||
# 之前判断,否则 _ensure_run 已把 FAILED 改成 QUEUED、判断会失效。
|
||||
previous = self.db.get_run(fresh["run_id"]) if fresh.get("run_id") else None
|
||||
if previous is not None and previous["status"] == "FAILED":
|
||||
failed_node = previous["current_node_id"]
|
||||
failed_index = order.index(failed_node) if failed_node in order else 0
|
||||
if failed_index != order.index(node_spec.id):
|
||||
return "FAILED"
|
||||
run_id = self._ensure_run(job, fresh, version, work_dir)
|
||||
run_dir = work_dir / "runs" / run_id
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
# 清除可能残留的信号(重启/强杀/异常中断后):暂停信号会让本次执行误暂停,
|
||||
# 保持常驻信号会让后续单独重跑该节点时不再卸载模型。
|
||||
(run_dir / PAUSE_FLAG).unlink(missing_ok=True)
|
||||
(run_dir / KEEP_MODEL_FLAG).unlink(missing_ok=True)
|
||||
if node_spec.node_type.startswith(LLM_NODE_PREFIX):
|
||||
(run_dir / KEEP_MODEL_FLAG).write_text("", encoding="utf-8")
|
||||
try:
|
||||
scheduler = WorkflowScheduler(self.db, work_dir)
|
||||
scheduler.execute_run(run_id, stop_after=None if is_last_stage else node_spec.id)
|
||||
finally:
|
||||
# 信号只在本阶段有效:残留会让后续单独重跑该节点时也不卸载模型。
|
||||
(run_dir / KEEP_MODEL_FLAG).unlink(missing_ok=True)
|
||||
|
||||
run = self.db.get_run(run_id)
|
||||
if run is None:
|
||||
# execute_run 期间 run 记录被删除(极端外部操作),直接返回。
|
||||
return None
|
||||
if run["status"] == "COMPLETED":
|
||||
# 放置最终产物到视频旁并清理过程文件。
|
||||
self._finalize_video(fresh, run_id, video, work_dir, definition)
|
||||
return "COMPLETED"
|
||||
# FAILED 或 PAUSED:由调用方根据 run 状态更新视频状态与任务状态。
|
||||
self.db.update_batch_video(fresh["id"], status=run["status"], error=run.get("error"), updated_at=_now_iso())
|
||||
return run["status"]
|
||||
|
||||
def _ensure_run(self, job: dict, item: dict, version: dict, work_dir: Path) -> str:
|
||||
"""确保视频有可执行的 run,返回 run_id。
|
||||
|
||||
run 缺失时新建(input_uri 指向本地视频,不上传副本);已存在的按断点
|
||||
续跑语义复位:PAUSED/FAILED 显式置 QUEUED(保留产物,只重跑未完成
|
||||
节点);RUNNING 是分阶段执行的上一个阶段或进程被杀的残留,同样置回。
|
||||
"""
|
||||
video = Path(item["video_path"])
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -454,8 +542,6 @@ class BatchWorker:
|
||||
# run 记录已不存在(收尾异常删除了 run 但状态未同步):重新新建。
|
||||
run_id = None
|
||||
if run_id is None:
|
||||
# 首次处理:创建 source=batch 的运行,input_uri 指向本地视频
|
||||
# (不上传副本),由调度器按 DAG 执行。
|
||||
run_id = f"run_{uuid.uuid4().hex[:12]}"
|
||||
now = _now_iso()
|
||||
self.db.create_run({
|
||||
@@ -471,38 +557,22 @@ class BatchWorker:
|
||||
"updated_at": now,
|
||||
})
|
||||
self.db.update_batch_video(item["id"], run_id=run_id, updated_at=_now_iso())
|
||||
|
||||
run = self.db.get_run(run_id)
|
||||
# 已完成(收尾前中断):直接补做收尾。
|
||||
if run["status"] == "COMPLETED":
|
||||
self._finalize_video(item, run_id, video, work_dir, definition)
|
||||
return
|
||||
# 暂停的 run 显式 resume 回 QUEUED,由 execute_run 从产物表断点续跑。
|
||||
if run["status"] == "PAUSED":
|
||||
# 暂停的 run 显式 resume 回 QUEUED,由 execute_run 从产物表断点续跑。
|
||||
self.db.resume_run(run_id, _now_iso())
|
||||
elif run["status"] == "FAILED":
|
||||
# 失败重跑:保留产物记录只置 QUEUED,由 execute_run 跳过已完成
|
||||
# 节点、仅重跑失败节点,避免浪费抽帧/OCR 等长耗时成果。
|
||||
elif run["status"] in ("FAILED", "RUNNING"):
|
||||
# 保留产物记录只置 QUEUED:execute_run 跳过已完成节点、只重跑失败节点,
|
||||
# 避免浪费抽帧/ASR 等长耗时成果。
|
||||
self.db.update_run(run_id, status="QUEUED", error=None, updated_at=_now_iso())
|
||||
elif run["status"] == "RUNNING":
|
||||
# 上次进程被杀残留:恢复 QUEUED(保留产物)由 execute_run 续跑。
|
||||
self.db.update_run(run_id, status="QUEUED", updated_at=_now_iso())
|
||||
# 清除可能残留的暂停信号(重启/异常中断后),避免本次执行误暂停。
|
||||
(work_dir / "runs" / run_id / PAUSE_FLAG).unlink(missing_ok=True)
|
||||
return run_id
|
||||
|
||||
scheduler = WorkflowScheduler(self.db, work_dir)
|
||||
scheduler.execute_run(run_id)
|
||||
|
||||
run = self.db.get_run(run_id)
|
||||
if run is None:
|
||||
# execute_run 期间 run 记录被删除(极端外部操作),直接返回。
|
||||
return
|
||||
if run["status"] == "COMPLETED":
|
||||
# 放置最终产物到视频旁并清理过程文件。
|
||||
self._finalize_video(item, run_id, video, work_dir, definition)
|
||||
else:
|
||||
# FAILED 或 PAUSED:由调用方根据 run 状态更新视频状态与任务状态。
|
||||
self.db.update_batch_video(item["id"], status=run["status"], error=run.get("error"), updated_at=_now_iso())
|
||||
def _release_llm_model(self, params: dict) -> None:
|
||||
"""LLM 阶段结束释放本机模型显存(分块流水线里每组一次,而非每视频一次)。"""
|
||||
try:
|
||||
release_local_model(params.get("model"))
|
||||
except Exception: # noqa: BLE001 - 释放失败不影响批次推进
|
||||
logger.warning("释放本地 LLM 模型失败", exc_info=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 收尾:产物放置与过程文件清理
|
||||
|
||||
Reference in New Issue
Block a user