Use per-clip metadata stamps and date ordering
This commit is contained in:
parent
0e7f309bd8
commit
5a69a31e89
1 changed files with 55 additions and 14 deletions
69
movmaker.py
69
movmaker.py
|
|
@ -424,16 +424,20 @@ def infer_place_from_gps(media: list[MediaItem]) -> str | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def media_date(path: Path) -> datetime:
|
def media_embedded_date(path: Path) -> datetime | None:
|
||||||
ext = path.suffix.lower()
|
ext = path.suffix.lower()
|
||||||
dt = None
|
|
||||||
if ext in IMAGE_EXTS:
|
if ext in IMAGE_EXTS:
|
||||||
try:
|
try:
|
||||||
dt = exif_image_date(path)
|
dt = exif_image_date(path)
|
||||||
|
if dt is not None:
|
||||||
|
return dt
|
||||||
except Exception:
|
except Exception:
|
||||||
dt = None
|
pass
|
||||||
if dt is None:
|
return ffprobe_creation_date(path)
|
||||||
dt = ffprobe_creation_date(path)
|
|
||||||
|
|
||||||
|
def media_date(path: Path) -> datetime:
|
||||||
|
dt = media_embedded_date(path)
|
||||||
if dt is None:
|
if dt is None:
|
||||||
dt = datetime.fromtimestamp(path.stat().st_mtime)
|
dt = datetime.fromtimestamp(path.stat().st_mtime)
|
||||||
return dt
|
return dt
|
||||||
|
|
@ -470,6 +474,26 @@ def display_date(dt: datetime) -> str:
|
||||||
return f"{dt.strftime('%B')} {dt.year}"
|
return f"{dt.strftime('%B')} {dt.year}"
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_clip_stamps(media: list[MediaItem], data: MovieData) -> list[str]:
|
||||||
|
geocode_cache: dict[tuple[float, float], str | None] = {}
|
||||||
|
stamps: list[str] = []
|
||||||
|
for item in media:
|
||||||
|
embedded_date = media_embedded_date(item.path)
|
||||||
|
date_text = display_date(embedded_date) if embedded_date else data.date
|
||||||
|
place_text = data.place
|
||||||
|
gps = media_gps(item.path)
|
||||||
|
if gps:
|
||||||
|
key = (round(gps[0], 4), round(gps[1], 4))
|
||||||
|
if key not in geocode_cache:
|
||||||
|
geocode_cache[key] = reverse_geocode_city_country(*gps)
|
||||||
|
place_text = geocode_cache[key] or place_text
|
||||||
|
stamp = " | ".join(x for x in [data.title, date_text, place_text] if x)
|
||||||
|
stamps.append(stamp)
|
||||||
|
if stamp:
|
||||||
|
console(f"Clip stamp: {item.path.name}: {ascii_safe(stamp)}")
|
||||||
|
return stamps
|
||||||
|
|
||||||
|
|
||||||
def timestamp() -> str:
|
def timestamp() -> str:
|
||||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
@ -515,6 +539,7 @@ def scan_input(input_dir: Path, image_duration: float) -> tuple[list[MediaItem],
|
||||||
if not media:
|
if not media:
|
||||||
raise SystemExit(f"No images or videos found in {input_dir}")
|
raise SystemExit(f"No images or videos found in {input_dir}")
|
||||||
|
|
||||||
|
media.sort(key=lambda item: (media_date(item.path), natural_key(item.path)))
|
||||||
return media, audio, parse_data_file(data_file)
|
return media, audio, parse_data_file(data_file)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -747,6 +772,7 @@ def build_video(
|
||||||
data: MovieData,
|
data: MovieData,
|
||||||
creation_date: datetime,
|
creation_date: datetime,
|
||||||
captions: list[str],
|
captions: list[str],
|
||||||
|
clip_stamps: list[str],
|
||||||
fade: float,
|
fade: float,
|
||||||
audio_fade: float,
|
audio_fade: float,
|
||||||
audio_fade_in: float,
|
audio_fade_in: float,
|
||||||
|
|
@ -899,16 +925,30 @@ def build_video(
|
||||||
caption_label = next_label
|
caption_label = next_label
|
||||||
|
|
||||||
stamp_size = max(16, height // 58)
|
stamp_size = max(16, height // 58)
|
||||||
stamp_lines = [x for x in [data.title, data.date, data.place] if x]
|
stamp_left_label = caption_label
|
||||||
stamp_text = drawtext_escape(" | ".join(stamp_lines))
|
for idx, stamp_text in enumerate(clip_stamps):
|
||||||
stamp_left_label = "stamp_left"
|
if not stamp_text:
|
||||||
if stamp_text:
|
continue
|
||||||
|
if n == 1:
|
||||||
|
start = intro_duration
|
||||||
|
end = total_duration
|
||||||
|
elif idx == 0:
|
||||||
|
start = intro_duration
|
||||||
|
end = intro_duration + transition_offsets[0]
|
||||||
|
elif idx == n - 1:
|
||||||
|
start = intro_duration + transition_offsets[idx - 1] + fade
|
||||||
|
end = total_duration
|
||||||
|
else:
|
||||||
|
start = intro_duration + transition_offsets[idx - 1] + fade
|
||||||
|
end = intro_duration + transition_offsets[idx]
|
||||||
|
if end <= start:
|
||||||
|
continue
|
||||||
|
next_label = f"stamp_left_{idx}"
|
||||||
filters.append(
|
filters.append(
|
||||||
f"[{caption_label}]drawtext=fontfile='{stamp_font}':{textfile_arg(' | '.join(stamp_lines))}:x=24:y=h-text_h-18:"
|
f"[{stamp_left_label}]drawtext=fontfile='{stamp_font}':{textfile_arg(stamp_text)}:x=24:y=h-text_h-18:"
|
||||||
f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{stamp_left_label}]"
|
f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black:enable='between(t,{start:.3f},{end:.3f})'[{next_label}]"
|
||||||
)
|
)
|
||||||
else:
|
stamp_left_label = next_label
|
||||||
filters.append(f"[{caption_label}]copy[{stamp_left_label}]")
|
|
||||||
filters.append(
|
filters.append(
|
||||||
f"[{stamp_left_label}]drawtext=fontfile='{stamp_font}':{textfile_arg('Bubulescu.Org')}:x=w-text_w-24:y=h-text_h-18:"
|
f"[{stamp_left_label}]drawtext=fontfile='{stamp_font}':{textfile_arg('Bubulescu.Org')}:x=w-text_w-24:y=h-text_h-18:"
|
||||||
f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{final_label}]"
|
f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{final_label}]"
|
||||||
|
|
@ -1049,6 +1089,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
# Use probed normalized duration for accuracy.
|
# Use probed normalized duration for accuracy.
|
||||||
durations = [ffprobe_duration(out) for out in clips]
|
durations = [ffprobe_duration(out) for out in clips]
|
||||||
captions = [data.captions.get(item.path.name, data.captions.get(str(item.path), "")) for item in media]
|
captions = [data.captions.get(item.path.name, data.captions.get(str(item.path), "")) for item in media]
|
||||||
|
clip_stamps = prepare_clip_stamps(media, data)
|
||||||
audio = concat_or_mix_audio(audio_files, tmp / "music.m4a")
|
audio = concat_or_mix_audio(audio_files, tmp / "music.m4a")
|
||||||
if len(audio_files) > 1:
|
if len(audio_files) > 1:
|
||||||
console(f"Audio: {audio if audio else 'none'}")
|
console(f"Audio: {audio if audio else 'none'}")
|
||||||
|
|
@ -1056,7 +1097,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
artist, title = audio_metadata(audio)
|
artist, title = audio_metadata(audio)
|
||||||
if artist or title:
|
if artist or title:
|
||||||
console("Audio metadata: " + " - ".join(x for x in [artist, title] if x))
|
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)
|
build_video(clips, durations, audio, output, data, first_media_date, captions, clip_stamps, fade, audio_fade, audio_fade_in, video_fade, width, height, fps, preset, crf, tmp)
|
||||||
|
|
||||||
console(f"Done: {output}")
|
console(f"Done: {output}")
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue