Spaces:
Sleeping
Sleeping
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() | |