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()?
|
self.get_ffmpeg_path()?
|
||||||
};
|
};
|
||||||
|
|
||||||
let output = tokio::process::Command::new(&ffmpeg_path)
|
let mut command = tokio::process::Command::new(&ffmpeg_path);
|
||||||
.args(args)
|
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()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| TvaiError::FfmpegError(format!("Failed to execute FFmpeg: {}", e)))?;
|
.map_err(|e| TvaiError::FfmpegError(format!("Failed to execute FFmpeg: {}", e)))?;
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ pub enum InterpolationModel {
|
|||||||
Apo8,
|
Apo8,
|
||||||
/// Apollo Fast v1 - Fast interpolation
|
/// Apollo Fast v1 - Fast interpolation
|
||||||
Apf1,
|
Apf1,
|
||||||
|
/// Apollo Fast v2 - Improved fast interpolation (most common)
|
||||||
|
Apf2,
|
||||||
/// Chronos v2 - Animation content
|
/// Chronos v2 - Animation content
|
||||||
Chr2,
|
Chr2,
|
||||||
/// Chronos Fast v3 - Fast animation interpolation
|
/// Chronos Fast v3 - Fast animation interpolation
|
||||||
@@ -112,6 +114,7 @@ impl InterpolationModel {
|
|||||||
match self {
|
match self {
|
||||||
Self::Apo8 => "apo-8",
|
Self::Apo8 => "apo-8",
|
||||||
Self::Apf1 => "apf-1",
|
Self::Apf1 => "apf-1",
|
||||||
|
Self::Apf2 => "apf-2",
|
||||||
Self::Chr2 => "chr-2",
|
Self::Chr2 => "chr-2",
|
||||||
Self::Chf3 => "chf-3",
|
Self::Chf3 => "chf-3",
|
||||||
}
|
}
|
||||||
@@ -122,6 +125,7 @@ impl InterpolationModel {
|
|||||||
match self {
|
match self {
|
||||||
Self::Apo8 => "High quality interpolation for most content",
|
Self::Apo8 => "High quality interpolation for most content",
|
||||||
Self::Apf1 => "Fast interpolation with good quality",
|
Self::Apf1 => "Fast interpolation with good quality",
|
||||||
|
Self::Apf2 => "Improved fast interpolation - most commonly used",
|
||||||
Self::Chr2 => "Specialized for animation and cartoon content",
|
Self::Chr2 => "Specialized for animation and cartoon content",
|
||||||
Self::Chf3 => "Fast interpolation for animation content",
|
Self::Chf3 => "Fast interpolation for animation content",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,19 @@ use crate::utils::temp::TempFileManager;
|
|||||||
/// Progress callback function type
|
/// Progress callback function type
|
||||||
pub type ProgressCallback = Box<dyn Fn(f32) + Send + Sync>;
|
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
|
/// Processing options with progress callback
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ProcessingOptions {
|
pub struct ProcessingOptions {
|
||||||
@@ -293,6 +306,138 @@ impl TvaiProcessor {
|
|||||||
self.ffmpeg_manager.get_version(for_topaz).await
|
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
|
/// Create processing metadata
|
||||||
pub fn create_metadata(
|
pub fn create_metadata(
|
||||||
&self,
|
&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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -37,22 +37,50 @@ pub struct ModelManager {
|
|||||||
impl ModelManager {
|
impl ModelManager {
|
||||||
/// 创建新的模型管理器
|
/// 创建新的模型管理器
|
||||||
pub fn new(topaz_path: &Path) -> Result<Self, TvaiError> {
|
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_exe = topaz_path.join("Topaz Video AI.exe");
|
||||||
let topaz_ffmpeg = topaz_path.join("ffmpeg.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() {
|
if !topaz_ffmpeg.exists() {
|
||||||
return Err(TvaiError::TopazNotFound(
|
return Err(TvaiError::TopazNotFound(
|
||||||
format!("Topaz FFmpeg not found: {}", topaz_ffmpeg.display())
|
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 {
|
Ok(Self {
|
||||||
models_dir,
|
models_dir,
|
||||||
topaz_exe: if topaz_exe.exists() { Some(topaz_exe) } else { None },
|
topaz_exe: if topaz_exe.exists() { Some(topaz_exe) } else { None },
|
||||||
|
|||||||
Reference in New Issue
Block a user