From 47cd4f145d06c50bdbe9ed46eeae457a4a2a070f Mon Sep 17 00:00:00 2001 From: linyq Date: Mon, 19 May 2025 02:41:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20ffmpeg=20=E7=A1=AC?= =?UTF-8?q?=E4=BB=B6=E5=8A=A0=E9=80=9F=E5=85=BC=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/asgi.py | 8 + app/services/clip_video.py | 71 ++---- app/services/material.py | 103 +++------ app/services/merger_video.py | 194 ++++++---------- app/utils/ffmpeg_utils.py | 419 +++++++++++++++++++++++++++++++++++ app/utils/video_processor.py | 170 +++----------- config.example.toml | 2 +- webui.py | 22 +- 8 files changed, 596 insertions(+), 393 deletions(-) create mode 100644 app/utils/ffmpeg_utils.py diff --git a/app/asgi.py b/app/asgi.py index aec304c..ac06685 100644 --- a/app/asgi.py +++ b/app/asgi.py @@ -13,6 +13,7 @@ from app.config import config from app.models.exception import HttpException from app.router import root_api_router from app.utils import utils +from app.utils import ffmpeg_utils def exception_handler(request: Request, e: HttpException): @@ -80,3 +81,10 @@ def shutdown_event(): @app.on_event("startup") def startup_event(): logger.info("startup event") + + # 检测FFmpeg硬件加速 + hwaccel_info = ffmpeg_utils.detect_hardware_acceleration() + if hwaccel_info["available"]: + logger.info(f"FFmpeg硬件加速检测结果: 可用 | 类型: {hwaccel_info['type']} | 编码器: {hwaccel_info['encoder']} | 独立显卡: {hwaccel_info['is_dedicated_gpu']} | 参数: {hwaccel_info['hwaccel_args']}") + else: + logger.warning(f"FFmpeg硬件加速不可用: {hwaccel_info['message']}, 将使用CPU软件编码") diff --git a/app/services/clip_video.py b/app/services/clip_video.py index 1329333..72f57e2 100644 --- a/app/services/clip_video.py +++ b/app/services/clip_video.py @@ -5,7 +5,7 @@ @Project: NarratoAI @File : clip_video @Author : 小林同学 -@Date : 2025/5/6 下午6:14 +@Date : 2025/5/6 下午6:14 ''' import os @@ -16,14 +16,16 @@ from loguru import logger from typing import Dict, List, Optional from pathlib import Path +from app.utils import ffmpeg_utils + def parse_timestamp(timestamp: str) -> tuple: """ 解析时间戳字符串,返回开始和结束时间 - + Args: timestamp: 格式为'HH:MM:SS-HH:MM:SS'或'HH:MM:SS,sss-HH:MM:SS,sss'的时间戳字符串 - + Returns: tuple: (开始时间, 结束时间) 格式为'HH:MM:SS'或'HH:MM:SS,sss' """ @@ -34,37 +36,37 @@ def parse_timestamp(timestamp: str) -> tuple: def calculate_end_time(start_time: str, duration: float, extra_seconds: float = 1.0) -> str: """ 根据开始时间和持续时间计算结束时间 - + Args: start_time: 开始时间,格式为'HH:MM:SS'或'HH:MM:SS,sss'(带毫秒) duration: 持续时间,单位为秒 extra_seconds: 额外添加的秒数,默认为1秒 - + Returns: str: 计算后的结束时间,格式与输入格式相同 """ # 检查是否包含毫秒 has_milliseconds = ',' in start_time milliseconds = 0 - + if has_milliseconds: time_part, ms_part = start_time.split(',') h, m, s = map(int, time_part.split(':')) milliseconds = int(ms_part) else: h, m, s = map(int, start_time.split(':')) - + # 转换为总毫秒数 - total_milliseconds = ((h * 3600 + m * 60 + s) * 1000 + milliseconds + + total_milliseconds = ((h * 3600 + m * 60 + s) * 1000 + milliseconds + int((duration + extra_seconds) * 1000)) - + # 计算新的时、分、秒、毫秒 ms_new = total_milliseconds % 1000 total_seconds = total_milliseconds // 1000 h_new = int(total_seconds // 3600) m_new = int((total_seconds % 3600) // 60) s_new = int(total_seconds % 60) - + # 返回与输入格式一致的时间字符串 if has_milliseconds: return f"{h_new:02d}:{m_new:02d}:{s_new:02d},{ms_new:03d}" @@ -75,44 +77,12 @@ def calculate_end_time(start_time: str, duration: float, extra_seconds: float = def check_hardware_acceleration() -> Optional[str]: """ 检查系统支持的硬件加速选项 - + Returns: Optional[str]: 硬件加速参数,如果不支持则返回None """ - # 检查NVIDIA GPU支持 - try: - nvidia_check = subprocess.run( - ["ffmpeg", "-hwaccel", "cuda", "-i", "/dev/null", "-f", "null", "-"], - stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False - ) - if nvidia_check.returncode == 0: - return "cuda" - except Exception: - pass - - # 检查MacOS videotoolbox支持 - try: - videotoolbox_check = subprocess.run( - ["ffmpeg", "-hwaccel", "videotoolbox", "-i", "/dev/null", "-f", "null", "-"], - stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False - ) - if videotoolbox_check.returncode == 0: - return "videotoolbox" - except Exception: - pass - - # 检查Intel Quick Sync支持 - try: - qsv_check = subprocess.run( - ["ffmpeg", "-hwaccel", "qsv", "-i", "/dev/null", "-f", "null", "-"], - stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False - ) - if qsv_check.returncode == 0: - return "qsv" - except Exception: - pass - - return None + # 使用集中式硬件加速检测 + return ffmpeg_utils.get_ffmpeg_hwaccel_type() def clip_video( @@ -123,13 +93,13 @@ def clip_video( ) -> Dict[str, str]: """ 根据时间戳裁剪视频 - + Args: video_origin_path: 原始视频的路径 tts_result: 包含时间戳和持续时间信息的列表 output_dir: 输出目录路径,默认为None时会自动生成 task_id: 任务ID,用于生成唯一的输出目录,默认为None时会自动生成 - + Returns: Dict[str, str]: 时间戳到裁剪后视频路径的映射 """ @@ -152,12 +122,11 @@ def clip_video( # 确保输出目录存在 Path(output_dir).mkdir(parents=True, exist_ok=True) - # 检查硬件加速支持 + # 获取硬件加速支持 hwaccel = check_hardware_acceleration() hwaccel_args = [] if hwaccel: - hwaccel_args = ["-hwaccel", hwaccel] - logger.info(f"使用硬件加速: {hwaccel}") + hwaccel_args = ffmpeg_utils.get_ffmpeg_hwaccel_args() # 存储裁剪结果 result = {} @@ -170,7 +139,7 @@ def clip_video( # 根据持续时间计算真正的结束时间(加上1秒余量) duration = item["duration"] calculated_end_time = calculate_end_time(start_time, duration) - + # 转换为FFmpeg兼容的时间格式(逗号替换为点) ffmpeg_start_time = start_time.replace(',', '.') ffmpeg_end_time = calculated_end_time.replace(',', '.') diff --git a/app/services/material.py b/app/services/material.py index c048a92..1d4913a 100644 --- a/app/services/material.py +++ b/app/services/material.py @@ -14,6 +14,7 @@ from moviepy.video.io.VideoFileClip import VideoFileClip from app.config import config from app.models.schema import VideoAspect, VideoConcatMode, MaterialInfo from app.utils import utils +from app.utils import ffmpeg_utils requested_count = 0 @@ -257,10 +258,10 @@ def time_to_seconds(time_str: str) -> float: """ 将时间字符串转换为秒数 支持格式: 'HH:MM:SS,mmm' (时:分:秒,毫秒) - + Args: time_str: 时间字符串,如 "00:00:20,100" - + Returns: float: 转换后的秒数(包含毫秒) """ @@ -282,7 +283,7 @@ def time_to_seconds(time_str: str) -> float: raise ValueError("时间格式必须为 HH:MM:SS,mmm") return seconds + ms - + except ValueError as e: logger.error(f"时间格式错误: {time_str}") raise ValueError(f"时间格式错误: 必须为 HH:MM:SS,mmm 格式") from e @@ -291,10 +292,10 @@ def time_to_seconds(time_str: str) -> float: def format_timestamp(seconds: float) -> str: """ 将秒数转换为可读的时间格式 (HH:MM:SS,mmm) - + Args: seconds: 秒数(可包含毫秒) - + Returns: str: 格式化的时间字符串,如 "00:00:20,100" """ @@ -303,57 +304,26 @@ def format_timestamp(seconds: float) -> str: seconds_remain = seconds % 60 whole_seconds = int(seconds_remain) milliseconds = int((seconds_remain - whole_seconds) * 1000) - + return f"{hours:02d}:{minutes:02d}:{whole_seconds:02d},{milliseconds:03d}" def _detect_hardware_acceleration() -> Optional[str]: """ 检测系统可用的硬件加速器 - + Returns: Optional[str]: 硬件加速参数,如果不支持则返回None """ - # 检查NVIDIA GPU支持 - try: - nvidia_check = subprocess.run( - ["ffmpeg", "-hwaccel", "cuda", "-i", "/dev/null", "-f", "null", "-"], - stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False - ) - if nvidia_check.returncode == 0: - return "cuda" - except Exception: - pass - - # 检查MacOS videotoolbox支持 - try: - videotoolbox_check = subprocess.run( - ["ffmpeg", "-hwaccel", "videotoolbox", "-i", "/dev/null", "-f", "null", "-"], - stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False - ) - if videotoolbox_check.returncode == 0: - return "videotoolbox" - except Exception: - pass - - # 检查Intel Quick Sync支持 - try: - qsv_check = subprocess.run( - ["ffmpeg", "-hwaccel", "qsv", "-i", "/dev/null", "-f", "null", "-"], - stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False - ) - if qsv_check.returncode == 0: - return "qsv" - except Exception: - pass - - return None + # 使用集中式硬件加速检测 + hwaccel_type = ffmpeg_utils.get_ffmpeg_hwaccel_type() + return hwaccel_type def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> str: """ 保存剪辑后的视频 - + Args: timestamp: 需要裁剪的时间戳,格式为 'HH:MM:SS,mmm-HH:MM:SS,mmm' 例如: '00:00:00,000-00:00:20,100' @@ -374,7 +344,7 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> st # 解析时间戳 start_str, end_str = timestamp.split('-') - + # 格式化输出文件名(使用连字符替代冒号和逗号) safe_start_time = start_str.replace(':', '-').replace(',', '-') safe_end_time = end_str.replace(':', '-').replace(',', '-') @@ -391,48 +361,47 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> st if not os.path.exists(origin_video): logger.error(f"源视频文件不存在: {origin_video}") return '' - + # 获取视频总时长 try: - probe_cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration", + probe_cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", origin_video] total_duration = float(subprocess.check_output(probe_cmd).decode('utf-8').strip()) except subprocess.CalledProcessError as e: logger.error(f"获取视频时长失败: {str(e)}") return '' - + # 计算时间点 start = time_to_seconds(start_str) end = time_to_seconds(end_str) - + # 验证时间段 if start >= total_duration: logger.warning(f"起始时间 {format_timestamp(start)} ({start:.3f}秒) 超出视频总时长 {format_timestamp(total_duration)} ({total_duration:.3f}秒)") return '' - + if end > total_duration: logger.warning(f"结束时间 {format_timestamp(end)} ({end:.3f}秒) 超出视频总时长 {format_timestamp(total_duration)} ({total_duration:.3f}秒),将自动调整为视频结尾") end = total_duration - + if end <= start: logger.warning(f"结束时间 {format_timestamp(end)} 必须大于起始时间 {format_timestamp(start)}") return '' - + # 计算剪辑时长 duration = end - start # logger.info(f"开始剪辑视频: {format_timestamp(start)} - {format_timestamp(end)},时长 {format_timestamp(duration)}") - - # 检测可用的硬件加速选项 + + # 获取硬件加速选项 hwaccel = _detect_hardware_acceleration() hwaccel_args = [] if hwaccel: - hwaccel_args = ["-hwaccel", hwaccel] - logger.info(f"使用硬件加速: {hwaccel}") - + hwaccel_args = ffmpeg_utils.get_ffmpeg_hwaccel_args() + # 转换为FFmpeg兼容的时间格式(逗号替换为点) ffmpeg_start_time = start_str.replace(',', '.') ffmpeg_end_time = end_str.replace(',', '.') - + # 构建FFmpeg命令 ffmpeg_cmd = [ "ffmpeg", "-y", *hwaccel_args, @@ -444,36 +413,36 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> st "-strict", "experimental", video_path ] - + # 执行FFmpeg命令 # logger.info(f"裁剪视频片段: {timestamp} -> {ffmpeg_start_time}到{ffmpeg_end_time}") # logger.debug(f"执行命令: {' '.join(ffmpeg_cmd)}") - + process = subprocess.run( - ffmpeg_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + ffmpeg_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, text=True, check=False # 不抛出异常,我们会检查返回码 ) - + # 检查是否成功 if process.returncode != 0: logger.error(f"视频剪辑失败: {process.stderr}") if os.path.exists(video_path): os.remove(video_path) return '' - + # 验证生成的视频文件 if os.path.exists(video_path) and os.path.getsize(video_path) > 0: # 检查视频是否可播放 probe_cmd = ["ffprobe", "-v", "error", video_path] validate_result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - + if validate_result.returncode == 0: logger.info(f"视频剪辑成功: {video_path}") return video_path - + logger.error("视频文件验证失败") if os.path.exists(video_path): os.remove(video_path) @@ -506,14 +475,14 @@ def clip_videos(task_id: str, timestamp_terms: List[str], origin_video: str, pro saved_video_path = save_clip_video(timestamp=item, origin_video=origin_video, save_dir=material_directory) if saved_video_path: video_paths.update({index+1:saved_video_path}) - + # 更新进度 if progress_callback: progress_callback(index + 1, total_items) except Exception as e: logger.error(f"视频裁剪失败: {utils.to_json(item)} =>\n{str(traceback.format_exc())}") return {} - + logger.success(f"裁剪 {len(video_paths)} videos") # logger.debug(json.dumps(video_paths, indent=4, ensure_ascii=False)) return video_paths diff --git a/app/services/merger_video.py b/app/services/merger_video.py index 5e6084b..af2c526 100644 --- a/app/services/merger_video.py +++ b/app/services/merger_video.py @@ -5,7 +5,7 @@ @Project: NarratoAI @File : merger_video @Author : 小林同学 -@Date : 2025/5/6 下午7:38 +@Date : 2025/5/6 下午7:38 ''' import os @@ -15,6 +15,8 @@ from enum import Enum from typing import List, Optional, Tuple from loguru import logger +from app.utils import ffmpeg_utils + class VideoAspect(Enum): """视频宽高比枚举""" @@ -43,7 +45,7 @@ class VideoAspect(Enum): def check_ffmpeg_installation() -> bool: """ 检查ffmpeg是否已安装 - + Returns: bool: 如果安装则返回True,否则返回False """ @@ -58,88 +60,36 @@ def check_ffmpeg_installation() -> bool: def get_hardware_acceleration_option() -> Optional[str]: """ 根据系统环境选择合适的硬件加速选项 - + Returns: Optional[str]: 硬件加速参数,如果不支持则返回None """ - try: - # 检测操作系统 - is_windows = os.name == 'nt' - - # 检查NVIDIA GPU支持 - nvidia_check = subprocess.run( - ['ffmpeg', '-hide_banner', '-hwaccels'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True - ) - output = nvidia_check.stdout.lower() - - # 首先尝试获取系统信息,Windows系统使用更安全的检测方法 - if is_windows: - try: - # 尝试检测显卡信息 - gpu_info = subprocess.run( - ['wmic', 'path', 'win32_VideoController', 'get', 'name'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False - ) - gpu_info_output = gpu_info.stdout.lower() - - # 检测是否为AMD显卡 - if 'amd' in gpu_info_output or 'radeon' in gpu_info_output: - logger.info("检测到AMD显卡,为避免兼容性问题,将使用软件编码") - return None - - # 检测是否为集成显卡 - if 'intel' in gpu_info_output and ('hd graphics' in gpu_info_output or 'uhd graphics' in gpu_info_output): - # 在Windows上,Intel集成显卡可能不稳定,建议使用软件编码 - logger.info("检测到Intel集成显卡,为避免兼容性问题,将使用软件编码") - return None - except Exception as e: - logger.warning(f"获取显卡信息失败: {str(e)},将谨慎处理硬件加速") - - # 根据ffmpeg支持的硬件加速器决定使用哪种 - if 'cuda' in output and not is_windows: - # 在非Windows系统上使用CUDA - return 'cuda' - elif 'nvenc' in output and not is_windows: - # 在非Windows系统上使用NVENC - return 'nvenc' - elif 'qsv' in output and not (is_windows and ('amd' in gpu_info_output if 'gpu_info_output' in locals() else False)): - # 只有在非AMD系统上使用QSV - return 'qsv' - elif 'videotoolbox' in output: # macOS - return 'videotoolbox' - elif 'vaapi' in output and not is_windows: # Linux VA-API - return 'vaapi' - else: - logger.info("没有找到支持的硬件加速器或系统不兼容,将使用软件编码") - return None - except Exception as e: - logger.warning(f"检测硬件加速器时出错: {str(e)},将使用软件编码") - return None + # 使用集中式硬件加速检测 + return ffmpeg_utils.get_ffmpeg_hwaccel_type() def check_video_has_audio(video_path: str) -> bool: """ 检查视频是否包含音频流 - + Args: video_path: 视频文件路径 - + Returns: bool: 如果视频包含音频流则返回True,否则返回False """ if not os.path.exists(video_path): logger.warning(f"视频文件不存在: {video_path}") return False - + probe_cmd = [ - 'ffprobe', '-v', 'error', - '-select_streams', 'a:0', - '-show_entries', 'stream=codec_type', - '-of', 'csv=p=0', + 'ffprobe', '-v', 'error', + '-select_streams', 'a:0', + '-show_entries', 'stream=codec_type', + '-of', 'csv=p=0', video_path ] - + try: result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False) return result.stdout.strip() == 'audio' @@ -151,11 +101,11 @@ def check_video_has_audio(video_path: str) -> bool: def create_ffmpeg_concat_file(video_paths: List[str], concat_file_path: str) -> str: """ 创建ffmpeg合并所需的concat文件 - + Args: video_paths: 需要合并的视频文件路径列表 concat_file_path: concat文件的输出路径 - + Returns: str: concat文件的路径 """ @@ -169,10 +119,10 @@ def create_ffmpeg_concat_file(video_paths: List[str], concat_file_path: str) -> else: # Unix/Mac系统 # 转义特殊字符 abs_path = abs_path.replace('\\', '\\\\').replace(':', '\\:') - + # 处理路径中的单引号 (如果有) abs_path = abs_path.replace("'", "\\'") - + f.write(f"file '{abs_path}'\n") return concat_file_path @@ -187,7 +137,7 @@ def process_single_video( ) -> str: """ 处理单个视频:调整分辨率、帧率等 - + Args: input_path: 输入视频路径 output_path: 输出视频路径 @@ -195,7 +145,7 @@ def process_single_video( target_height: 目标高度 keep_audio: 是否保留音频 hwaccel: 硬件加速选项 - + Returns: str: 处理后的视频路径 """ @@ -212,14 +162,14 @@ def process_single_video( try: # 对视频进行快速探测,检测其基本信息 probe_cmd = [ - 'ffprobe', '-v', 'error', - '-select_streams', 'v:0', - '-show_entries', 'stream=codec_name,width,height', - '-of', 'csv=p=0', + 'ffprobe', '-v', 'error', + '-select_streams', 'v:0', + '-show_entries', 'stream=codec_name,width,height', + '-of', 'csv=p=0', input_path ] result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False) - + # 如果探测成功,使用硬件加速;否则降级到软件编码 if result.returncode != 0: logger.warning(f"视频探测失败,为安全起见,禁用硬件加速: {result.stderr}") @@ -231,15 +181,9 @@ def process_single_video( # 添加硬件加速参数(根据前面的安全检查可能已经被禁用) if hwaccel: try: - if hwaccel == 'cuda' or hwaccel == 'nvenc': - command.extend(['-hwaccel', 'cuda']) - elif hwaccel == 'qsv': - command.extend(['-hwaccel', 'qsv']) - elif hwaccel == 'videotoolbox': - command.extend(['-hwaccel', 'videotoolbox']) - elif hwaccel == 'vaapi': - command.extend(['-hwaccel', 'vaapi', '-vaapi_device', '/dev/dri/renderD128']) - logger.info(f"应用硬件加速: {hwaccel}") + # 使用集中式硬件加速参数 + hwaccel_args = ffmpeg_utils.get_ffmpeg_hwaccel_args() + command.extend(hwaccel_args) except Exception as e: logger.warning(f"应用硬件加速参数时出错: {str(e)},将使用软件编码") # 重置命令,移除可能添加了一半的硬件加速参数 @@ -270,7 +214,7 @@ def process_single_video( # 选择编码器 - 考虑到Windows和特定硬件的兼容性 use_software_encoder = True - + if hwaccel: if hwaccel == 'cuda' or hwaccel == 'nvenc': try: @@ -289,7 +233,7 @@ def process_single_video( elif hwaccel == 'vaapi' and not is_windows: # Linux VA-API command.extend(['-c:v', 'h264_vaapi', '-profile', '100']) use_software_encoder = False - + # 如果前面的条件未能应用硬件编码器,使用软件编码 if use_software_encoder: logger.info("使用软件编码器(libx264)") @@ -315,14 +259,14 @@ def process_single_video( except subprocess.CalledProcessError as e: error_msg = e.stderr.decode() if e.stderr else str(e) logger.error(f"处理视频失败: {error_msg}") - + # 如果使用硬件加速失败,尝试使用软件编码 if hwaccel: logger.info("尝试使用软件编码作为备选方案") try: # 构建新的命令,使用软件编码 fallback_cmd = ['ffmpeg', '-y', '-i', input_path] - + # 保持原有的音频设置 if not keep_audio: fallback_cmd.extend(['-an']) @@ -332,7 +276,7 @@ def process_single_video( fallback_cmd.extend(['-c:a', 'aac', '-b:a', '128k']) else: fallback_cmd.extend(['-an']) - + # 保持原有的视频过滤器 fallback_cmd.extend([ '-vf', f"{scale_filter},{pad_filter}", @@ -346,7 +290,7 @@ def process_single_video( '-pix_fmt', 'yuv420p', output_path ]) - + logger.info(f"执行备选FFmpeg命令: {' '.join(fallback_cmd)}") subprocess.run(fallback_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) logger.info(f"使用软件编码成功处理视频: {output_path}") @@ -355,7 +299,7 @@ def process_single_video( fallback_error_msg = fallback_error.stderr.decode() if fallback_error.stderr else str(fallback_error) logger.error(f"备选软件编码也失败: {fallback_error_msg}") raise RuntimeError(f"无法处理视频 {input_path}: 硬件加速和软件编码都失败") - + # 如果不是硬件加速导致的问题,或者备选方案也失败了,抛出原始错误 raise RuntimeError(f"处理视频失败: {error_msg}") @@ -409,7 +353,7 @@ def combine_clip_videos( # 重组视频路径和原声设置为一个字典列表结构 video_segments = [] - + # 检查视频路径和原声设置列表长度是否匹配 if len(video_paths) != len(video_ost_list): logger.warning(f"视频路径列表({len(video_paths)})和原声设置列表({len(video_ost_list)})长度不匹配") @@ -417,16 +361,16 @@ def combine_clip_videos( min_length = min(len(video_paths), len(video_ost_list)) video_paths = video_paths[:min_length] video_ost_list = video_ost_list[:min_length] - + # 创建视频处理配置字典列表 for i, (video_path, video_ost) in enumerate(zip(video_paths, video_ost_list)): if not os.path.exists(video_path): logger.warning(f"视频不存在,跳过: {video_path}") continue - + # 检查是否有音频流 has_audio = check_video_has_audio(video_path) - + # 构建视频片段配置 segment = { "index": i, @@ -435,11 +379,11 @@ def combine_clip_videos( "has_audio": has_audio, "keep_audio": video_ost > 0 and has_audio # 只有当ost>0且实际有音频时才保留 } - + # 记录日志 if video_ost > 0 and not has_audio: logger.warning(f"视频 {video_path} 设置为保留原声(ost={video_ost}),但该视频没有音频流") - + video_segments.append(segment) # 处理每个视频片段 @@ -495,20 +439,20 @@ def combine_clip_videos( if not processed_videos: raise ValueError("没有有效的视频片段可以合并") - + # 按原始索引排序处理后的视频 processed_videos.sort(key=lambda x: x["index"]) - + # 第二阶段:分步骤合并视频 - 避免复杂的filter_complex滤镜 try: # 1. 首先,将所有没有音频的视频或音频被禁用的视频合并到一个临时文件中 video_paths_only = [video["path"] for video in processed_videos] video_concat_path = os.path.join(temp_dir, "video_concat.mp4") - + # 创建concat文件,用于合并视频流 concat_file = os.path.join(temp_dir, "concat_list.txt") create_ffmpeg_concat_file(video_paths_only, concat_file) - + # 合并所有视频流,但不包含音频 concat_cmd = [ 'ffmpeg', '-y', @@ -522,19 +466,19 @@ def combine_clip_videos( '-threads', str(threads), video_concat_path ] - + subprocess.run(concat_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) logger.info("视频流合并完成") - + # 2. 提取并合并有音频的片段 audio_segments = [video for video in processed_videos if video["keep_audio"]] - + if not audio_segments: # 如果没有音频片段,直接使用无音频的合并视频作为最终结果 shutil.copy(video_concat_path, output_video_path) logger.info("无音频视频合并完成") return output_video_path - + # 创建音频中间文件 audio_files = [] for i, segment in enumerate(audio_segments): @@ -554,11 +498,11 @@ def combine_clip_videos( "path": audio_file }) logger.info(f"提取音频 {i+1}/{len(audio_segments)} 完成") - + # 3. 计算每个音频片段的时间位置 audio_timings = [] current_time = 0.0 - + # 获取每个视频片段的时长 for i, video in enumerate(processed_videos): duration_cmd = [ @@ -569,7 +513,7 @@ def combine_clip_videos( ] result = subprocess.run(duration_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) duration = float(result.stdout.strip()) - + # 如果当前片段需要保留音频,记录时间位置 if video["keep_audio"]: for audio in audio_files: @@ -580,9 +524,9 @@ def combine_clip_videos( "index": video["index"] }) break - + current_time += duration - + # 4. 创建静音音频轨道作为基础 silence_audio = os.path.join(temp_dir, "silence.aac") create_silence_cmd = [ @@ -595,28 +539,28 @@ def combine_clip_videos( silence_audio ] subprocess.run(create_silence_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - + # 5. 创建复杂滤镜命令以混合音频 filter_script = os.path.join(temp_dir, "filter_script.txt") with open(filter_script, 'w') as f: f.write(f"[0:a]volume=0.0[silence];\n") # 首先静音背景轨道 - + # 添加每个音频文件 for i, timing in enumerate(audio_timings): f.write(f"[{i+1}:a]adelay={int(timing['start']*1000)}|{int(timing['start']*1000)}[a{i}];\n") - + # 混合所有音频 mix_str = "[silence]" for i in range(len(audio_timings)): mix_str += f"[a{i}]" mix_str += f"amix=inputs={len(audio_timings)+1}:duration=longest[aout]" f.write(mix_str) - + # 6. 构建音频合并命令 audio_inputs = ['-i', silence_audio] for timing in audio_timings: audio_inputs.extend(['-i', timing["file"]]) - + mixed_audio = os.path.join(temp_dir, "mixed_audio.aac") audio_mix_cmd = [ 'ffmpeg', '-y' @@ -627,10 +571,10 @@ def combine_clip_videos( '-b:a', '128k', mixed_audio ] - + subprocess.run(audio_mix_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) logger.info("音频混合完成") - + # 7. 将合并的视频和混合的音频组合在一起 final_cmd = [ 'ffmpeg', '-y', @@ -643,22 +587,22 @@ def combine_clip_videos( '-shortest', output_video_path ] - + subprocess.run(final_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) logger.info("视频最终合并完成") - + return output_video_path - + except subprocess.CalledProcessError as e: logger.error(f"合并视频过程中出错: {e.stderr.decode() if e.stderr else str(e)}") - + # 尝试备用合并方法 - 最简单的无音频合并 logger.info("尝试备用合并方法 - 无音频合并") try: concat_file = os.path.join(temp_dir, "concat_list.txt") video_paths_only = [video["path"] for video in processed_videos] create_ffmpeg_concat_file(video_paths_only, concat_file) - + backup_cmd = [ 'ffmpeg', '-y', '-f', 'concat', @@ -668,14 +612,14 @@ def combine_clip_videos( '-an', # 无音频 output_video_path ] - + subprocess.run(backup_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) logger.warning("使用备用方法(无音频)成功合并视频") return output_video_path except Exception as backup_error: logger.error(f"备用合并方法也失败: {str(backup_error)}") raise RuntimeError(f"无法合并视频: {str(backup_error)}") - + except Exception as e: logger.error(f"合并视频时出错: {str(e)}") raise diff --git a/app/utils/ffmpeg_utils.py b/app/utils/ffmpeg_utils.py new file mode 100644 index 0000000..59c7321 --- /dev/null +++ b/app/utils/ffmpeg_utils.py @@ -0,0 +1,419 @@ +""" +FFmpeg 工具模块 - 提供 FFmpeg 相关的工具函数,特别是硬件加速检测 +""" +import os +import platform +import subprocess +from typing import Dict, List, Optional, Tuple, Union +from loguru import logger + +# 全局变量,存储检测到的硬件加速信息 +_FFMPEG_HW_ACCEL_INFO = { + "available": False, + "type": None, + "encoder": None, + "hwaccel_args": [], + "message": "", + "is_dedicated_gpu": False +} + + +def check_ffmpeg_installation() -> bool: + """ + 检查ffmpeg是否已安装 + + Returns: + bool: 如果安装则返回True,否则返回False + """ + try: + subprocess.run(['ffmpeg', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) + return True + except (subprocess.SubprocessError, FileNotFoundError): + logger.error("ffmpeg未安装或不在系统PATH中,请安装ffmpeg") + return False + + +def detect_hardware_acceleration() -> Dict[str, Union[bool, str, List[str], None]]: + """ + 检测系统可用的硬件加速器,并存储结果到全局变量 + + Returns: + Dict: 包含硬件加速信息的字典 + """ + global _FFMPEG_HW_ACCEL_INFO + + # 如果已经检测过,直接返回结果 + if _FFMPEG_HW_ACCEL_INFO["type"] is not None: + return _FFMPEG_HW_ACCEL_INFO + + # 检查ffmpeg是否已安装 + if not check_ffmpeg_installation(): + _FFMPEG_HW_ACCEL_INFO["message"] = "FFmpeg未安装或不在系统PATH中" + return _FFMPEG_HW_ACCEL_INFO + + # 检测操作系统 + system = platform.system().lower() + logger.debug(f"检测硬件加速 - 操作系统: {system}") + + # 获取FFmpeg支持的硬件加速器列表 + try: + hwaccels_cmd = subprocess.run( + ['ffmpeg', '-hide_banner', '-hwaccels'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + supported_hwaccels = hwaccels_cmd.stdout.lower() + except Exception as e: + logger.error(f"获取FFmpeg硬件加速器列表失败: {str(e)}") + supported_hwaccels = "" + + # 根据操作系统检测不同的硬件加速器 + if system == 'darwin': # macOS + _detect_macos_acceleration(supported_hwaccels) + elif system == 'windows': # Windows + _detect_windows_acceleration(supported_hwaccels) + elif system == 'linux': # Linux + _detect_linux_acceleration(supported_hwaccels) + else: + logger.warning(f"不支持的操作系统: {system}") + _FFMPEG_HW_ACCEL_INFO["message"] = f"不支持的操作系统: {system}" + + # 记录检测结果已经在启动时输出,这里不再重复输出 + + return _FFMPEG_HW_ACCEL_INFO + + +def _detect_macos_acceleration(supported_hwaccels: str) -> None: + """ + 检测macOS系统的硬件加速 + + Args: + supported_hwaccels: FFmpeg支持的硬件加速器列表 + """ + global _FFMPEG_HW_ACCEL_INFO + + if 'videotoolbox' in supported_hwaccels: + # 测试videotoolbox + try: + test_cmd = subprocess.run( + ["ffmpeg", "-hwaccel", "videotoolbox", "-i", "/dev/null", "-f", "null", "-"], + stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False + ) + if test_cmd.returncode == 0: + _FFMPEG_HW_ACCEL_INFO["available"] = True + _FFMPEG_HW_ACCEL_INFO["type"] = "videotoolbox" + _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_videotoolbox" + _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "videotoolbox"] + # macOS的Metal GPU加速通常是集成GPU + _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = False + return + except Exception as e: + logger.debug(f"测试videotoolbox失败: {str(e)}") + + _FFMPEG_HW_ACCEL_INFO["message"] = "macOS系统未检测到可用的videotoolbox硬件加速" + + +def _detect_windows_acceleration(supported_hwaccels: str) -> None: + """ + 检测Windows系统的硬件加速 + + Args: + supported_hwaccels: FFmpeg支持的硬件加速器列表 + """ + global _FFMPEG_HW_ACCEL_INFO + + # 在Windows上,首先检查显卡信息 + gpu_info = _get_windows_gpu_info() + + # 检查是否为AMD显卡 + if 'amd' in gpu_info.lower() or 'radeon' in gpu_info.lower(): + logger.info("检测到AMD显卡,为避免兼容性问题,将使用软件编码") + _FFMPEG_HW_ACCEL_INFO["message"] = "检测到AMD显卡,为避免兼容性问题,将使用软件编码" + return + + # 检查是否为Intel集成显卡 + is_intel_integrated = False + if 'intel' in gpu_info.lower() and ('hd graphics' in gpu_info.lower() or 'uhd graphics' in gpu_info.lower()): + logger.info("检测到Intel集成显卡") + is_intel_integrated = True + + # 检测NVIDIA CUDA支持 + if 'cuda' in supported_hwaccels and 'nvidia' in gpu_info.lower(): + try: + test_cmd = subprocess.run( + ["ffmpeg", "-hwaccel", "cuda", "-i", "/dev/null", "-f", "null", "-"], + stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False + ) + if test_cmd.returncode == 0: + _FFMPEG_HW_ACCEL_INFO["available"] = True + _FFMPEG_HW_ACCEL_INFO["type"] = "cuda" + _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_nvenc" + _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "cuda"] + _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = True + return + except Exception as e: + logger.debug(f"测试CUDA失败: {str(e)}") + + # 检测Intel QSV支持(如果是Intel显卡) + if 'qsv' in supported_hwaccels and 'intel' in gpu_info.lower(): + try: + test_cmd = subprocess.run( + ["ffmpeg", "-hwaccel", "qsv", "-i", "/dev/null", "-f", "null", "-"], + stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False + ) + if test_cmd.returncode == 0: + _FFMPEG_HW_ACCEL_INFO["available"] = True + _FFMPEG_HW_ACCEL_INFO["type"] = "qsv" + _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_qsv" + _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "qsv"] + _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = not is_intel_integrated + return + except Exception as e: + logger.debug(f"测试QSV失败: {str(e)}") + + # 检测D3D11VA支持 + if 'd3d11va' in supported_hwaccels: + try: + test_cmd = subprocess.run( + ["ffmpeg", "-hwaccel", "d3d11va", "-i", "/dev/null", "-f", "null", "-"], + stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False + ) + if test_cmd.returncode == 0: + _FFMPEG_HW_ACCEL_INFO["available"] = True + _FFMPEG_HW_ACCEL_INFO["type"] = "d3d11va" + _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264" # D3D11VA只用于解码,编码仍使用软件编码器 + _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "d3d11va"] + _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = not is_intel_integrated + return + except Exception as e: + logger.debug(f"测试D3D11VA失败: {str(e)}") + + # 检测DXVA2支持 + if 'dxva2' in supported_hwaccels: + try: + test_cmd = subprocess.run( + ["ffmpeg", "-hwaccel", "dxva2", "-i", "/dev/null", "-f", "null", "-"], + stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False + ) + if test_cmd.returncode == 0: + _FFMPEG_HW_ACCEL_INFO["available"] = True + _FFMPEG_HW_ACCEL_INFO["type"] = "dxva2" + _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264" # DXVA2只用于解码,编码仍使用软件编码器 + _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "dxva2"] + _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = not is_intel_integrated + return + except Exception as e: + logger.debug(f"测试DXVA2失败: {str(e)}") + + _FFMPEG_HW_ACCEL_INFO["message"] = f"Windows系统未检测到可用的硬件加速,显卡信息: {gpu_info}" + + +def _detect_linux_acceleration(supported_hwaccels: str) -> None: + """ + 检测Linux系统的硬件加速 + + Args: + supported_hwaccels: FFmpeg支持的硬件加速器列表 + """ + global _FFMPEG_HW_ACCEL_INFO + + # 获取Linux显卡信息 + gpu_info = _get_linux_gpu_info() + is_nvidia = 'nvidia' in gpu_info.lower() + is_intel = 'intel' in gpu_info.lower() + is_amd = 'amd' in gpu_info.lower() or 'radeon' in gpu_info.lower() + + # 检测NVIDIA CUDA支持 + if 'cuda' in supported_hwaccels and is_nvidia: + try: + test_cmd = subprocess.run( + ["ffmpeg", "-hwaccel", "cuda", "-i", "/dev/null", "-f", "null", "-"], + stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False + ) + if test_cmd.returncode == 0: + _FFMPEG_HW_ACCEL_INFO["available"] = True + _FFMPEG_HW_ACCEL_INFO["type"] = "cuda" + _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_nvenc" + _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "cuda"] + _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = True + return + except Exception as e: + logger.debug(f"测试CUDA失败: {str(e)}") + + # 检测VAAPI支持 + if 'vaapi' in supported_hwaccels: + # 检查是否存在渲染设备 + render_devices = ['/dev/dri/renderD128', '/dev/dri/renderD129'] + render_device = None + for device in render_devices: + if os.path.exists(device): + render_device = device + break + + if render_device: + try: + test_cmd = subprocess.run( + ["ffmpeg", "-hwaccel", "vaapi", "-vaapi_device", render_device, + "-i", "/dev/null", "-f", "null", "-"], + stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False + ) + if test_cmd.returncode == 0: + _FFMPEG_HW_ACCEL_INFO["available"] = True + _FFMPEG_HW_ACCEL_INFO["type"] = "vaapi" + _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_vaapi" + _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "vaapi", "-vaapi_device", render_device] + # 根据显卡类型判断是否为独立显卡 + _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = is_nvidia or (is_amd and not is_intel) + return + except Exception as e: + logger.debug(f"测试VAAPI失败: {str(e)}") + + # 检测Intel QSV支持 + if 'qsv' in supported_hwaccels and is_intel: + try: + test_cmd = subprocess.run( + ["ffmpeg", "-hwaccel", "qsv", "-i", "/dev/null", "-f", "null", "-"], + stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, check=False + ) + if test_cmd.returncode == 0: + _FFMPEG_HW_ACCEL_INFO["available"] = True + _FFMPEG_HW_ACCEL_INFO["type"] = "qsv" + _FFMPEG_HW_ACCEL_INFO["encoder"] = "h264_qsv" + _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] = ["-hwaccel", "qsv"] + _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] = False # Intel QSV通常是集成GPU + return + except Exception as e: + logger.debug(f"测试QSV失败: {str(e)}") + + _FFMPEG_HW_ACCEL_INFO["message"] = f"Linux系统未检测到可用的硬件加速,显卡信息: {gpu_info}" + + +def _get_windows_gpu_info() -> str: + """ + 获取Windows系统的显卡信息 + + Returns: + str: 显卡信息字符串 + """ + try: + gpu_info = subprocess.run( + ['wmic', 'path', 'win32_VideoController', 'get', 'name'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False + ) + return gpu_info.stdout + except Exception as e: + logger.warning(f"获取Windows显卡信息失败: {str(e)}") + return "Unknown GPU" + + +def _get_linux_gpu_info() -> str: + """ + 获取Linux系统的显卡信息 + + Returns: + str: 显卡信息字符串 + """ + try: + # 尝试使用lspci命令 + gpu_info = subprocess.run( + ['lspci', '-v', '-nn', '|', 'grep', '-i', 'vga\\|display'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True, check=False + ) + if gpu_info.stdout: + return gpu_info.stdout + + # 如果lspci命令失败,尝试使用glxinfo + gpu_info = subprocess.run( + ['glxinfo', '|', 'grep', '-i', 'vendor\\|renderer'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True, check=False + ) + if gpu_info.stdout: + return gpu_info.stdout + + return "Unknown GPU" + except Exception as e: + logger.warning(f"获取Linux显卡信息失败: {str(e)}") + return "Unknown GPU" + + +def get_ffmpeg_hwaccel_args() -> List[str]: + """ + 获取FFmpeg硬件加速参数 + + Returns: + List[str]: FFmpeg硬件加速参数列表 + """ + # 如果还没有检测过,先进行检测 + if _FFMPEG_HW_ACCEL_INFO["type"] is None: + detect_hardware_acceleration() + + return _FFMPEG_HW_ACCEL_INFO["hwaccel_args"] + + +def get_ffmpeg_hwaccel_type() -> Optional[str]: + """ + 获取FFmpeg硬件加速类型 + + Returns: + Optional[str]: 硬件加速类型,如果不支持则返回None + """ + # 如果还没有检测过,先进行检测 + if _FFMPEG_HW_ACCEL_INFO["type"] is None: + detect_hardware_acceleration() + + return _FFMPEG_HW_ACCEL_INFO["type"] if _FFMPEG_HW_ACCEL_INFO["available"] else None + + +def get_ffmpeg_hwaccel_encoder() -> Optional[str]: + """ + 获取FFmpeg硬件加速编码器 + + Returns: + Optional[str]: 硬件加速编码器,如果不支持则返回None + """ + # 如果还没有检测过,先进行检测 + if _FFMPEG_HW_ACCEL_INFO["type"] is None: + detect_hardware_acceleration() + + return _FFMPEG_HW_ACCEL_INFO["encoder"] if _FFMPEG_HW_ACCEL_INFO["available"] else None + + +def get_ffmpeg_hwaccel_info() -> Dict[str, Union[bool, str, List[str], None]]: + """ + 获取FFmpeg硬件加速信息 + + Returns: + Dict: 包含硬件加速信息的字典 + """ + # 如果还没有检测过,先进行检测 + if _FFMPEG_HW_ACCEL_INFO["type"] is None: + detect_hardware_acceleration() + + return _FFMPEG_HW_ACCEL_INFO + + +def is_ffmpeg_hwaccel_available() -> bool: + """ + 检查是否有可用的FFmpeg硬件加速 + + Returns: + bool: 如果有可用的硬件加速则返回True,否则返回False + """ + # 如果还没有检测过,先进行检测 + if _FFMPEG_HW_ACCEL_INFO["type"] is None: + detect_hardware_acceleration() + + return _FFMPEG_HW_ACCEL_INFO["available"] + + +def is_dedicated_gpu() -> bool: + """ + 检查是否使用独立显卡进行硬件加速 + + Returns: + bool: 如果使用独立显卡则返回True,否则返回False + """ + # 如果还没有检测过,先进行检测 + if _FFMPEG_HW_ACCEL_INFO["type"] is None: + detect_hardware_acceleration() + + return _FFMPEG_HW_ACCEL_INFO["is_dedicated_gpu"] diff --git a/app/utils/video_processor.py b/app/utils/video_processor.py index 1d3dd9b..9bc3ab8 100644 --- a/app/utils/video_processor.py +++ b/app/utils/video_processor.py @@ -19,6 +19,8 @@ from typing import List, Dict from loguru import logger from tqdm import tqdm +from app.utils import ffmpeg_utils + class VideoProcessor: def __init__(self, video_path: str): @@ -63,7 +65,7 @@ class VideoProcessor: if '=' in line: key, value = line.split('=', 1) info[key] = value - + # 处理帧率(可能是分数形式) if 'r_frame_rate' in info: try: @@ -71,9 +73,9 @@ class VideoProcessor: info['fps'] = str(num / den) except ValueError: info['fps'] = info.get('r_frame_rate', '25') - + return info - + except subprocess.CalledProcessError as e: logger.error(f"获取视频信息失败: {e.stderr}") return { @@ -83,7 +85,7 @@ class VideoProcessor: 'duration': '0' } - def extract_frames_by_interval(self, output_dir: str, interval_seconds: float = 5.0, + def extract_frames_by_interval(self, output_dir: str, interval_seconds: float = 5.0, use_hw_accel: bool = True) -> List[int]: """ 按指定时间间隔提取视频帧 @@ -98,57 +100,51 @@ class VideoProcessor: """ if not os.path.exists(output_dir): os.makedirs(output_dir) - + # 计算起始时间和帧提取点 start_time = 0 end_time = self.duration extraction_times = [] - + current_time = start_time while current_time < end_time: extraction_times.append(current_time) current_time += interval_seconds - + if not extraction_times: logger.warning("未找到需要提取的帧") return [] # 确定硬件加速器选项 hw_accel = [] - if use_hw_accel: - # 尝试检测可用的硬件加速器 - hw_accel_options = self._detect_hw_accelerator() - if hw_accel_options: - hw_accel = hw_accel_options - logger.info(f"使用硬件加速: {' '.join(hw_accel)}") - else: - logger.warning("未检测到可用的硬件加速器,使用软件解码") - + if use_hw_accel and ffmpeg_utils.is_ffmpeg_hwaccel_available(): + hw_accel = ffmpeg_utils.get_ffmpeg_hwaccel_args() + # 提取帧 frame_numbers = [] for i, timestamp in enumerate(tqdm(extraction_times, desc="提取视频帧")): frame_number = int(timestamp * self.fps) frame_numbers.append(frame_number) - + # 格式化时间戳字符串 (HHMMSSmmm) hours = int(timestamp // 3600) minutes = int((timestamp % 3600) // 60) seconds = int(timestamp % 60) milliseconds = int((timestamp % 1) * 1000) time_str = f"{hours:02d}{minutes:02d}{seconds:02d}{milliseconds:03d}" - + output_path = os.path.join(output_dir, f"keyframe_{frame_number:06d}_{time_str}.jpg") - + # 使用ffmpeg提取单帧 cmd = [ "ffmpeg", "-hide_banner", "-loglevel", "error", ] - + # 添加硬件加速参数 cmd.extend(hw_accel) - + cmd.extend([ "-ss", str(timestamp), "-i", self.video_path, @@ -157,12 +153,12 @@ class VideoProcessor: "-y", output_path ]) - + try: subprocess.run(cmd, check=True, capture_output=True) except subprocess.CalledProcessError as e: logger.warning(f"提取帧 {frame_number} 失败: {e.stderr}") - + logger.info(f"成功提取了 {len(frame_numbers)} 个视频帧") return frame_numbers @@ -173,119 +169,9 @@ class VideoProcessor: Returns: List[str]: 硬件加速器ffmpeg命令参数 """ - # 检测操作系统 - import platform - system = platform.system().lower() - - # 测试不同的硬件加速器 - accelerators = [] - - if system == 'darwin': # macOS - # 测试 videotoolbox (Apple 硬件加速) - test_cmd = [ - "ffmpeg", - "-hide_banner", - "-loglevel", "error", - "-hwaccel", "videotoolbox", - "-i", self.video_path, - "-t", "0.1", - "-f", "null", - "-" - ] - try: - subprocess.run(test_cmd, capture_output=True, check=True) - return ["-hwaccel", "videotoolbox"] - except subprocess.CalledProcessError: - pass - - elif system == 'linux': - # 测试 VAAPI - test_cmd = [ - "ffmpeg", - "-hide_banner", - "-loglevel", "error", - "-hwaccel", "vaapi", - "-i", self.video_path, - "-t", "0.1", - "-f", "null", - "-" - ] - try: - subprocess.run(test_cmd, capture_output=True, check=True) - return ["-hwaccel", "vaapi"] - except subprocess.CalledProcessError: - pass - - # 尝试 CUDA - test_cmd = [ - "ffmpeg", - "-hide_banner", - "-loglevel", "error", - "-hwaccel", "cuda", - "-i", self.video_path, - "-t", "0.1", - "-f", "null", - "-" - ] - try: - subprocess.run(test_cmd, capture_output=True, check=True) - return ["-hwaccel", "cuda"] - except subprocess.CalledProcessError: - pass - - elif system == 'windows': - # 测试 CUDA - test_cmd = [ - "ffmpeg", - "-hide_banner", - "-loglevel", "error", - "-hwaccel", "cuda", - "-i", self.video_path, - "-t", "0.1", - "-f", "null", - "-" - ] - try: - subprocess.run(test_cmd, capture_output=True, check=True) - return ["-hwaccel", "cuda"] - except subprocess.CalledProcessError: - pass - - # 测试 D3D11VA - test_cmd = [ - "ffmpeg", - "-hide_banner", - "-loglevel", "error", - "-hwaccel", "d3d11va", - "-i", self.video_path, - "-t", "0.1", - "-f", "null", - "-" - ] - try: - subprocess.run(test_cmd, capture_output=True, check=True) - return ["-hwaccel", "d3d11va"] - except subprocess.CalledProcessError: - pass - - # 测试 DXVA2 - test_cmd = [ - "ffmpeg", - "-hide_banner", - "-loglevel", "error", - "-hwaccel", "dxva2", - "-i", self.video_path, - "-t", "0.1", - "-f", "null", - "-" - ] - try: - subprocess.run(test_cmd, capture_output=True, check=True) - return ["-hwaccel", "dxva2"] - except subprocess.CalledProcessError: - pass - - # 如果没有找到可用的硬件加速器 + # 使用集中式硬件加速检测 + if ffmpeg_utils.is_ffmpeg_hwaccel_available(): + return ffmpeg_utils.get_ffmpeg_hwaccel_args() return [] def process_video_pipeline(self, @@ -294,7 +180,7 @@ class VideoProcessor: use_hw_accel: bool = True) -> None: """ 执行简化的视频处理流程,直接从原视频按固定时间间隔提取帧 - + Args: output_dir: 输出目录 interval_seconds: 帧提取间隔(秒) @@ -302,7 +188,7 @@ class VideoProcessor: """ # 创建输出目录 os.makedirs(output_dir, exist_ok=True) - + try: # 直接从原视频提取关键帧 logger.info(f"从视频间隔 {interval_seconds} 秒提取关键帧...") @@ -311,7 +197,7 @@ class VideoProcessor: interval_seconds=interval_seconds, use_hw_accel=use_hw_accel ) - + logger.info(f"处理完成!视频帧已保存在: {output_dir}") except Exception as e: @@ -324,16 +210,16 @@ if __name__ == "__main__": import time start_time = time.time() - + # 使用示例 processor = VideoProcessor("./resource/videos/test.mp4") - + # 设置间隔为3秒提取帧 processor.process_video_pipeline( output_dir="output", interval_seconds=3.0, use_hw_accel=True ) - + end_time = time.time() print(f"处理完成!总耗时: {end_time - start_time:.2f} 秒") diff --git a/config.example.toml b/config.example.toml index 35967b3..adafb84 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,5 +1,5 @@ [app] - project_version="0.6.1" + project_version="0.6.2" # 支持视频理解的大模型提供商 # gemini (谷歌, 需要 VPN) # siliconflow (硅基流动) diff --git a/webui.py b/webui.py index 94217fc..4180824 100644 --- a/webui.py +++ b/webui.py @@ -7,6 +7,7 @@ from webui.components import basic_settings, video_settings, audio_settings, sub review_settings, merge_settings, system_settings # from webui.utils import cache, file_utils from app.utils import utils +from app.utils import ffmpeg_utils from app.models.schema import VideoClipParams, VideoAspect @@ -64,7 +65,7 @@ def init_log(): try: for handler_id in logger._core.handlers: logger.remove(handler_id) - + # 重新添加带有高级过滤的处理器 def advanced_filter(record): """更复杂的过滤器,在应用启动后安全使用""" @@ -74,7 +75,7 @@ def init_log(): "CUDA initialization" ] return not any(msg in record["message"] for msg in ignore_messages) - + logger.add( sys.stdout, level=_lvl, @@ -91,7 +92,7 @@ def init_log(): colorize=True ) logger.error(f"设置高级日志过滤器失败: {e}") - + # 将高级过滤器设置放到启动主逻辑后 import threading threading.Timer(5.0, setup_advanced_filters).start() @@ -192,7 +193,14 @@ def main(): """主函数""" init_log() init_global_state() - + + # 检测FFmpeg硬件加速 + hwaccel_info = ffmpeg_utils.detect_hardware_acceleration() + if hwaccel_info["available"]: + logger.info(f"FFmpeg硬件加速检测结果: 可用 | 类型: {hwaccel_info['type']} | 编码器: {hwaccel_info['encoder']} | 独立显卡: {hwaccel_info['is_dedicated_gpu']} | 参数: {hwaccel_info['hwaccel_args']}") + else: + logger.warning(f"FFmpeg硬件加速不可用: {hwaccel_info['message']}, 将使用CPU软件编码") + # 仅初始化基本资源,避免过早地加载依赖PyTorch的资源 # 检查是否能分解utils.init_resources()为基本资源和高级资源(如依赖PyTorch的资源) try: @@ -218,15 +226,15 @@ def main(): audio_settings.render_audio_panel(tr) with panel[2]: subtitle_settings.render_subtitle_panel(tr) - + # 渲染视频审查面板 review_settings.render_review_panel(tr) - + # 放到最后渲染可能使用PyTorch的部分 # 渲染系统设置面板 with panel[2]: system_settings.render_system_panel(tr) - + # 放到最后渲染生成按钮和处理逻辑 render_generate_button()