import argparse import subprocess from pathlib import Path def resolve_path(value: str) -> Path: return Path(value).expanduser().resolve() def run(command: list[str]) -> None: print("Running:", " ".join(command)) subprocess.run(command, check=True) def main() -> None: parser = argparse.ArgumentParser( description="Replace a video's original audio with a new voiceover audio file." ) parser.add_argument("--video", required=True, help="Input video file, for example input/tutorial.mp4") parser.add_argument("--audio", required=True, help="Input voiceover audio file, for example output/voiceover.wav") parser.add_argument("--output", required=True, help="Output MP4 file, for example output/final_video.mp4") parser.add_argument( "--reencode-video", action="store_true", help="Re-encode video with H.264 instead of copying. Use only if the copied output has playback issues.", ) args = parser.parse_args() video_file = resolve_path(args.video) audio_file = resolve_path(args.audio) output_file = resolve_path(args.output) if not video_file.exists(): raise FileNotFoundError(f"Video not found: {video_file}") if not audio_file.exists(): raise FileNotFoundError(f"Audio not found: {audio_file}") output_file.parent.mkdir(parents=True, exist_ok=True) video_codec_args = ["-c:v", "libx264", "-preset", "medium", "-crf", "18"] if args.reencode_video else ["-c:v", "copy"] command = [ "ffmpeg", "-y", "-i", str(video_file), "-i", str(audio_file), "-map", "0:v:0", # video from first input "-map", "1:a:0", # audio from second input only, so original video audio is muted/removed *video_codec_args, "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", str(output_file), ] run(command) print(f"Done: {output_file}") if __name__ == "__main__": main()