cut / app.py
seawolf2357's picture
Update app.py
c93f126 verified
"""
🎡 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"<style>{css}</style>")
# HOME Badge
gr.HTML("""
<div style="text-align: center; margin: 20px 0 10px 0;">
<a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;">
<img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
</a>
</div>
""")
# Header Title
gr.Markdown(
"""
# 🎡 AUDIO SEGMENT EXTRACTOR 🎡
""",
elem_classes="header-text"
)
gr.Markdown(
"""
<p class="subtitle">βœ‚οΈ Cut any segment from your audio file! 🎢</p>
""",
)
# 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()