mirror of
https://github.com/linyqh/NarratoAI.git
synced 2025-12-11 02:12:50 +00:00
feat(video_processor): 优化视频帧提取功能,增强Windows系统兼容性
在video_processor.py中,添加了对Windows N卡硬件加速的支持,优化了帧提取过程,改进了提取成功率的统计和错误处理。同时,在generate_script_docu.py中,增强了对硬件加速失败的处理逻辑,提供了详细的错误信息和解决建议,提升了用户体验。
This commit is contained in:
parent
a65e8e4a95
commit
6715c29057
284
app/config/ffmpeg_config.py
Normal file
284
app/config/ffmpeg_config.py
Normal file
@ -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
|
||||
@ -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]:
|
||||
"""
|
||||
|
||||
189
test_video_extraction.py
Normal file
189
test_video_extraction.py
Normal file
@ -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]} <video_path>")
|
||||
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()
|
||||
280
webui/components/ffmpeg_diagnostics.py
Normal file
280
webui/components/ffmpeg_diagnostics.py
Normal file
@ -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 <video_path>` 进行完整测试")
|
||||
|
||||
# 简单的兼容性测试
|
||||
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()
|
||||
@ -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. 视觉分析(批量分析每一帧)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user