Support selected video clip audio
This commit is contained in:
parent
fa4c6dd95f
commit
8a87fe4cc6
1 changed files with 88 additions and 11 deletions
99
movmaker.py
99
movmaker.py
|
|
@ -49,6 +49,7 @@ class MovieData:
|
|||
place: str = ""
|
||||
description: str = ""
|
||||
captions: dict[str, str] | None = None
|
||||
video_audio: set[str] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -587,6 +588,12 @@ def parse_data_file(path: Path | None) -> MovieData:
|
|||
data.place = value
|
||||
elif low in {"description", "desc", "synopsis"}:
|
||||
data.description = value
|
||||
elif low.endswith(" audio"):
|
||||
file_key = key[:-6].strip()
|
||||
if file_key and value.lower() in {"1", "yes", "true", "on"}:
|
||||
if data.video_audio is None:
|
||||
data.video_audio = set()
|
||||
data.video_audio.add(file_key)
|
||||
elif key:
|
||||
data.captions[key] = value
|
||||
i += 1
|
||||
|
|
@ -649,6 +656,20 @@ def normalize_clip(item: MediaItem, out: Path, width: int, height: int, fps: int
|
|||
run(cmd)
|
||||
|
||||
|
||||
def media_has_audio(path: Path) -> bool:
|
||||
try:
|
||||
out = capture([
|
||||
tool_path(FFPROBE, "ffprobe"), "-v", "error",
|
||||
"-select_streams", "a:0",
|
||||
"-show_entries", "stream=index",
|
||||
"-of", "csv=p=0",
|
||||
str(path),
|
||||
])
|
||||
return bool(out.strip())
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def audio_metadata(path: Path) -> tuple[str, str]:
|
||||
try:
|
||||
out = capture([
|
||||
|
|
@ -770,6 +791,7 @@ def build_video(
|
|||
clips: list[Path],
|
||||
durations: list[float],
|
||||
audio: Path | None,
|
||||
clip_audio_paths: list[Path | None],
|
||||
output: Path,
|
||||
data: MovieData,
|
||||
creation_date: datetime,
|
||||
|
|
@ -801,6 +823,10 @@ def build_video(
|
|||
cmd += ["-i", str(clip)]
|
||||
if audio:
|
||||
cmd += ["-stream_loop", "-1", "-i", str(audio)]
|
||||
clip_audio_input_start = len(clips) + (1 if audio else 0)
|
||||
for clip_audio in clip_audio_paths:
|
||||
if clip_audio is not None:
|
||||
cmd += ["-i", str(clip_audio)]
|
||||
|
||||
filters: list[str] = []
|
||||
n = len(clips)
|
||||
|
|
@ -936,13 +962,13 @@ def build_video(
|
|||
end = total_duration
|
||||
elif idx == 0:
|
||||
start = intro_duration
|
||||
end = intro_duration + transition_offsets[0]
|
||||
end = intro_duration + transition_offsets[0] + fade / 2
|
||||
elif idx == n - 1:
|
||||
start = intro_duration + transition_offsets[idx - 1] + fade
|
||||
start = intro_duration + transition_offsets[idx - 1] + fade / 2
|
||||
end = total_duration
|
||||
else:
|
||||
start = intro_duration + transition_offsets[idx - 1] + fade
|
||||
end = intro_duration + transition_offsets[idx]
|
||||
start = intro_duration + transition_offsets[idx - 1] + fade / 2
|
||||
end = intro_duration + transition_offsets[idx] + fade / 2
|
||||
if end <= start:
|
||||
continue
|
||||
next_label = f"stamp_left_{idx}"
|
||||
|
|
@ -956,6 +982,48 @@ def build_video(
|
|||
f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{final_label}]"
|
||||
)
|
||||
|
||||
audio_mix_labels: list[str] = []
|
||||
if audio:
|
||||
audio_index = len(clips)
|
||||
audio_fade_out_start = max(0.0, total_duration - audio_fade)
|
||||
filters.append(
|
||||
f"[{audio_index}:a:0]atrim=0:{total_duration:.3f},asetpts=PTS-STARTPTS,"
|
||||
f"afade=t=in:st=0:d={audio_fade_in:.3f},afade=t=out:st={audio_fade_out_start:.3f}:d={audio_fade:.3f}[bg_audio]"
|
||||
)
|
||||
audio_mix_labels.append("[bg_audio]")
|
||||
clip_audio_input = clip_audio_input_start
|
||||
for idx, clip_audio in enumerate(clip_audio_paths):
|
||||
if clip_audio is None:
|
||||
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]
|
||||
segment = max(0.0, end - start)
|
||||
if segment <= 0:
|
||||
clip_audio_input += 1
|
||||
continue
|
||||
fade_len = min(fade, segment / 2)
|
||||
delay_ms = max(0, int(start * 1000))
|
||||
label = f"clip_audio_{idx}"
|
||||
filters.append(
|
||||
f"[{clip_audio_input}:a:0]atrim=0:{segment:.3f},asetpts=PTS-STARTPTS,"
|
||||
f"afade=t=in:st=0:d={fade_len:.3f},afade=t=out:st={max(0.0, segment - fade_len):.3f}:d={fade_len:.3f},"
|
||||
f"adelay={delay_ms}:all=1[{label}]"
|
||||
)
|
||||
audio_mix_labels.append(f"[{label}]")
|
||||
clip_audio_input += 1
|
||||
if audio_mix_labels:
|
||||
filters.append("".join(audio_mix_labels) + f"amix=inputs={len(audio_mix_labels)}:duration=longest:normalize=0[aout]")
|
||||
|
||||
cmd += ["-filter_complex", ";".join(filters), "-map", f"[{final_label}]"]
|
||||
metadata = {
|
||||
"title": data.title,
|
||||
|
|
@ -973,11 +1041,8 @@ def build_video(
|
|||
if value:
|
||||
cmd += ["-metadata", f"{key}={value}"]
|
||||
|
||||
if audio:
|
||||
audio_index = len(clips)
|
||||
audio_fade_out_start = max(0.0, total_duration - audio_fade)
|
||||
audio_filter = f"afade=t=in:st=0:d={audio_fade_in:.3f},afade=t=out:st={audio_fade_out_start:.3f}:d={audio_fade:.3f}"
|
||||
cmd += ["-map", f"{audio_index}:a:0", "-af", audio_filter, "-c:a", "aac", "-b:a", "192k", "-shortest"]
|
||||
if audio_mix_labels:
|
||||
cmd += ["-map", "[aout]", "-c:a", "aac", "-b:a", "192k", "-shortest"]
|
||||
else:
|
||||
cmd += ["-an"]
|
||||
|
||||
|
|
@ -993,7 +1058,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
parser.add_argument("--fade", type=float, default=3.0, help="crossfade duration in seconds")
|
||||
parser.add_argument("--audio-fade", type=float, default=10.0, help="music fade-out duration in seconds")
|
||||
parser.add_argument("--audio-fade-in", type=float, default=2.0, help="music fade-in duration in seconds")
|
||||
parser.add_argument("--video-fade", type=float, default=1.5, help="video fade-in/fade-out duration in seconds")
|
||||
parser.add_argument("--video-fade", type=float, default=1.0, help="video fade-in/fade-out duration in seconds")
|
||||
parser.add_argument("--resolution", type=parse_resolution, default=(1920, 1080), help="output resolution, e.g. 1920x1080")
|
||||
parser.add_argument("--fps", type=int, default=30, help="output frames per second")
|
||||
parser.add_argument("--preset", default="veryfast", help="x264 speed preset, e.g. ultrafast, superfast, veryfast")
|
||||
|
|
@ -1091,6 +1156,18 @@ def main(argv: list[str] | None = None) -> int:
|
|||
# Use probed normalized duration for accuracy.
|
||||
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]
|
||||
video_audio = data.video_audio or set()
|
||||
clip_audio_paths = []
|
||||
for item in media:
|
||||
use_clip_audio = item.kind == "video" and (item.path.name in video_audio or str(item.path) in video_audio)
|
||||
if use_clip_audio and media_has_audio(item.path):
|
||||
clip_audio_paths.append(item.path)
|
||||
console(f"Using video file audio: {item.path.name}")
|
||||
elif use_clip_audio:
|
||||
clip_audio_paths.append(None)
|
||||
console(f"Video file has no audio stream: {item.path.name}")
|
||||
else:
|
||||
clip_audio_paths.append(None)
|
||||
clip_stamps = prepare_clip_stamps(media, data)
|
||||
audio = concat_or_mix_audio(audio_files, tmp / "music.m4a")
|
||||
if len(audio_files) > 1:
|
||||
|
|
@ -1099,7 +1176,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
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, clip_stamps, fade, audio_fade, audio_fade_in, video_fade, width, height, fps, preset, crf, tmp)
|
||||
build_video(clips, durations, audio, clip_audio_paths, 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}")
|
||||
return 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue