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:
imeepos
2025-08-13 15:42:09 +08:00
parent 5921523772
commit 97bada92d9
4 changed files with 229 additions and 9 deletions

View File

@@ -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)))?;

View File

@@ -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",
}

View File

@@ -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::*;

View File

@@ -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 },