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
68pub 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
86pub 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
104pub 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
125pub 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}