fix(render): loop a bounded music bed so long reels render (MusicGen CPU cap)

A multi-segment reel needs music as long as the reel, but MusicGen-small stalls on
CPU past ~45s (confirmed on the 49s downhill/soccer reels). Decouple music-generation
length from reel length: generate a bounded bed (music-gen-max-seconds, default 15s)
and loop it across the timeline (-stream_loop -1 on the music input; the existing
atrim bounds it to the cue). Reel length stays uncapped; music generation stays
feasible. Verified: 7-segment 45s downhill reel now renders (14.9s bed looped,
1080x1920/24fps/yuv420p/-15.6 LUFS).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
JSLMPR 2026-07-27 03:08:15 +02:00
parent 5b98c0538f
commit e6a7aecd97
3 changed files with 24 additions and 2 deletions

View File

@ -545,6 +545,13 @@ public class VideoClippingProperties {
/** R12 cinematic motion blur (light frame blend) in styled shots. Emulates shutter-angle blur. */
private boolean cinematicMotionBlur = true;
/**
* Max seconds of music to GENERATE. Local MusicGen can't reliably produce long scores on CPU (it stalls
* ~45s+), so a longer reel generates a bounded bed and the renderer loops it to fill the timeline. This
* decouples reel length from music-generation feasibility the reel itself stays uncapped.
*/
private double musicGenMaxSeconds = 15.0;
/** R13 snap montage cut boundaries onto the generated score's beat grid (cut-to-the-music). */
private boolean beatSyncEnabled = false;
@ -787,6 +794,14 @@ public class VideoClippingProperties {
this.cinematicMotionBlur = cinematicMotionBlur;
}
public double getMusicGenMaxSeconds() {
return musicGenMaxSeconds;
}
public void setMusicGenMaxSeconds(double musicGenMaxSeconds) {
this.musicGenMaxSeconds = musicGenMaxSeconds;
}
public boolean isBeatSyncEnabled() {
return beatSyncEnabled;
}

View File

@ -26,6 +26,7 @@ public class HighlightAssetPreparationService {
private final EditAssetProvider assetProvider;
private final EditAssetLibrary assetLibrary;
private final VideoClippingProperties.Editing.Assets assets;
private final double musicGenMaxSeconds;
private final ObjectMapper objectMapper;
public HighlightAssetPreparationService(VideoClippingProperties properties, HighlightProjectStore store,
@ -35,6 +36,7 @@ public class HighlightAssetPreparationService {
this.assetProvider = assetProvider;
this.assetLibrary = assetLibrary;
this.assets = properties.getEditing().getAssets();
this.musicGenMaxSeconds = properties.getEditing().getMusicGenMaxSeconds();
this.objectMapper = objectMapper;
}
@ -61,9 +63,12 @@ public class HighlightAssetPreparationService {
copy(Path.of(music.get().path()), target);
resolvedAssets.add(target.toString());
} else {
// Generate a BOUNDED music bed (MusicGen stalls on long scores); the renderer loops it to fill the
// reel. The reel length itself is unaffected.
double musicSeconds = Math.min(highlight.targetDurationSeconds(), musicGenMaxSeconds);
requestFiles.add(writeRequest(requestDirectory, projectId, highlight.highlightId(), "music",
"music_bed", highlight.musicDirection(), musicDirectory.resolve("music.wav"),
highlight.targetDurationSeconds(), false));
musicSeconds, false));
}
if (!highlight.voiceover().isEmpty()) {

View File

@ -631,7 +631,9 @@ public class HighlightFfmpegRenderer {
}
}
if (music != null) {
command.addAll(List.of("-i", music.toString()));
// Loop the (bounded) generated bed so it fills the whole reel; the atrim below bounds it to the cue.
// Lets a long reel use a short, feasible-to-generate score instead of one impractically-long score.
command.addAll(List.of("-stream_loop", "-1", "-i", music.toString()));
filters.append("[").append(input).append(":a]");
if (musicCue != null) {
double duration = musicCue.timelineEndSeconds() - musicCue.timelineStartSeconds();