# audio_split_zip_gradio.py # Gradio app: upload big audio -> split into N-second parts -> download ZIP (mp3 parts) # UI: English + custom neon/glass style with subtle animations import os import math import re import time import tempfile import zipfile import subprocess from pathlib import Path import gradio as gr # ------------------------- # Subprocess helpers # ------------------------- def _run(cmd): p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if p.returncode != 0: raise RuntimeError(f"Command failed:\n{' '.join(cmd)}\n\n{p.stderr.strip()}") return p.stdout, p.stderr def ffprobe_info(path: str) -> dict: """ Returns: duration_sec: float|None bitrate_bps: int|None (tries stream bitrate, then format bitrate) """ info = {"duration_sec": None, "bitrate_bps": None} out, _ = _run([ "ffprobe", "-v", "error", "-show_entries", "format=duration,bit_rate", "-of", "default=noprint_wrappers=1:nokey=0", path ]) for line in out.splitlines(): if "=" not in line: continue k, v = line.split("=", 1) k, v = k.strip(), v.strip() if k == "duration": try: info["duration_sec"] = float(v) except Exception: pass elif k == "bit_rate": try: info["bitrate_bps"] = int(v) except Exception: pass out2, _ = _run([ "ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries", "stream=bit_rate", "-of", "default=noprint_wrappers=1:nokey=0", path ]) for line in out2.splitlines(): if "=" not in line: continue k, v = line.split("=", 1) if k.strip() == "bit_rate": try: info["bitrate_bps"] = int(v.strip()) except Exception: pass return info def to_kbps(bps: int | None) -> int | None: if not bps: return None return max(1, int(round(bps / 1000))) def sanitize_filename(name: str) -> str: name = re.sub(r"[^\w\-.() ]+", "_", name, flags=re.UNICODE).strip().strip(".") return name or "audio" def format_seconds(s: float) -> str: s = max(0, float(s)) hh = int(s // 3600) mm = int((s % 3600) // 60) ss = int(s % 60) return f"{hh:02d}:{mm:02d}:{ss:02d}" if hh > 0 else f"{mm:02d}:{ss:02d}" # ------------------------- # Temp cleanup # ------------------------- def cleanup_tmpdirs(prefix="audiosplit_", older_than_seconds=6 * 3600): tmp_root = Path(tempfile.gettempdir()) now = time.time() for p in tmp_root.glob(prefix + "*"): try: if not p.is_dir(): continue age = now - p.stat().st_mtime if age > older_than_seconds: import shutil shutil.rmtree(p, ignore_errors=True) except Exception: pass # ------------------------- # Main logic # ------------------------- def split_and_zip(file_path: str, chunk_seconds: int, quality_mode: str, custom_kbps: int): if not file_path or not os.path.exists(file_path): raise gr.Error("Please upload an audio file.") try: chunk_seconds = int(chunk_seconds) except Exception: raise gr.Error("Chunk length must be an integer (seconds).") if chunk_seconds <= 0: raise gr.Error("Chunk length must be greater than 0 seconds.") cleanup_tmpdirs() info = ffprobe_info(file_path) duration = info.get("duration_sec") src_kbps = to_kbps(info.get("bitrate_bps")) if duration is None: raise gr.Error("Couldn't detect audio duration. Check ffmpeg/ffprobe and the file format.") # bitrate selection if quality_mode == "Auto (same as source)": out_kbps = src_kbps if src_kbps else 192 out_quality_text = f"Auto → {out_kbps} kbps" + ("" if src_kbps else " (fallback)") else: out_kbps = int(max(8, min(320, int(custom_kbps)))) out_quality_text = f"Custom → {out_kbps} kbps" base = sanitize_filename(Path(file_path).stem) tmpdir = tempfile.mkdtemp(prefix="audiosplit_") parts_dir = os.path.join(tmpdir, "parts") os.makedirs(parts_dir, exist_ok=True) total_parts = int(math.ceil(duration / chunk_seconds)) digits = max(3, len(str(total_parts))) created = [] for i in range(total_parts): start = i * chunk_seconds remaining = max(0.0, duration - start) this_len = min(float(chunk_seconds), remaining) out_name = ( f"{base}_part_{str(i+1).zfill(digits)}_" f"{format_seconds(start)}-{format_seconds(start+this_len)}.mp3" ) out_path = os.path.join(parts_dir, out_name) cmd = [ "ffmpeg", "-y", "-ss", str(start), "-t", str(this_len), "-i", file_path, "-vn", "-c:a", "libmp3lame", "-b:a", f"{out_kbps}k", out_path ] _run(cmd) created.append(out_path) zip_path = os.path.join(tmpdir, f"{base}_split_{chunk_seconds}s_mp3.zip") with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: for p in created: zf.write(p, arcname=os.path.basename(p)) src_txt = f"{src_kbps} kbps" if src_kbps else "unknown" status = ( f"Input: {Path(file_path).name}\n" f"Duration: {duration:.2f} sec\n" f"Chunk length: {chunk_seconds} sec\n" f"Parts: {total_parts}\n" f"Source bitrate: {src_txt}\n" f"Output: mp3\n" f"Quality: {out_quality_text}\n" f"ZIP is ready." ) return zip_path, status # ------------------------- # UI styling # ------------------------- CSS = r""" /* --------- Neon / Glass Theme (subtle animations) --------- */ :root{ --bg0:#05060a; --bg1:#070a12; --panel: rgba(255,255,255,0.06); --panel2: rgba(0,0,0,0.25); --stroke: rgba(255,255,255,0.14); --txt: rgba(255,255,255,0.92); --muted: rgba(255,255,255,0.70); --neon1:#7c3aed; /* purple */ --neon2:#22d3ee; /* cyan */ --neon3:#f97316; /* orange */ --good:#34d399; --bad:#fb7185; } .gradio-container { color: var(--txt) !important; background: radial-gradient(900px 600px at 20% 10%, rgba(124,58,237,0.35), transparent 55%), radial-gradient(700px 500px at 80% 20%, rgba(34,211,238,0.25), transparent 60%), radial-gradient(700px 500px at 50% 90%, rgba(249,115,22,0.15), transparent 60%), linear-gradient(180deg, var(--bg0), var(--bg1)) !important; } #app { max-width: 980px; margin: 0 auto; padding: 18px 14px 28px; } .glass { background: var(--panel); border: 1px solid var(--stroke); border-radius: 18px; padding: 16px; backdrop-filter: blur(10px); box-shadow: 0 0 0 1px rgba(255,255,255,0.04) inset, 0 14px 50px rgba(0,0,0,0.55); } .header { border-radius: 18px; padding: 16px 18px; border: 1px solid rgba(255,255,255,0.12); background: linear-gradient(135deg, rgba(124,58,237,0.16), rgba(34,211,238,0.10) 55%, rgba(249,115,22,0.07)); box-shadow: 0 14px 50px rgba(0,0,0,0.55); } #title { margin: 0; font-size: 24px; letter-spacing: 0.2px; line-height: 1.15; background: linear-gradient(90deg, var(--neon2), var(--neon1), var(--neon3)); background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: hueflow 6s ease-in-out infinite; } @keyframes hueflow { 0% { background-position: 0% 50%; filter: drop-shadow(0 0 10px rgba(34,211,238,0.18)); } 50% { background-position: 100% 50%; filter: drop-shadow(0 0 14px rgba(124,58,237,0.22)); } 100% { background-position: 0% 50%; filter: drop-shadow(0 0 10px rgba(249,115,22,0.15)); } } .subtitle { margin-top: 6px; color: var(--muted); font-size: 13px; } .smallnote { color: var(--muted); font-size: 13px; } /* Make inputs feel more "custom" */ .gradio-container input[type="text"], .gradio-container input[type="number"], .gradio-container textarea { background: rgba(0,0,0,0.25) !important; border: 1px solid rgba(255,255,255,0.14) !important; border-radius: 14px !important; } .gradio-container label { color: var(--muted) !important; } /* Button: neon glow + hover lift */ .gradio-container button.primary { border-radius: 16px !important; border: 1px solid rgba(255,255,255,0.16) !important; background: linear-gradient(90deg, rgba(34,211,238,0.22), rgba(124,58,237,0.22), rgba(249,115,22,0.18)) !important; box-shadow: 0 10px 30px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.05) inset; transition: transform .15s ease, box-shadow .15s ease, filter .15s ease; } .gradio-container button.primary:hover { transform: translateY(-1px); filter: brightness(1.05); box-shadow: 0 14px 44px rgba(0,0,0,0.60), 0 0 22px rgba(34,211,238,0.12), 0 0 24px rgba(124,58,237,0.10); } /* File input area a bit nicer */ .gradio-container .file-preview, .gradio-container .upload-container { border-radius: 16px !important; border: 1px dashed rgba(255,255,255,0.18) !important; background: rgba(0,0,0,0.16) !important; } /* Mobile: bigger text and spacing */ @media (max-width: 640px) { #app { padding: 12px 10px 24px; } #title { font-size: 20px; } .subtitle, .smallnote { font-size: 13px; } .glass { padding: 14px; } .gradio-container button.primary { width: 100%; } } """ # ------------------------- # UI # ------------------------- with gr.Blocks() as demo: with gr.Column(elem_id="app"): with gr.Column(elem_classes=["header"]): gr.Markdown("## Neon Audio Splitter → ZIP", elem_id="title") gr.Markdown( "
" "Upload a large audio file, choose chunk length, optionally override bitrate, and download a ZIP of MP3 parts." "
" ) with gr.Column(elem_classes=["glass"]): inp = gr.File(label="Audio file", file_count="single", type="filepath") chunk_seconds = gr.Number(label="Chunk length (seconds)", value=10, precision=0) quality_mode = gr.Radio( label="Bitrate mode", choices=["Auto (same as source)", "Custom (8..320 kbps)"], value="Auto (same as source)" ) custom_bitrate = gr.Slider( label="Custom bitrate (kbps)", minimum=8, maximum=320, value=192, step=1, visible=False, interactive=True ) gr.Markdown( "
" "Auto tries to read the original bitrate via ffprobe. " "If it can't be detected, it uses 192 kbps as a fallback." "
" ) btn = gr.Button("Split & Download ZIP", variant="primary") with gr.Column(elem_classes=["glass"]): out_zip = gr.File(label="ZIP archive (download)") status = gr.Textbox(label="Status", lines=8) def toggle_custom(mode): return gr.update(visible=mode.startswith("Custom")) quality_mode.change(toggle_custom, inputs=quality_mode, outputs=custom_bitrate) btn.click( split_and_zip, inputs=[inp, chunk_seconds, quality_mode, custom_bitrate], outputs=[out_zip, status] ) if __name__ == "__main__": demo.launch( server_name="0.0.0.0", server_port=7860, ssr_mode=False, css=CSS )