""" 🎵 Audio Segment Extractor - Upload audio file - Timeline visualization - Start/End time segment extraction - Download """ import gradio as gr import os import json import subprocess import tempfile import time # ============================================ # Temp directory for audio processing # ============================================ UPLOAD_DIR = tempfile.mkdtemp() # ============================================ # 🎵 Audio Processing Functions # ============================================ def get_audio_info_and_waveform(audio_file): """Get audio info and generate waveform data""" if not audio_file: return None, 0, 0, 10, "🎵 Please upload an audio file" try: path = audio_file.name if hasattr(audio_file, 'name') else audio_file # Get audio info probe_cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration,bit_rate,format_name:stream=sample_rate,channels,codec_name', '-of', 'json', path] result = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=30) info = json.loads(result.stdout) format_info = info.get('format', {}) stream_info = info.get('streams', [{}])[0] if info.get('streams') else {} duration = float(format_info.get('duration', 0)) bit_rate = int(format_info.get('bit_rate', 0)) // 1000 if format_info.get('bit_rate') else 0 format_name = format_info.get('format_name', 'N/A') sample_rate = stream_info.get('sample_rate', 'N/A') channels = stream_info.get('channels', 'N/A') codec = stream_info.get('codec_name', 'N/A') # File size file_size = os.path.getsize(path) / (1024 * 1024) # MB info_text = f"""🎵 Audio Information ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⏱️ Duration: {duration:.2f}s ({duration/60:.1f}min) 🎚️ Bitrate: {bit_rate} kbps 🔊 Sample Rate: {sample_rate} Hz 📢 Channels: {channels} 🎛️ Codec: {codec} 📁 Format: {format_name} 💾 Size: {file_size:.2f} MB ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━""" # Return audio preview and update sliders return path, 0, min(duration, 10), duration, info_text except Exception as e: return None, 0, 10, 100, f"❌ Failed to get info: {str(e)}" def extract_audio_segment(audio_file, start_time, end_time): """Extract audio segment""" if not audio_file: return None, "❌ Please upload an audio file", None try: path = audio_file.name if hasattr(audio_file, 'name') else audio_file start_time = float(start_time) if start_time else 0 end_time = float(end_time) if end_time else 10 if end_time <= start_time: return None, "❌ End time must be greater than start time", None duration = end_time - start_time # Determine output file extension ext = os.path.splitext(path)[1].lower() if ext not in ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac']: ext = '.mp3' output_path = os.path.join(UPLOAD_DIR, f"audio_cut_{int(time.time())}{ext}") # Try stream copy first (fast, lossless) cmd = [ 'ffmpeg', '-y', '-i', path, '-ss', str(start_time), '-t', str(duration), '-c', 'copy', output_path ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) # If copy fails, try re-encoding if not os.path.exists(output_path) or os.path.getsize(output_path) == 0: output_path = os.path.join(UPLOAD_DIR, f"audio_cut_{int(time.time())}.mp3") cmd = [ 'ffmpeg', '-y', '-i', path, '-ss', str(start_time), '-t', str(duration), '-acodec', 'libmp3lame', '-b:a', '192k', output_path ] subprocess.run(cmd, capture_output=True, timeout=120) if os.path.exists(output_path) and os.path.getsize(output_path) > 0: file_size = os.path.getsize(output_path) / 1024 # KB status_text = f"""✅ Audio Extraction Complete! ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⏱️ Start: {start_time:.2f}s ⏱️ End: {end_time:.2f}s ⏱️ Duration: {duration:.2f}s 📁 Size: {file_size:.1f} KB ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━""" return output_path, status_text, output_path else: return None, "❌ Audio extraction failed", None except Exception as e: return None, f"❌ Error: {str(e)}", None def format_time(seconds): """Convert seconds to MM:SS.ms format""" if seconds is None: return "00:00.00" mins = int(seconds // 60) secs = seconds % 60 return f"{mins:02d}:{secs:05.2f}" def update_time_display(start_time, end_time): """Update time display based on slider values""" if start_time is not None and end_time is not None: duration = end_time - start_time if duration < 0: return "⚠️ End time must be greater than start time" return f"🕐 {format_time(start_time)} → {format_time(end_time)} (Duration: {duration:.2f}s)" return "🕐 Select a segment" def update_end_slider(start_time, max_duration): """Update end slider minimum based on start time""" return gr.update(minimum=start_time + 0.1) # ============================================ # 🎨 Comic Classic Theme CSS # ============================================ css = """ /* ===== 🎨 Google Fonts Import ===== */ @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap'); /* ===== 🎨 Comic Classic Background ===== */ .gradio-container { background-color: #FEF9C3 !important; background-image: radial-gradient(#1F2937 1px, transparent 1px) !important; background-size: 20px 20px !important; min-height: 100vh !important; font-family: 'Comic Neue', cursive, sans-serif !important; } /* ===== Hide HuggingFace Elements ===== */ .tabs, .tab-nav, .huggingface-space-header, #space-header, .space-header, [class*="space-header"], .h-header, .gradio-container > header, .contain > header, .svelte-1ed2p3z, .space-header-badge, .header-badge, [data-testid="space-header"], .gr-prose > div:first-child, .contain > div:first-child > div:first-child, .app > div:first-child > header, iframe[src*="huggingface"], .huggingface-logo, .hf-logo, .svelte-kqij2n, .svelte-1ax1toq, .embed-container > div:first-child { display: none !important; visibility: hidden !important; height: 0 !important; width: 0 !important; overflow: hidden !important; opacity: 0 !important; pointer-events: none !important; } /* ===== Hide Footer ===== */ footer, .footer, .gradio-container footer, .built-with, [class*="footer"], .gradio-footer, .main-footer, div[class*="footer"], .show-api, .built-with-gradio, a[href*="gradio.app"], a[href*="huggingface.co/spaces"] { display: none !important; visibility: hidden !important; height: 0 !important; padding: 0 !important; margin: 0 !important; } /* ===== Main Container ===== */ #col-container { max-width: 1000px; margin: 0 auto; } /* ===== 🎨 Header Title - Comic Style ===== */ .header-text h1 { font-family: 'Bangers', cursive !important; color: #1F2937 !important; font-size: 3.5rem !important; font-weight: 400 !important; text-align: center !important; margin-bottom: 0.5rem !important; text-shadow: 4px 4px 0px #FACC15, 6px 6px 0px #1F2937 !important; letter-spacing: 3px !important; -webkit-text-stroke: 2px #1F2937 !important; } /* ===== 🎨 Subtitle ===== */ .subtitle { text-align: center !important; font-family: 'Comic Neue', cursive !important; font-size: 1.2rem !important; color: #1F2937 !important; margin-bottom: 1.5rem !important; font-weight: 700 !important; } /* ===== 🎨 Cards/Panels - Comic Frame Style ===== */ .gr-panel, .gr-box, .gr-form, .block, .gr-group { background: #FFFFFF !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; box-shadow: 6px 6px 0px #1F2937 !important; transition: all 0.2s ease !important; } .gr-panel:hover, .block:hover { transform: translate(-2px, -2px) !important; box-shadow: 8px 8px 0px #1F2937 !important; } /* ===== 🎨 Input Fields ===== */ textarea, input[type="text"], input[type="number"] { background: #FFFFFF !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; color: #1F2937 !important; font-family: 'Comic Neue', cursive !important; font-size: 1rem !important; font-weight: 700 !important; transition: all 0.2s ease !important; } textarea:focus, input[type="text"]:focus, input[type="number"]:focus { border-color: #3B82F6 !important; box-shadow: 4px 4px 0px #3B82F6 !important; outline: none !important; } textarea::placeholder { color: #9CA3AF !important; font-weight: 400 !important; } /* ===== 🎨 Primary Button - Comic Blue ===== */ .gr-button-primary, button.primary, .gr-button.primary { background: #3B82F6 !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; color: #FFFFFF !important; font-family: 'Bangers', cursive !important; font-weight: 400 !important; font-size: 1.3rem !important; letter-spacing: 2px !important; padding: 14px 28px !important; box-shadow: 5px 5px 0px #1F2937 !important; transition: all 0.1s ease !important; text-shadow: 1px 1px 0px #1F2937 !important; } .gr-button-primary:hover, button.primary:hover, .gr-button.primary:hover { background: #2563EB !important; transform: translate(-2px, -2px) !important; box-shadow: 7px 7px 0px #1F2937 !important; } .gr-button-primary:active, button.primary:active, .gr-button.primary:active { transform: translate(3px, 3px) !important; box-shadow: 2px 2px 0px #1F2937 !important; } /* ===== 🎨 Secondary Button - Comic Red ===== */ .gr-button-secondary, button.secondary { background: #EF4444 !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; color: #FFFFFF !important; font-family: 'Bangers', cursive !important; font-weight: 400 !important; font-size: 1.1rem !important; letter-spacing: 1px !important; box-shadow: 4px 4px 0px #1F2937 !important; transition: all 0.1s ease !important; text-shadow: 1px 1px 0px #1F2937 !important; } .gr-button-secondary:hover, button.secondary:hover { background: #DC2626 !important; transform: translate(-2px, -2px) !important; box-shadow: 6px 6px 0px #1F2937 !important; } .gr-button-secondary:active, button.secondary:active { transform: translate(2px, 2px) !important; box-shadow: 2px 2px 0px #1F2937 !important; } /* ===== 🎨 Extract Button - Comic Green ===== */ .extract-btn { background: #10B981 !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; color: #FFFFFF !important; font-family: 'Bangers', cursive !important; font-weight: 400 !important; font-size: 1.3rem !important; letter-spacing: 2px !important; box-shadow: 5px 5px 0px #1F2937 !important; transition: all 0.1s ease !important; text-shadow: 1px 1px 0px #1F2937 !important; } .extract-btn:hover { background: #059669 !important; transform: translate(-2px, -2px) !important; box-shadow: 7px 7px 0px #1F2937 !important; } .extract-btn:active { transform: translate(3px, 3px) !important; box-shadow: 2px 2px 0px #1F2937 !important; } /* ===== 🎨 Slider ===== */ input[type="range"] { accent-color: #10B981 !important; height: 8px !important; } .gr-slider { background: #FACC15 !important; } /* ===== 🎨 Labels ===== */ label, .gr-input-label, .gr-block-label { color: #1F2937 !important; font-family: 'Comic Neue', cursive !important; font-weight: 700 !important; font-size: 1rem !important; } span.gr-label { color: #1F2937 !important; } /* ===== 🎨 Info Text ===== */ .gr-info, .info { color: #6B7280 !important; font-family: 'Comic Neue', cursive !important; font-size: 0.9rem !important; } /* ===== 🎨 Status Text Area ===== */ .status-text textarea { background: #E0F2FE !important; border: 3px solid #0EA5E9 !important; color: #0C4A6E !important; font-family: 'Comic Neue', cursive !important; font-weight: 700 !important; font-size: 0.95rem !important; } /* ===== 🎨 Info Box ===== */ .info-box textarea { background: #FEF3C7 !important; border: 3px solid #F59E0B !important; color: #92400E !important; font-family: 'Courier New', monospace !important; font-weight: 600 !important; font-size: 0.9rem !important; } /* ===== 🎨 Time Display ===== */ .time-display { font-family: 'Bangers', cursive !important; font-size: 1.5rem !important; color: #1F2937 !important; text-align: center !important; padding: 15px !important; background: linear-gradient(135deg, #FACC15 0%, #FDE68A 100%) !important; border: 3px solid #1F2937 !important; border-radius: 8px !important; box-shadow: 4px 4px 0px #1F2937 !important; letter-spacing: 1px !important; } /* ===== 🎨 Audio Player ===== */ audio { width: 100% !important; border-radius: 8px !important; border: 3px solid #1F2937 !important; box-shadow: 4px 4px 0px #1F2937 !important; } /* ===== 🎨 Scrollbar - Comic Style ===== */ ::-webkit-scrollbar { width: 12px; height: 12px; } ::-webkit-scrollbar-track { background: #FEF9C3; border: 2px solid #1F2937; } ::-webkit-scrollbar-thumb { background: #3B82F6; border: 2px solid #1F2937; border-radius: 0px; } ::-webkit-scrollbar-thumb:hover { background: #EF4444; } /* ===== 🎨 Selection Highlight ===== */ ::selection { background: #FACC15; color: #1F2937; } /* ===== 🎨 Links ===== */ a { color: #3B82F6 !important; text-decoration: none !important; font-weight: 700 !important; border-bottom: 2px solid #3B82F6 !important; } a:hover { color: #EF4444 !important; border-bottom-color: #EF4444 !important; } /* ===== 🎨 Row/Column Spacing ===== */ .gr-row { gap: 1.5rem !important; } .gr-column { gap: 1rem !important; } /* ===== 🎨 File Upload Area ===== */ .gr-file-upload { border: 3px dashed #1F2937 !important; border-radius: 8px !important; background: #FEF9C3 !important; } .gr-file-upload:hover { border-color: #3B82F6 !important; background: #EFF6FF !important; } /* ===== Responsive Adjustments ===== */ @media (max-width: 768px) { .header-text h1 { font-size: 2.2rem !important; text-shadow: 3px 3px 0px #FACC15, 4px 4px 0px #1F2937 !important; } .gr-button-primary, button.primary { padding: 12px 20px !important; font-size: 1.1rem !important; } .gr-panel, .block { box-shadow: 4px 4px 0px #1F2937 !important; } .time-display { font-size: 1.2rem !important; } } /* ===== 🎨 Disable Dark Mode ===== */ @media (prefers-color-scheme: dark) { .gradio-container { background-color: #FEF9C3 !important; } } """ # ============================================ # Gradio UI # ============================================ with gr.Blocks(title="Audio Segment Extractor") as demo: # Inject CSS via HTML gr.HTML(f"") # HOME Badge gr.HTML("""
""") # Header Title gr.Markdown( """ # 🎵 AUDIO SEGMENT EXTRACTOR 🎵 """, elem_classes="header-text" ) gr.Markdown( """✂️ Cut any segment from your audio file! 🎶
""", ) # State for max duration max_duration = gr.State(100) with gr.Row(equal_height=False): # ============================================ # Left Column - Input # ============================================ with gr.Column(scale=1, min_width=350): # File upload audio_input = gr.File( label="🎵 Upload Audio File", file_types=["audio"], ) # Audio info audio_info = gr.Textbox( label="📊 Audio Information", placeholder="Upload an audio file to see information...", lines=10, interactive=False, elem_classes="info-box" ) # Original audio preview gr.Markdown("### 🎧 Original Audio Preview") original_audio = gr.Audio( label="", type="filepath", interactive=False, ) # ============================================ # Right Column - Controls & Output # ============================================ with gr.Column(scale=1, min_width=350): # Segment selection gr.Markdown("### ✂️ Select Extraction Segment") # Start time slider start_time = gr.Slider( minimum=0, maximum=100, value=0, step=0.1, label="⏱️ Start Time (seconds)", info="Drag to select start time" ) # End time slider end_time = gr.Slider( minimum=0, maximum=100, value=10, step=0.1, label="⏱️ End Time (seconds)", info="Drag to select end time" ) # Time display time_display = gr.Textbox( label="", value="🕐 Select a segment", interactive=False, elem_classes="time-display" ) # Extract button extract_btn = gr.Button( "✂️ EXTRACT AUDIO SEGMENT!", variant="primary", size="lg", elem_classes="extract-btn" ) # Extraction status extract_status = gr.Textbox( label="📋 Extraction Status", placeholder="Extraction results will appear here...", lines=8, interactive=False, elem_classes="status-text" ) # Extracted audio preview gr.Markdown("### 🎧 Extracted Audio Preview") extracted_audio = gr.Audio( label="", type="filepath", interactive=False, ) # Download download_file = gr.File( label="📥 Download", ) # ============================================ # Event Handlers # ============================================ # File upload - get info and update sliders audio_input.change( fn=get_audio_info_and_waveform, inputs=[audio_input], outputs=[original_audio, start_time, end_time, max_duration, audio_info] ).then( fn=lambda d: (gr.update(maximum=d), gr.update(maximum=d)), inputs=[max_duration], outputs=[start_time, end_time] ) # Start time change - update time display start_time.change( fn=update_time_display, inputs=[start_time, end_time], outputs=[time_display] ) # End time change - update time display end_time.change( fn=update_time_display, inputs=[start_time, end_time], outputs=[time_display] ) # Extract button click extract_btn.click( fn=extract_audio_segment, inputs=[audio_input, start_time, end_time], outputs=[extracted_audio, extract_status, download_file] ) if __name__ == "__main__": demo.launch()