diff --git a/server/main.py b/server/main.py index 9cd864e..587bf11 100644 --- a/server/main.py +++ b/server/main.py @@ -53,6 +53,40 @@ from server.detector import get_detector 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 = None @@ -142,8 +176,33 @@ def _build_ffmpeg_vaapi_writer( fps: float, width: int, height: int, + out_fps_str: str = "", ) -> _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_rate(rawパイプの入力レート) + 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 = [ "ffmpeg", "-hide_banner", @@ -164,7 +223,7 @@ def _build_ffmpeg_vaapi_writer( "-", "-an", "-vf", - "format=nv12,hwupload", + vf, "-c:v", "h264_vaapi", "-qp", @@ -180,13 +239,14 @@ def _build_video_writer( fps: float, width: int, height: int, + out_fps_str: str = "", ) -> object: """Create writer with VAAPI preference and OpenCV fallback.""" format_key = fmt.lower() if format_key in {"mp4", "mov"}: 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)") return writer 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)) 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: tasks[task_id].status = TaskStatus.FAILED tasks[task_id].message = "Invalid source video dimensions" @@ -617,7 +686,7 @@ def process_bake_task(task_id: str, req: BakeRequest): frame_count = 0 writer = None 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: 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") 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) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0) frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0) finally: 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 { "video_path": req.video_path, "fps": fps,