Minor updates

This commit is contained in:
hbrain 2026-05-25 11:51:09 +00:00
parent e512b5012b
commit 8a0901590b
2 changed files with 36 additions and 20 deletions

View file

@ -23,7 +23,8 @@ sudo apt install ffmpeg
The default title/stamp styling uses these fonts:
- Bebas Neue for the opening title, date, and location
- Noto Sans Bold for the bottom-left/bottom-right stamps and per-file captions
- Noto Sans Bold for the bottom-left/bottom-right stamps
- IBM Plex Sans SemiBold for per-file captions
Both fonts support common Danish and Croatian characters such as `æ ø å Æ Ø Å č ć ž š đ Č Ć Ž Š Đ`.
@ -33,7 +34,7 @@ Install them locally for the current user:
mkdir -p ~/.local/share/fonts/google
curl -L -o ~/.local/share/fonts/google/BebasNeue-Regular.ttf \
'https://github.com/google/fonts/raw/main/ofl/bebasneue/BebasNeue-Regular.ttf'
sudo apt install fonts-noto-core
sudo apt install fonts-noto-core fonts-ibm-plex
fc-cache -f
```
@ -42,6 +43,7 @@ Check that fonts are available:
```bash
fc-match 'Bebas Neue'
fc-match 'Noto Sans'
fc-match 'IBM Plex Sans'
```
## Optional Jamendo music download
@ -124,7 +126,7 @@ Those three lines mean:
If the date line is omitted, movmaker uses the date from the first picture/video and displays it like:
```text
May 14th 2026
May 2026
```
If the location line is omitted and GPS coordinates are found in the first picture/video that has them, movmaker reverse geocodes them and uses that as the location. It tries to return only `city/town/village, country`; if no city/town/village is found, it uses just the country.
@ -188,7 +190,7 @@ python3 movmaker.py input --preview
- fades music in over 2 seconds and out over 10 seconds by default
- fades video in/out over 1.5 seconds by default
- overlays the opening title for the first 6 seconds: large title, then date/location on one line
- displays optional per-file captions at bottom center with a thin black outline; use `|` to split caption lines
- 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
- uses safe ASCII only for generated filenames, so special letters become readable equivalents like `Č` -> `C`, `å` -> `a`, `ø` -> `o`

View file

@ -466,7 +466,7 @@ def ordinal_day(day: int) -> str:
def display_date(dt: datetime) -> str:
return f"{dt.strftime('%B')} {ordinal_day(dt.day)} {dt.year}"
return f"{dt.strftime('%B')} {dt.year}"
def timestamp() -> str:
@ -477,14 +477,6 @@ def console(message: str) -> None:
print(f"[{timestamp()}] {ascii_safe(message)}")
def estimate_output_size(duration: float, width: int, height: int, fps: int, crf: int) -> int:
pixels = width * height
quality_factor = max(0.45, (28 - crf) / 8)
bitrate_mbps = max(2.0, pixels * fps * quality_factor / 7_000_000)
audio_mbps = 0.192
return int(duration * (bitrate_mbps + audio_mbps) * 1_000_000 / 8)
def default_output_path(first_media: Path, data: MovieData, out_dir: Path) -> Path:
date = media_date(first_media).strftime("%Y%m%d")
title = slugify(data.title or first_media.parent.name or "movie")
@ -601,6 +593,22 @@ def normalize_clip(item: MediaItem, out: Path, width: int, height: int, fps: int
run(cmd)
def audio_metadata(path: Path) -> tuple[str, str]:
try:
out = capture([
tool_path(FFPROBE, "ffprobe"), "-v", "error",
"-show_entries", "format_tags=artist,title",
"-of", "json",
str(path),
])
tags = json.loads(out).get("format", {}).get("tags", {})
artist = tags.get("artist") or tags.get("ARTIST") or ""
title = tags.get("title") or tags.get("TITLE") or ""
return str(artist), str(title)
except Exception:
return "", ""
def load_jamendo_client_id() -> str | None:
if os.environ.get("JAMENDO_CLIENT_ID"):
return os.environ["JAMENDO_CLIENT_ID"]
@ -667,6 +675,8 @@ def download_jamendo_music(genre: str, out_dir: Path) -> Path | None:
except Exception as exc:
console(f"Could not download Jamendo music: {exc}")
return None
if title or artist:
console("Audio metadata: " + " - ".join(x for x in [artist, title] if x))
log_dir = out_dir / ".logs"
log_dir.mkdir(parents=True, exist_ok=True)
attribution = log_dir / f"{out.stem}.txt"
@ -770,7 +780,7 @@ def build_video(
title_label = "titled"
title_font = str(Path.home() / ".local/share/fonts/google/BebasNeue-Regular.ttf")
stamp_font = "/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf"
caption_font = "Montserrat\\:style=Bold"
caption_font = "/usr/share/fonts/truetype/ibm-plex/IBMPlexSans-SemiBold.ttf"
if title_lines:
title_font_size = max(60, height // 10)
subtitle_font_size = max(40, height // 18)
@ -839,8 +849,8 @@ def build_video(
y_expr = f"h-text_h-62-{block_height - line_idx * line_gap}"
next_label = f"caption{idx}_{line_idx}"
filters.append(
f"[{caption_label}]drawtext=font='{caption_font}':{textfile_arg(caption_line)}:x=(w-text_w)/2:y={y_expr}:"
f"fontsize={caption_size}:fontcolor=white:borderw=1:bordercolor=black:"
f"[{caption_label}]drawtext=fontfile='{caption_font}':{textfile_arg(caption_line)}:x=(w-text_w)/2:y={y_expr}:"
f"fontsize={caption_size}:fontcolor=0xF2F2EE:borderw=0:shadowcolor=black@0.80:shadowx=3:shadowy=3:"
f"alpha='{caption_alpha}':enable='between(t,{start:.3f},{end:.3f})'[{next_label}]"
)
caption_label = next_label
@ -958,6 +968,10 @@ def main(argv: list[str] | None = None) -> int:
console(f"Found {len(media)} media file(s), {len(audio_files)} audio file(s).")
console(f"Audio: {audio_files[0] if audio_files else 'none'}")
if audio_files:
artist, title = audio_metadata(audio_files[0])
if artist or title:
console("Audio metadata: " + " - ".join(x for x in [artist, title] if x))
console(f"Output: {output}")
console(f"Log: {LOG_PATH}")
if data.title or data.date or data.place:
@ -989,14 +1003,14 @@ def main(argv: list[str] | None = None) -> int:
# Use probed normalized duration for accuracy.
durations = [ffprobe_duration(out) for out in clips]
estimated_duration = durations[0] if len(durations) == 1 else durations[0] + sum(durations[1:]) - fade * (len(durations) - 1)
estimated_size = estimate_output_size(estimated_duration, width, height, fps, crf)
captions = [data.captions.get(item.path.name, data.captions.get(str(item.path), "")) for item in media]
audio = concat_or_mix_audio(audio_files, tmp / "music.m4a")
if len(audio_files) > 1:
console(f"Audio: {audio if audio else 'none'}")
console(f"Estimated output: video length {estimated_duration:.1f}s, size about {format_bytes(estimated_size)}")
if audio:
artist, title = audio_metadata(audio)
if artist or title:
console("Audio metadata: " + " - ".join(x for x in [artist, title] if x))
build_video(clips, durations, audio, output, data, first_media_date, captions, fade, audio_fade, audio_fade_in, video_fade, width, height, fps, preset, crf, tmp)
console(f"Done: {output}")