fix: ソースFPSの認識の問題

This commit is contained in:
Keisuke Hirata 2026-02-22 16:19:52 +09:00
parent 0fdff5423e
commit 32e4fbceb2

View File

@ -53,6 +53,40 @@ from server.detector import get_detector
app = FastAPI(title="Face Mask Inference Server") app = FastAPI(title="Face Mask Inference Server")
def _get_r_frame_rate(video_path: str) -> tuple:
"""ffprobe でコンテナ宣言の r_frame_rate を取得する。
Returns:
(fps_float, fps_str): fps_str "120/1" のような分数文字列
取得失敗時は (0.0, "")
"""
try:
result = subprocess.run(
[
"ffprobe", "-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=r_frame_rate",
"-of", "default=noprint_wrappers=1:nokey=1",
video_path,
],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
rate_str = result.stdout.strip()
if "/" in rate_str:
num, den = rate_str.split("/")
fps_float = float(num) / float(den)
else:
fps_float = float(rate_str)
rate_str = str(fps_float)
return fps_float, rate_str
except Exception:
pass
return 0.0, ""
# GPU status cache # GPU status cache
_gpu_status_cache = None _gpu_status_cache = None
@ -142,8 +176,33 @@ def _build_ffmpeg_vaapi_writer(
fps: float, fps: float,
width: int, width: int,
height: int, height: int,
out_fps_str: str = "",
) -> _FFmpegPipeWriter: ) -> _FFmpegPipeWriter:
"""Create ffmpeg h264_vaapi writer with QP=24 (balanced quality/speed).""" """Create ffmpeg h264_vaapi writer with QP=24 (balanced quality/speed).
fps: ソース動画の avg_frame_raterawパイプの入力レート
out_fps_str: 出力コンテナに宣言する r_frame_rate"120/1"
ソースと異なる場合は fps フィルタでフレームを補完する
"""
# ソースの avg_fps と出力の r_fps が有意に異なる場合のみ fps フィルタを挿入
needs_fps_filter = bool(out_fps_str)
if needs_fps_filter:
try:
if "/" in out_fps_str:
num, den = out_fps_str.split("/")
out_fps_float = float(num) / float(den)
else:
out_fps_float = float(out_fps_str)
needs_fps_filter = abs(out_fps_float - fps) > 0.01
except ValueError:
needs_fps_filter = False
if needs_fps_filter:
vf = f"format=nv12,fps={out_fps_str},hwupload"
print(f"[FaceMask] fps filter: {fps:.3f} -> {out_fps_str}")
else:
vf = "format=nv12,hwupload"
cmd = [ cmd = [
"ffmpeg", "ffmpeg",
"-hide_banner", "-hide_banner",
@ -164,7 +223,7 @@ def _build_ffmpeg_vaapi_writer(
"-", "-",
"-an", "-an",
"-vf", "-vf",
"format=nv12,hwupload", vf,
"-c:v", "-c:v",
"h264_vaapi", "h264_vaapi",
"-qp", "-qp",
@ -180,13 +239,14 @@ def _build_video_writer(
fps: float, fps: float,
width: int, width: int,
height: int, height: int,
out_fps_str: str = "",
) -> object: ) -> object:
"""Create writer with VAAPI preference and OpenCV fallback.""" """Create writer with VAAPI preference and OpenCV fallback."""
format_key = fmt.lower() format_key = fmt.lower()
if format_key in {"mp4", "mov"}: if format_key in {"mp4", "mov"}:
try: try:
writer = _build_ffmpeg_vaapi_writer(output_path, fps, width, height) writer = _build_ffmpeg_vaapi_writer(output_path, fps, width, height, out_fps_str)
print("[FaceMask] Using output encoder: ffmpeg h264_vaapi (-qp 24)") print("[FaceMask] Using output encoder: ffmpeg h264_vaapi (-qp 24)")
return writer return writer
except Exception as e: except Exception as e:
@ -417,6 +477,15 @@ def process_bake_task(task_id: str, req: BakeRequest):
src_frames = int(temp_cap.get(cv2.CAP_PROP_FRAME_COUNT)) src_frames = int(temp_cap.get(cv2.CAP_PROP_FRAME_COUNT))
temp_cap.release() temp_cap.release()
# ffprobe で r_frame_rate を取得し、出力コンテナの宣言 FPS をソースに合わせる。
# 例: 120fps タイムベースで記録された 60fps 動画は r_frame_rate=120/1 だが
# cv2 は avg_frame_rate=60fps を返すため、Bake 後に Blender がFPSを別値で認識してしまう。
r_fps_float, r_fps_str = _get_r_frame_rate(req.video_path)
if r_fps_float > 0:
print(f"[FaceMask] r_frame_rate={r_fps_str}, avg_fps={src_fps:.3f}")
else:
r_fps_str = ""
if src_width <= 0 or src_height <= 0: if src_width <= 0 or src_height <= 0:
tasks[task_id].status = TaskStatus.FAILED tasks[task_id].status = TaskStatus.FAILED
tasks[task_id].message = "Invalid source video dimensions" tasks[task_id].message = "Invalid source video dimensions"
@ -617,7 +686,7 @@ def process_bake_task(task_id: str, req: BakeRequest):
frame_count = 0 frame_count = 0
writer = None writer = None
try: try:
writer = _build_video_writer(req.output_path, req.format, src_fps, src_width, src_height) writer = _build_video_writer(req.output_path, req.format, src_fps, src_width, src_height, r_fps_str)
while True: while True:
if cancel_event and cancel_event.is_set(): if cancel_event and cancel_event.is_set():
@ -859,13 +928,20 @@ def get_video_info(req: VideoInfoRequest):
raise HTTPException(status_code=400, detail="Failed to open video") raise HTTPException(status_code=400, detail="Failed to open video")
try: try:
fps = float(cap.get(cv2.CAP_PROP_FPS) or 0.0) avg_fps = float(cap.get(cv2.CAP_PROP_FPS) or 0.0)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 0) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 0)
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0) frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
finally: finally:
cap.release() cap.release()
# Blender は r_frame_rate でタイムライン配置を計算するため、
# cv2 の avg_frame_rate ではなく r_frame_rate を fps として返す。
# 例: 120fps タイムベース記録の 60fps 動画で r_frame_rate=120 を返すことで
# compute_strip_frame_range の fps_ratio が Blender の解釈と一致する。
r_fps_float, _ = _get_r_frame_rate(req.video_path)
fps = r_fps_float if r_fps_float > 0 else avg_fps
return { return {
"video_path": req.video_path, "video_path": req.video_path,
"fps": fps, "fps": fps,