From 1c8b526c3cc6b0c1acc7ab37f162c6964baca9ca Mon Sep 17 00:00:00 2001 From: linyq Date: Mon, 7 Jul 2025 20:48:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(video=5Fprocessor):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E5=B8=A7=E6=8F=90=E5=8F=96=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BAWindows=E7=B3=BB=E7=BB=9F=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在video_processor.py中,添加了对Windows N卡硬件加速的支持,优化了帧提取过程,改进了提取成功率的统计和错误处理。同时,在generate_script_docu.py中,增强了对硬件加速失败的处理逻辑,提供了详细的错误信息和解决建议,提升了用户体验。 --- app/config/ffmpeg_config.py | 284 +++++++++++++++++++++++++ app/utils/video_processor.py | 248 +++++++++++++++++---- test_video_extraction.py | 189 ++++++++++++++++ webui/components/ffmpeg_diagnostics.py | 280 ++++++++++++++++++++++++ webui/tools/generate_script_docu.py | 46 +++- 5 files changed, 1005 insertions(+), 42 deletions(-) create mode 100644 app/config/ffmpeg_config.py create mode 100644 test_video_extraction.py create mode 100644 webui/components/ffmpeg_diagnostics.py diff --git a/app/config/ffmpeg_config.py b/app/config/ffmpeg_config.py new file mode 100644 index 0000000..05f822d --- /dev/null +++ b/app/config/ffmpeg_config.py @@ -0,0 +1,284 @@ +""" +FFmpeg 配置管理模块 +专门用于管理 FFmpeg 兼容性设置和优化参数 +""" + +import os +import platform +from typing import Dict, List, Optional +from dataclasses import dataclass +from loguru import logger + + +@dataclass +class FFmpegProfile: + """FFmpeg 配置文件""" + name: str + description: str + hwaccel_enabled: bool + hwaccel_type: Optional[str] + encoder: str + quality_preset: str + pixel_format: str + additional_args: List[str] + compatibility_level: int # 1-5, 5为最高兼容性 + + +class FFmpegConfigManager: + """FFmpeg 配置管理器""" + + # 预定义的配置文件 + PROFILES = { + # 高性能配置(适用于现代硬件) + "high_performance": FFmpegProfile( + name="high_performance", + description="高性能配置(NVIDIA/AMD 独立显卡)", + hwaccel_enabled=True, + hwaccel_type="auto", + encoder="auto", + quality_preset="fast", + pixel_format="yuv420p", + additional_args=["-preset", "fast"], + compatibility_level=2 + ), + + # 兼容性配置(适用于有问题的硬件) + "compatibility": FFmpegProfile( + name="compatibility", + description="兼容性配置(解决滤镜链问题)", + hwaccel_enabled=False, + hwaccel_type=None, + encoder="libx264", + quality_preset="medium", + pixel_format="yuv420p", + additional_args=["-preset", "medium", "-crf", "23"], + compatibility_level=5 + ), + + # Windows N 卡优化配置 + "windows_nvidia": FFmpegProfile( + name="windows_nvidia", + description="Windows NVIDIA 显卡优化配置", + hwaccel_enabled=True, + hwaccel_type="nvenc_pure", # 纯编码器,避免解码问题 + encoder="h264_nvenc", + quality_preset="medium", + pixel_format="yuv420p", + additional_args=["-preset", "medium", "-cq", "23"], + compatibility_level=3 + ), + + # macOS 优化配置 + "macos_videotoolbox": FFmpegProfile( + name="macos_videotoolbox", + description="macOS VideoToolbox 优化配置", + hwaccel_enabled=True, + hwaccel_type="videotoolbox", + encoder="h264_videotoolbox", + quality_preset="medium", + pixel_format="yuv420p", + additional_args=["-q:v", "65"], + compatibility_level=3 + ), + + # 通用软件配置 + "universal_software": FFmpegProfile( + name="universal_software", + description="通用软件编码配置(最高兼容性)", + hwaccel_enabled=False, + hwaccel_type=None, + encoder="libx264", + quality_preset="medium", + pixel_format="yuv420p", + additional_args=["-preset", "medium", "-crf", "23"], + compatibility_level=5 + ) + } + + @classmethod + def get_recommended_profile(cls) -> str: + """ + 根据系统环境推荐最佳配置文件 + + Returns: + str: 推荐的配置文件名称 + """ + system = platform.system().lower() + + # 检测硬件加速可用性 + try: + from app.utils import ffmpeg_utils + hwaccel_info = ffmpeg_utils.get_ffmpeg_hwaccel_info() + hwaccel_available = hwaccel_info.get("available", False) + hwaccel_type = hwaccel_info.get("type", "software") + gpu_vendor = hwaccel_info.get("gpu_vendor", "unknown") + except Exception as e: + logger.warning(f"无法检测硬件加速信息: {e}") + hwaccel_available = False + hwaccel_type = "software" + gpu_vendor = "unknown" + + # 根据平台和硬件推荐配置 + if system == "windows": + if hwaccel_available and gpu_vendor == "nvidia": + return "windows_nvidia" + elif hwaccel_available: + return "high_performance" + else: + return "compatibility" + elif system == "darwin": + if hwaccel_available and hwaccel_type == "videotoolbox": + return "macos_videotoolbox" + else: + return "universal_software" + elif system == "linux": + if hwaccel_available: + return "high_performance" + else: + return "universal_software" + else: + return "universal_software" + + @classmethod + def get_profile(cls, profile_name: str) -> FFmpegProfile: + """ + 获取指定的配置文件 + + Args: + profile_name: 配置文件名称 + + Returns: + FFmpegProfile: 配置文件对象 + """ + if profile_name not in cls.PROFILES: + logger.warning(f"未知的配置文件: {profile_name},使用默认配置") + profile_name = "universal_software" + + return cls.PROFILES[profile_name] + + @classmethod + def get_extraction_command(cls, + input_path: str, + output_path: str, + timestamp: float, + profile_name: Optional[str] = None) -> List[str]: + """ + 根据配置文件生成关键帧提取命令 + + Args: + input_path: 输入视频路径 + output_path: 输出图片路径 + timestamp: 时间戳 + profile_name: 配置文件名称,None 表示自动选择 + + Returns: + List[str]: FFmpeg 命令列表 + """ + if profile_name is None: + profile_name = cls.get_recommended_profile() + + profile = cls.get_profile(profile_name) + + # 构建基础命令 + cmd = [ + "ffmpeg", + "-hide_banner", + "-loglevel", "error", + ] + + # 添加硬件加速参数 + if profile.hwaccel_enabled and profile.hwaccel_type: + if profile.hwaccel_type == "auto": + # 自动检测硬件加速 + try: + from app.utils import ffmpeg_utils + hw_args = ffmpeg_utils.get_ffmpeg_hwaccel_args() + cmd.extend(hw_args) + except Exception: + pass + elif profile.hwaccel_type == "nvenc_pure": + # 纯 NVENC 编码器,不使用硬件解码 + pass + else: + # 指定的硬件加速类型 + cmd.extend(["-hwaccel", profile.hwaccel_type]) + + # 添加输入参数 + cmd.extend([ + "-ss", str(timestamp), + "-i", input_path, + "-vframes", "1", + ]) + + # 添加质量和格式参数 + if profile.encoder == "libx264": + cmd.extend(["-q:v", "2"]) + elif profile.encoder == "h264_nvenc": + cmd.extend(["-cq", "23"]) + elif profile.encoder == "h264_videotoolbox": + cmd.extend(["-q:v", "65"]) + else: + cmd.extend(["-q:v", "2"]) + + # 添加像素格式 + cmd.extend(["-pix_fmt", profile.pixel_format]) + + # 添加额外参数 + cmd.extend(profile.additional_args) + + # 添加输出参数 + cmd.extend(["-y", output_path]) + + return cmd + + @classmethod + def list_profiles(cls) -> Dict[str, str]: + """ + 列出所有可用的配置文件 + + Returns: + Dict[str, str]: 配置文件名称到描述的映射 + """ + return {name: profile.description for name, profile in cls.PROFILES.items()} + + @classmethod + def get_compatibility_report(cls) -> Dict[str, any]: + """ + 生成兼容性报告 + + Returns: + Dict: 兼容性报告 + """ + recommended_profile = cls.get_recommended_profile() + profile = cls.get_profile(recommended_profile) + + try: + from app.utils import ffmpeg_utils + hwaccel_info = ffmpeg_utils.get_ffmpeg_hwaccel_info() + except Exception: + hwaccel_info = {"available": False, "message": "检测失败"} + + return { + "system": platform.system(), + "recommended_profile": recommended_profile, + "profile_description": profile.description, + "compatibility_level": profile.compatibility_level, + "hardware_acceleration": hwaccel_info, + "suggestions": cls._get_suggestions(profile, hwaccel_info) + } + + @classmethod + def _get_suggestions(cls, profile: FFmpegProfile, hwaccel_info: Dict) -> List[str]: + """生成优化建议""" + suggestions = [] + + if not hwaccel_info.get("available", False): + suggestions.append("建议更新显卡驱动以启用硬件加速") + + if profile.compatibility_level >= 4: + suggestions.append("当前使用高兼容性配置,性能可能较低") + + if platform.system().lower() == "windows" and "nvidia" in hwaccel_info.get("gpu_vendor", "").lower(): + suggestions.append("Windows NVIDIA 用户建议使用纯编码器模式避免滤镜链问题") + + return suggestions diff --git a/app/utils/video_processor.py b/app/utils/video_processor.py index 9bc3ab8..920fe53 100644 --- a/app/utils/video_processor.py +++ b/app/utils/video_processor.py @@ -20,6 +20,7 @@ from loguru import logger from tqdm import tqdm from app.utils import ffmpeg_utils +from app.config.ffmpeg_config import FFmpegConfigManager class VideoProcessor: @@ -90,6 +91,8 @@ class VideoProcessor: """ 按指定时间间隔提取视频帧 + 优化了 Windows 系统兼容性,特别是 N 卡硬件加速的滤镜链问题 + Args: output_dir: 输出目录 interval_seconds: 帧提取间隔(秒) @@ -115,52 +118,225 @@ class VideoProcessor: logger.warning("未找到需要提取的帧") return [] - # 确定硬件加速器选项 - hw_accel = [] + # 获取硬件加速信息 + hwaccel_info = ffmpeg_utils.get_ffmpeg_hwaccel_info() + hwaccel_type = hwaccel_info.get("type", "software") + + # 提取帧 - 使用优化的进度条 + frame_numbers = [] + successful_extractions = 0 + failed_extractions = 0 + + logger.info(f"开始提取 {len(extraction_times)} 个关键帧,使用 {hwaccel_type} 加速") + + with tqdm(total=len(extraction_times), desc="提取视频帧", unit="帧") as pbar: + for i, timestamp in enumerate(extraction_times): + 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 命令 - 针对 Windows N 卡优化 + success = self._extract_single_frame_optimized( + timestamp, output_path, use_hw_accel, hwaccel_type + ) + + if success: + successful_extractions += 1 + pbar.set_postfix({ + "成功": successful_extractions, + "失败": failed_extractions, + "当前": f"{timestamp:.1f}s" + }) + else: + failed_extractions += 1 + pbar.set_postfix({ + "成功": successful_extractions, + "失败": failed_extractions, + "当前": f"{timestamp:.1f}s (失败)" + }) + + pbar.update(1) + + # 统计结果 + total_attempts = len(extraction_times) + success_rate = (successful_extractions / total_attempts) * 100 if total_attempts > 0 else 0 + + logger.info(f"关键帧提取完成: 成功 {successful_extractions}/{total_attempts} 帧 ({success_rate:.1f}%)") + + if failed_extractions > 0: + logger.warning(f"有 {failed_extractions} 帧提取失败,可能是硬件加速兼容性问题") + + # 验证实际生成的文件 + actual_files = [f for f in os.listdir(output_dir) if f.endswith('.jpg')] + logger.info(f"实际生成文件数量: {len(actual_files)} 个") + + if len(actual_files) == 0: + logger.error("未生成任何关键帧文件,可能需要禁用硬件加速") + raise Exception("关键帧提取完全失败,请检查视频文件和 FFmpeg 配置") + + return frame_numbers + + def _extract_single_frame_optimized(self, timestamp: float, output_path: str, + use_hw_accel: bool, hwaccel_type: str) -> bool: + """ + 优化的单帧提取方法,解决 Windows N 卡硬件加速兼容性问题 + + Args: + timestamp: 时间戳(秒) + output_path: 输出文件路径 + use_hw_accel: 是否使用硬件加速 + hwaccel_type: 硬件加速类型 + + Returns: + bool: 是否成功提取 + """ + # 策略1: 优先尝试纯编码器方案(避免硬件解码滤镜链问题) + if use_hw_accel and hwaccel_type in ["nvenc", "cuda"]: + # 对于 NVIDIA 显卡,优先使用纯软件解码 + NVENC 编码 + if self._try_extract_with_software_decode(timestamp, output_path): + return True + logger.debug(f"纯软件解码方案失败,尝试其他方案") + + # 策略2: 尝试标准硬件加速 if use_hw_accel and ffmpeg_utils.is_ffmpeg_hwaccel_available(): hw_accel = ffmpeg_utils.get_ffmpeg_hwaccel_args() + if self._try_extract_with_hwaccel(timestamp, output_path, hw_accel): + return True + logger.debug(f"硬件加速方案失败,回退到软件方案") - # 提取帧 - frame_numbers = [] - for i, timestamp in enumerate(tqdm(extraction_times, desc="提取视频帧")): - frame_number = int(timestamp * self.fps) - frame_numbers.append(frame_number) + # 策略3: 软件方案(最后的备用方案) + return self._try_extract_with_software(timestamp, output_path) - # 格式化时间戳字符串 (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}" + def _try_extract_with_software_decode(self, timestamp: float, output_path: str) -> bool: + """ + 使用纯软件解码提取帧(推荐用于 Windows N 卡) - output_path = os.path.join(output_dir, f"keyframe_{frame_number:06d}_{time_str}.jpg") + Args: + timestamp: 时间戳 + output_path: 输出路径 - # 使用ffmpeg提取单帧 - cmd = [ - "ffmpeg", - "-hide_banner", - "-loglevel", "error", - ] + Returns: + bool: 是否成功 + """ + # 使用 Windows NVIDIA 优化配置 + cmd = FFmpegConfigManager.get_extraction_command( + input_path=self.video_path, + output_path=output_path, + timestamp=timestamp, + profile_name="windows_nvidia" + ) - # 添加硬件加速参数 - cmd.extend(hw_accel) + return self._execute_ffmpeg_command(cmd, f"软件解码提取帧 {timestamp:.1f}s") - cmd.extend([ - "-ss", str(timestamp), - "-i", self.video_path, - "-vframes", "1", - "-q:v", "1", # 最高质量 - "-y", - output_path - ]) + def _try_extract_with_hwaccel(self, timestamp: float, output_path: str, hw_accel: List[str]) -> bool: + """ + 使用硬件加速提取帧 - try: - subprocess.run(cmd, check=True, capture_output=True) - except subprocess.CalledProcessError as e: - logger.warning(f"提取帧 {frame_number} 失败: {e.stderr}") + Args: + timestamp: 时间戳 + output_path: 输出路径 + hw_accel: 硬件加速参数 - logger.info(f"成功提取了 {len(frame_numbers)} 个视频帧") - return frame_numbers + Returns: + bool: 是否成功 + """ + cmd = [ + "ffmpeg", + "-hide_banner", + "-loglevel", "error", + ] + + # 添加硬件加速参数 + cmd.extend(hw_accel) + + cmd.extend([ + "-ss", str(timestamp), + "-i", self.video_path, + "-vframes", "1", + "-q:v", "2", + "-pix_fmt", "yuv420p", + "-y", + output_path + ]) + + return self._execute_ffmpeg_command(cmd, f"硬件加速提取帧 {timestamp:.1f}s") + + def _try_extract_with_software(self, timestamp: float, output_path: str) -> bool: + """ + 使用纯软件方案提取帧(最后的备用方案) + + Args: + timestamp: 时间戳 + output_path: 输出路径 + + Returns: + bool: 是否成功 + """ + # 使用最高兼容性配置 + cmd = FFmpegConfigManager.get_extraction_command( + input_path=self.video_path, + output_path=output_path, + timestamp=timestamp, + profile_name="compatibility" + ) + + # 软件方案使用更详细的日志 + cmd[cmd.index("-loglevel") + 1] = "warning" + + return self._execute_ffmpeg_command(cmd, f"软件方案提取帧 {timestamp:.1f}s") + + def _execute_ffmpeg_command(self, cmd: List[str], description: str) -> bool: + """ + 执行 FFmpeg 命令并处理结果 + + Args: + cmd: FFmpeg 命令列表 + description: 操作描述 + + Returns: + bool: 是否成功 + """ + try: + # 在 Windows 上使用 UTF-8 编码 + is_windows = os.name == 'nt' + process_kwargs = { + "check": True, + "capture_output": True, + "timeout": 30 # 30秒超时 + } + + if is_windows: + process_kwargs["encoding"] = 'utf-8' + process_kwargs["text"] = True + + result = subprocess.run(cmd, **process_kwargs) + + # 验证输出文件 + output_path = cmd[-1] + if os.path.exists(output_path) and os.path.getsize(output_path) > 0: + return True + else: + logger.debug(f"{description} - 输出文件无效") + return False + + except subprocess.CalledProcessError as e: + error_msg = e.stderr if hasattr(e, 'stderr') and e.stderr else str(e) + logger.debug(f"{description} - 命令执行失败: {error_msg}") + return False + except subprocess.TimeoutExpired: + logger.debug(f"{description} - 命令执行超时") + return False + except Exception as e: + logger.debug(f"{description} - 未知错误: {str(e)}") + return False def _detect_hw_accelerator(self) -> List[str]: """ diff --git a/test_video_extraction.py b/test_video_extraction.py new file mode 100644 index 0000000..2e539dd --- /dev/null +++ b/test_video_extraction.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +""" +视频关键帧提取测试脚本 +用于验证 Windows 系统 FFmpeg 兼容性修复效果 +""" + +import os +import sys +import tempfile +import traceback +from pathlib import Path + +# 添加项目根目录到 Python 路径 +project_root = Path(__file__).parent +sys.path.insert(0, str(project_root)) + +from loguru import logger +from app.utils import video_processor, ffmpeg_utils + + +def test_ffmpeg_compatibility(): + """测试 FFmpeg 兼容性""" + print("=" * 60) + print("🔧 FFmpeg 兼容性测试") + print("=" * 60) + + # 检查 FFmpeg 安装 + if not ffmpeg_utils.check_ffmpeg_installation(): + print("❌ FFmpeg 未安装或不在系统 PATH 中") + return False + + print("✅ FFmpeg 已安装") + + # 获取硬件加速信息 + hwaccel_info = ffmpeg_utils.get_ffmpeg_hwaccel_info() + print(f"🎮 硬件加速状态: {hwaccel_info.get('message', '未知')}") + print(f"🔧 加速类型: {hwaccel_info.get('type', 'software')}") + print(f"🎯 编码器: {hwaccel_info.get('encoder', 'libx264')}") + + return True + + +def test_video_extraction(video_path: str, output_dir: str = None): + """测试视频关键帧提取""" + print("\n" + "=" * 60) + print("🎬 视频关键帧提取测试") + print("=" * 60) + + if not os.path.exists(video_path): + print(f"❌ 视频文件不存在: {video_path}") + return False + + # 创建临时输出目录 + if output_dir is None: + output_dir = tempfile.mkdtemp(prefix="keyframes_test_") + + try: + # 初始化视频处理器 + print(f"📁 输入视频: {video_path}") + print(f"📁 输出目录: {output_dir}") + + processor = video_processor.VideoProcessor(video_path) + + # 显示视频信息 + print(f"📊 视频信息:") + print(f" - 分辨率: {processor.width}x{processor.height}") + print(f" - 帧率: {processor.fps:.1f} fps") + print(f" - 时长: {processor.duration:.1f} 秒") + print(f" - 总帧数: {processor.total_frames}") + + # 测试关键帧提取 + print("\n🚀 开始提取关键帧...") + + # 先测试硬件加速方案 + print("\n1️⃣ 测试硬件加速方案:") + try: + processor.process_video_pipeline( + output_dir=output_dir, + interval_seconds=10.0, # 10秒间隔,减少测试时间 + use_hw_accel=True + ) + + # 检查结果 + extracted_files = [f for f in os.listdir(output_dir) if f.endswith('.jpg')] + print(f"✅ 硬件加速成功,提取了 {len(extracted_files)} 个关键帧") + + if len(extracted_files) > 0: + return True + + except Exception as e: + print(f"⚠️ 硬件加速失败: {str(e)}") + + # 清理失败的文件 + for f in os.listdir(output_dir): + if f.endswith('.jpg'): + os.remove(os.path.join(output_dir, f)) + + # 测试软件方案 + print("\n2️⃣ 测试软件方案:") + try: + # 强制使用软件编码 + ffmpeg_utils.force_software_encoding() + + processor.process_video_pipeline( + output_dir=output_dir, + interval_seconds=10.0, + use_hw_accel=False + ) + + # 检查结果 + extracted_files = [f for f in os.listdir(output_dir) if f.endswith('.jpg')] + print(f"✅ 软件方案成功,提取了 {len(extracted_files)} 个关键帧") + + if len(extracted_files) > 0: + return True + else: + print("❌ 软件方案也未能提取到关键帧") + return False + + except Exception as e: + print(f"❌ 软件方案也失败: {str(e)}") + return False + + except Exception as e: + print(f"❌ 测试过程中发生错误: {str(e)}") + print(f"详细错误信息:\n{traceback.format_exc()}") + return False + + finally: + # 清理临时文件 + try: + import shutil + if output_dir and os.path.exists(output_dir): + shutil.rmtree(output_dir) + print(f"🧹 已清理临时目录: {output_dir}") + except Exception as e: + print(f"⚠️ 清理临时目录失败: {e}") + + +def main(): + """主函数""" + print("🎯 视频关键帧提取兼容性测试工具") + print("专门用于测试 Windows 系统 FFmpeg 兼容性修复效果") + + # 测试 FFmpeg 兼容性 + if not test_ffmpeg_compatibility(): + return + + # 获取测试视频路径 + if len(sys.argv) > 1: + video_path = sys.argv[1] + else: + # 尝试找到项目中的测试视频 + possible_paths = [ + "./resource/videos/test.mp4", + "./storage/videos/test.mp4", + "./test_video.mp4" + ] + + video_path = None + for path in possible_paths: + if os.path.exists(path): + video_path = path + break + + if not video_path: + print("\n❌ 未找到测试视频文件") + print("请提供视频文件路径作为参数:") + print(f"python {sys.argv[0]} ") + return + + # 执行测试 + success = test_video_extraction(video_path) + + print("\n" + "=" * 60) + if success: + print("🎉 测试成功!关键帧提取功能正常工作") + print("💡 建议:如果之前遇到问题,现在应该已经修复") + else: + print("❌ 测试失败!可能需要进一步调试") + print("💡 建议:") + print(" 1. 检查视频文件是否损坏") + print(" 2. 尝试更新显卡驱动") + print(" 3. 检查 FFmpeg 版本是否过旧") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/webui/components/ffmpeg_diagnostics.py b/webui/components/ffmpeg_diagnostics.py new file mode 100644 index 0000000..21252a8 --- /dev/null +++ b/webui/components/ffmpeg_diagnostics.py @@ -0,0 +1,280 @@ +""" +FFmpeg 诊断和配置组件 +为用户提供 FFmpeg 兼容性诊断和配置选项 +""" + +import streamlit as st +import platform +from typing import Dict, Any +from loguru import logger + +try: + from app.utils import ffmpeg_utils + from app.config.ffmpeg_config import FFmpegConfigManager +except ImportError as e: + logger.error(f"导入模块失败: {e}") + ffmpeg_utils = None + FFmpegConfigManager = None + + +def show_ffmpeg_diagnostics(): + """显示 FFmpeg 诊断信息""" + st.subheader("🔧 FFmpeg 兼容性诊断") + + if ffmpeg_utils is None or FFmpegConfigManager is None: + st.error("❌ 无法加载 FFmpeg 工具模块") + return + + # 基础信息 + col1, col2 = st.columns(2) + + with col1: + st.write("**系统信息**") + st.write(f"- 操作系统: {platform.system()} {platform.release()}") + st.write(f"- 架构: {platform.machine()}") + st.write(f"- Python: {platform.python_version()}") + + with col2: + st.write("**FFmpeg 状态**") + + # 检查 FFmpeg 安装 + if ffmpeg_utils.check_ffmpeg_installation(): + st.success("✅ FFmpeg 已安装") + else: + st.error("❌ FFmpeg 未安装或不在 PATH 中") + st.info("请安装 FFmpeg 并确保其在系统 PATH 中") + return + + # 硬件加速信息 + st.write("**硬件加速检测**") + + try: + hwaccel_info = ffmpeg_utils.get_ffmpeg_hwaccel_info() + + if hwaccel_info.get("available", False): + st.success(f"✅ {hwaccel_info.get('message', '硬件加速可用')}") + + # 显示详细信息 + with st.expander("硬件加速详情"): + st.write(f"- 加速类型: {hwaccel_info.get('type', '未知')}") + st.write(f"- 编码器: {hwaccel_info.get('encoder', '未知')}") + st.write(f"- GPU 厂商: {hwaccel_info.get('gpu_vendor', '未知')}") + st.write(f"- 独立显卡: {'是' if hwaccel_info.get('is_dedicated_gpu', False) else '否'}") + + if hwaccel_info.get("tested_methods"): + st.write(f"- 测试的方法: {', '.join(hwaccel_info['tested_methods'])}") + else: + st.warning(f"⚠️ {hwaccel_info.get('message', '硬件加速不可用')}") + + except Exception as e: + st.error(f"❌ 硬件加速检测失败: {str(e)}") + + # 配置文件推荐 + st.write("**推荐配置**") + + try: + recommended_profile = FFmpegConfigManager.get_recommended_profile() + profile = FFmpegConfigManager.get_profile(recommended_profile) + + st.info(f"🎯 推荐配置: **{profile.description}**") + + # 显示配置详情 + with st.expander("配置详情"): + st.write(f"- 配置名称: {profile.name}") + st.write(f"- 硬件加速: {'启用' if profile.hwaccel_enabled else '禁用'}") + st.write(f"- 编码器: {profile.encoder}") + st.write(f"- 质量预设: {profile.quality_preset}") + st.write(f"- 兼容性等级: {profile.compatibility_level}/5") + + except Exception as e: + st.error(f"❌ 配置推荐失败: {str(e)}") + + # 兼容性报告 + if st.button("🔍 生成详细兼容性报告"): + try: + report = FFmpegConfigManager.get_compatibility_report() + + st.write("**详细兼容性报告**") + st.json(report) + + # 显示建议 + if report.get("suggestions"): + st.write("**优化建议**") + for suggestion in report["suggestions"]: + st.write(f"- {suggestion}") + + except Exception as e: + st.error(f"❌ 生成报告失败: {str(e)}") + + +def show_ffmpeg_settings(): + """显示 FFmpeg 设置选项""" + st.subheader("⚙️ FFmpeg 设置") + + if FFmpegConfigManager is None: + st.error("❌ 无法加载配置管理器") + return + + # 配置文件选择 + profiles = FFmpegConfigManager.list_profiles() + + # 获取当前推荐配置 + try: + recommended_profile = FFmpegConfigManager.get_recommended_profile() + except Exception: + recommended_profile = "universal_software" + + # 配置文件选择器 + selected_profile = st.selectbox( + "选择 FFmpeg 配置文件", + options=list(profiles.keys()), + index=list(profiles.keys()).index(recommended_profile) if recommended_profile in profiles else 0, + format_func=lambda x: f"{profiles[x]} {'(推荐)' if x == recommended_profile else ''}", + help="不同的配置文件针对不同的硬件和兼容性需求进行了优化" + ) + + # 显示选中配置的详情 + if selected_profile: + profile = FFmpegConfigManager.get_profile(selected_profile) + + st.write("**配置详情**") + col1, col2 = st.columns(2) + + with col1: + st.write(f"- 硬件加速: {'✅ 启用' if profile.hwaccel_enabled else '❌ 禁用'}") + st.write(f"- 编码器: {profile.encoder}") + st.write(f"- 质量预设: {profile.quality_preset}") + + with col2: + st.write(f"- 像素格式: {profile.pixel_format}") + st.write(f"- 兼容性等级: {profile.compatibility_level}/5") + if profile.additional_args: + st.write(f"- 额外参数: {' '.join(profile.additional_args)}") + + # 高级设置 + with st.expander("🔧 高级设置"): + st.write("**强制设置选项**") + + col1, col2 = st.columns(2) + + with col1: + if st.button("🚫 强制禁用硬件加速"): + try: + ffmpeg_utils.force_software_encoding() + st.success("✅ 已强制禁用硬件加速") + st.info("这将使用纯软件编码,兼容性最高但性能较低") + except Exception as e: + st.error(f"❌ 操作失败: {str(e)}") + + with col2: + if st.button("🔄 重置硬件加速检测"): + try: + ffmpeg_utils.reset_hwaccel_detection() + st.success("✅ 已重置硬件加速检测") + st.info("下次使用时将重新检测硬件加速能力") + except Exception as e: + st.error(f"❌ 操作失败: {str(e)}") + + # 测试按钮 + st.write("**测试功能**") + + if st.button("🧪 测试 FFmpeg 兼容性"): + with st.spinner("正在测试 FFmpeg 兼容性..."): + try: + # 这里可以调用测试脚本 + st.info("请在终端运行 `python test_video_extraction.py ` 进行完整测试") + + # 简单的兼容性测试 + if ffmpeg_utils and ffmpeg_utils.check_ffmpeg_installation(): + hwaccel_info = ffmpeg_utils.get_ffmpeg_hwaccel_info() + if hwaccel_info.get("available"): + st.success("✅ 基础兼容性测试通过") + else: + st.warning("⚠️ 硬件加速不可用,但软件编码应该可以工作") + else: + st.error("❌ FFmpeg 不可用") + + except Exception as e: + st.error(f"❌ 测试失败: {str(e)}") + + +def show_troubleshooting_guide(): + """显示故障排除指南""" + st.subheader("🆘 故障排除指南") + + # 常见问题 + st.write("**常见问题及解决方案**") + + with st.expander("❌ 关键帧提取失败 - 滤镜链错误"): + st.write(""" + **问题描述**: 出现 "Impossible to convert between the formats" 错误 + + **解决方案**: + 1. 在设置中选择 "兼容性配置" 或 "Windows NVIDIA 优化配置" + 2. 点击 "强制禁用硬件加速" 按钮 + 3. 重新尝试关键帧提取 + 4. 如果仍然失败,请更新显卡驱动程序 + """) + + with st.expander("⚠️ 硬件加速不可用"): + st.write(""" + **可能原因**: + - 显卡驱动程序过旧 + - FFmpeg 版本不支持当前硬件 + - 系统缺少必要的运行库 + + **解决方案**: + 1. 更新显卡驱动程序到最新版本 + 2. 对于 NVIDIA 用户,安装 CUDA 工具包 + 3. 对于 AMD 用户,安装 AMD Media SDK + 4. 使用软件编码作为备用方案 + """) + + with st.expander("🐌 处理速度很慢"): + st.write(""" + **优化建议**: + 1. 启用硬件加速(如果可用) + 2. 选择 "高性能配置" + 3. 降低视频质量设置 + 4. 增加关键帧提取间隔 + 5. 关闭其他占用 GPU 的程序 + """) + + with st.expander("📁 文件权限问题"): + st.write(""" + **解决方案**: + 1. 确保对输出目录有写入权限 + 2. 以管理员身份运行程序(Windows) + 3. 检查磁盘空间是否充足 + 4. 避免使用包含特殊字符的文件路径 + """) + + # 联系支持 + st.write("**需要更多帮助?**") + st.info(""" + 如果上述解决方案都无法解决您的问题,请: + 1. 运行 `python test_video_extraction.py` 生成详细的测试报告 + 2. 记录具体的错误信息和系统环境 + 3. 联系技术支持并提供相关信息 + """) + + +def render_ffmpeg_diagnostics_page(): + """渲染 FFmpeg 诊断页面""" + st.title("🔧 FFmpeg 诊断与配置") + + # 选项卡 + tab1, tab2, tab3 = st.tabs(["🔍 诊断信息", "⚙️ 配置设置", "🆘 故障排除"]) + + with tab1: + show_ffmpeg_diagnostics() + + with tab2: + show_ffmpeg_settings() + + with tab3: + show_troubleshooting_guide() + + +if __name__ == "__main__": + render_ffmpeg_diagnostics_page() diff --git a/webui/tools/generate_script_docu.py b/webui/tools/generate_script_docu.py index ea14b0e..dde125c 100644 --- a/webui/tools/generate_script_docu.py +++ b/webui/tools/generate_script_docu.py @@ -65,11 +65,32 @@ def generate_script_docu(params): # 初始化视频处理器 processor = video_processor.VideoProcessor(params.video_origin_path) + + # 显示视频信息 + st.info(f"视频信息: {processor.width}x{processor.height}, {processor.fps:.1f}fps, {processor.duration:.1f}秒") + # 处理视频并提取关键帧 - processor.process_video_pipeline( - output_dir=video_keyframes_dir, - interval_seconds=st.session_state.get('frame_interval_input'), - ) + update_progress(15, "正在提取关键帧...") + + try: + processor.process_video_pipeline( + output_dir=video_keyframes_dir, + interval_seconds=st.session_state.get('frame_interval_input'), + ) + except Exception as extract_error: + # 如果硬件加速失败,尝试强制使用软件方案 + logger.warning(f"硬件加速提取失败: {extract_error}") + st.warning("硬件加速提取失败,正在尝试软件方案...") + + # 强制使用软件编码重试 + from app.utils import ffmpeg_utils + ffmpeg_utils.force_software_encoding() + + processor.process_video_pipeline( + output_dir=video_keyframes_dir, + interval_seconds=st.session_state.get('frame_interval_input'), + use_hw_accel=False # 明确禁用硬件加速 + ) # 获取所有关键文件路径 for filename in sorted(os.listdir(video_keyframes_dir)): @@ -77,9 +98,13 @@ def generate_script_docu(params): keyframe_files.append(os.path.join(video_keyframes_dir, filename)) if not keyframe_files: - raise Exception("未提取到任何关键帧") + # 检查目录中是否有其他文件 + all_files = os.listdir(video_keyframes_dir) + logger.error(f"关键帧目录内容: {all_files}") + raise Exception("未提取到任何关键帧文件,可能是 FFmpeg 兼容性问题") update_progress(20, f"关键帧提取完成,共 {len(keyframe_files)} 帧") + st.success(f"✅ 成功提取 {len(keyframe_files)} 个关键帧") except Exception as e: # 如果提取失败,清理创建的目录 @@ -90,7 +115,16 @@ def generate_script_docu(params): except Exception as cleanup_err: logger.error(f"清理失败的关键帧目录时出错: {cleanup_err}") - raise Exception(f"关键帧提取失败: {str(e)}") + # 提供更详细的错误信息和解决建议 + error_msg = str(e) + if "滤镜链" in error_msg or "filter" in error_msg.lower(): + suggestion = "建议:这可能是硬件加速兼容性问题,请尝试在设置中禁用硬件加速" + elif "cuda" in error_msg.lower() or "nvenc" in error_msg.lower(): + suggestion = "建议:NVIDIA 显卡驱动可能需要更新,或尝试禁用硬件加速" + else: + suggestion = "建议:检查视频文件是否损坏,或尝试转换为标准格式" + + raise Exception(f"关键帧提取失败: {error_msg}\n{suggestion}") """ 2. 视觉分析(批量分析每一帧)