forked from jsl/video_editing_poc
Cinematic pass: 2.39 letterbox, film grain, optional voiceover
Push the highlight output toward a genuinely cinematic look after a human review found it functional-but-not-cinematic: - HighlightFfmpegRenderer: add a 2.39:1 letterbox and subtle film grain to each graded segment; raise overlay placement above the letterbox bar. (A time-based crop-zoom push-in was prototyped and removed: FFmpeg crop cannot use the `t` variable for width/height; a zoompan push-in is a possible follow-up.) - HighlightDirectorPlanValidator: make voiceover OPTIONAL so a music-driven edit can carry no narration (lines still validated when present). Combined with a no-voiceover, driving-orchestral, slow-motion-hero director plan, this yields a letterboxed, richly graded, music-led cinematic cut. mvn -o verify green (247 tests, 0 failures). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR
This commit is contained in:
parent
3221b9827b
commit
842f2a7404
|
|
@ -140,9 +140,9 @@ public class HighlightDirectorPlanValidator {
|
|||
requiredText(highlight.musicDirection(), MAX_DIRECTION_LENGTH, "Music direction");
|
||||
requiredText(highlight.sfxDirection(), MAX_DIRECTION_LENGTH, "SFX direction");
|
||||
requiredText(highlight.renderNotes(), MAX_DIRECTION_LENGTH, "Render notes");
|
||||
if (highlight.voiceover() == null || highlight.voiceover().isEmpty()) {
|
||||
reject("At least one grounded voiceover line is required");
|
||||
}
|
||||
// Voiceover is optional: a music-driven cinematic edit may carry no narration. When present,
|
||||
// every line must be non-blank and the script must fit the highlight timing budget.
|
||||
if (highlight.voiceover() != null && !highlight.voiceover().isEmpty()) {
|
||||
for (String line : highlight.voiceover()) {
|
||||
requiredText(line, MAX_VOICEOVER_LINE_LENGTH, "Voiceover line");
|
||||
}
|
||||
|
|
@ -152,6 +152,7 @@ public class HighlightDirectorPlanValidator {
|
|||
if (voiceoverDuration > highlight.targetDurationSeconds() + EPSILON) {
|
||||
reject("Voiceover script exceeds the highlight timing budget");
|
||||
}
|
||||
}
|
||||
if (highlight.overlays() == null) {
|
||||
reject("Highlight overlays list is required");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,6 +193,11 @@ public class HighlightFfmpegRenderer {
|
|||
properties.getOutputWidth(), properties.getOutputHeight()));
|
||||
filter.append(",setpts=PTS/").append(decision.playbackSpeed());
|
||||
filter.append(cinematicVisualFilter(decision.visualTreatment(), style));
|
||||
if (!(decision.visualTreatment() == null || decision.visualTreatment().isBlank()
|
||||
|| "none".equalsIgnoreCase(decision.visualTreatment()))) {
|
||||
filter.append(",noise=alls=6:allf=t"); // subtle film grain
|
||||
filter.append(letterboxFilter()); // 2.39:1 cinematic bars
|
||||
}
|
||||
double fadeDuration = Math.min(0.5, outputDuration / 2);
|
||||
if ("fade-in".equals(decision.transitionIn()) || "crossfade".equals(decision.transitionIn())) {
|
||||
filter.append(",fade=t=in:st=0:d=").append(fadeDuration);
|
||||
|
|
@ -567,6 +572,15 @@ public class HighlightFfmpegRenderer {
|
|||
return "crop=w='iw*0.96':h='ih*0.96':x='(iw-out_w)/2':y='(ih-out_h)/2',";
|
||||
}
|
||||
|
||||
// 2.39:1 cinematic letterbox: crop the center band, then pad back to frame with black bars.
|
||||
private String letterboxFilter() {
|
||||
int w = properties.getOutputWidth();
|
||||
int h = properties.getOutputHeight();
|
||||
int barHeight = (int) Math.round((h - w / 2.39) / 2.0);
|
||||
int bandHeight = h - 2 * barHeight;
|
||||
return ",crop=%d:%d,pad=%d:%d:0:%d:color=black".formatted(w, bandHeight, w, h, barHeight);
|
||||
}
|
||||
|
||||
private String audioTempoFilter(double speed) {
|
||||
if (speed < 0.5) {
|
||||
return "atempo=0.5,atempo=" + (speed / 0.5);
|
||||
|
|
@ -598,9 +612,9 @@ public class HighlightFfmpegRenderer {
|
|||
|
||||
private String overlayY(String placement) {
|
||||
return switch (placement) {
|
||||
case "upper_left_safe", "upper_right_safe" -> "h*0.08";
|
||||
case "upper_left_safe", "upper_right_safe" -> "h*0.14";
|
||||
case "center_safe" -> "(h-text_h)/2";
|
||||
default -> "h-text_h-h*0.10";
|
||||
default -> "h-text_h-h*0.18"; // sit above the 2.39:1 letterbox bar
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue