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 = ""
|
place: str = ""
|
||||||
description: str = ""
|
description: str = ""
|
||||||
captions: dict[str, str] | None = None
|
captions: dict[str, str] | None = None
|
||||||
|
video_audio: set[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -587,6 +588,12 @@ def parse_data_file(path: Path | None) -> MovieData:
|
||||||
data.place = value
|
data.place = value
|
||||||
elif low in {"description", "desc", "synopsis"}:
|
elif low in {"description", "desc", "synopsis"}:
|
||||||
data.description = value
|
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:
|
elif key:
|
||||||
data.captions[key] = value
|
data.captions[key] = value
|
||||||
i += 1
|
i += 1
|
||||||
|
|
@ -649,6 +656,20 @@ def normalize_clip(item: MediaItem, out: Path, width: int, height: int, fps: int
|
||||||
run(cmd)
|
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]:
|
def audio_metadata(path: Path) -> tuple[str, str]:
|
||||||
try:
|
try:
|
||||||
out = capture([
|
out = capture([
|
||||||
|
|
@ -770,6 +791,7 @@ def build_video(
|
||||||
clips: list[Path],
|
clips: list[Path],
|
||||||
durations: list[float],
|
durations: list[float],
|
||||||
audio: Path | None,
|
audio: Path | None,
|
||||||
|
clip_audio_paths: list[Path | None],
|
||||||
output: Path,
|
output: Path,
|
||||||
data: MovieData,
|
data: MovieData,
|
||||||
creation_date: datetime,
|
creation_date: datetime,
|
||||||
|
|
@ -801,6 +823,10 @@ def build_video(
|
||||||
cmd += ["-i", str(clip)]
|
cmd += ["-i", str(clip)]
|
||||||
if audio:
|
if audio:
|
||||||
cmd += ["-stream_loop", "-1", "-i", str(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] = []
|
filters: list[str] = []
|
||||||
n = len(clips)
|
n = len(clips)
|
||||||
|
|
@ -936,13 +962,13 @@ def build_video(
|
||||||
end = total_duration
|
end = total_duration
|
||||||
elif idx == 0:
|
elif idx == 0:
|
||||||
start = intro_duration
|
start = intro_duration
|
||||||
end = intro_duration + transition_offsets[0]
|
end = intro_duration + transition_offsets[0] + fade / 2
|
||||||
elif idx == n - 1:
|
elif idx == n - 1:
|
||||||
start = intro_duration + transition_offsets[idx - 1] + fade
|
start = intro_duration + transition_offsets[idx - 1] + fade / 2
|
||||||
end = total_duration
|
end = total_duration
|
||||||
else:
|
else:
|
||||||
start = intro_duration + transition_offsets[idx - 1] + fade
|
start = intro_duration + transition_offsets[idx - 1] + fade / 2
|
||||||
end = intro_duration + transition_offsets[idx]
|
end = intro_duration + transition_offsets[idx] + fade / 2
|
||||||
if end <= start:
|
if end <= start:
|
||||||
continue
|
continue
|
||||||
next_label = f"stamp_left_{idx}"
|
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}]"
|
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}]"]
|
cmd += ["-filter_complex", ";".join(filters), "-map", f"[{final_label}]"]
|
||||||
metadata = {
|
metadata = {
|
||||||
"title": data.title,
|
"title": data.title,
|
||||||
|
|
@ -973,11 +1041,8 @@ def build_video(
|
||||||
if value:
|
if value:
|
||||||
cmd += ["-metadata", f"{key}={value}"]
|
cmd += ["-metadata", f"{key}={value}"]
|
||||||
|
|
||||||
if audio:
|
if audio_mix_labels:
|
||||||
audio_index = len(clips)
|
cmd += ["-map", "[aout]", "-c:a", "aac", "-b:a", "192k", "-shortest"]
|
||||||
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"]
|
|
||||||
else:
|
else:
|
||||||
cmd += ["-an"]
|
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("--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", 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("--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("--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("--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")
|
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.
|
# 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]
|
||||||
|
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)
|
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:
|
||||||
|
|
@ -1099,7 +1176,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, 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}")
|
console(f"Done: {output}")
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue