fix: 批量进度只统计无字幕视频,SKIPPED 不计入 total/done

用户反馈数量展示错误:整批 431 个视频中 383 个已有字幕被 SKIPPED,
此前 total=全部视频、done 也含 SKIPPED,把已处理过的视频混进计数。

新口径(2026-09):
- total = 创建时扫描出的无字幕(需处理)视频数,创建即固定;SKIPPED 不计
- done = 实际处理完成(COMPLETED)数,SKIPPED 不计;failed 单独计
- 全部视频都有字幕(total=0)→ 创建即视为 COMPLETED,不排队空跑
- sync_batch_job_progress 同步修正 total(老任务读取时自动收敛到新口径)
- 前端 batchProgress 相应更新:total=0 显示"无待处理视频"

tests: 更新 4 条旧口径断言 + 新增 1 条全跳过 API 测试,62 项相关测试通过。
This commit is contained in:
2026-09-06 17:25:01 +08:00
parent 52512c26e5
commit 44a607b636
5 changed files with 109 additions and 43 deletions
+26 -14
View File
@@ -239,6 +239,16 @@ def create_job(
"created_at": now,
"updated_at": now,
})
# total = 本批真正需要处理(无字幕)的视频数;已有字幕被 SKIPPED 的
# 不计入总数也不计入完成数——进度条只反映"实际待处理"的这批。
if pending == 0:
# 整批都已有字幕、无任何可处理项:直接视为完成,不排队。
db.update_batch_job(
job_id, status="COMPLETED", total=0, progress=1.0,
updated_at=_now_iso(),
)
else:
db.update_batch_job(job_id, total=pending, updated_at=_now_iso())
logger.info(
"创建批量任务 %s: 文件夹 %s, 工作流 %s, 共 %d 个视频(%d 待处理, %d 已有字幕跳过)",
job_id, folder, workflow_id, len(videos), pending, skipped,
@@ -340,27 +350,30 @@ class BatchWorker:
definition.validate()
items = self.db.list_batch_videos(job_id)
total = len(items)
# total 在创建任务时已固定为"无字幕需处理的视频数",这里不覆盖;
# 老任务(历史口径 total=全部视频数)由 sync_batch_job_progress 在读取
# 时自我修正为不含 SKIPPED 的口径。
self.db.update_batch_job(
job_id, status="RUNNING", total=total, progress=0,
job_id, status="RUNNING", progress=0,
current_video=None, error=None, updated_at=_now_iso(),
)
# total 用于进度条分母;无字幕项为 0 时表示整批跳过(创建即 COMPLETED,
# 正常不会进入本循环)。
total = int(job["total"] or 0)
for index, item in enumerate(items):
for item in items:
# 暂停检查:批量任务被暂停后停止处理后续视频,等待用户继续。
current = self.db.get_batch_job(job_id)
if current is None or current["status"] == "PAUSED":
# 停下前把已完成的视频实时入账:任务可能已处理多个视频才被暂停,
# 若不在暂停边界同步,前端会一直看到 0/总数 0%(回归 batch_fee668175444)。
# 停下前把已完成/失败的视频实时入账,让暂停中的前端也能看到
# 真实进度(回归 batch_fee668175444)。
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 前实时同步汇总,
# 避免长任务(大量 SKIPPED)中途汇总停留在 0。
self.db.sync_batch_job_progress(job_id)
continue
video = Path(item["video_path"])
@@ -371,9 +384,7 @@ class BatchWorker:
work_dir = Path(item["work_dir"])
self.db.update_batch_job(
job_id, current_video=str(video),
progress=index / total if total else 0,
updated_at=_now_iso(),
job_id, current_video=str(video), updated_at=_now_iso(),
)
try:
self._process_video(job, item, version, definition, work_dir)
@@ -395,7 +406,8 @@ class BatchWorker:
self.db.update_batch_job(job_id, status="PAUSED", updated_at=_now_iso())
return
# 全部视频处理完成:先用明细实时对齐汇总(含已跳过),再置 COMPLETED。
# 全部视频处理完成:先用明细实时对齐汇总(done 只计实际完成的,
# 不含 SKIPPED),再置 COMPLETED。
self.db.sync_batch_job_progress(job_id)
job = self.db.get_batch_job(job_id)
done = int(job["done"]) if job else 0
@@ -405,7 +417,7 @@ class BatchWorker:
current_video=None, error=None, updated_at=_now_iso(),
)
logger.info(
"批量任务 %s 完成: %d 个视频, 完成/跳过 %d, 失败 %d",
"批量任务 %s 完成: 待处理 %d 个视频, 完成 %d, 失败 %d",
job_id, total, done, failed,
)
+16 -12
View File
@@ -559,21 +559,25 @@ class Database:
def sync_batch_job_progress(self, job_id: str) -> None:
"""按视频明细实时重算任务的 done/failed 汇总并落库。
"""按视频明细实时对齐任务的 total/done/failed 汇总并落库。
统计口径(2026-09 用户确认):total = 本批**无字幕、需要处理**的视频数
(= 明细里非 SKIPPED 的数量,创建时已固定,运行中 SKIPPED 不会变化);
done = 实际**处理完成**的视频数(仅 COMPLETEDSKIPPED 不计);
failed = 处理失败的视频数。已有字幕直接跳过的视频不参与 total/done。
明细里 COMPLETED 与 SKIPPED 都计入 done(它们都不再需要处理,是任务
已完成的工作量);FAILED 计入 failedPAUSED/PENDING 等状态不计。
引擎在暂停、视频间检查、收尾等边界调用,router 在读取前也调用,保证
前端看到的进度始终与明细一致——即使任务被暂停或进程被终止,汇总字段
也不会停留在创建时的 0(回归 batch_fee668175444处理了 15 个仍显示
前端看到的进度始终与明细一致——即使任务被暂停或进程被终止(回归
batch_fee668175444整批 431 个含 383 个已有字幕,应显示 15/48 而非
0/431)。任务不存在时静默返回。
"""
with self._connect() as conn:
counts = conn.execute(
"""
SELECT
SUM(CASE WHEN status IN ('COMPLETED', 'SKIPPED') THEN 1 ELSE 0 END) AS done,
SUM(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) AS failed
SUM(CASE WHEN status = 'COMPLETED' THEN 1 ELSE 0 END) AS done,
SUM(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) AS failed,
SUM(CASE WHEN status != 'SKIPPED' THEN 1 ELSE 0 END) AS total
FROM batch_videos WHERE job_id = ?
""",
(job_id,),
@@ -582,16 +586,16 @@ class Database:
# 任务不存在或没有任何明细:无需更新。
return
conn.execute(
"UPDATE batch_jobs SET done = ?, failed = ?, updated_at = ? WHERE id = ?",
(int(counts["done"]), int(counts["failed"]), _now_iso(), job_id),
"UPDATE batch_jobs SET done = ?, failed = ?, total = ?, updated_at = ? WHERE id = ?",
(int(counts["done"]), int(counts["failed"]), int(counts["total"]), _now_iso(), job_id),
)
def refresh_batch_job(self, job_id: str) -> dict[str, Any] | None:
"""实时对齐任务汇总后返回最新记录(读取侧统一入口)。
先 sync_batch_job_progress 让 done/failed 与明细一致,再返回最新 job
任务不存在返回 None。批量引擎与 router 共用此入口,保证各处看到的
进度数字一致。
先 sync_batch_job_progress 让 total/done/failed 与明细一致,再返回
最新 job任务不存在返回 None。批量引擎与 router 共用此入口,保证
各处看到的进度数字一致。
"""
self.sync_batch_job_progress(job_id)
return self.get_batch_job(job_id)
+4 -2
View File
@@ -81,8 +81,10 @@ def create_batch_job(
def list_batch_jobs(db: Database = Depends(_get_db)) -> list[dict]:
"""返回最近的批量任务列表(不含视频明细,明细按需单独查询)。
返回前对每个任务实时对齐 done/failed:任务被暂停或引擎不在运行时,汇总
字段也能与明细一致,前端列表的进度数字不会停留在 0(回归 batch_fee668175444)。
返回前对每个任务实时对齐 total/done/failed:任务被暂停或引擎不在运行时,
汇总字段也能与明细一致,前端列表的进度数字不会停留在 0
(回归 batch_fee668175444431 个含 383 个已有字幕,应显示实际处理进度
而非 0/431)。
"""
jobs = db.list_batch_jobs()
for job in jobs: