fix(intro): precompose alpha-adjusted logo PNG and overlay to respect transparency across ffmpeg builds

This commit is contained in:
hbrain 2026-05-28 19:20:56 +00:00
parent 801f17ef10
commit 192c57ee13

View file

@ -31,11 +31,15 @@ IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
VIDEO_EXTS = {".mp4", ".mov", ".mkv", ".avi", ".webm", ".m4v"} VIDEO_EXTS = {".mp4", ".mov", ".mkv", ".avi", ".webm", ".m4v"}
AUDIO_EXTS = {".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg"} AUDIO_EXTS = {".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg"}
DATA_NAMES = {"data.txt", "info.txt", "title.txt"} DATA_NAMES = {"data.txt", "info.txt", "title.txt"}
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
FFMPEG = BASE_DIR / "ffmpeg" / "ffmpeg" FFMPEG = BASE_DIR / "ffmpeg" / "ffmpeg"
FFPROBE = BASE_DIR / "ffmpeg" / "ffprobe" FFPROBE = BASE_DIR / "ffmpeg" / "ffprobe"
INTRO_LOGO_NAME = "intro.png" INTRO_LOGO_NAME = "intro.png"
DEFAULT_INTRO_LOGO = BASE_DIR / "img" / "moto_travel.png" DEFAULT_INTRO_LOGO = BASE_DIR / "img" / "moto_travel.png"
JAMENDO_ENV = Path.home() / ".config" / "movmaker" / "jamendo.env"
LOG_PATH: Path | None = None
INTRO_LOGO_TARGET_HEIGHT_RATIO = 0.99 INTRO_LOGO_TARGET_HEIGHT_RATIO = 0.99
INTRO_LOGO_EXTRA_HOLD = 6.0 INTRO_LOGO_EXTRA_HOLD = 6.0
INTRO_LOGO_FADE_OFFSET = 3.0 INTRO_LOGO_FADE_OFFSET = 3.0
@ -56,8 +60,6 @@ CLIP_AUDIO_SIDECHAIN_THRESHOLD = 0.04
CLIP_AUDIO_SIDECHAIN_RATIO = 8.0 CLIP_AUDIO_SIDECHAIN_RATIO = 8.0
CLIP_AUDIO_SIDECHAIN_ATTACK = 5.0 CLIP_AUDIO_SIDECHAIN_ATTACK = 5.0
CLIP_AUDIO_SIDECHAIN_RELEASE = 250.0 CLIP_AUDIO_SIDECHAIN_RELEASE = 250.0
JAMENDO_ENV = Path.home() / ".config" / "movmaker" / "jamendo.env"
LOG_PATH: Path | None = None
def tool_path(local_path: Path, fallback: str) -> str: def tool_path(local_path: Path, fallback: str) -> str:
@ -707,6 +709,11 @@ def drawtext_escape(text: str) -> str:
def render_intro_logo_frame(logo: Path, out: Path, width: int, height: int, alpha: float) -> None: def render_intro_logo_frame(logo: Path, out: Path, width: int, height: int, alpha: float) -> None:
"""Build a single black frame with the intro logo centered and scaled. """Build a single black frame with the intro logo centered and scaled.
This implementation first renders a temporary PNG of the logo with the
requested alpha applied, then overlays that precomposed PNG onto a black
background. This avoids passing problematic options to the overlay filter
and works reliably across ffmpeg builds.
Args: Args:
logo: Source PNG to overlay. logo: Source PNG to overlay.
out: Destination PNG path for the composed frame. out: Destination PNG path for the composed frame.
@ -718,17 +725,30 @@ def render_intro_logo_frame(logo: Path, out: Path, width: int, height: int, alph
raise ValueError("intro logo frame must have positive width/height") raise ValueError("intro logo frame must have positive width/height")
alpha = min(1.0, max(0.0, alpha)) alpha = min(1.0, max(0.0, alpha))
target_logo_height = max(1, int(round(height * INTRO_LOGO_TARGET_HEIGHT_RATIO))) target_logo_height = max(1, int(round(height * INTRO_LOGO_TARGET_HEIGHT_RATIO)))
filters = (
f"[1:v]scale=w={width}:h={target_logo_height}:force_original_aspect_ratio=decrease," ffmpeg_exe = tool_path(FFMPEG, "ffmpeg")
f"format=rgba,colorchannelmixer=aa={alpha:.3f}[logo];"
f"[0:v]format=rgba[bg];" # Prepare a temporary logo PNG with the requested alpha applied.
f"[bg][logo]overlay=x=(W-w)/2:y=(H-h)/2:format=rgba[vout]" tmp_logo = out.with_name(out.stem + "_alpha.png")
) cmd_logo = [
cmd = [ ffmpeg_exe, "-y",
tool_path(FFMPEG, "ffmpeg"), "-y",
"-f", "lavfi", "-i", f"color=c=black:s={width}x{height}",
"-i", str(logo), "-i", str(logo),
"-filter_complex", filters, "-vf", (
f"scale=w={width}:h={target_logo_height}:force_original_aspect_ratio=decrease,"
f"format=rgba,colorchannelmixer=aa={alpha:.3f}"
),
"-frames:v", "1",
"-pix_fmt", "rgba",
str(tmp_logo),
]
run(cmd_logo)
# Overlay the precomposed logo onto a black background and write the single frame.
cmd = [
ffmpeg_exe, "-y",
"-f", "lavfi", "-i", f"color=c=black:s={width}x{height}",
"-i", str(tmp_logo),
"-filter_complex", "[0:v][1:v]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[vout]",
"-map", "[vout]", "-map", "[vout]",
"-frames:v", "1", "-frames:v", "1",
"-pix_fmt", "rgba", "-pix_fmt", "rgba",
@ -736,7 +756,14 @@ def render_intro_logo_frame(logo: Path, out: Path, width: int, height: int, alph
"-update", "1", "-update", "1",
str(out), str(out),
] ]
run(cmd) try:
run(cmd)
finally:
try:
if tmp_logo.exists():
tmp_logo.unlink()
except Exception:
pass
def normalize_clip(item: MediaItem, out: Path, width: int, height: int, fps: int, preset: str, crf: int) -> None: def normalize_clip(item: MediaItem, out: Path, width: int, height: int, fps: int, preset: str, crf: int) -> None: