Skip to main content

scuisei_rs/
lib.rs

1#![deny(clippy::pedantic)]
2
3pub mod analysis;
4pub mod cli;
5mod decoder;
6pub mod detector;
7pub mod error;
8mod formatter;
9mod postprocess;
10pub mod simd_metrics;
11
12#[cfg(feature = "python")]
13mod python;
14
15use crate::cli::OutputFormat;
16pub use analysis::{AnalysisResult, AnalyzeOptions, analyze_video};
17pub use detector::{DetectorConfig, XvidDetectorConfig};
18pub use error::{SCuiseiError, SCuiseiResult};
19use std::io::Write;
20use std::path::PathBuf;
21
22fn build_xvid_config(cli: &cli::Cli) -> detector::XvidDetectorConfig {
23    detector::XvidDetectorConfig {
24        search_radius: cli.me_search_radius,
25        intra_thresh: cli.me_intra_thresh,
26        intra_thresh2: cli.me_intra_thresh2,
27    }
28}
29
30fn build_adaptive_config(cli: &cli::Cli) -> detector::DetectorConfig {
31    detector::DetectorConfig {
32        window_size: cli.window_size,
33        sigma: cli.sigma,
34        base_threshold: cli.base_threshold,
35        min_hist_distance: cli.min_hist_distance,
36        min_score: cli.min_score,
37        sad_weight: cli.sad_weight,
38        hist_weight: cli.hist_weight,
39    }
40}
41
42impl AnalyzeOptions {
43    #[must_use]
44    pub fn from_cli(cli: &cli::Cli, dump_scores: bool) -> Self {
45        Self {
46            input: cli.input.clone(),
47            native_res: cli.native_res,
48            hwdec: cli.hwdec.clone(),
49            dump_scores,
50            xvid_config: build_xvid_config(cli),
51            adaptive_config: build_adaptive_config(cli),
52        }
53    }
54
55    #[must_use]
56    pub fn defaults_for_input(input: impl Into<PathBuf>) -> Self {
57        Self {
58            input: input.into(),
59            native_res: false,
60            hwdec: None,
61            dump_scores: false,
62            xvid_config: detector::XvidDetectorConfig::default(),
63            adaptive_config: detector::DetectorConfig::default(),
64        }
65    }
66}
67
68/// Write comma-separated keyframe indices and a trailing newline.
69///
70/// # Errors
71/// Returns an error if writing to the output stream fails.
72pub fn write_frames_csv<W: Write>(writer: &mut W, keyframes: &[usize]) -> SCuiseiResult<()> {
73    for (index, frame) in keyframes.iter().enumerate() {
74        if index > 0 {
75            write!(writer, ",")
76                .map_err(|error| SCuiseiError::io("failed to write frame separator", &error))?;
77        }
78        write!(writer, "{frame}")
79            .map_err(|error| SCuiseiError::io("failed to write frame index", &error))?;
80    }
81    writeln!(writer)
82        .map_err(|error| SCuiseiError::io("failed to write trailing newline", &error))?;
83    Ok(())
84}
85
86/// Write AGI keyframe format v1 output with a trailing newline.
87///
88/// # Errors
89/// Returns an error if writing to the output stream fails.
90pub fn write_agi<W: Write>(writer: &mut W, keyframes: &[usize]) -> SCuiseiResult<()> {
91    writeln!(writer, "# keyframe format v1")
92        .map_err(|error| SCuiseiError::io("failed to write AGI header", &error))?;
93    writeln!(writer, "fps 0")
94        .map_err(|error| SCuiseiError::io("failed to write AGI fps line", &error))?;
95    writeln!(writer).map_err(|error| SCuiseiError::io("failed to write AGI spacer", &error))?;
96
97    for frame in keyframes {
98        writeln!(writer, "{frame} I -1")
99            .map_err(|error| SCuiseiError::io("failed to write AGI keyframe line", &error))?;
100    }
101    Ok(())
102}
103
104/// Write SCXvid-compatible frame decisions (`i`/`p`) to a writer.
105///
106/// # Errors
107/// Returns an error if writing to the output stream fails.
108pub fn write_pass_log<W: Write>(writer: &mut W, pass_decisions: &[bool]) -> SCuiseiResult<()> {
109    let mut pass_formatter =
110        formatter::ScxvidPassFormatter::new(writer).map_err(SCuiseiError::from)?;
111    for (index, is_cut) in pass_decisions.iter().copied().enumerate() {
112        if index == 0 {
113            pass_formatter
114                .write_first_frame()
115                .map_err(SCuiseiError::from)?;
116            continue;
117        }
118        pass_formatter
119            .write_frame(is_cut)
120            .map_err(SCuiseiError::from)?;
121    }
122    Ok(())
123}
124
125/// Run the CLI command using the shared library pipeline.
126///
127/// # Errors
128/// Returns an error if analysis fails or writing formatted output fails.
129pub fn run_cli<W: Write>(cli: &cli::Cli, writer: &mut W) -> SCuiseiResult<()> {
130    let dump_scores = std::env::var_os("SCUISEI_DUMP_SCORES").is_some();
131    let options = AnalyzeOptions::from_cli(cli, dump_scores);
132    let result = analyze_video(&options)?;
133
134    match cli.format {
135        OutputFormat::Agi => write_agi(writer, &result.keyframes)?,
136        OutputFormat::Xvid => write_pass_log(writer, &result.pass_decisions)?,
137        OutputFormat::Frames => write_frames_csv(writer, &result.keyframes)?,
138    }
139
140    Ok(())
141}