"""Split audio/video files into overlapping chunks. This script merges the earlier pydub audio splitter and the ffmpeg MP4-video splitter into one ffmpeg-based tool. Features -------- - Accepts audio input or video input. - Exports audio chunks, e.g. mp3, wav, m4a, flac, ogg. - Exports video chunks, especially mp4. - If --image is specified, the image is used as the video track. - If --image is omitted and the input has a video stream, the source video is used. - If --image is omitted and the input is audio-only, a black video track is generated. - Trims by --start and --end before splitting. - Splits by estimated maximum file size with overlap. Requirements ------------ ffmpeg and ffprobe must be installed and available from PATH, or specified with --ffmpeg and --ffprobe. Examples -------- # Audio input -> audio output python split_media_with_overlap.py input.mp3 --format mp3 # Video input -> audio output python split_media_with_overlap.py input.mp4 --format mp3 --start 00:10:00 --end 00:25:00 # Audio input -> video output with a still image python split_media_with_overlap.py input.mp3 --format mp4 --image cover.png # Video input -> video output using the original video stream python split_media_with_overlap.py input.mp4 --format mp4 --start 600 --end 1500 # Old positional style is also accepted: # python split_media_with_overlap.py input.mp3 mp3 128k 150 60 """ from __future__ import annotations import argparse import json import os import re import shutil import subprocess import sys from pathlib import Path # ====== Default settings ====== DEFAULT_OUTPUT_DIR = "split_fixed" DEFAULT_OUTPUT_FORMAT = "mp3" DEFAULT_AUDIO_BITRATE = "128k" DEFAULT_VIDEO_BITRATE = "300k" DEFAULT_MAX_SIZE_MB = 150.0 DEFAULT_OVERLAP_SEC = 60.0 DEFAULT_WIDTH = 1280 DEFAULT_HEIGHT = 720 DEFAULT_FPS = 30 AUDIO_FORMATS = { "mp3", "wav", "m4a", "aac", "flac", "ogg", "opus", } VIDEO_FORMATS = { "mp4", "m4v", "mov", "mkv", "avi", "webm", "wmv", } VIDEO_EXTENSIONS = {f".{fmt}" for fmt in VIDEO_FORMATS} # ----------------------------------------------------------------------------- # Utilities # ----------------------------------------------------------------------------- def parse_time_to_sec(value: str | float | int | None) -> float | None: """Convert a time specification to seconds. Accepted forms: 90 -> 90 seconds 90.5 -> 90.5 seconds 01:30 -> 1 minute 30 seconds 01:02:03 -> 1 hour 2 minutes 3 seconds 01:02:03.5 -> 1 hour 2 minutes 3.5 seconds """ if value is None or value == "": return None text = str(value).strip() if re.fullmatch(r"\d+(\.\d+)?", text): return float(text) parts = text.split(":") if len(parts) not in (2, 3): raise argparse.ArgumentTypeError( f"時刻指定が不正です: {text} 例: 90, 01:30, 01:02:03" ) try: nums = [float(p) for p in parts] except ValueError as exc: raise argparse.ArgumentTypeError( f"時刻指定が不正です: {text} 例: 90, 01:30, 01:02:03" ) from exc if len(nums) == 2: hours = 0.0 minutes, seconds = nums else: hours, minutes, seconds = nums if hours < 0 or minutes < 0 or seconds < 0 or minutes >= 60 or seconds >= 60: raise argparse.ArgumentTypeError( f"時刻指定が不正です: {text} 分・秒は 0 以上 60 未満で指定してください。" ) return hours * 3600 + minutes * 60 + seconds def parse_bitrate_kbps(bitrate: str | int | float) -> float: """Convert bitrate such as '128k' or '1.2M' to kbps.""" text = str(bitrate).strip().lower() multiplier = 1.0 if text.endswith("k"): text = text[:-1] elif text.endswith("m"): text = text[:-1] multiplier = 1000.0 try: kbps = float(text) * multiplier except ValueError as exc: raise argparse.ArgumentTypeError( f"ビットレートが不正です: {bitrate} 例: 128k, 1M" ) from exc if kbps <= 0: raise argparse.ArgumentTypeError("ビットレートは正の値にしてください。") return kbps def ffmpeg_time(seconds: float) -> str: """Format seconds for ffmpeg options.""" return f"{seconds:.3f}" def normalize_format(output_format: str | None, output_path: str | None = None) -> str: """Return lower-case format name without leading dot.""" if output_format: fmt = output_format.strip().lower().lstrip(".") elif output_path: fmt = Path(output_path).suffix.lower().lstrip(".") else: fmt = DEFAULT_OUTPUT_FORMAT if not fmt: fmt = DEFAULT_OUTPUT_FORMAT return fmt def is_video_format(fmt: str) -> bool: return fmt.lower().lstrip(".") in VIDEO_FORMATS def is_audio_format(fmt: str) -> bool: return fmt.lower().lstrip(".") in AUDIO_FORMATS def check_executable(path_or_name: str, label: str) -> str: """Return executable path if available, otherwise exit with a clear error.""" if os.path.basename(path_or_name) == path_or_name: resolved = shutil.which(path_or_name) else: resolved = path_or_name if resolved and os.path.exists(resolved): return resolved print(f"エラー: {label} が見つかりません: {path_or_name}") print("ffmpeg をインストールして PATH に追加するか、--ffmpeg / --ffprobe で場所を指定してください。") sys.exit(1) def run_command(cmd: list[str]) -> subprocess.CompletedProcess[str]: """Run a command and raise a readable error on failure.""" result = subprocess.run( cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, ) if result.returncode != 0: print("\nエラー: ffmpeg/ffprobe の実行に失敗しました。") print("実行コマンド:") print(" ".join(f'"{c}"' if " " in c else c for c in cmd)) print("\n--- stderr ---") print(result.stderr) sys.exit(result.returncode) return result def ffprobe_json(ffprobe: str, input_file: str) -> dict: """Read media metadata using ffprobe.""" cmd = [ ffprobe, "-v", "error", "-show_format", "-show_streams", "-of", "json", input_file, ] result = run_command(cmd) return json.loads(result.stdout) def get_duration_sec(metadata: dict) -> float: """Get duration from ffprobe metadata.""" fmt = metadata.get("format", {}) duration = fmt.get("duration") if duration is not None: return float(duration) durations = [] for stream in metadata.get("streams", []): if stream.get("duration") is not None: durations.append(float(stream["duration"])) if durations: return max(durations) print("エラー: 入力ファイルの長さを取得できませんでした。") sys.exit(1) def has_stream(metadata: dict, codec_type: str) -> bool: """Return True if a stream of codec_type exists.""" return any(s.get("codec_type") == codec_type for s in metadata.get("streams", [])) def get_input_summary(metadata: dict) -> tuple[bool, bool]: """Return (has_audio, has_video).""" return has_stream(metadata, "audio"), has_stream(metadata, "video") def build_video_filter(width: int, height: int) -> str: """Scale and pad video/image to a fixed canvas.""" return ( f"scale={width}:{height}:force_original_aspect_ratio=decrease," f"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2," "format=yuv420p" ) def make_output_dir(input_file: str, output_dir_arg: str) -> str: """Create output directory and return its absolute path.""" input_dir = os.path.dirname(os.path.abspath(input_file)) output_dir = output_dir_arg if not os.path.isabs(output_dir): output_dir = os.path.join(input_dir, output_dir) os.makedirs(output_dir, exist_ok=True) return output_dir def media_type_label(fmt: str) -> str: return "video" if is_video_format(fmt) else "audio" # ----------------------------------------------------------------------------- # Argument handling # ----------------------------------------------------------------------------- def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description=( "音声または動画を、指定範囲だけ切り出し、" "サイズ上限+オーバーラップ付きで分割出力します。" ), formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) # Backward compatible positional arguments: # old: input_file output_format bitrate max_size_mb overlap_sec parser.add_argument("input_file", help="入力音声/動画ファイル") parser.add_argument( "pos_output_format", nargs="?", default=None, help="旧形式互換: 出力形式 mp3/wav/mp4 など", ) parser.add_argument( "pos_audio_bitrate", nargs="?", default=None, help="旧形式互換: 音声ビットレート", ) parser.add_argument( "pos_max_size_mb", nargs="?", type=float, default=None, help="旧形式互換: チャンク最大サイズ MB", ) parser.add_argument( "pos_overlap_sec", nargs="?", type=float, default=None, help="旧形式互換: オーバーラップ秒数", ) parser.add_argument( "--format", "--output-format", dest="output_format", default=None, help="出力形式。mp3, wav, m4a, mp4, mkv など。", ) parser.add_argument( "--image", default=None, help=( "動画出力時に映像として使う静止画像。" "指定すると、入力動画の映像よりこちらを優先します。" ), ) parser.add_argument( "--output-dir", default=DEFAULT_OUTPUT_DIR, help="出力ディレクトリ。相対パスの場合は入力ファイルと同じ場所を基準にします。", ) parser.add_argument( "--audio-bitrate", "--bitrate", dest="audio_bitrate", default=None, help="音声ビットレート", ) parser.add_argument( "--video-bitrate", default=DEFAULT_VIDEO_BITRATE, help="映像ビットレート。静止画なら 200k〜500k 程度で十分です。", ) parser.add_argument( "--max-size-mb", type=float, default=None, help="チャンク最大サイズ MB。ビットレートから概算します。", ) parser.add_argument( "--overlap-sec", type=float, default=None, help="オーバーラップ時間 秒", ) parser.add_argument( "--start", type=parse_time_to_sec, default=0.0, help="切り出し開始時刻。秒、MM:SS、HH:MM:SS で指定できます。", ) parser.add_argument( "--end", type=parse_time_to_sec, default=None, help="切り出し終了時刻。省略時は末尾まで。秒、MM:SS、HH:MM:SS で指定できます。", ) parser.add_argument("--width", type=int, default=DEFAULT_WIDTH, help="出力動画の幅") parser.add_argument("--height", type=int, default=DEFAULT_HEIGHT, help="出力動画の高さ") parser.add_argument("--fps", type=int, default=DEFAULT_FPS, help="出力動画のフレームレート") parser.add_argument("--ffmpeg", default="ffmpeg", help="ffmpeg 実行ファイル") parser.add_argument("--ffprobe", default="ffprobe", help="ffprobe 実行ファイル") parser.add_argument( "--video-background", choices=["auto", "input", "image", "black"], default="auto", help=( "動画出力時の映像ソース。autoでは、--imageがあれば画像、" "入力に映像があれば入力映像、音声のみなら黒背景を使います。" ), ) parser.add_argument( "--pause", action="store_true", help="処理終了後にEnter待ちします。Windowsでダブルクリック実行する場合に使います。", ) return parser def apply_positional_compatibility(args: argparse.Namespace) -> argparse.Namespace: """Apply old positional arguments when new options are omitted.""" if args.output_format is None and args.pos_output_format is not None: args.output_format = args.pos_output_format if args.audio_bitrate is None and args.pos_audio_bitrate is not None: args.audio_bitrate = args.pos_audio_bitrate if args.max_size_mb is None and args.pos_max_size_mb is not None: args.max_size_mb = args.pos_max_size_mb if args.overlap_sec is None and args.pos_overlap_sec is not None: args.overlap_sec = args.pos_overlap_sec args.output_format = normalize_format(args.output_format) args.audio_bitrate = args.audio_bitrate or DEFAULT_AUDIO_BITRATE args.max_size_mb = args.max_size_mb if args.max_size_mb is not None else DEFAULT_MAX_SIZE_MB args.overlap_sec = args.overlap_sec if args.overlap_sec is not None else DEFAULT_OVERLAP_SEC return args def validate_args(args: argparse.Namespace, metadata: dict, duration_sec: float) -> tuple[float, float, bool, bool]: """Validate arguments and return start, end, input_has_audio, input_has_video.""" input_file = args.input_file if not os.path.isfile(input_file): print(f"エラー: ファイルが見つかりません: {input_file}") sys.exit(1) if args.image is not None and not os.path.isfile(args.image): print(f"エラー: 画像ファイルが見つかりません: {args.image}") sys.exit(1) output_format = args.output_format if not is_audio_format(output_format) and not is_video_format(output_format): print(f"エラー: 未対応または未分類の出力形式です: {output_format}") print("対応例: mp3, wav, m4a, flac, ogg, mp4, mov, mkv, webm") sys.exit(1) input_has_audio, input_has_video = get_input_summary(metadata) if not input_has_audio and not input_has_video: print("エラー: 入力ファイルに音声/映像ストリームが見つかりません。") sys.exit(1) if is_audio_format(output_format) and not input_has_audio: print("エラー: 音声出力には、入力ファイルに音声ストリームが必要です。") sys.exit(1) if args.video_background == "input" and not input_has_video: print("エラー: --video-background input には、入力ファイルに映像ストリームが必要です。") sys.exit(1) if args.video_background == "image" and args.image is None: print("エラー: --video-background image には --image が必要です。") sys.exit(1) if args.max_size_mb <= 0: print("エラー: --max-size-mb は正の値にしてください。") sys.exit(1) if args.overlap_sec < 0: print("エラー: --overlap-sec は 0 以上にしてください。") sys.exit(1) if args.width <= 0 or args.height <= 0: print("エラー: --width と --height は正の値にしてください。") sys.exit(1) if args.fps <= 0: print("エラー: --fps は正の値にしてください。") sys.exit(1) # Validate bitrate syntax early. parse_bitrate_kbps(args.audio_bitrate) if is_video_format(output_format): parse_bitrate_kbps(args.video_bitrate) start_sec = args.start or 0.0 end_sec = args.end if args.end is not None else duration_sec if start_sec < 0: print("エラー: --start は 0 以上にしてください。") sys.exit(1) if start_sec >= duration_sec: print(f"エラー: --start が入力ファイルの長さを超えています: {start_sec:.3f}秒") sys.exit(1) if end_sec > duration_sec: print( f"警告: --end が入力ファイルの長さを超えています。" f" {end_sec:.3f}秒 -> {duration_sec:.3f}秒 に丸めます。" ) end_sec = duration_sec if end_sec <= start_sec: print("エラー: --end は --start より後の時刻にしてください。") sys.exit(1) return start_sec, end_sec, input_has_audio, input_has_video # ----------------------------------------------------------------------------- # ffmpeg command builders # ----------------------------------------------------------------------------- def audio_codec_options(output_format: str, audio_bitrate: str) -> list[str]: """Return ffmpeg audio codec options for the requested output format.""" fmt = output_format.lower() if fmt == "wav": return ["-c:a", "pcm_s16le"] if fmt == "flac": return ["-c:a", "flac"] if fmt == "ogg": return ["-c:a", "libvorbis", "-b:a", audio_bitrate] if fmt == "opus": return ["-c:a", "libopus", "-b:a", audio_bitrate] if fmt == "mp3": return ["-c:a", "libmp3lame", "-b:a", audio_bitrate] # m4a/aac and fallback return ["-c:a", "aac", "-b:a", audio_bitrate] def video_codec_options(output_format: str, audio_bitrate: str, video_bitrate: str) -> list[str]: """Return ffmpeg A/V codec options for the requested video output format.""" fmt = output_format.lower() if fmt == "webm": return [ "-c:v", "libvpx-vp9", "-b:v", video_bitrate, "-c:a", "libopus", "-b:a", audio_bitrate, ] # MP4/MOV/MKV/AVI/WMV fallback: prioritize broadly playable H.264 + AAC. return [ "-c:v", "libx264", "-b:v", video_bitrate, "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", audio_bitrate, ] def choose_video_source(args: argparse.Namespace, input_has_video: bool) -> str: """Return 'image', 'input', or 'black' for video output.""" if args.video_background == "image": return "image" if args.video_background == "input": return "input" if args.video_background == "black": return "black" # auto if args.image: return "image" if input_has_video: return "input" return "black" def export_audio_chunk( *, ffmpeg: str, input_file: str, out_path: str, start_sec: float, duration_sec: float, output_format: str, audio_bitrate: str, ) -> None: """Export one audio-only chunk from either audio or video input.""" cmd = [ ffmpeg, "-y", "-ss", ffmpeg_time(start_sec), "-t", ffmpeg_time(duration_sec), "-i", input_file, "-map", "0:a:0", "-vn", *audio_codec_options(output_format, audio_bitrate), out_path, ] run_command(cmd) def export_video_chunk_from_image( *, ffmpeg: str, input_file: str, image_file: str, out_path: str, start_sec: float, duration_sec: float, output_format: str, audio_bitrate: str, video_bitrate: str, width: int, height: int, fps: int, input_has_audio: bool, ) -> None: """Export one video chunk using a still image as the video track.""" vf = build_video_filter(width, height) cmd = [ ffmpeg, "-y", "-loop", "1", "-framerate", str(fps), "-i", image_file, "-ss", ffmpeg_time(start_sec), "-t", ffmpeg_time(duration_sec), "-i", input_file, "-map", "0:v:0", ] if input_has_audio: cmd += ["-map", "1:a:0"] else: cmd += ["-an"] cmd += [ "-vf", vf, "-r", str(fps), *video_codec_options(output_format, audio_bitrate, video_bitrate), "-shortest", ] if output_format.lower() in {"mp4", "m4v", "mov"}: cmd += ["-movflags", "+faststart"] cmd.append(out_path) run_command(cmd) def export_video_chunk_from_source_video( *, ffmpeg: str, input_file: str, out_path: str, start_sec: float, duration_sec: float, output_format: str, audio_bitrate: str, video_bitrate: str, width: int, height: int, fps: int, input_has_audio: bool, ) -> None: """Export one video chunk using the source video's video stream.""" vf = build_video_filter(width, height) cmd = [ ffmpeg, "-y", "-ss", ffmpeg_time(start_sec), "-t", ffmpeg_time(duration_sec), "-i", input_file, "-map", "0:v:0", ] if input_has_audio: cmd += ["-map", "0:a:0"] else: cmd += ["-an"] cmd += [ "-vf", vf, "-r", str(fps), *video_codec_options(output_format, audio_bitrate, video_bitrate), ] if output_format.lower() in {"mp4", "m4v", "mov"}: cmd += ["-movflags", "+faststart"] cmd.append(out_path) run_command(cmd) def export_video_chunk_from_black( *, ffmpeg: str, input_file: str, out_path: str, start_sec: float, duration_sec: float, output_format: str, audio_bitrate: str, video_bitrate: str, width: int, height: int, fps: int, input_has_audio: bool, ) -> None: """Export one video chunk using a generated black video track.""" color_source = f"color=c=black:s={width}x{height}:r={fps}:d={ffmpeg_time(duration_sec)}" cmd = [ ffmpeg, "-y", "-f", "lavfi", "-i", color_source, "-ss", ffmpeg_time(start_sec), "-t", ffmpeg_time(duration_sec), "-i", input_file, "-map", "0:v:0", ] if input_has_audio: cmd += ["-map", "1:a:0"] else: cmd += ["-an"] cmd += [ *video_codec_options(output_format, audio_bitrate, video_bitrate), "-shortest", ] if output_format.lower() in {"mp4", "m4v", "mov"}: cmd += ["-movflags", "+faststart"] cmd.append(out_path) run_command(cmd) # ----------------------------------------------------------------------------- # Main splitting loop # ----------------------------------------------------------------------------- def estimate_chunk_seconds(args: argparse.Namespace) -> tuple[float, float]: """Return (max_chunk_sec, step_sec) estimated from bitrate and max size.""" audio_kbps = parse_bitrate_kbps(args.audio_bitrate) total_kbps = audio_kbps if is_video_format(args.output_format): video_kbps = parse_bitrate_kbps(args.video_bitrate) total_kbps += video_kbps bytes_per_sec = total_kbps * 1000.0 / 8.0 max_bytes = args.max_size_mb * 1024.0 * 1024.0 max_chunk_sec = max_bytes / bytes_per_sec step_sec = max_chunk_sec - args.overlap_sec if step_sec <= 0: print( "エラー: オーバーラップ時間が推定チャンク長以上です。\n" f"推定チャンク長: {max_chunk_sec:.3f}秒, " f"オーバーラップ: {args.overlap_sec:.3f}秒\n" "--overlap-sec を小さくするか、--max-size-mb を大きくしてください。" ) sys.exit(1) return max_chunk_sec, step_sec def print_settings( *, args: argparse.Namespace, output_dir: str, original_duration_sec: float, start_sec: float, end_sec: float, max_chunk_sec: float, step_sec: float, input_has_audio: bool, input_has_video: bool, video_source: str | None, ) -> None: """Print processing settings.""" selected_duration_sec = end_sec - start_sec print("\n入力ファイル:", args.input_file) print("入力ストリーム:", end=" ") labels = [] if input_has_audio: labels.append("audio") if input_has_video: labels.append("video") print("+".join(labels)) print("出力ディレクトリ:", output_dir) print(f"出力形式: {args.output_format} ({media_type_label(args.output_format)})") print(f"音声ビットレート: {args.audio_bitrate}") if is_video_format(args.output_format): print(f"映像ビットレート: {args.video_bitrate}") print(f"出力サイズ: {args.width} x {args.height}, {args.fps} fps") print(f"映像ソース: {video_source}") if args.image: print("使用する画像:", args.image) print(f"最大サイズ: {args.max_size_mb} MB") print(f"オーバーラップ長: {args.overlap_sec} 秒") print(f"元の長さ: {original_duration_sec:.3f} 秒") print(f"切り出し範囲: {start_sec:.3f} 秒 〜 {end_sec:.3f} 秒") print(f"切り出し後の長さ: {selected_duration_sec:.3f} 秒") print(f"推定チャンク長: 約 {max_chunk_sec:.3f} 秒") print(f"ステップ: {step_sec:.3f} 秒") print("\n分割開始") def main() -> None: parser = build_parser() args = apply_positional_compatibility(parser.parse_args()) ffmpeg = check_executable(args.ffmpeg, "ffmpeg") ffprobe = check_executable(args.ffprobe, "ffprobe") if not os.path.isfile(args.input_file): print(f"エラー: ファイルが見つかりません: {args.input_file}") sys.exit(1) metadata = ffprobe_json(ffprobe, args.input_file) original_duration_sec = get_duration_sec(metadata) start_sec, end_sec, input_has_audio, input_has_video = validate_args( args, metadata, original_duration_sec ) selected_duration_sec = end_sec - start_sec max_chunk_sec, step_sec = estimate_chunk_seconds(args) output_dir = make_output_dir(args.input_file, args.output_dir) filebody = Path(args.input_file).stem video_source = choose_video_source(args, input_has_video) if is_video_format(args.output_format) else None print_settings( args=args, output_dir=output_dir, original_duration_sec=original_duration_sec, start_sec=start_sec, end_sec=end_sec, max_chunk_sec=max_chunk_sec, step_sec=step_sec, input_has_audio=input_has_audio, input_has_video=input_has_video, video_source=video_source, ) chunk_index = 0 chunk_start_in_range = 0.0 while chunk_start_in_range < selected_duration_sec: chunk_end_in_range = min(chunk_start_in_range + max_chunk_sec, selected_duration_sec) chunk_duration = chunk_end_in_range - chunk_start_in_range original_chunk_start = start_sec + chunk_start_in_range original_chunk_end = start_sec + chunk_end_in_range out_path = os.path.join(output_dir, f"{filebody}_{chunk_index:03}.{args.output_format}") print( f"> チャンク {chunk_index + 1}: " f"元ファイル上 {original_chunk_start:.3f}秒 〜 {original_chunk_end:.3f}秒" ) if is_audio_format(args.output_format): export_audio_chunk( ffmpeg=ffmpeg, input_file=args.input_file, out_path=out_path, start_sec=original_chunk_start, duration_sec=chunk_duration, output_format=args.output_format, audio_bitrate=args.audio_bitrate, ) else: if video_source == "image": export_video_chunk_from_image( ffmpeg=ffmpeg, input_file=args.input_file, image_file=args.image, out_path=out_path, start_sec=original_chunk_start, duration_sec=chunk_duration, output_format=args.output_format, audio_bitrate=args.audio_bitrate, video_bitrate=args.video_bitrate, width=args.width, height=args.height, fps=args.fps, input_has_audio=input_has_audio, ) elif video_source == "input": export_video_chunk_from_source_video( ffmpeg=ffmpeg, input_file=args.input_file, out_path=out_path, start_sec=original_chunk_start, duration_sec=chunk_duration, output_format=args.output_format, audio_bitrate=args.audio_bitrate, video_bitrate=args.video_bitrate, width=args.width, height=args.height, fps=args.fps, input_has_audio=input_has_audio, ) elif video_source == "black": export_video_chunk_from_black( ffmpeg=ffmpeg, input_file=args.input_file, out_path=out_path, start_sec=original_chunk_start, duration_sec=chunk_duration, output_format=args.output_format, audio_bitrate=args.audio_bitrate, video_bitrate=args.video_bitrate, width=args.width, height=args.height, fps=args.fps, input_has_audio=input_has_audio, ) else: print(f"内部エラー: 未知の映像ソースです: {video_source}") sys.exit(1) print(f"> 保存: {out_path}({chunk_duration:.3f} 秒)") if chunk_end_in_range >= selected_duration_sec: break chunk_start_in_range += step_sec chunk_index += 1 print("\n✅ 指定範囲を、音声/動画としてオーバーラップ分割しました。") if args.pause: input("\nPress ENTER to terminate>>\n") if __name__ == "__main__": main()