#![allow(clippy::doc_markdown)]
use std::path::PathBuf;
use bpaf::Bpaf;
use ffcine::breakdown::{self, Options};
use ffcine::triage::transcribe::WhisperParams;
#[derive(Debug, Clone, Bpaf)]
#[allow(clippy::struct_excessive_bools)] pub struct BreakdownArgs {
#[bpaf(short('o'), long("out"), argument("DIR"))]
out: Option<PathBuf>,
#[bpaf(argument("SCORE"), fallback(0.3), display_fallback)]
threshold: f32,
#[bpaf(argument("SEC"), fallback(0.2), display_fallback)]
min_shot: f64,
#[bpaf(argument("SEC"), fallback(0.07), display_fallback)]
beat_tolerance: f64,
#[bpaf(argument("PX"), fallback(960), display_fallback)]
keyframe_size: u32,
#[bpaf(argument("PX"), fallback(1800), display_fallback)]
sheet_width: u32,
#[bpaf(argument("N"))]
columns: Option<u32>,
no_beats: bool,
no_transcribe: bool,
#[bpaf(argument("NAME"), fallback("large-v3-turbo".to_owned()))]
model: String,
#[bpaf(argument("PATH"))]
whisper_python: Option<PathBuf>,
#[bpaf(argument("LANG"))]
language: Option<String>,
whisper_cpu: bool,
no_ocr: bool,
cpu_decode: bool,
#[bpaf(positional("VIDEO"))]
video: PathBuf,
}
impl BreakdownArgs {
pub fn run(self) -> anyhow::Result<()> {
anyhow::ensure!(
self.beat_tolerance >= 0.0,
"--beat-tolerance must be 0 or more"
);
anyhow::ensure!(
(0.0..=1.0).contains(&self.threshold),
"--threshold must be 0..1"
);
let out = self.out.clone().unwrap_or_else(|| {
let stem = self
.video
.file_stem()
.map_or_else(|| "video".into(), |s| s.to_string_lossy().into_owned());
self.video.with_file_name(format!("{stem}-breakdown"))
});
let mut o = Options::new(&self.video, &out);
o.gpu = !self.cpu_decode;
o.scene.threshold = self.threshold;
o.scene.min_gap = self.min_shot;
o.beats = !self.no_beats;
o.beat_tolerance = self.beat_tolerance;
o.keyframe_size = self.keyframe_size;
o.sheet_width = self.sheet_width;
o.sheet_columns = self.columns;
o.ocr = !self.no_ocr;
o.whisper = (!self.no_transcribe).then(|| {
let mut w = WhisperParams {
model: self.model.clone(),
language: self.language.clone().unwrap_or_default(),
..Default::default()
};
if let Some(p) = &self.whisper_python {
w.python.clone_from(p);
}
if self.whisper_cpu {
w.device = "cpu".into();
w.compute = "int8".into();
}
w
});
let b = breakdown::run(&o)?;
let p = &b.pacing;
eprint!(
"{} shots, mean {:.2} s, {:.1} cuts/min",
p.shots, p.mean, p.cuts_per_minute
);
if let Some(m) = &b.music {
eprint!(
", {:.0} BPM, {}/{} cuts on the beat",
m.tempo, m.on_beat, m.cuts
);
}
eprintln!();
for f in ["breakdown.json", "shotlist.md", "contact-sheet.jpg"] {
let path = out.join(f);
if path.exists() {
println!("{}", path.display());
}
}
Ok(())
}
}