Feature/unified nl search #106

Open
cameron wants to merge 26 commits from feature/unified-nl-search into master
2 changed files with 33 additions and 7 deletions
Showing only changes of commit 740fc4d841 - Show all commits
+5 -5
View File
@@ -180,17 +180,17 @@ fn finish_job(
/// Render version: bump to invalidate every cached reel after a rendering / /// Render version: bump to invalidate every cached reel after a rendering /
/// scripting change that should produce a fresh result. /// scripting change that should produce a fresh result.
const RENDER_VERSION: u32 = 2; const RENDER_VERSION: u32 = 3;
/// Narration expressiveness — Chatterbox's `exaggeration` knob. A modest bump /// Narration expressiveness — Chatterbox's `exaggeration` knob. A slight bump
/// over the ~0.5 default warms up otherwise-flat narration; tune via /// over the ~0.5 default warms up otherwise-flat narration without over-acting;
/// `REEL_TTS_EXAGGERATION` (0.252.0). /// tune via `REEL_TTS_EXAGGERATION` (0.252.0).
fn reel_tts_exaggeration() -> f32 { fn reel_tts_exaggeration() -> f32 {
std::env::var("REEL_TTS_EXAGGERATION") std::env::var("REEL_TTS_EXAGGERATION")
.ok() .ok()
.and_then(|s| s.trim().parse::<f32>().ok()) .and_then(|s| s.trim().parse::<f32>().ok())
.filter(|x| x.is_finite()) .filter(|x| x.is_finite())
.unwrap_or(0.7) .unwrap_or(0.6)
} }
/// Cache key over everything that determines *which* media and *how* it's /// Cache key over everything that determines *which* media and *how* it's
+28 -2
View File
@@ -87,15 +87,20 @@ pub fn segment_filtergraph(opts: &SegmentOpts, duration: f64) -> String {
// Fade-out begins one fade-length before the end; clamp so a floor-length // Fade-out begins one fade-length before the end; clamp so a floor-length
// segment still gets a valid (non-negative) start time. // segment still gets a valid (non-negative) start time.
let fade_out_start = (duration - FADE_SECONDS).max(0.0); let fade_out_start = (duration - FADE_SECONDS).max(0.0);
// `fps` is normalized BEFORE the fades so the brightness ramp is computed
// on a true {fps}-frame timeline. If fps came after, the fade would be
// sampled at the looped still's coarse input cadence and then duplicated up
// to {fps}, which reads as a steppy / low-frame-rate dip.
format!( format!(
"[0:v]split=2[bg][fg];\ "[0:v]split=2[bg][fg];\
[bg]scale={w}:{h}:force_original_aspect_ratio=increase,\ [bg]scale={w}:{h}:force_original_aspect_ratio=increase,\
crop={w}:{h},boxblur=20:2[bgb];\ crop={w}:{h},boxblur=20:2[bgb];\
[fg]scale={w}:{h}:force_original_aspect_ratio=decrease[fgs];\ [fg]scale={w}:{h}:force_original_aspect_ratio=decrease[fgs];\
[bgb][fgs]overlay=(W-w)/2:(H-h)/2,\ [bgb][fgs]overlay=(W-w)/2:(H-h)/2,\
fps={fps},\
fade=t=in:st=0:d={FADE_SECONDS},\ fade=t=in:st=0:d={FADE_SECONDS},\
fade=t=out:st={fade_out_start:.3}:d={FADE_SECONDS},\ fade=t=out:st={fade_out_start:.3}:d={FADE_SECONDS},\
setsar=1,fps={fps},format=yuv420p[v];\ setsar=1,format=yuv420p[v];\
[1:a]apad[a]" [1:a]apad[a]"
) )
} }
@@ -134,11 +139,16 @@ pub fn build_segment_args(
duration: f64, duration: f64,
opts: &SegmentOpts, opts: &SegmentOpts,
) -> Vec<String> { ) -> Vec<String> {
let fps = opts.fps.to_string();
let mut args: Vec<String> = vec!["-y".into()]; let mut args: Vec<String> = vec!["-y".into()];
if opts.nvenc { if opts.nvenc {
args.extend(["-hwaccel".into(), "cuda".into()]); args.extend(["-hwaccel".into(), "cuda".into()]);
} }
args.extend([ args.extend([
// Read the looped still at the target rate so frames exist for the
// fade to ramp across (paired with the in-graph `fps` and CFR output).
"-framerate".into(),
fps.clone(),
"-loop".into(), "-loop".into(),
"1".into(), "1".into(),
"-i".into(), "-i".into(),
@@ -153,6 +163,10 @@ pub fn build_segment_args(
"[a]".into(), "[a]".into(),
"-t".into(), "-t".into(),
format!("{duration:.3}"), format!("{duration:.3}"),
// Force constant frame rate so the segment (and the concatenated reel)
// plays at a steady {fps} rather than a variable cadence.
"-r".into(),
fps,
]); ]);
args.extend(video_encoder_args(opts.nvenc)); args.extend(video_encoder_args(opts.nvenc));
args.extend( args.extend(
@@ -297,6 +311,16 @@ mod tests {
assert!(g.contains("fade=t=out:st=3.650:d=0.35")); assert!(g.contains("fade=t=out:st=3.650:d=0.35"));
} }
#[test]
fn filtergraph_normalizes_fps_before_fading() {
// The fps filter must precede the fades, else the brightness ramp is
// sampled at the still's coarse cadence and looks steppy.
let g = segment_filtergraph(&SegmentOpts::default(), 4.0);
let fps_at = g.find("fps=30").expect("fps in graph");
let fade_at = g.find("fade=t=in").expect("fade in graph");
assert!(fps_at < fade_at);
}
#[test] #[test]
fn filtergraph_fade_out_start_never_negative_at_floor() { fn filtergraph_fade_out_start_never_negative_at_floor() {
// A floor-length segment shorter than a fade still yields st >= 0. // A floor-length segment shorter than a fade still yields st >= 0.
@@ -314,10 +338,12 @@ mod tests {
&SegmentOpts::default(), &SegmentOpts::default(),
); );
let joined = args.join(" "); let joined = args.join(" ");
assert!(joined.contains("-loop 1 -i /img.jpg")); assert!(joined.contains("-framerate 30 -loop 1 -i /img.jpg"));
assert!(joined.contains("-i /a.wav")); assert!(joined.contains("-i /a.wav"));
assert!(joined.contains("apad")); assert!(joined.contains("apad"));
assert!(joined.contains("-t 4.000")); assert!(joined.contains("-t 4.000"));
// Constant frame rate forced on the output.
assert!(joined.contains("-r 30"));
assert!(joined.contains("libx264")); assert!(joined.contains("libx264"));
assert!(joined.ends_with("/out.mp4")); assert!(joined.ends_with("/out.mp4"));
} }