diff --git a/README.md b/README.md index c441e3f..c4d0f2c 100644 --- a/README.md +++ b/README.md @@ -256,9 +256,9 @@ Adjust these constants and rerun `movmaker.py` to change the intro behavior with - if no audio file exists in the input folder, searches Jamendo using `--music-genre` and downloads one random Creative Commons track that allows non-commercial reuse and derivative works; default query is `cinematic punk rock` - reads the Jamendo client ID from `JAMENDO_CLIENT_ID` or `~/.config/movmaker/jamendo.env` - fades music in over `DEFAULT_AUDIO_FADE_IN` seconds (2 s) and out over `DEFAULT_AUDIO_FADE_OUT` seconds (10 s) by default -- builds a black intro still (matching the first media item’s resolution) when `--intro-logo` is provided, scales the logo to `INTRO_LOGO_TARGET_HEIGHT_RATIO` of the frame height (75 % by default), shows it at `INTRO_LOGO_DEFAULT_ALPHA` opacity (1.0 = fully opaque) from frame 0, and keeps it on screen for `audio_fade_in + INTRO_LOGO_EXTRA_HOLD` seconds (with defaults this is `DEFAULT_AUDIO_FADE_IN + INTRO_LOGO_EXTRA_HOLD`, i.e. 8 s) before allowing the first crossfade +- builds a black intro still (matching the first media item’s resolution) when `--intro-logo` is provided, scales the logo to `INTRO_LOGO_TARGET_HEIGHT_RATIO` of the frame height (75 % by default), shows it at `INTRO_LOGO_DEFAULT_ALPHA` opacity (1.0 = fully opaque) from frame 0, and keeps it on screen for `audio_fade_in + INTRO_LOGO_EXTRA_HOLD` seconds (with defaults this is `DEFAULT_AUDIO_FADE_IN + INTRO_LOGO_EXTRA_HOLD`, i.e. 8 s) before allowing the first crossfade; the same clip is appended at the end so the last real scene crossfades back into the logo-on-black frame - opening title text fades in after `INTRO_TITLE_FADE_DELAY` seconds (2 s), stays visible for `INTRO_TITLE_VISIBLE` seconds (4 s), then fades out over `INTRO_TITLE_FADE_LENGTH` seconds (1 s) -- fades the first real clip in and the final clip out with the `--video-fade` duration (default `DEFAULT_VIDEO_FADE`, currently 1 s); the black intro/logo pad respects this fade-in only when `INTRO_VIDEO_FADE_ENABLED` is `True` +- fades the first real clip in with the `--video-fade` duration (default `DEFAULT_VIDEO_FADE`, currently 1 s) while leaving the logo intro instant-on when `INTRO_VIDEO_FADE_ENABLED` is `False`; when a logo intro is present, the reusable outro clip removes the need for a final global fade-out, so the crossfade into the outro handles the closing visuals while audio still follows `DEFAULT_AUDIO_FADE_OUT` - displays optional per-file captions at bottom center with slightly off-white text and a soft shadow; use `|` to split caption lines - adds persistent bottom-left title/date/place and bottom-right `Bubulescu.Org` stamps - preserves special characters in rendered overlays and metadata diff --git a/movmaker.py b/movmaker.py index 17d5ab4..8d5c863 100755 --- a/movmaker.py +++ b/movmaker.py @@ -36,7 +36,7 @@ FFMPEG = BASE_DIR / "ffmpeg" / "ffmpeg" FFPROBE = BASE_DIR / "ffmpeg" / "ffprobe" INTRO_LOGO_NAME = "intro.png" DEFAULT_INTRO_LOGO = BASE_DIR / "img" / "moto_travel.png" -INTRO_LOGO_TARGET_HEIGHT_RATIO = 0.75 +INTRO_LOGO_TARGET_HEIGHT_RATIO = 0.90 INTRO_LOGO_EXTRA_HOLD = 6.0 INTRO_LOGO_FADE_OFFSET = 3.0 INTRO_LOGO_DEFAULT_ALPHA = 1.0 @@ -883,6 +883,7 @@ def build_video( text_dir: Path, video_intro_duration: float | None = None, intro_visible_lead: float = 0.0, + skip_video_fade_out: bool = False, ) -> None: output.parent.mkdir(parents=True, exist_ok=True) @@ -937,7 +938,7 @@ def build_video( fade_label = current apply_fade_in = video_fade > 0 and INTRO_VIDEO_FADE_ENABLED - apply_fade_out = video_fade > 0 + apply_fade_out = video_fade > 0 and not skip_video_fade_out if apply_fade_in or apply_fade_out: fade_in_start = intro_duration fade_out_start = max(0.0, total_duration - video_fade) @@ -961,34 +962,44 @@ def build_video( subtitle_font_size = max(40, height // 18) main_title = data.title or title_lines[0] subtitle = " | ".join(x for x in [data.date, data.place] if x) - title_alpha = "if(lt(t\\,2)\\,0\\,if(lt(t\\,3)\\,(t-2)/1\\,if(lt(t\\,7)\\,1\\,if(lt(t\\,8)\\,8-t\\,0))))" + fade_in_start = INTRO_TITLE_FADE_DELAY + fade_in_end = fade_in_start + INTRO_TITLE_FADE_LENGTH + visible_end = fade_in_end + INTRO_TITLE_VISIBLE + fade_out_end = visible_end + INTRO_TITLE_FADE_LENGTH + title_alpha = ( + f"if(lt(t\\,{fade_in_start:.3f})\\,0\\," + f"if(lt(t\\,{fade_in_end:.3f})\\,(t-{fade_in_start:.3f})/{INTRO_TITLE_FADE_LENGTH:.3f}\\," + f"if(lt(t\\,{visible_end:.3f})\\,1\\," + f"if(lt(t\\,{fade_out_end:.3f})\\,({fade_out_end:.3f}-t)/{INTRO_TITLE_FADE_LENGTH:.3f}\\,0))))" + ) + title_enable = f"enable='between(t,{fade_in_start:.3f},{fade_out_end:.3f})'" if subtitle: title_y = f"(h-text_h)/2-{title_font_size // 2}" filters.append( f"[{current}]drawtext=fontfile='{title_font}':{textfile_arg(main_title)}:x=(w-text_w)/2:y={title_y}:" f"fontsize={title_font_size}:fontcolor=white:borderw=0:shadowcolor=black@0.75:shadowx=3:shadowy=3:" - f"alpha='{title_alpha}':enable='between(t,0,7)'[title0]" + f"alpha='{title_alpha}':{title_enable}[title0]" ) filters.append( f"[title0]drawtext=fontfile='{title_font}':{textfile_arg(main_title)}:x=(w-text_w)/2+1:y={title_y}:" f"fontsize={title_font_size}:fontcolor=white:borderw=0:shadowcolor=black@0.75:shadowx=3:shadowy=3:" - f"alpha='{title_alpha}':enable='between(t,0,7)'[title0b]" + f"alpha='{title_alpha}':{title_enable}[title0b]" ) filters.append( f"[title0b]drawtext=fontfile='{title_font}':{textfile_arg(subtitle)}:x=(w-text_w)/2:y=(h-text_h)/2+{subtitle_font_size}:" f"fontsize={subtitle_font_size}:fontcolor=white:borderw=0:shadowcolor=black@0.75:shadowx=2:shadowy=2:" - f"alpha='{title_alpha}':enable='between(t,0,7)'[{title_label}]" + f"alpha='{title_alpha}':{title_enable}[{title_label}]" ) else: filters.append( f"[{current}]drawtext=fontfile='{title_font}':{textfile_arg(main_title)}:x=(w-text_w)/2:y=(h-text_h)/2:" f"fontsize={title_font_size}:fontcolor=white:borderw=0:shadowcolor=black@0.75:shadowx=3:shadowy=3:" - f"alpha='{title_alpha}':enable='between(t,0,7)'[title0]" + f"alpha='{title_alpha}':{title_enable}[title0]" ) filters.append( f"[title0]drawtext=fontfile='{title_font}':{textfile_arg(main_title)}:x=(w-text_w)/2+1:y=(h-text_h)/2:" f"fontsize={title_font_size}:fontcolor=white:borderw=0:shadowcolor=black@0.75:shadowx=3:shadowy=3:" - f"alpha='{title_alpha}':enable='between(t,0,7)'[{title_label}]" + f"alpha='{title_alpha}':{title_enable}[{title_label}]" ) else: filters.append(f"[{current}]copy[{title_label}]") @@ -1265,6 +1276,8 @@ def main(argv: list[str] | None = None) -> int: console("Audio metadata: " + " - ".join(x for x in [artist, title] if x)) video_intro_duration = audio_fade_in intro_visible_lead = 0.0 + intro_clip_path: Path | None = None + intro_clip_duration = 0.0 if intro_logo_path is not None and audio_fade_in > 0: if first_media_resolution is None: console("Warning: Could not determine first media resolution; skipping intro logo frame") @@ -1278,12 +1291,22 @@ def main(argv: list[str] | None = None) -> int: normalize_clip(intro_item, intro_clip, width, height, fps, preset, crf) clips = [intro_clip, *clips] durations = [intro_item.duration, *durations] + intro_clip_path = intro_clip + intro_clip_duration = intro_item.duration captions = ["", *captions] clip_audio_paths = [None, *clip_audio_paths] clip_stamps = ["", *clip_stamps] intro_visible_lead = min(intro_item.duration, INTRO_LOGO_FADE_OFFSET) video_intro_duration = 0.0 - build_video(clips, durations, audio, clip_audio_paths, output, data, first_media_date, captions, clip_stamps, fade, audio_fade, audio_fade_in, video_fade, args.intro_logo, intro_logo_alpha, width, height, fps, preset, crf, tmp, video_intro_duration, intro_visible_lead=intro_visible_lead) + outro_added = False + if intro_clip_path is not None: + clips.append(intro_clip_path) + durations.append(intro_clip_duration) + captions.append("") + clip_audio_paths.append(None) + clip_stamps.append("") + outro_added = True + build_video(clips, durations, audio, clip_audio_paths, output, data, first_media_date, captions, clip_stamps, fade, audio_fade, audio_fade_in, video_fade, args.intro_logo, intro_logo_alpha, width, height, fps, preset, crf, tmp, video_intro_duration, intro_visible_lead=intro_visible_lead, skip_video_fade_out=outro_added) console(f"Done: {output}") return 0