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
This commit is contained in:
@@ -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)))?;
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -11,6 +11,19 @@ use crate::utils::temp::TempFileManager;
|
||||
/// Progress callback function type
|
||||
pub type ProgressCallback = Box<dyn Fn(f32) + Send + Sync>;
|
||||
|
||||
/// Video information extracted from analysis
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VideoInfo {
|
||||
/// Detected frames per second
|
||||
pub fps: Option<f64>,
|
||||
/// Time base rate
|
||||
pub tbr: Option<f64>,
|
||||
/// 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<VideoInfo, TvaiError> {
|
||||
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<f64> {
|
||||
// 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::<f64>().ok();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract tbr value from FFmpeg output line
|
||||
fn extract_tbr_from_line(line: &str) -> Option<f64> {
|
||||
// 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::<f64>().ok();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -37,22 +37,50 @@ pub struct ModelManager {
|
||||
impl ModelManager {
|
||||
/// 创建新的模型管理器
|
||||
pub fn new(topaz_path: &Path) -> Result<Self, TvaiError> {
|
||||
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 },
|
||||
|
||||
Reference in New Issue
Block a user