feat: subtitle-ocr 进度日志增加预计剩余时间 ETA

- 进度日志追加"预计剩余 X分Y秒"(剩余帧/当前速度,速度未知时不显示)
This commit is contained in:
2026-08-23 16:25:18 +08:00
parent 5688f38d62
commit 87abde33d4
2 changed files with 53 additions and 3 deletions
+27 -3
View File
@@ -59,6 +59,28 @@ def _load_partial(output_dir: Path) -> dict[int, str]:
result[int(item["frame"])] = str(item["text"])
return result
def _format_eta(seconds: float) -> str:
"""把剩余秒数格式化为可读的预计完成时间(如 34分13秒 / 2小时05分)。"""
total = max(0, int(seconds))
hours, remainder = divmod(total, 3600)
minutes, secs = divmod(remainder, 60)
if hours:
return f"{hours}小时{minutes:02d}"
if minutes:
return f"{minutes}{secs:02d}"
return f"{secs}"
def _eta_suffix(done: int, total: int, rate: float) -> str:
"""根据当前处理速度计算剩余时间后缀(供进度日志追加)。
剩余时间 = 剩余帧数 / 当前速度;速度为 0(刚开始或耗时不可测)时
返回空串,进度提示不显示 ETA。
"""
if rate <= 0:
return ""
return f", 预计剩余 {_format_eta((total - done) / rate)}"
# 默认垃圾词:无文字帧的模型输出可能反复出现这些词。
def _sampling_interval(manifest: list[dict], default: float) -> float:
"""从帧清单时间轴推导采样间隔(相邻帧时间差的中位数)。
@@ -206,12 +228,14 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
if pending:
# 进度日志:打印已识别帧数、总数、平均处理速度(帧/s)、最近窗口平均
# 单帧耗时当前线程数——便于判断多线程是否因单帧处理过慢而未启用
# (窗口平均响应 ≥ fast_threshold 时自适应池不会扩容)。
# 单帧耗时当前线程数与**预计剩余完成时间**(剩余帧/当前速度)——
# 便于判断多线程是否因单帧处理过慢而未启用(窗口平均响应 ≥
# fast_threshold 时自适应池不会扩容)以及整体还需要多久。
def log_progress(done: int, total: int, rate: float, avg_time: float, workers: int) -> None:
logger.info(
"OCR 进度 %d/%d 帧 (%.1f 帧/s, 平均 %.2fs/帧, 线程 %d/%d)",
"OCR 进度 %d/%d 帧 (%.1f 帧/s, 平均 %.2fs/帧, 线程 %d/%d%s)",
done, total, rate, avg_time, workers, pool.max_workers,
_eta_suffix(done, total, rate),
)
# 自适应并发调用 vlm-ocr10s 窗口内平均响应 < 0.3s 则加 1 线程(上限
+26
View File
@@ -539,3 +539,29 @@ def test_ocr_interrupts_on_pause_flag(monkeypatch, tmp_path) -> None:
assert "暂停" in (response.error or "")
assert calls == [] # 一帧都没有真正 OCR。
assert not (out_dir / "ocr_partial.jsonl").exists() # 未处理帧不入存档。
def test_format_eta() -> None:
"""预计剩余时间的格式化:秒/分/小时三种量级与边界值。"""
from nodes.subtitle_ocr import _format_eta
assert _format_eta(0) == "0秒"
assert _format_eta(59.9) == "59秒" # 不足 1 分只显示秒。
assert _format_eta(61) == "1分01秒"
assert _format_eta(2058.4) == "34分18秒"
assert _format_eta(3600) == "1小时00分" # 1 小时整。
assert _format_eta(3725) == "1小时02分" # 超过 1 小时只显示到分钟。
assert _format_eta(-5) == "0秒" # 负数钳制为 0。
def test_eta_suffix() -> None:
"""进度日志的 ETA 后缀:速度为 0 不显示,速度正常时按剩余帧估算。"""
from nodes.subtitle_ocr import _eta_suffix
# 速率未知(0/负)→ 不显示 ETA。
assert _eta_suffix(done=100, total=100, rate=0) == ""
assert _eta_suffix(done=100, total=100, rate=-1) == ""
# 2673/22222 帧、9.5 帧/s:剩余 (22222-2673)/9.5 ≈ 2057.8s ≈ 34分。
assert _eta_suffix(done=2673, total=22222, rate=9.5) == ", 预计剩余 34分17秒"
# 全部完成时剩余 0 秒。
assert _eta_suffix(done=22222, total=22222, rate=9.5) == ", 预计剩余 0秒"