Reels: portrait canvas with blurred fill, fade transitions, warmer TTS

Fixes the "image is tiny" problem: a 1920x1080 landscape reel letterboxes
to a ~25%-height band on a portrait phone. Switch to a portrait 1080x1920
canvas and fill it per photo with a blurred, zoomed copy of the image
behind the sharp fitted photo — so the frame is always full regardless of
the photo's orientation, with no black bars and no cropping of the subject.

Add a quick 0.35s fade in/out baked into each segment so concatenated
photos dip smoothly instead of hard-cutting (fade-out lands in the
narration's silent tail, so speech isn't clipped). Drop the unused
Ken Burns branch — motion can return deliberately later.

Warm up the narration a touch: thread Chatterbox's `exaggeration` through
synthesize_serialized and default reels to 0.7 (tunable via
REEL_TTS_EXAGGERATION). Bump RENDER_VERSION so existing landscape reels
re-render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cameron Cordes
2026-06-12 23:10:26 -04:00
parent 42453d5786
commit 7715a7a905
3 changed files with 101 additions and 66 deletions
+27 -11
View File
@@ -180,7 +180,18 @@ 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 = 1;
const RENDER_VERSION: u32 = 2;
/// 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.252.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)
}
/// Cache key over everything that determines *which* media and *how* it's
/// voiced — but not the (non-deterministic) narration text. Same inputs → same
@@ -470,16 +481,21 @@ async fn run_reel_job(
}
};
let audio_bytes =
match crate::ai::tts::synthesize_serialized(&client, line, voice.as_deref(), "wav")
.await
{
Ok(b) => b,
Err(e) => {
log::warn!("reel {job_id}: skipping segment {i}, TTS failed: {e}");
continue;
}
};
let audio_bytes = match crate::ai::tts::synthesize_serialized(
&client,
line,
voice.as_deref(),
"wav",
Some(reel_tts_exaggeration()),
)
.await
{
Ok(b) => b,
Err(e) => {
log::warn!("reel {job_id}: skipping segment {i}, TTS failed: {e}");
continue;
}
};
let audio_path = work.path().join(format!("narration_{i:03}.wav"));
if let Err(e) = tokio::fs::write(&audio_path, &audio_bytes).await {
log::warn!("reel {job_id}: skipping segment {i}, writing audio failed: {e}");