File size: 2,437 Bytes
04a1f81
e7495da
1f2ab31
f05a886
04a1f81
 
0df918b
04a1f81
 
0df918b
 
04a1f81
 
fbb769b
04a1f81
 
 
 
 
fbb769b
04a1f81
 
 
 
 
fbb769b
 
 
7b12459
fbb769b
 
0df918b
d330638
 
7b12459
fbb769b
 
d330638
 
7b12459
 
 
d330638
0df918b
04a1f81
0df918b
 
04a1f81
fbb769b
04a1f81
 
d330638
fbb769b
65e7f7d
 
fbb769b
04a1f81
fbb769b
04a1f81
 
d330638
fbb769b
 
0df918b
fbb769b
 
 
 
d330638
 
 
04a1f81
 
 
 
0df918b
f05a886
 
 
7b12459
0df918b
f05a886
e7495da
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
use pyo3::prelude::*;
use std::process::Command;
use std::path::Path;

#[pyfunction]
fn render_video(
    id: usize,
    image_path: String,
    audio_path: String,
    duration: f64,
    words: Vec<(String, (u32, u32, u32, u32))>,
    output_dir: String,
) -> PyResult<String> {
    // Validate paths
    if !Path::new(&image_path).exists() {
        return Err(pyo3::exceptions::PyFileNotFoundError::new_err(format!(
            "Image not found: {}",
            image_path
        )));
    }
    if !Path::new(&audio_path).exists() {
        return Err(pyo3::exceptions::PyFileNotFoundError::new_err(format!(
            "Audio not found: {}",
            audio_path
        )));
    }

    let output_path = format!("{}/clip{}.mp4", output_dir, id);
    let duration_str = duration.to_string();
    let n_words = words.len() as f64;
    let highlight_duration = duration / n_words;

    // Build filter graph
    let mut filters = vec![];
    for (i, (_, (x, y, w, h))) in words.iter().enumerate() {
        let start = i as f64 * highlight_duration;
        let end = start + highlight_duration;
        filters.push(format!(
            "drawbox=x={x}:y={y}:w={w}:h={h}:color=yellow@0.5:t=fill:enable='between(t,{start:.2},{end:.2})'"
        ));
    }

    let filter_chain = filters.join(",");

    // FFmpeg command
    let status = Command::new("ffmpeg")
        .args(&[
            "-y",
            "-loop", "1",
            "-i", &image_path,
            "-i", &audio_path,
            "-vf", &filter_chain,
            "-c:v", "libx264",
            "-preset", "fast",
            "-crf", "18",
            "-pix_fmt", "yuv420p",
            "-movflags", "+faststart",
            "-c:a", "aac",
            "-b:a", "192k",
            "-r", "60",
            "-fps_mode", "vfr",  // Replaces deprecated -vsync
            "-t", &duration_str,
            &output_path,
        ])
        .status();

    match status {
        Ok(exit_status) if exit_status.success() => Ok(output_path),
        Ok(_) => Err(pyo3::exceptions::PyRuntimeError::new_err(
            "FFmpeg command failed. Check filter syntax.",
        )),
        Err(e) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
            "Failed to execute FFmpeg: {}",
            e
        ))),
    }
}

#[pymodule]
fn rust_highlight(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(render_video, m)?)?;
    Ok(())
}