From 97bada92d9c93d01b35cada8514b7c225764b4e7 Mon Sep 17 00:00:00 2001 From: imeepos Date: Wed, 13 Aug 2025 15:42:09 +0800 Subject: [PATCH] Fix Topaz Video AI FFmpeg integration issues - Add apf-2 model support to InterpolationModel enum - Fix working directory setting for Topaz FFmpeg execution - Add video analysis and framerate normalization functionality - Update model manager to detect multiple model directories - Add VideoInfo struct for input video analysis - Implement parameter validation and safe value handling - Add debug logging for Topaz FFmpeg execution Resolves issues with: - 'Unable to parse option value 0 as video rate' error - Missing fps parameter in tvai_fi filter - Model file detection and access - Framerate inconsistency handling --- cargos/tvai/src/core/ffmpeg.rs | 21 ++- cargos/tvai/src/core/models.rs | 4 + cargos/tvai/src/core/processor.rs | 171 +++++++++++++++++++++++++ cargos/tvai/src/utils/model_manager.rs | 42 +++++- 4 files changed, 229 insertions(+), 9 deletions(-) diff --git a/cargos/tvai/src/core/ffmpeg.rs b/cargos/tvai/src/core/ffmpeg.rs index bc5d90f..33da382 100644 --- a/cargos/tvai/src/core/ffmpeg.rs +++ b/cargos/tvai/src/core/ffmpeg.rs @@ -115,8 +115,25 @@ impl FfmpegManager { self.get_ffmpeg_path()? }; - let output = tokio::process::Command::new(&ffmpeg_path) - .args(args) + let mut command = tokio::process::Command::new(&ffmpeg_path); + command.args(args); + + // 如果使用 Topaz FFmpeg,设置工作目录为 Topaz 安装目录 + // 这对模型文件查找很重要 + if for_topaz { + if let Some(topaz_dir) = ffmpeg_path.parent() { + command.current_dir(topaz_dir); + + // 添加调试信息 + #[cfg(debug_assertions)] + { + eprintln!("DEBUG: Setting working directory to: {}", topaz_dir.display()); + eprintln!("DEBUG: FFmpeg path: {}", ffmpeg_path.display()); + } + } + } + + let output = command .output() .await .map_err(|e| TvaiError::FfmpegError(format!("Failed to execute FFmpeg: {}", e)))?; diff --git a/cargos/tvai/src/core/models.rs b/cargos/tvai/src/core/models.rs index 5a2f11b..ecacfc8 100644 --- a/cargos/tvai/src/core/models.rs +++ b/cargos/tvai/src/core/models.rs @@ -100,6 +100,8 @@ pub enum InterpolationModel { Apo8, /// Apollo Fast v1 - Fast interpolation Apf1, + /// Apollo Fast v2 - Improved fast interpolation (most common) + Apf2, /// Chronos v2 - Animation content Chr2, /// Chronos Fast v3 - Fast animation interpolation @@ -112,6 +114,7 @@ impl InterpolationModel { match self { Self::Apo8 => "apo-8", Self::Apf1 => "apf-1", + Self::Apf2 => "apf-2", Self::Chr2 => "chr-2", Self::Chf3 => "chf-3", } @@ -122,6 +125,7 @@ impl InterpolationModel { match self { Self::Apo8 => "High quality interpolation for most content", Self::Apf1 => "Fast interpolation with good quality", + Self::Apf2 => "Improved fast interpolation - most commonly used", Self::Chr2 => "Specialized for animation and cartoon content", Self::Chf3 => "Fast interpolation for animation content", } diff --git a/cargos/tvai/src/core/processor.rs b/cargos/tvai/src/core/processor.rs index 994e9ff..c500120 100644 --- a/cargos/tvai/src/core/processor.rs +++ b/cargos/tvai/src/core/processor.rs @@ -11,6 +11,19 @@ use crate::utils::temp::TempFileManager; /// Progress callback function type pub type ProgressCallback = Box; +/// Video information extracted from analysis +#[derive(Debug, Clone)] +pub struct VideoInfo { + /// Detected frames per second + pub fps: Option, + /// Time base rate + pub tbr: Option, + /// Whether there's a framerate inconsistency + pub has_framerate_inconsistency: bool, + /// Target framerate to use for processing + pub target_framerate: f64, +} + /// Processing options with progress callback #[derive(Default)] pub struct ProcessingOptions { @@ -293,6 +306,138 @@ impl TvaiProcessor { self.ffmpeg_manager.get_version(for_topaz).await } + /// Analyze input video to detect framerate issues + pub async fn analyze_input_video(&self, input_path: &Path) -> Result { + let input_path_str = input_path.to_string_lossy().to_string(); + let args = vec![ + "-hide_banner", + "-i", &input_path_str, + "-f", "null", + "-", + ]; + + let output = self.ffmpeg_manager.execute_command(&args, false).await?; + let stderr = String::from_utf8_lossy(&output.stderr); + + // Parse video information from stderr + let mut fps = None; + let mut tbr = None; + let mut has_inconsistency = false; + + for line in stderr.lines() { + if line.contains("Stream #") && line.contains("Video:") { + // Extract fps and tbr values + if let Some(fps_match) = extract_fps_from_line(line) { + fps = Some(fps_match); + } + if let Some(tbr_match) = extract_tbr_from_line(line) { + tbr = Some(tbr_match); + } + } + } + + // Check for framerate inconsistency + if let (Some(fps_val), Some(tbr_val)) = (fps, tbr) { + // If fps and tbr differ significantly, we have an inconsistency + let diff = (fps_val - tbr_val).abs(); + has_inconsistency = diff > 1.0; // More than 1 fps difference + } + + // Determine target framerate + let target_framerate = match fps { + Some(f) if f > 0.0 => { + // Round to nearest standard framerate + if f <= 24.5 { 24.0 } + else if f <= 25.5 { 25.0 } + else if f <= 30.5 { 30.0 } + else { 60.0 } + } + _ => 24.0, // Default fallback + }; + + Ok(VideoInfo { + fps, + tbr, + has_framerate_inconsistency: has_inconsistency, + target_framerate, + }) + } + + /// Process video with automatic framerate handling + pub async fn process_video_with_filters( + &self, + input_path: &Path, + output_path: &Path, + filter_string: &str, + for_topaz: bool, + progress_callback: Option<&ProgressCallback>, + ) -> Result<(), TvaiError> { + // Validate input + self.validate_input_file(input_path)?; + self.validate_output_path(output_path)?; + + // Analyze input video + let video_info = self.analyze_input_video(input_path).await?; + + // Build FFmpeg command + let mut args = vec!["-hide_banner".to_string()]; + + // Add framerate normalization if needed + if video_info.has_framerate_inconsistency { + args.extend([ + "-r".to_string(), + video_info.target_framerate.to_string() + ]); + } + + // Input file + args.extend(["-i".to_string(), input_path.to_string_lossy().to_string()]); + + // Add filter + if !filter_string.is_empty() { + args.extend(["-filter_complex".to_string(), filter_string.to_string()]); + } + + // Output settings + args.extend([ + "-c:v".to_string(), "h264_nvenc".to_string(), + "-preset".to_string(), "p4".to_string(), + "-crf".to_string(), "18".to_string(), + "-pix_fmt".to_string(), "yuv420p".to_string(), + ]); + + // Force output framerate if needed + if video_info.has_framerate_inconsistency { + args.extend(["-r".to_string(), video_info.target_framerate.to_string()]); + } + + // Force overwrite and output file + args.extend(["-y".to_string(), output_path.to_string_lossy().to_string()]); + + // Convert to string references + let args_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); + + // Execute command + if let Some(callback) = progress_callback { + callback(0.0); + } + + let output = self.ffmpeg_manager.execute_command(&args_refs, for_topaz).await?; + + if let Some(callback) = progress_callback { + callback(1.0); + } + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(TvaiError::ProcessingError( + format!("FFmpeg processing failed: {}", stderr) + )); + } + + Ok(()) + } + /// Create processing metadata pub fn create_metadata( &self, @@ -317,6 +462,32 @@ impl Drop for TvaiProcessor { } } +/// Extract fps value from FFmpeg output line +fn extract_fps_from_line(line: &str) -> Option { + // Look for pattern like "24.12 fps" + if let Some(fps_pos) = line.find(" fps") { + let before_fps = &line[..fps_pos]; + if let Some(space_pos) = before_fps.rfind(' ') { + let fps_str = &before_fps[space_pos + 1..]; + return fps_str.parse::().ok(); + } + } + None +} + +/// Extract tbr value from FFmpeg output line +fn extract_tbr_from_line(line: &str) -> Option { + // Look for pattern like "60 tbr" + if let Some(tbr_pos) = line.find(" tbr") { + let before_tbr = &line[..tbr_pos]; + if let Some(space_pos) = before_tbr.rfind(' ') { + let tbr_str = &before_tbr[space_pos + 1..]; + return tbr_str.parse::().ok(); + } + } + None +} + #[cfg(test)] mod tests { use super::*; diff --git a/cargos/tvai/src/utils/model_manager.rs b/cargos/tvai/src/utils/model_manager.rs index 86b9ed0..f36bf77 100644 --- a/cargos/tvai/src/utils/model_manager.rs +++ b/cargos/tvai/src/utils/model_manager.rs @@ -37,22 +37,50 @@ pub struct ModelManager { impl ModelManager { /// 创建新的模型管理器 pub fn new(topaz_path: &Path) -> Result { - let models_dir = topaz_path.join("models"); let topaz_exe = topaz_path.join("Topaz Video AI.exe"); let topaz_ffmpeg = topaz_path.join("ffmpeg.exe"); - if !models_dir.exists() { - return Err(TvaiError::TopazNotFound( - format!("Models directory not found: {}", models_dir.display()) - )); - } - if !topaz_ffmpeg.exists() { return Err(TvaiError::TopazNotFound( format!("Topaz FFmpeg not found: {}", topaz_ffmpeg.display()) )); } + // 检查多个可能的模型目录位置 + let possible_models_dirs = vec![ + topaz_path.join("models"), + PathBuf::from(r"C:\ProgramData\Topaz Labs LLC\Topaz Video AI\models"), + ]; + + let mut models_dir = None; + for dir in possible_models_dirs { + if dir.exists() { + // 检查是否包含模型文件 + if let Ok(entries) = std::fs::read_dir(&dir) { + let has_models = entries + .filter_map(|e| e.ok()) + .any(|e| { + let name = e.file_name().to_string_lossy().to_lowercase(); + name.ends_with(".tz") || (name.ends_with(".json") && + !name.contains("audio-codecs") && + !name.contains("benchmarks") && + !name.contains("proxy")) + }); + + if has_models { + models_dir = Some(dir); + break; + } + } + } + } + + let models_dir = models_dir.ok_or_else(|| { + TvaiError::TopazNotFound( + "No valid models directory found. Please ensure Topaz Video AI is properly installed and models are downloaded.".to_string() + ) + })?; + Ok(Self { models_dir, topaz_exe: if topaz_exe.exists() { Some(topaz_exe) } else { None },