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:
+26
-14
@@ -239,6 +239,16 @@ def create_job(
|
|||||||
"created_at": now,
|
"created_at": now,
|
||||||
"updated_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(
|
logger.info(
|
||||||
"创建批量任务 %s: 文件夹 %s, 工作流 %s, 共 %d 个视频(%d 待处理, %d 已有字幕跳过)",
|
"创建批量任务 %s: 文件夹 %s, 工作流 %s, 共 %d 个视频(%d 待处理, %d 已有字幕跳过)",
|
||||||
job_id, folder, workflow_id, len(videos), pending, skipped,
|
job_id, folder, workflow_id, len(videos), pending, skipped,
|
||||||
@@ -340,27 +350,30 @@ class BatchWorker:
|
|||||||
definition.validate()
|
definition.validate()
|
||||||
|
|
||||||
items = self.db.list_batch_videos(job_id)
|
items = self.db.list_batch_videos(job_id)
|
||||||
total = len(items)
|
# total 在创建任务时已固定为"无字幕需处理的视频数",这里不覆盖;
|
||||||
|
# 老任务(历史口径 total=全部视频数)由 sync_batch_job_progress 在读取
|
||||||
|
# 时自我修正为不含 SKIPPED 的口径。
|
||||||
self.db.update_batch_job(
|
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(),
|
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)
|
current = self.db.get_batch_job(job_id)
|
||||||
if current is None or current["status"] == "PAUSED":
|
if current is None or current["status"] == "PAUSED":
|
||||||
# 停下前把已完成的视频实时入账:任务可能已处理多个视频才被暂停,
|
# 停下前把已完成/失败的视频实时入账,让暂停中的前端也能看到
|
||||||
# 若不在暂停边界同步,前端会一直看到 0/总数 0%(回归 batch_fee668175444)。
|
# 真实进度(回归 batch_fee668175444)。
|
||||||
self.db.sync_batch_job_progress(job_id)
|
self.db.sync_batch_job_progress(job_id)
|
||||||
logger.info("批量任务 %s 已暂停,停止在视频 %s", job_id, item["video_path"])
|
logger.info("批量任务 %s 已暂停,停止在视频 %s", job_id, item["video_path"])
|
||||||
return
|
return
|
||||||
|
|
||||||
# 已完成/已跳过的视频不再处理(跳过决策在创建任务时已定)。
|
# 已完成/已跳过的视频不再处理:COMPLETED 由断点续跑逻辑跳过,
|
||||||
|
# SKIPPED 在创建任务时已定(不参与 total/done,故无需同步进度)。
|
||||||
if item["status"] in ("COMPLETED", "SKIPPED"):
|
if item["status"] in ("COMPLETED", "SKIPPED"):
|
||||||
# 已完成的视频同样是任务进度的一部分:continue 前实时同步汇总,
|
|
||||||
# 避免长任务(大量 SKIPPED)中途汇总停留在 0。
|
|
||||||
self.db.sync_batch_job_progress(job_id)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
video = Path(item["video_path"])
|
video = Path(item["video_path"])
|
||||||
@@ -371,9 +384,7 @@ class BatchWorker:
|
|||||||
|
|
||||||
work_dir = Path(item["work_dir"])
|
work_dir = Path(item["work_dir"])
|
||||||
self.db.update_batch_job(
|
self.db.update_batch_job(
|
||||||
job_id, current_video=str(video),
|
job_id, current_video=str(video), updated_at=_now_iso(),
|
||||||
progress=index / total if total else 0,
|
|
||||||
updated_at=_now_iso(),
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
self._process_video(job, item, version, definition, work_dir)
|
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())
|
self.db.update_batch_job(job_id, status="PAUSED", updated_at=_now_iso())
|
||||||
return
|
return
|
||||||
|
|
||||||
# 全部视频处理完成:先用明细实时对齐汇总(含已跳过),再置 COMPLETED。
|
# 全部视频处理完成:先用明细实时对齐汇总(done 只计实际完成的,
|
||||||
|
# 不含 SKIPPED),再置 COMPLETED。
|
||||||
self.db.sync_batch_job_progress(job_id)
|
self.db.sync_batch_job_progress(job_id)
|
||||||
job = self.db.get_batch_job(job_id)
|
job = self.db.get_batch_job(job_id)
|
||||||
done = int(job["done"]) if job else 0
|
done = int(job["done"]) if job else 0
|
||||||
@@ -405,7 +417,7 @@ class BatchWorker:
|
|||||||
current_video=None, error=None, updated_at=_now_iso(),
|
current_video=None, error=None, updated_at=_now_iso(),
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"批量任务 %s 完成: 共 %d 个视频, 完成/跳过 %d, 失败 %d",
|
"批量任务 %s 完成: 待处理 %d 个视频, 完成 %d, 失败 %d",
|
||||||
job_id, total, done, failed,
|
job_id, total, done, failed,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+16
-12
@@ -559,21 +559,25 @@ class Database:
|
|||||||
|
|
||||||
|
|
||||||
def sync_batch_job_progress(self, job_id: str) -> None:
|
def sync_batch_job_progress(self, job_id: str) -> None:
|
||||||
"""按视频明细实时重算任务的 done/failed 汇总并落库。
|
"""按视频明细实时对齐任务的 total/done/failed 汇总并落库。
|
||||||
|
|
||||||
|
统计口径(2026-09 用户确认):total = 本批**无字幕、需要处理**的视频数
|
||||||
|
(= 明细里非 SKIPPED 的数量,创建时已固定,运行中 SKIPPED 不会变化);
|
||||||
|
done = 实际**处理完成**的视频数(仅 COMPLETED,SKIPPED 不计);
|
||||||
|
failed = 处理失败的视频数。已有字幕直接跳过的视频不参与 total/done。
|
||||||
|
|
||||||
明细里 COMPLETED 与 SKIPPED 都计入 done(它们都不再需要处理,是任务
|
|
||||||
已完成的工作量);FAILED 计入 failed;PAUSED/PENDING 等状态不计。
|
|
||||||
引擎在暂停、视频间检查、收尾等边界调用,router 在读取前也调用,保证
|
引擎在暂停、视频间检查、收尾等边界调用,router 在读取前也调用,保证
|
||||||
前端看到的进度始终与明细一致——即使任务被暂停或进程被终止,汇总字段
|
前端看到的进度始终与明细一致——即使任务被暂停或进程被终止(回归
|
||||||
也不会停留在创建时的 0(回归 batch_fee668175444:处理了 15 个仍显示
|
batch_fee668175444:整批 431 个含 383 个已有字幕,应显示 15/48 而非
|
||||||
0/431)。任务不存在时静默返回。
|
0/431)。任务不存在时静默返回。
|
||||||
"""
|
"""
|
||||||
with self._connect() as conn:
|
with self._connect() as conn:
|
||||||
counts = conn.execute(
|
counts = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
SUM(CASE WHEN status IN ('COMPLETED', 'SKIPPED') THEN 1 ELSE 0 END) AS done,
|
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 = '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 = ?
|
FROM batch_videos WHERE job_id = ?
|
||||||
""",
|
""",
|
||||||
(job_id,),
|
(job_id,),
|
||||||
@@ -582,16 +586,16 @@ class Database:
|
|||||||
# 任务不存在或没有任何明细:无需更新。
|
# 任务不存在或没有任何明细:无需更新。
|
||||||
return
|
return
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE batch_jobs SET done = ?, failed = ?, updated_at = ? WHERE id = ?",
|
"UPDATE batch_jobs SET done = ?, failed = ?, total = ?, updated_at = ? WHERE id = ?",
|
||||||
(int(counts["done"]), int(counts["failed"]), _now_iso(), job_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:
|
def refresh_batch_job(self, job_id: str) -> dict[str, Any] | None:
|
||||||
"""实时对齐任务汇总后返回最新记录(读取侧统一入口)。
|
"""实时对齐任务汇总后返回最新记录(读取侧统一入口)。
|
||||||
|
|
||||||
先 sync_batch_job_progress 让 done/failed 与明细一致,再返回最新 job;
|
先 sync_batch_job_progress 让 total/done/failed 与明细一致,再返回
|
||||||
任务不存在返回 None。批量引擎与 router 共用此入口,保证各处看到的
|
最新 job;任务不存在返回 None。批量引擎与 router 共用此入口,保证
|
||||||
进度数字一致。
|
各处看到的进度数字一致。
|
||||||
"""
|
"""
|
||||||
self.sync_batch_job_progress(job_id)
|
self.sync_batch_job_progress(job_id)
|
||||||
return self.get_batch_job(job_id)
|
return self.get_batch_job(job_id)
|
||||||
|
|||||||
@@ -81,8 +81,10 @@ def create_batch_job(
|
|||||||
def list_batch_jobs(db: Database = Depends(_get_db)) -> list[dict]:
|
def list_batch_jobs(db: Database = Depends(_get_db)) -> list[dict]:
|
||||||
"""返回最近的批量任务列表(不含视频明细,明细按需单独查询)。
|
"""返回最近的批量任务列表(不含视频明细,明细按需单独查询)。
|
||||||
|
|
||||||
返回前对每个任务实时对齐 done/failed:任务被暂停或引擎不在运行时,汇总
|
返回前对每个任务实时对齐 total/done/failed:任务被暂停或引擎不在运行时,
|
||||||
字段也能与明细一致,前端列表的进度数字不会停留在 0(回归 batch_fee668175444)。
|
汇总字段也能与明细一致,前端列表的进度数字不会停留在 0
|
||||||
|
(回归 batch_fee668175444:431 个含 383 个已有字幕,应显示实际处理进度
|
||||||
|
而非 0/431)。
|
||||||
"""
|
"""
|
||||||
jobs = db.list_batch_jobs()
|
jobs = db.list_batch_jobs()
|
||||||
for job in jobs:
|
for job in jobs:
|
||||||
|
|||||||
+58
-13
@@ -318,14 +318,19 @@ def test_create_job_skips_videos_with_existing_subtitles(tmp_path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_create_job_all_videos_skipped_still_created(tmp_path) -> None:
|
def test_create_job_all_videos_skipped_still_created(tmp_path) -> None:
|
||||||
"""文件夹里全部视频都已有字幕时任务仍可创建(全部 SKIPPED,不再处理)。"""
|
"""文件夹里全部视频都已有字幕时任务直接视为完成(无字幕视频数为 0)。
|
||||||
|
|
||||||
|
新口径(2026-09):total = 扫描出的无字幕视频数,全被 SKIPPED 时
|
||||||
|
total=0 且没有可处理项,创建即置 COMPLETED,不排队空跑。
|
||||||
|
"""
|
||||||
db = _db(tmp_path)
|
db = _db(tmp_path)
|
||||||
_seed_echo_workflow(db)
|
_seed_echo_workflow(db)
|
||||||
folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4"))
|
folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4"))
|
||||||
(folder / "a.CN.srt").write_text("x", encoding="utf-8")
|
(folder / "a.CN.srt").write_text("x", encoding="utf-8")
|
||||||
(folder / "b.CN_dual_eye.ass").write_text("x", encoding="utf-8")
|
(folder / "b.CN_dual_eye.ass").write_text("x", encoding="utf-8")
|
||||||
job = _make_job(db, folder)
|
job = _make_job(db, folder)
|
||||||
assert job["status"] == "QUEUED"
|
assert job["status"] == "COMPLETED"
|
||||||
|
assert job["total"] == 0 and job["done"] == 0
|
||||||
assert all(v["status"] == "SKIPPED" for v in db.list_batch_videos(job["id"]))
|
assert all(v["status"] == "SKIPPED" for v in db.list_batch_videos(job["id"]))
|
||||||
|
|
||||||
|
|
||||||
@@ -364,31 +369,39 @@ def test_batch_worker_processes_all_videos_and_cleans_up(tmp_path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_batch_worker_skips_videos_with_existing_subtitles(tmp_path) -> None:
|
def test_batch_worker_skips_videos_with_existing_subtitles(tmp_path) -> None:
|
||||||
"""已有字幕的视频在创建时记 SKIPPED,引擎运行时不再为它触发流水线。"""
|
"""已有字幕的视频在创建时记 SKIPPED,不进入 total/done,只统计真正处理的。
|
||||||
|
|
||||||
|
新口径(2026-09):total=无字幕视频数(不含 SKIPPED),done=实际完成数。
|
||||||
|
"""
|
||||||
db = _db(tmp_path)
|
db = _db(tmp_path)
|
||||||
_seed_echo_workflow(db)
|
_seed_echo_workflow(db)
|
||||||
folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4"))
|
folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4"))
|
||||||
(folder / "a.CN_dual_eye.ass").write_text("已处理", encoding="utf-8")
|
(folder / "a.CN_dual_eye.ass").write_text("已处理", encoding="utf-8")
|
||||||
job = _make_job(db, folder)
|
job = _make_job(db, folder)
|
||||||
|
# 创建时 total 已固定为无字幕数(b 一个),SKIPPED 不计入。
|
||||||
|
assert db.get_batch_job(job["id"])["total"] == 1
|
||||||
BatchWorker(db, interval_seconds=0.05)._process_job(job)
|
BatchWorker(db, interval_seconds=0.05)._process_job(job)
|
||||||
videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])}
|
videos = {Path(v["video_path"]).name: v for v in db.list_batch_videos(job["id"])}
|
||||||
assert videos["a.mp4"]["status"] == "SKIPPED"
|
assert videos["a.mp4"]["status"] == "SKIPPED"
|
||||||
assert videos["a.mp4"]["run_id"] is None
|
assert videos["a.mp4"]["run_id"] is None
|
||||||
assert videos["b.mp4"]["status"] == "COMPLETED"
|
assert videos["b.mp4"]["status"] == "COMPLETED"
|
||||||
assert db.get_batch_job(job["id"])["done"] == 2
|
job = db.get_batch_job(job["id"])
|
||||||
|
# done 只计实际完成的 b(1 个),不再把 SKIPPED 的 a 计入。
|
||||||
|
assert job["done"] == 1 and job["total"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_batch_worker_all_skipped_job_completes(tmp_path) -> None:
|
def test_batch_worker_all_skipped_job_completes(tmp_path) -> None:
|
||||||
"""任务里全部视频都是 SKIPPED 时引擎正常完成,不创建任何 run。"""
|
"""全部视频都有字幕时创建即视为完成(total=0),不创建任何 run。"""
|
||||||
db = _db(tmp_path)
|
db = _db(tmp_path)
|
||||||
_seed_echo_workflow(db)
|
_seed_echo_workflow(db)
|
||||||
folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4"))
|
folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4"))
|
||||||
(folder / "a.srt").write_text("x", encoding="utf-8")
|
(folder / "a.srt").write_text("x", encoding="utf-8")
|
||||||
(folder / "b.CN.srt").write_text("x", encoding="utf-8")
|
(folder / "b.CN.srt").write_text("x", encoding="utf-8")
|
||||||
job = _make_job(db, folder)
|
job = _make_job(db, folder)
|
||||||
BatchWorker(db, interval_seconds=0.05)._process_job(job)
|
# 新口径:无字幕视频数为 0 → 创建即 COMPLETED,引擎不需要再跑。
|
||||||
assert db.get_batch_job(job["id"])["status"] == "COMPLETED"
|
assert db.get_batch_job(job["id"])["status"] == "COMPLETED"
|
||||||
assert db.get_batch_job(job["id"])["done"] == 2
|
assert db.get_batch_job(job["id"])["total"] == 0
|
||||||
|
assert db.get_batch_job(job["id"])["done"] == 0
|
||||||
assert db.list_runs() == []
|
assert db.list_runs() == []
|
||||||
|
|
||||||
|
|
||||||
@@ -917,7 +930,10 @@ def test_batch_progress_sync_recounts_done_failed(tmp_path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_batch_progress_sync_counts_skipped(tmp_path) -> None:
|
def test_batch_progress_sync_counts_skipped(tmp_path) -> None:
|
||||||
"""SKIPPED 视频同样计入 done(它们不需要处理,属于已完成的工作量)。"""
|
"""SKIPPED 视频不计入 total 也不计入 done(它不需要本批处理)。
|
||||||
|
|
||||||
|
新口径(2026-09):进度只反映"本批无字幕待处理"的视频。
|
||||||
|
"""
|
||||||
db = _db(tmp_path)
|
db = _db(tmp_path)
|
||||||
_seed_echo_workflow(db)
|
_seed_echo_workflow(db)
|
||||||
folder = _video_folder(tmp_path, names=("a.mp4",))
|
folder = _video_folder(tmp_path, names=("a.mp4",))
|
||||||
@@ -925,7 +941,9 @@ def test_batch_progress_sync_counts_skipped(tmp_path) -> None:
|
|||||||
# 把 a 改为 SKIPPED(等同创建时旁挂字幕被跳过的语义)。
|
# 把 a 改为 SKIPPED(等同创建时旁挂字幕被跳过的语义)。
|
||||||
db.update_batch_video(db.list_batch_videos(job["id"])[0]["id"], status="SKIPPED", updated_at=_now_iso())
|
db.update_batch_video(db.list_batch_videos(job["id"])[0]["id"], status="SKIPPED", updated_at=_now_iso())
|
||||||
db.sync_batch_job_progress(job["id"])
|
db.sync_batch_job_progress(job["id"])
|
||||||
assert db.get_batch_job(job["id"])["done"] == 1
|
job = db.get_batch_job(job["id"])
|
||||||
|
assert job["done"] == 0
|
||||||
|
assert job["total"] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_batch_progress_sync_returns_refreshed_job(tmp_path) -> None:
|
def test_batch_progress_sync_returns_refreshed_job(tmp_path) -> None:
|
||||||
@@ -979,22 +997,29 @@ def test_batch_worker_paused_job_reports_real_done(tmp_path, monkeypatch) -> Non
|
|||||||
|
|
||||||
|
|
||||||
def test_batch_worker_skipped_continue_syncs_done(tmp_path) -> None:
|
def test_batch_worker_skipped_continue_syncs_done(tmp_path) -> None:
|
||||||
"""循环里遇到 SKIPPED/COMPLETED 的 continue 分支也会把汇总实时对齐。"""
|
"""循环里遇到 SKIPPED/COMPLETED 的 continue 分支不把它们计入 done。
|
||||||
|
|
||||||
|
新口径:total=无字幕视频数(本例 a 有字幕 SKIPPED → total=1 即 b),
|
||||||
|
done 只计实际完成(b=COMPLETED → done=1)。SKIPPED 不占分母也不占分子。
|
||||||
|
"""
|
||||||
db = _db(tmp_path)
|
db = _db(tmp_path)
|
||||||
_seed_echo_workflow(db)
|
_seed_echo_workflow(db)
|
||||||
folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4"))
|
folder = _video_folder(tmp_path, names=("a.mp4", "b.mp4"))
|
||||||
(folder / "a.CN.srt").write_text("x", encoding="utf-8") # a 创建即 SKIPPED
|
(folder / "a.CN.srt").write_text("x", encoding="utf-8") # a 创建即 SKIPPED
|
||||||
job = _make_job(db, folder)
|
job = _make_job(db, folder)
|
||||||
job_id = job["id"]
|
job_id = job["id"]
|
||||||
# b 也手工置为 COMPLETED(引擎会因 continue 分支跳过它)。
|
# 创建即固定:total=无字幕数(仅 b=1)。
|
||||||
|
assert db.get_batch_job(job_id)["total"] == 1
|
||||||
|
# b 手工置为 COMPLETED(模拟此前已完成);a 在循环里走 continue 分支。
|
||||||
db.update_batch_video(
|
db.update_batch_video(
|
||||||
[v for v in db.list_batch_videos(job_id) if v["video_path"].endswith("b.mp4")][0]["id"],
|
[v for v in db.list_batch_videos(job_id) if v["video_path"].endswith("b.mp4")][0]["id"],
|
||||||
status="COMPLETED", updated_at=_now_iso(),
|
status="COMPLETED", updated_at=_now_iso(),
|
||||||
)
|
)
|
||||||
# 任务先置 PAUSED:首轮循环在 a(SKIPPED)即停下,若 continue 前未对齐则 done=0。
|
|
||||||
db.update_batch_job(job_id, status="PAUSED", updated_at=_now_iso())
|
db.update_batch_job(job_id, status="PAUSED", updated_at=_now_iso())
|
||||||
BatchWorker(db, interval_seconds=0.05)._run_job(job_id)
|
BatchWorker(db, interval_seconds=0.05)._run_job(job_id)
|
||||||
assert db.get_batch_job(job_id)["done"] == 2
|
job = db.get_batch_job(job_id)
|
||||||
|
# done 只计实际完成的 b(1),SKIPPED 的 a 不计;total 保持 1。
|
||||||
|
assert job["done"] == 1 and job["total"] == 1
|
||||||
|
|
||||||
def test_batch_worker_ghost_job_id_returns(tmp_path) -> None:
|
def test_batch_worker_ghost_job_id_returns(tmp_path) -> None:
|
||||||
"""_run_job 在任务不存在时直接返回(幽灵任务处理无副作用)。"""
|
"""_run_job 在任务不存在时直接返回(幽灵任务处理无副作用)。"""
|
||||||
@@ -1059,6 +1084,8 @@ def test_batch_api_create_list_detail(tmp_path) -> None:
|
|||||||
detail = client.get(f"/api/batch/jobs/{job['id']}").json()
|
detail = client.get(f"/api/batch/jobs/{job['id']}").json()
|
||||||
assert detail["workflow_id"] == "echo-app"
|
assert detail["workflow_id"] == "echo-app"
|
||||||
assert len(detail["videos"]) == 2
|
assert len(detail["videos"]) == 2
|
||||||
|
# 新口径:total = 无字幕需处理数(b 一个),SKIPPED 的 a 不占分母。
|
||||||
|
assert detail["total"] == 1 and detail["done"] == 0
|
||||||
|
|
||||||
missing = client.get("/api/batch/jobs/ghost")
|
missing = client.get("/api/batch/jobs/ghost")
|
||||||
assert missing.status_code == 404
|
assert missing.status_code == 404
|
||||||
@@ -1066,6 +1093,24 @@ def test_batch_api_create_list_detail(tmp_path) -> None:
|
|||||||
client.__exit__(None, None, None)
|
client.__exit__(None, None, None)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_api_all_skipped_created_completed(tmp_path) -> None:
|
||||||
|
"""全部视频已有字幕时,创建响应直接为 COMPLETED,total=0。"""
|
||||||
|
client, folder = _client_with_echo_workflow(tmp_path)
|
||||||
|
try:
|
||||||
|
# 两个视频都已有旁挂字幕 → 无任何无字幕项。
|
||||||
|
(folder / "a.CN.srt").write_text("x", encoding="utf-8")
|
||||||
|
(folder / "b.CN_dual_eye.ass").write_text("x", encoding="utf-8")
|
||||||
|
job = client.post(
|
||||||
|
"/api/batch/jobs",
|
||||||
|
json={"folder": str(folder), "workflow_id": "echo-app"},
|
||||||
|
).json()
|
||||||
|
assert job["status"] == "COMPLETED"
|
||||||
|
assert job["total"] == 0 and job["done"] == 0
|
||||||
|
assert all(v["status"] == "SKIPPED" for v in job["videos"])
|
||||||
|
finally:
|
||||||
|
client.__exit__(None, None, None)
|
||||||
def test_batch_api_creation_errors(tmp_path) -> None:
|
def test_batch_api_creation_errors(tmp_path) -> None:
|
||||||
"""批量 API 拒绝:文件夹不存在、无视频、未发布工作流。"""
|
"""批量 API 拒绝:文件夹不存在、无视频、未发布工作流。"""
|
||||||
client, folder = _client_with_echo_workflow(tmp_path)
|
client, folder = _client_with_echo_workflow(tmp_path)
|
||||||
|
|||||||
+5
-2
@@ -54,12 +54,15 @@ async function createBatchJob() {
|
|||||||
await loadBatchJobs();
|
await loadBatchJobs();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染单个任务的进度:已完成/跳过与失败计入不同颜色,总数为任务视频数。
|
// 渲染单个任务的进度:total=本批无字幕(需处理)的视频数,done=实际处理
|
||||||
|
// 完成的视频数。SKIPPED(已有字幕)不参与 total/done——进度只反映真正
|
||||||
|
// 待处理的这批(2026-09 口径调整)。失败视频单独红字提示。
|
||||||
function batchProgress(job) {
|
function batchProgress(job) {
|
||||||
const total = job.total || 0;
|
const total = job.total || 0;
|
||||||
const done = job.done || 0;
|
const done = job.done || 0;
|
||||||
const failed = job.failed || 0;
|
const failed = job.failed || 0;
|
||||||
if (!total) return "待扫描";
|
// total=0:本批没有需要处理的视频(全部已有字幕被跳过),视为无处理量。
|
||||||
|
if (!total) return "无待处理视频";
|
||||||
const percent = Math.round(((done + failed) / total) * 100);
|
const percent = Math.round(((done + failed) / total) * 100);
|
||||||
// 有失败视频时追加红色失败计数,避免把"部分失败"误看成全部完成。
|
// 有失败视频时追加红色失败计数,避免把"部分失败"误看成全部完成。
|
||||||
const failedText = failed > 0 ? ` <span class="danger">,失败 ${failed}</span>` : "";
|
const failedText = failed > 0 ? ` <span class="danger">,失败 ${failed}</span>` : "";
|
||||||
|
|||||||
Reference in New Issue
Block a user