feat(extract): 抽帧打印进度日志
ffmpeg 增加 -progress pipe:1 并解析 frame=N:每约 10 个检查点打印 '抽帧进度 X/Y 帧 (Z 帧/s)',长视频抽帧期间可见实时进度与处理速度。 测试覆盖进度行解析、进度日志输出与失败路径(Popen mock)。
This commit is contained in:
+43
-4
@@ -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=value(frame/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
|
||||
# 的重复循环 bug(M-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"))
|
||||
|
||||
Reference in New Issue
Block a user