import gradio as gr import os import uuid from pydub import AudioSegment from pydub.silence import split_on_silence import re import time import subprocess import threading # ─── File Helpers ───────────────────────────────────────────────────────────── def clean_file_name(file_path): file_name = os.path.basename(file_path) file_name, file_extension = os.path.splitext(file_name) cleaned = re.sub(r'[^a-zA-Z\d]+', '_', file_name) clean_name = re.sub(r'_+', '_', cleaned).strip('_') if clean_name.endswith('_tmp'): clean_name = clean_name[:-4] random_uuid = uuid.uuid4().hex[:6] clean_file_path = os.path.join( os.path.dirname(file_path), f"{clean_name}_{random_uuid}{file_extension}" ) return clean_file_path def calculate_duration(file_path): audio = AudioSegment.from_file(file_path) return len(audio) / 1000.0 # ─── File Tracking & Cleanup ────────────────────────────────────────────────── FILE_TIMESTAMPS = {} def track_file(file_path): FILE_TIMESTAMPS[file_path] = time.time() def cleanup_tracked_files(max_age_seconds=3600): now = time.time() to_delete = [] for file_path, created_time in list(FILE_TIMESTAMPS.items()): if now - created_time > max_age_seconds: if os.path.exists(file_path): try: os.remove(file_path) except Exception as e: pass to_delete.append(file_path) for f in to_delete: FILE_TIMESTAMPS.pop(f, None) _CLEANUP_STARTED = False def start_cleanup_worker(interval=3600): global _CLEANUP_STARTED if _CLEANUP_STARTED: return _CLEANUP_STARTED = True def worker(): while True: cleanup_tracked_files() time.sleep(interval) threading.Thread(target=worker, daemon=True).start() # ─── Audio Conversion ───────────────────────────────────────────────────────── def convert_to_wav(audio_path): if not os.path.isfile(audio_path): return None # Get extension ext = os.path.splitext(audio_path)[1].lower() # If already mp3 or wav, return original file if ext in [".mp3", ".wav"]: return audio_path # Clean filename file_name = os.path.splitext(os.path.basename(audio_path))[0] clean_name = re.sub(r'[^a-zA-Z0-9]+', '_', file_name) clean_name = re.sub(r'_+', '_', clean_name).strip('_') # Output wav path wav_path = os.path.join( os.path.dirname(audio_path), f"{clean_name}_tmp.wav" ) try: subprocess.run( [ "ffmpeg", "-y", "-i", audio_path, "-ar", "16000", "-ac", "1", wav_path ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True ) if os.path.isfile(wav_path) and os.path.getsize(wav_path) > 0: return wav_path except Exception: pass return None # ───pydub ───────────────────────────────────────────── # def remove_silence_pydub(file_path, minimum_silence=50): # sound = AudioSegment.from_file(file_path) # auto-detects format # audio_chunks = split_on_silence(sound, # min_silence_len=100, # silence_thresh=-45, # keep_silence=minimum_silence) # combined = AudioSegment.empty() # for chunk in audio_chunks: # combined += chunk # output_path=clean_file_name(file_path) # combined.export(output_path) # return output_path def remove_silence_pydub(file_path, minimum_silence=50): sound = AudioSegment.from_file(file_path) # Try splitting with default -45 dBFS audio_chunks = split_on_silence( sound, min_silence_len=100, silence_thresh=-45, keep_silence=minimum_silence ) # If no chunks were extracted (e.g. whole file is quieter than -45dBFS) # retry with a dynamic threshold relative to the audio's actual loudness if not audio_chunks: dynamic_thresh = sound.dBFS - 16 audio_chunks = split_on_silence( sound, min_silence_len=100, silence_thresh=dynamic_thresh, keep_silence=minimum_silence ) combined = AudioSegment.empty() for chunk in audio_chunks: combined += chunk if len(combined) == 0: combined = sound output_path = clean_file_name(file_path) ext = os.path.splitext(output_path)[1].lower().replace('.', '') if not ext: ext = "wav" output_path += ".wav" combined.export(output_path, format=ext) return output_path # ─── Main Processing ────────────────────────────────────────────────────────── def process_audio(audio_file, seconds_float): if audio_file is None: return None, None, "" if not os.path.exists(audio_file): return None, None, "" try: seconds = max(0.0, float(seconds_float)) except ValueError: seconds = 0.05 track_file(audio_file) converted_audio = convert_to_wav(audio_file) if converted_audio: track_file(converted_audio) audio_file = converted_audio else: return None, None, "Invalid file format or conversion failed." keep_silence = int(seconds * 1000) try: before = calculate_duration(audio_file) # Process strictly with PyDub output_audio_file = remove_silence_pydub( audio_file, minimum_silence=keep_silence ) track_file(output_audio_file) after = calculate_duration(output_audio_file) removed = before - after percent = (removed / before * 100) if before > 0 else 0 def fmt(s): m = int(s // 60) sec = s % 60 if m > 0: return f"{m}m {sec:.1f}s" return f"{sec:.2f}s" mode_label = "" # Sleek, Dark-Themed Result Card result_html = f"""
Original
{fmt(before)}
New
{fmt(after)}
Removed
{percent:.1f}%
{fmt(removed)}
""" return output_audio_file, output_audio_file, result_html except Exception as e: return None, None, f"

Error: {str(e)}

" # ----------------------------- # CSS FOR LAYOUT & THEMING # ----------------------------- css = """ /* APP & MAIN */ body, html { background: #171717 !important; font-family: 'Inter', sans-serif !important; color: white !important; margin: 0; padding: 0; } /* 🟢 FORCING THE WIDE LAYOUT 🟢 */ .gradio-container { max-width: 1500px !important; /* Forces container to stretch out */ width: 95% !important; /* Uses 95% of the screen width */ margin: auto !important; background: #171717 !important; padding: 20px 24px !important; } /* REMOVE DEFAULT FOOTER */ footer { display: none !important; } .dark { --body-background-fill: #171717 !important; --block-background-fill: #262626 !important; --block-border-color: #333 !important; } /* TOPBAR */ .topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; } .logo { display: flex; align-items: center; gap: 12px; } .logo-icon { width: 32px; height: 32px; border-radius: 8px; background: white; color: black; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 15px; } .logo-text { font-size: 18px; font-weight: 600; } .nav { display: flex; gap: 22px; align-items: center; } .nav a { color: #a1a1aa; text-decoration: none; cursor: pointer; transition: 0.2s; font-size: 13px; font-weight: 500; } .nav a:hover { color: white; } .nav-yt { color: #ff4e4e !important; } .nav-kofi { color: #29abe0 !important; } /* HERO */ .hero { text-align: center; margin-bottom: 35px; } .hero h1 { font-size: 42px; font-weight: 800; letter-spacing: -0.05em; margin-bottom: 6px; } /* BLUE HERO TEXT */ .hero span { color: #3b82f6; } .hero p { color: #a1a1aa; font-size: 15px; margin: 4px 0 16px 0; } .badges { display: flex; justify-content: center; gap: 10px; } .badge { background: #262626; border: 1px solid #333; padding: 6px 14px; border-radius: 999px; color: #d4d4d8; font-size: 11px; font-weight: 500; } /* CARDS / PANELS */ .panel { background: #262626 !important; border: 1px solid #333 !important; border-radius: 14px !important; padding: 24px !important; box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; } /* FORCE SIDE BY SIDE ON DESKTOP */ @media (min-width: 768px) { .side-by-side { flex-wrap: nowrap !important; } } /* TITLES */ .section-title { font-size: 12px; text-transform: uppercase; color: #888; letter-spacing: 1px; margin-bottom: 14px; font-weight: 600; } /* AUDIO COMPONENT */ audio { border-radius: 8px !important; background: #1f1f1f !important; } /* CUSTOM BLUE BUTTON */ #process-btn { background: #3b82f6 !important; color: white !important; border: none !important; border-radius: 8px !important; height: 48px !important; font-size: 15px !important; font-weight: 600 !important; margin-top: 14px !important; transition: all 0.2s ease-in-out !important; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3) !important; } #process-btn:hover { background: #2563eb !important; box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4) !important; transform: translateY(-1px); } /* INPUTS */ .gr-textbox, .gr-dropdown { background: #1f1f1f !important; border: 1px solid #3a3a3a !important; border-radius: 8px !important; } /* MOBILE RESPONSIVENESS */ @media(max-width: 900px) { .topbar { flex-direction: column; gap: 16px; } .nav { flex-wrap: wrap; justify-content: center; } } """ # ----------------------------- # START BACKGROUND WORKERS # ----------------------------- start_cleanup_worker() # ----------------------------- # UI # ----------------------------- with gr.Blocks( theme=gr.themes.Base(), css=css, title="Remove Silence" ) as demo: # TOPBAR gr.HTML("""
""") # HERO gr.HTML("""

REMOVE SILENCE

Drop your audio and instantly clean pauses for Shorts, TikTok & Reels

100% Free
No Sign-Up
""") with gr.Row(equal_height=False, elem_classes="side-by-side"): # LEFT PANEL (UPLOAD) - Adjusted min_width to stretch beautifully in the new wide container with gr.Column(scale=1, min_width=450): with gr.Group(elem_classes="panel"): gr.HTML('
Upload & Settings
') input_audio = gr.Audio( type="filepath", label="", show_label=False ) with gr.Row(): keep_silence = gr.Number( label="Keep Silence Upto (In seconds)", value=0.05 ) # Blue submit button process_btn = gr.Button("Remove Silence", elem_id="process-btn") # RIGHT PANEL (RESULT) with gr.Column(scale=1, min_width=450): with gr.Group(elem_classes="panel"): gr.HTML('
Result
') output_audio = gr.Audio( label="Play Audio", show_label=False ) download_audio = gr.File( label="Download Audio", show_label=False ) stats_html = gr.HTML() # PROCESS process_btn.click( fn=process_audio, inputs=[input_audio, keep_silence], outputs=[output_audio, download_audio, stats_html] ) demo.launch()