feat(extract): 抽帧打印进度日志

ffmpeg 增加 -progress pipe:1 并解析 frame=N:每约 10 个检查点打印
'抽帧进度 X/Y 帧 (Z 帧/s)',长视频抽帧期间可见实时进度与处理速度。
测试覆盖进度行解析、进度日志输出与失败路径(Popen mock)。
This commit is contained in:
2026-08-17 00:03:51 +08:00
parent 4746e0363f
commit 885cf07921
2 changed files with 121 additions and 4 deletions
+43 -4
View File
@@ -12,6 +12,7 @@ from __future__ import annotations
import json
import re
import subprocess
import time
from pathlib import Path
from nodes.ffmpeg import _ffmpeg_bin
@@ -96,6 +97,20 @@ def _frame_step(fps: float, interval: float) -> int:
return max(1, int(round(interval * fps)))
def _parse_progress_line(line: str) -> int | None:
"""解析 ffmpeg -progress 输出行,返回 frame=N 的数值;其他行返回 None。
-progress 每 ~0.5s 输出一组 key=valueframe/fps/out_time_ms/progress 等),
这里只关心 frame=(已写出的选中帧数),用于实时进度与速度统计。
"""
stripped = line.strip()
if not stripped.startswith("frame="):
return None
try:
return int(stripped.split("=", 1)[1])
except ValueError:
return None
def invoke(request: InvokeRequest) -> InvokeResponse:
"""按帧间隔抽取并裁切视频帧,输出 frames.json 清单。"""
video_uri = request.inputs.get("video_uri")
@@ -137,13 +152,18 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
# 单次解码全片:select 按帧号(n mod step == 0)精确取帧,随后对选中帧
# 裁切字幕区域并压缩到 720p 内(仅缩小)——过大的输入图会触发 glm-ocr
# 的重复循环 bugM-RoPE delta),源头规避。
result = subprocess.run(
# -progress 把 frame=N 进度写入管道,实时打印抽帧进度与速度。
expected_frames = (int(duration * fps) + step - 1) // step
process = subprocess.Popen(
[
ffmpeg_bin,
"-y",
"-hide_banner",
"-loglevel",
"error",
"-nostats",
"-progress",
"pipe:1",
"-i",
str(video),
"-vf",
@@ -157,11 +177,30 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
"vfr",
str(frames_dir / "frame_%04d.png"),
],
capture_output=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if result.returncode != 0:
return InvokeResponse(status="failed", error=result.stderr[-500:] or "ffmpeg failed")
extract_started = time.monotonic()
last_logged = 0
progress_step = max(1, expected_frames // 10)
for line in process.stdout:
done = _parse_progress_line(line)
if done is None:
continue
# 每写出约 10 个检查点打一次日志,避免刷屏;同时给出处理速度。
if done - last_logged >= progress_step:
elapsed = time.monotonic() - extract_started
logger.info(
"抽帧进度 %d/%d 帧 (%.1f 帧/s)",
min(done, expected_frames), expected_frames,
done / elapsed if elapsed > 0 else 0.0,
)
last_logged = done
process.wait()
stderr = process.stderr.read()
if process.returncode != 0:
return InvokeResponse(status="failed", error=stderr[-500:] or "ffmpeg failed")
# 第 k 个输出文件对应原始帧号 k×step,时间 = 帧号 / fps(帧精确,无累计偏差)。
files = sorted(frames_dir.glob("frame_*.png"))