From 0cc797ab2278266c694da1240331e6bc941d24cc Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 19 Jul 2026 13:08:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(P0):=20=E8=A7=86=E9=A2=91=E5=B0=81=E9=9D=A2?= =?UTF-8?q?=E7=BC=A9=E7=95=A5=E5=9B=BE=E6=8A=BD=E5=B8=A7=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=88=B015%=E5=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从固定第1秒改为按视频时长15%处抽帧,避开片头转场/纯色画面 - 新增 seek_ratio(默认0.15)和 min_seek_seconds(默认1秒)参数 - 新增 _format_seek_time 工具函数格式化时间 - -ss 移到 -i 前面(input seeking),速度更快,缩略图精度足够 - probe失败或ffmpeg失败都有兜底(第1秒 → 第0帧) --- .../video_processing/thumbnail_generator.py | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/apps/worker/video_processing/thumbnail_generator.py b/apps/worker/video_processing/thumbnail_generator.py index 6b9f3ed4f..175a1f9a1 100755 --- a/apps/worker/video_processing/thumbnail_generator.py +++ b/apps/worker/video_processing/thumbnail_generator.py @@ -16,8 +16,10 @@ def extract_first_frame( width: int = 640, height: int = -1, timeout: int = 30, + seek_ratio: float = 0.15, + min_seek_seconds: float = 1.0, ) -> str: - """抽取视频第一帧作为封面图。 + """抽取视频封面图(默认取视频时长 15% 处的帧,避开片头纯色画面)。 Args: video_path: 视频文件路径 @@ -25,6 +27,8 @@ def extract_first_frame( width: 输出宽度(默认 640,-1 表示按比例缩放) height: 输出高度(默认 -1,按比例缩放) timeout: 超时时间(秒) + seek_ratio: 抽帧位置占视频时长的比例(默认 0.15,即 15% 处) + min_seek_seconds: 最小抽帧时间(秒),避免极短视频 seek 到 0 Returns: 生成的缩略图文件路径 @@ -32,24 +36,35 @@ def extract_first_frame( Raises: subprocess.CalledProcessError: ffmpeg 执行失败 """ - from video_processing.ffmpeg_utils import FFMPEG_BIN, run_ffmpeg + from video_processing.ffmpeg_utils import FFMPEG_BIN, probe_duration, run_ffmpeg if output_path is None: tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) tmp.close() output_path = tmp.name - # -ss 00:00:01 取第1秒帧(避免首帧黑屏) + # 计算抽帧时间点:取视频时长 * seek_ratio,最少 min_seek_seconds 秒 + try: + duration = probe_duration(video_path) + seek_time = max(min_seek_seconds, duration * seek_ratio) + except Exception: + # probe 失败时 fallback 到第1秒 + seek_time = min_seek_seconds + + # 格式化为 HH:MM:SS.xx + seek_str = _format_seek_time(seek_time) + + # -ss 放在 -i 前面(input seeking,更快但精度稍低,缩略图够用) # -vframes 1 只取一帧 # -q:v 2 jpeg 高质量 scale_filter = f"scale={width}:{height}:force_original_aspect_ratio=decrease" cmd = [ FFMPEG_BIN, "-y", + "-ss", + seek_str, "-i", video_path, - "-ss", - "00:00:01", "-vframes", "1", "-vf", @@ -62,7 +77,7 @@ def extract_first_frame( try: run_ffmpeg(cmd, capture_output=True, timeout=timeout) except Exception: - # 短视频可能没有第1秒,退回到第0帧 + # 失败时退回到第0帧兜底 cmd2 = [ FFMPEG_BIN, "-y", @@ -86,6 +101,14 @@ def extract_first_frame( return output_path +def _format_seek_time(seconds: float) -> str: + """将秒数格式化为 HH:MM:SS.xx 格式。""" + h = int(seconds // 3600) + m = int((seconds % 3600) // 60) + s = seconds % 60 + return f"{h:02d}:{m:02d}:{s:05.2f}" + + def generate_and_upload_thumbnail( video_path: str, storage_key: str, -- 2.54.0