LG3370's picture
Fix WEBM upload: replace gr.Video with gr.File having explicit file_types
540171b verified
Raw
History Blame Contribute Delete
9.22 kB
import gradio as gr
import subprocess
import json
import os
import tempfile
from pydub import AudioSegment
def hms_to_seconds(hms: str) -> float:
"""Convert hh:mm:ss or mm:ss or ss to seconds."""
parts = hms.strip().split(":")
if len(parts) == 1:
return float(parts[0])
elif len(parts) == 2:
m, s = parts
return float(m) * 60 + float(s)
elif len(parts) == 3:
h, m, s = parts
return float(h) * 3600 + float(m) * 60 + float(s)
return 0.0
def seconds_to_hms(seconds: float) -> str:
"""Convert seconds to hh:mm:ss string."""
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = seconds % 60
return f"{h:02d}:{m:02d}:{s:06.3f}"
def get_video_duration(video_path: str) -> float:
"""Use ffprobe to get video duration in seconds."""
cmd = [
"ffprobe", "-v", "error",
"-show_entries", "format=duration",
"-of", "json",
video_path,
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
data = json.loads(result.stdout)
return float(data["format"]["duration"])
def extract_audio(video_path: str, output_wav: str, start_s: float = None, end_s: float = None) -> str:
"""Extract audio from video to WAV using ffmpeg, optionally clipping to interval."""
cmd = ["ffmpeg", "-y", "-i", video_path, "-vn", "-acodec", "pcm_s16le", "-ar", "44100", "-ac", "2"]
if start_s is not None:
cmd += ["-ss", str(start_s)]
if end_s is not None:
cmd += ["-to", str(end_s)]
cmd += [output_wav]
subprocess.run(cmd, capture_output=True, check=True)
return output_wav
FORMAT_EXT = {"MP3": "mp3", "M4A": "m4a", "WAV": "wav", "OGG": "ogg", "WEBM": "webm"}
MAX_SEGMENTS = 10
def segment_audio(
audio_path: str,
segment_sec: float,
overlap_sec: float,
n_segments: int = None,
output_dir: str = None,
out_format: str = "mp3",
) -> list:
"""Segment audio by fixed time or fixed number of segments, with overlap.
If segment_sec >= duration, return one segment of the entire audio.
Shorter last segment is included if it has non-zero length.
Segments are capped at MAX_SEGMENTS (10).
"""
audio = AudioSegment.from_file(audio_path)
duration_ms = len(audio)
# If asking for 1 segment, return entire interval
if n_segments == 1:
ext = FORMAT_EXT.get(out_format.upper(), "mp3")
seg_path = os.path.join(output_dir, f"seg_000.{ext}")
audio.export(seg_path, format=ext)
return [seg_path]
# If segment length >= duration, return one segment of entire audio
segment_ms = int(segment_sec * 1000)
overlap_ms = int(overlap_sec * 1000)
if segment_ms >= duration_ms:
ext = FORMAT_EXT.get(out_format.upper(), "mp3")
seg_path = os.path.join(output_dir, f"seg_000.{ext}")
audio.export(seg_path, format=ext)
return [seg_path]
step_ms = segment_ms - overlap_ms
if n_segments is not None and n_segments > 0:
# Fixed number of segments — cap at MAX_SEGMENTS
n_segments = min(n_segments, MAX_SEGMENTS)
if n_segments == 1:
ext = FORMAT_EXT.get(out_format.upper(), "mp3")
seg_path = os.path.join(output_dir, f"seg_000.{ext}")
audio.export(seg_path, format=ext)
return [seg_path]
step_ms = (duration_ms - segment_ms) // max(n_segments - 1, 1)
ext = FORMAT_EXT.get(out_format.upper(), "mp3")
segments = []
start = 0
idx = 0
while start < duration_ms and idx < MAX_SEGMENTS:
end = min(start + segment_ms, duration_ms)
if end <= start:
break
seg = audio[start:end]
seg_path = os.path.join(output_dir, f"seg_{idx:03d}.{ext}")
seg.export(seg_path, format=ext)
segments.append(seg_path)
start += step_ms
idx += 1
if n_segments is not None and idx >= n_segments:
break
return segments
def process_video(
video_file,
start_hms,
end_hms,
mode,
segment_len,
n_segments,
overlap,
out_format,
):
"""Main processing function."""
if video_file is None:
return [], gr.update(visible=False), gr.update(visible=True, value="Please upload a video.")
start_s = hms_to_seconds(start_hms)
end_s = hms_to_seconds(end_hms)
start_s = max(0, float(start_s))
end_s = max(start_s, float(end_s))
tmpdir = tempfile.mkdtemp()
wav_path = os.path.join(tmpdir, "audio.wav")
extract_audio(video_file, wav_path, start_s, end_s)
seg_len = float(segment_len)
ov = float(overlap)
n_seg = int(n_segments) if mode == "Fixed number of segments" else None
if seg_len <= 0 and n_seg is None:
return [], gr.update(visible=False), gr.update(visible=True, value="Segment length must be > 0.")
paths = segment_audio(wav_path, seg_len, ov, n_segments=n_seg, output_dir=tmpdir, out_format=out_format)
if not paths:
return [], gr.update(visible=False), gr.update(visible=True, value="No segments produced. Check your parameters.")
return paths, gr.update(visible=True), gr.update(visible=False)
# ---------------------------------------------------------------------------
# Build UI
# ---------------------------------------------------------------------------
with gr.Blocks(title="Video Audio Extractor & Segmenter") as demo:
gr.Markdown("""
# 🎬 Video → 🔊 Audio Extractor & Segmenter
Upload a video file (MP4, WEBM, AVI, MOV, MKV, etc.), pick a time interval (hh:mm:ss), choose segmentation mode, and extract audio segments.
""")
# Use gr.File with explicit file types so WEBM is not dimmed in the picker.
# gr.Video has no file_types parameter; the frontend hardcodes a restrictive accept filter.
video_input = gr.File(
file_types=["video", ".webm", ".mp4", ".m4v", ".avi", ".mov", ".mkv", ".ogv"],
label="Upload Video",
file_count="single",
)
duration_state = gr.State(value=60.0)
with gr.Row():
discover_btn = gr.Button("🔄 Discover Duration", scale=1)
duration_text = gr.Textbox(label="Video Duration", value="Not discovered yet", interactive=False, scale=2)
with gr.Row():
start_input = gr.Textbox(
value="00:00:00",
label="Start Time (hh:mm:ss)",
placeholder="00:00:00",
)
end_input = gr.Textbox(
value="00:00:10",
label="End Time (hh:mm:ss)",
placeholder="00:00:10",
)
with gr.Row():
mode = gr.Radio(
["Fixed segment length", "Fixed number of segments"],
value="Fixed segment length",
label="Segmentation Mode",
)
seg_len = gr.Number(value=5, minimum=0.5, step=0.5, label="Segment Length (s)")
n_seg = gr.Number(value=3, minimum=1, maximum=MAX_SEGMENTS, step=1, label=f"Number of Segments (max {MAX_SEGMENTS})", visible=False)
overlap = gr.Slider(0, 20, value=0, step=0.5, label="Overlap (s)")
out_format = gr.Dropdown(
["MP3", "M4A", "WAV", "OGG", "WEBM"],
value="MP3",
label="Output Format",
)
process_btn = gr.Button("▶️ Extract & Segment Audio", variant="primary")
error_msg = gr.Markdown(visible=False)
segments_container = gr.Markdown(visible=False)
segment_state = gr.State([])
@gr.render(inputs=segment_state)
def show_segments(paths):
if not paths:
gr.Markdown("Segments will appear here after processing.")
return
gr.Markdown(f"**Generated {len(paths)} segment(s):**")
for i, p in enumerate(paths):
with gr.Row():
gr.Audio(value=p, label=f"Segment {i + 1}", interactive=False)
def discover_duration(video_file):
if video_file is None:
return 60.0, "No video uploaded", "00:00:00", "00:01:00"
dur = get_video_duration(video_file)
return dur, f"{seconds_to_hms(dur)} (total {dur:.2f} s)", "00:00:00", seconds_to_hms(min(10.0, dur))
discover_btn.click(
fn=discover_duration,
inputs=video_input,
outputs=[duration_state, duration_text, start_input, end_input],
)
def toggle_mode(m):
if m == "Fixed segment length":
return gr.update(visible=True), gr.update(visible=False)
else:
return gr.update(visible=False), gr.update(visible=True)
mode.change(
fn=toggle_mode,
inputs=mode,
outputs=[seg_len, n_seg],
)
def on_process(video_file, start_hms, end_hms, mode_val, seg_len_val, n_seg_val, overlap_val, out_format_val):
paths, container_update, err_update = process_video(
video_file, start_hms, end_hms, mode_val, seg_len_val, n_seg_val, overlap_val, out_format_val
)
return paths, container_update, err_update
process_btn.click(
fn=on_process,
inputs=[video_input, start_input, end_input, mode, seg_len, n_seg, overlap, out_format],
outputs=[segment_state, segments_container, error_msg],
)
if __name__ == "__main__":
demo.launch()