import gradio as gr from pydub import AudioSegment from moviepy.editor import VideoFileClip import tempfile import mimetypes import os import subprocess import json import moviepy.video.fx.all as vfx # FFmpegの音声速度変更用のatempoフィルター生成 def build_atempo_filters(speed): if not 0.5 <= speed <= 2.0: factors = [] while speed > 2.0: factors.append(2.0) speed /= 2.0 while speed < 0.5: factors.append(0.5) speed /= 0.5 factors.append(speed) return ",".join(f"atempo={round(f, 5)}" for f in factors) else: return f"atempo={round(speed, 5)}" # FFmpegで動画の速度変更 def change_video_speed_ffmpeg(video_file, speed, export_format): result = subprocess.run( [ "ffprobe", "-v", "error", "-select_streams", "a", "-show_entries", "stream=index", "-of", "json", video_file ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True ) audio_info = json.loads(result.stdout) has_audio = bool(audio_info.get("streams")) format_ext = os.path.splitext(video_file)[-1].lstrip(".") if export_format == "元のまま" else export_format output_file = tempfile.NamedTemporaryFile(suffix=f".{format_ext}", delete=False).name setpts_filter = f"setpts={1/speed}*PTS" atempo_filter = build_atempo_filters(speed) if has_audio: filter_complex = f"[0:v]{setpts_filter}[v];[0:a]{atempo_filter}[a]" map_args = ["-map", "[v]", "-map", "[a]"] codec_args = ["-c:v", "libx264", "-c:a", "aac"] else: filter_complex = f"[0:v]{setpts_filter}[v]" map_args = ["-map", "[v]"] codec_args = ["-c:v", "libx264"] cmd = [ "ffmpeg", "-y", "-i", video_file, "-filter_complex", filter_complex, *map_args, *codec_args, output_file ] subprocess.run(cmd, check=True) return output_file # FFmpegで音声の速度変更 def change_audio_speed_ffmpeg(audio_file, speed, export_format): atempo_filter = build_atempo_filters(speed) format_ext = os.path.splitext(audio_file)[-1].lstrip(".") if export_format == "元のまま" else export_format output_file = tempfile.NamedTemporaryFile(suffix=f".{format_ext}", delete=False).name cmd = [ "ffmpeg", "-y", "-i", audio_file, "-filter:a", atempo_filter, "-c:a", "aac" if format_ext in ["mp4", "mov"] else "libmp3lame", output_file ] subprocess.run(cmd, check=True) return output_file # Pydubで音声の速度変更 def change_audio_speed_pydub(audio_file, speed, export_format): audio = AudioSegment.from_file(audio_file) new_frame_rate = int(audio.frame_rate * speed) faster_audio = audio._spawn(audio.raw_data, overrides={'frame_rate': new_frame_rate}) faster_audio = faster_audio.set_frame_rate(audio.frame_rate) format_ext = os.path.splitext(audio_file)[-1].lstrip(".") if export_format == "元のまま" else export_format with tempfile.NamedTemporaryFile(suffix=f".{format_ext}", delete=False) as tmpfile: faster_audio.export(tmpfile.name, format=format_ext) return tmpfile.name # moviepyで動画の速度変更 def change_video_speed_moviepy(video_file, speed, export_format): clip = VideoFileClip(video_file) new_clip = clip.fx(vfx.speedx, speed) format_ext = os.path.splitext(video_file)[-1].lstrip(".") if export_format == "元のまま" else export_format output_file = tempfile.NamedTemporaryFile(suffix=f".{format_ext}", delete=False).name new_clip.write_videofile( output_file, codec="libx264" if format_ext in ["mp4", "mov"] else None, audio_codec="aac", verbose=False, logger=None ) return output_file # 全体のラッパー関数(処理方法の分岐) def change_speed(file_path, speed, export_format, method): mime, _ = mimetypes.guess_type(file_path) print(speed) if mime and mime.startswith("audio"): if method == "FFmpeg": return change_audio_speed_ffmpeg(file_path, speed, export_format) else: return change_audio_speed_pydub(file_path, speed, export_format) elif mime and mime.startswith("video"): if method == "FFmpeg": return change_video_speed_ffmpeg(file_path, speed, export_format) else: return change_video_speed_moviepy(file_path, speed, export_format) else: raise gr.Error("対応していないファイル形式です。音声または動画をアップロードしてください。") # Gradio UI with gr.Blocks() as demo: gr.Markdown("# 🎬 音声・動画の再生速度を細かく調整して変換") gr.Markdown(""" | 項目 | Gradio(入力) | Pydub/MoviePy | FFmpeg(フィルター) | | -------- | --------------------------------- | --------------------------------------------- | ----------------------------------- | | 入力形式 | `Slider(step=0.0001)` → 小数点以下4桁以上 | Python `float` → `AudioSegment` / `speedx` | `float` → `atempo` / `setpts` | | 表現精度(理論) | 小数点以下4桁程度 | 倍精度浮動小数点 → 最大小数点以下15桁 | 同左(内部処理は浮動小数点) | | 実効分解能 | 0.0001単位 | 音声: サンプリングレート依存(約20μs)、動画: フレーム単位 | 音声: `atempo` (0.5–2.0連結対応)、動画: µs単位 | | 出力方式 | `float` → `ChangeSpeed(speed)` | `AudioSegment.export()` / `write_videofile()` | `ffmpeg` CLI を直接呼び出し | """) with gr.Row(): with gr.Column(): file_input = gr.File(label="音声または動画をアップロード", file_types=[".mp3", ".wav", ".mp4", ".mov"]) speed_input = gr.Slider( minimum=0.1, maximum=4.0, value=1.0, step=0.0001, label="再生速度(例:0.75 = 25%遅く、1.25 = 25%速く)" ) export_format_input = gr.Dropdown( choices=["元のまま", "wav", "mp3", "mp4", "mov"], value="元のまま", label="出力フォーマット" ) method_input = gr.Dropdown( choices=["FFmpeg", "Pydub/MoviePy"], value="FFmpeg", label="処理方法" ) btn = gr.Button("変換") with gr.Column(): file_output = gr.File(label="変換後のファイルをダウンロード") btn.click(fn=change_speed, inputs=[file_input, speed_input, export_format_input, method_input], outputs=file_output) if __name__ == "__main__": demo.launch()