Reels: fix steppy fade (fps before fade) and ease the expression bump
The fade looked steppy/low-frame-rate because the filtergraph normalized fps AFTER the fade filters: the brightness ramp was sampled at the looped still's coarse input cadence, then duplicated up to 30fps. Move fps ahead of the fades, pin the still's input framerate (-framerate), and force CFR output (-r) so the dip ramps across a full 30 frames and plays steadily. Ease narration expressiveness from 0.7 to 0.6 (still tunable via REEL_TTS_EXAGGERATION). Bump RENDER_VERSION so existing reels re-render. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+5
-5
@@ -180,17 +180,17 @@ fn finish_job(
|
||||
|
||||
/// Render version: bump to invalidate every cached reel after a rendering /
|
||||
/// 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
|
||||
/// over the ~0.5 default warms up otherwise-flat narration; tune via
|
||||
/// `REEL_TTS_EXAGGERATION` (0.25–2.0).
|
||||
/// Narration expressiveness — Chatterbox's `exaggeration` knob. A slight bump
|
||||
/// over the ~0.5 default warms up otherwise-flat narration without over-acting;
|
||||
/// tune via `REEL_TTS_EXAGGERATION` (0.25–2.0).
|
||||
fn reel_tts_exaggeration() -> f32 {
|
||||
std::env::var("REEL_TTS_EXAGGERATION")
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<f32>().ok())
|
||||
.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
|
||||
|
||||
+28
-2
@@ -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
|
||||
// segment still gets a valid (non-negative) start time.
|
||||
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!(
|
||||
"[0:v]split=2[bg][fg];\
|
||||
[bg]scale={w}:{h}:force_original_aspect_ratio=increase,\
|
||||
crop={w}:{h},boxblur=20:2[bgb];\
|
||||
[fg]scale={w}:{h}:force_original_aspect_ratio=decrease[fgs];\
|
||||
[bgb][fgs]overlay=(W-w)/2:(H-h)/2,\
|
||||
fps={fps},\
|
||||
fade=t=in:st=0: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]"
|
||||
)
|
||||
}
|
||||
@@ -134,11 +139,16 @@ pub fn build_segment_args(
|
||||
duration: f64,
|
||||
opts: &SegmentOpts,
|
||||
) -> Vec<String> {
|
||||
let fps = opts.fps.to_string();
|
||||
let mut args: Vec<String> = vec!["-y".into()];
|
||||
if opts.nvenc {
|
||||
args.extend(["-hwaccel".into(), "cuda".into()]);
|
||||
}
|
||||
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(),
|
||||
"1".into(),
|
||||
"-i".into(),
|
||||
@@ -153,6 +163,10 @@ pub fn build_segment_args(
|
||||
"[a]".into(),
|
||||
"-t".into(),
|
||||
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(
|
||||
@@ -297,6 +311,16 @@ mod tests {
|
||||
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]
|
||||
fn filtergraph_fade_out_start_never_negative_at_floor() {
|
||||
// A floor-length segment shorter than a fade still yields st >= 0.
|
||||
@@ -314,10 +338,12 @@ mod tests {
|
||||
&SegmentOpts::default(),
|
||||
);
|
||||
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("apad"));
|
||||
assert!(joined.contains("-t 4.000"));
|
||||
// Constant frame rate forced on the output.
|
||||
assert!(joined.contains("-r 30"));
|
||||
assert!(joined.contains("libx264"));
|
||||
assert!(joined.ends_with("/out.mp4"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user