Update app.py
Browse files
app.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
"""
|
| 2 |
π΅ Audio Segment Extractor
|
| 3 |
-
-
|
| 4 |
-
-
|
| 5 |
-
-
|
| 6 |
-
-
|
| 7 |
"""
|
| 8 |
|
| 9 |
import gradio as gr
|
|
@@ -14,24 +14,24 @@ import tempfile
|
|
| 14 |
import time
|
| 15 |
|
| 16 |
# ============================================
|
| 17 |
-
#
|
| 18 |
# ============================================
|
| 19 |
UPLOAD_DIR = tempfile.mkdtemp()
|
| 20 |
|
| 21 |
|
| 22 |
# ============================================
|
| 23 |
-
# π΅
|
| 24 |
# ============================================
|
| 25 |
|
| 26 |
def get_audio_info_and_waveform(audio_file):
|
| 27 |
-
"""
|
| 28 |
if not audio_file:
|
| 29 |
-
return None, 0, 10, "π΅
|
| 30 |
|
| 31 |
try:
|
| 32 |
path = audio_file.name if hasattr(audio_file, 'name') else audio_file
|
| 33 |
|
| 34 |
-
#
|
| 35 |
probe_cmd = ['ffprobe', '-v', 'error',
|
| 36 |
'-show_entries', 'format=duration,bit_rate,format_name:stream=sample_rate,channels,codec_name',
|
| 37 |
'-of', 'json', path]
|
|
@@ -48,50 +48,50 @@ def get_audio_info_and_waveform(audio_file):
|
|
| 48 |
channels = stream_info.get('channels', 'N/A')
|
| 49 |
codec = stream_info.get('codec_name', 'N/A')
|
| 50 |
|
| 51 |
-
#
|
| 52 |
file_size = os.path.getsize(path) / (1024 * 1024) # MB
|
| 53 |
|
| 54 |
-
info_text = f"""π΅
|
| 55 |
ββββββββββββββββββββββββββββββ
|
| 56 |
-
β±οΈ
|
| 57 |
-
ποΈ
|
| 58 |
-
π
|
| 59 |
-
π’
|
| 60 |
-
ποΈ
|
| 61 |
-
π
|
| 62 |
-
πΎ
|
| 63 |
ββββββββββββββββββββββββββββββ"""
|
| 64 |
|
| 65 |
-
#
|
| 66 |
-
return path, 0,
|
| 67 |
|
| 68 |
except Exception as e:
|
| 69 |
-
return None, 0, 10, f"β
|
| 70 |
|
| 71 |
|
| 72 |
-
def extract_audio_segment(audio_file,
|
| 73 |
-
"""
|
| 74 |
if not audio_file:
|
| 75 |
-
return None, "β
|
| 76 |
|
| 77 |
try:
|
| 78 |
path = audio_file.name if hasattr(audio_file, 'name') else audio_file
|
| 79 |
-
start_time = float(
|
| 80 |
-
end_time = float(
|
| 81 |
|
| 82 |
if end_time <= start_time:
|
| 83 |
-
return None, "β
|
| 84 |
|
| 85 |
duration = end_time - start_time
|
| 86 |
|
| 87 |
-
#
|
| 88 |
ext = os.path.splitext(path)[1].lower()
|
| 89 |
if ext not in ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac']:
|
| 90 |
ext = '.mp3'
|
| 91 |
|
| 92 |
output_path = os.path.join(UPLOAD_DIR, f"audio_cut_{int(time.time())}{ext}")
|
| 93 |
|
| 94 |
-
#
|
| 95 |
cmd = [
|
| 96 |
'ffmpeg', '-y', '-i', path,
|
| 97 |
'-ss', str(start_time), '-t', str(duration),
|
|
@@ -100,7 +100,7 @@ def extract_audio_segment(audio_file, time_range):
|
|
| 100 |
|
| 101 |
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
| 102 |
|
| 103 |
-
# copy
|
| 104 |
if not os.path.exists(output_path) or os.path.getsize(output_path) == 0:
|
| 105 |
output_path = os.path.join(UPLOAD_DIR, f"audio_cut_{int(time.time())}.mp3")
|
| 106 |
cmd = [
|
|
@@ -113,24 +113,24 @@ def extract_audio_segment(audio_file, time_range):
|
|
| 113 |
if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
|
| 114 |
file_size = os.path.getsize(output_path) / 1024 # KB
|
| 115 |
|
| 116 |
-
status_text = f"""β
|
| 117 |
ββββββββββββββββββββββββββββββ
|
| 118 |
-
β±οΈ
|
| 119 |
-
β±οΈ
|
| 120 |
-
β±οΈ
|
| 121 |
-
π
|
| 122 |
ββββββββββββββββββββββββββββββ"""
|
| 123 |
|
| 124 |
return output_path, status_text, output_path
|
| 125 |
else:
|
| 126 |
-
return None, "β
|
| 127 |
|
| 128 |
except Exception as e:
|
| 129 |
-
return None, f"β
|
| 130 |
|
| 131 |
|
| 132 |
def format_time(seconds):
|
| 133 |
-
"""
|
| 134 |
if seconds is None:
|
| 135 |
return "00:00.00"
|
| 136 |
mins = int(seconds // 60)
|
|
@@ -138,14 +138,19 @@ def format_time(seconds):
|
|
| 138 |
return f"{mins:02d}:{secs:05.2f}"
|
| 139 |
|
| 140 |
|
| 141 |
-
def update_time_display(
|
| 142 |
-
"""
|
| 143 |
-
if
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
return f"π {format_time(
|
| 148 |
-
return "π
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
|
| 151 |
# ============================================
|
|
@@ -156,7 +161,7 @@ css = """
|
|
| 156 |
/* ===== π¨ Google Fonts Import ===== */
|
| 157 |
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
|
| 158 |
|
| 159 |
-
/* ===== π¨ Comic Classic
|
| 160 |
.gradio-container {
|
| 161 |
background-color: #FEF9C3 !important;
|
| 162 |
background-image:
|
|
@@ -166,7 +171,7 @@ css = """
|
|
| 166 |
font-family: 'Comic Neue', cursive, sans-serif !important;
|
| 167 |
}
|
| 168 |
|
| 169 |
-
/* =====
|
| 170 |
.tabs,
|
| 171 |
.tab-nav,
|
| 172 |
.huggingface-space-header,
|
|
@@ -198,7 +203,7 @@ iframe[src*="huggingface"],
|
|
| 198 |
pointer-events: none !important;
|
| 199 |
}
|
| 200 |
|
| 201 |
-
/* ===== Footer
|
| 202 |
footer,
|
| 203 |
.footer,
|
| 204 |
.gradio-container footer,
|
|
@@ -218,13 +223,13 @@ a[href*="huggingface.co/spaces"] {
|
|
| 218 |
margin: 0 !important;
|
| 219 |
}
|
| 220 |
|
| 221 |
-
/* =====
|
| 222 |
#col-container {
|
| 223 |
max-width: 1000px;
|
| 224 |
margin: 0 auto;
|
| 225 |
}
|
| 226 |
|
| 227 |
-
/* ===== π¨
|
| 228 |
.header-text h1 {
|
| 229 |
font-family: 'Bangers', cursive !important;
|
| 230 |
color: #1F2937 !important;
|
|
@@ -239,7 +244,7 @@ a[href*="huggingface.co/spaces"] {
|
|
| 239 |
-webkit-text-stroke: 2px #1F2937 !important;
|
| 240 |
}
|
| 241 |
|
| 242 |
-
/* ===== π¨
|
| 243 |
.subtitle {
|
| 244 |
text-align: center !important;
|
| 245 |
font-family: 'Comic Neue', cursive !important;
|
|
@@ -249,7 +254,7 @@ a[href*="huggingface.co/spaces"] {
|
|
| 249 |
font-weight: 700 !important;
|
| 250 |
}
|
| 251 |
|
| 252 |
-
/* ===== π¨
|
| 253 |
.gr-panel,
|
| 254 |
.gr-box,
|
| 255 |
.gr-form,
|
|
@@ -268,7 +273,7 @@ a[href*="huggingface.co/spaces"] {
|
|
| 268 |
box-shadow: 8px 8px 0px #1F2937 !important;
|
| 269 |
}
|
| 270 |
|
| 271 |
-
/* ===== π¨
|
| 272 |
textarea,
|
| 273 |
input[type="text"],
|
| 274 |
input[type="number"] {
|
|
@@ -295,7 +300,7 @@ textarea::placeholder {
|
|
| 295 |
font-weight: 400 !important;
|
| 296 |
}
|
| 297 |
|
| 298 |
-
/* ===== π¨ Primary
|
| 299 |
.gr-button-primary,
|
| 300 |
button.primary,
|
| 301 |
.gr-button.primary {
|
|
@@ -328,7 +333,7 @@ button.primary:active,
|
|
| 328 |
box-shadow: 2px 2px 0px #1F2937 !important;
|
| 329 |
}
|
| 330 |
|
| 331 |
-
/* ===== π¨ Secondary
|
| 332 |
.gr-button-secondary,
|
| 333 |
button.secondary {
|
| 334 |
background: #EF4444 !important;
|
|
@@ -357,7 +362,7 @@ button.secondary:active {
|
|
| 357 |
box-shadow: 2px 2px 0px #1F2937 !important;
|
| 358 |
}
|
| 359 |
|
| 360 |
-
/* ===== π¨ Extract
|
| 361 |
.extract-btn {
|
| 362 |
background: #10B981 !important;
|
| 363 |
border: 3px solid #1F2937 !important;
|
|
@@ -383,9 +388,9 @@ button.secondary:active {
|
|
| 383 |
box-shadow: 2px 2px 0px #1F2937 !important;
|
| 384 |
}
|
| 385 |
|
| 386 |
-
/* ===== π¨
|
| 387 |
input[type="range"] {
|
| 388 |
-
accent-color: #
|
| 389 |
height: 8px !important;
|
| 390 |
}
|
| 391 |
|
|
@@ -393,27 +398,7 @@ input[type="range"] {
|
|
| 393 |
background: #FACC15 !important;
|
| 394 |
}
|
| 395 |
|
| 396 |
-
/* ===== π¨
|
| 397 |
-
.range-slider input[type="range"] {
|
| 398 |
-
accent-color: #10B981 !important;
|
| 399 |
-
}
|
| 400 |
-
|
| 401 |
-
/* ===== π¨ μμ½λμΈ - λ§νμ μ€νμΌ ===== */
|
| 402 |
-
.gr-accordion {
|
| 403 |
-
background: #FACC15 !important;
|
| 404 |
-
border: 3px solid #1F2937 !important;
|
| 405 |
-
border-radius: 8px !important;
|
| 406 |
-
box-shadow: 4px 4px 0px #1F2937 !important;
|
| 407 |
-
}
|
| 408 |
-
|
| 409 |
-
.gr-accordion-header {
|
| 410 |
-
color: #1F2937 !important;
|
| 411 |
-
font-family: 'Comic Neue', cursive !important;
|
| 412 |
-
font-weight: 700 !important;
|
| 413 |
-
font-size: 1.1rem !important;
|
| 414 |
-
}
|
| 415 |
-
|
| 416 |
-
/* ===== π¨ λΌλ²¨ μ€νμΌ ===== */
|
| 417 |
label,
|
| 418 |
.gr-input-label,
|
| 419 |
.gr-block-label {
|
|
@@ -427,7 +412,7 @@ span.gr-label {
|
|
| 427 |
color: #1F2937 !important;
|
| 428 |
}
|
| 429 |
|
| 430 |
-
/* ===== π¨
|
| 431 |
.gr-info,
|
| 432 |
.info {
|
| 433 |
color: #6B7280 !important;
|
|
@@ -435,7 +420,7 @@ span.gr-label {
|
|
| 435 |
font-size: 0.9rem !important;
|
| 436 |
}
|
| 437 |
|
| 438 |
-
/* ===== π¨
|
| 439 |
.status-text textarea {
|
| 440 |
background: #E0F2FE !important;
|
| 441 |
border: 3px solid #0EA5E9 !important;
|
|
@@ -445,7 +430,7 @@ span.gr-label {
|
|
| 445 |
font-size: 0.95rem !important;
|
| 446 |
}
|
| 447 |
|
| 448 |
-
/* ===== π¨
|
| 449 |
.info-box textarea {
|
| 450 |
background: #FEF3C7 !important;
|
| 451 |
border: 3px solid #F59E0B !important;
|
|
@@ -455,7 +440,7 @@ span.gr-label {
|
|
| 455 |
font-size: 0.9rem !important;
|
| 456 |
}
|
| 457 |
|
| 458 |
-
/* ===== π¨
|
| 459 |
.time-display {
|
| 460 |
font-family: 'Bangers', cursive !important;
|
| 461 |
font-size: 1.5rem !important;
|
|
@@ -469,7 +454,7 @@ span.gr-label {
|
|
| 469 |
letter-spacing: 1px !important;
|
| 470 |
}
|
| 471 |
|
| 472 |
-
/* ===== π¨
|
| 473 |
audio {
|
| 474 |
width: 100% !important;
|
| 475 |
border-radius: 8px !important;
|
|
@@ -477,7 +462,7 @@ audio {
|
|
| 477 |
box-shadow: 4px 4px 0px #1F2937 !important;
|
| 478 |
}
|
| 479 |
|
| 480 |
-
/* ===== π¨
|
| 481 |
::-webkit-scrollbar {
|
| 482 |
width: 12px;
|
| 483 |
height: 12px;
|
|
@@ -498,13 +483,13 @@ audio {
|
|
| 498 |
background: #EF4444;
|
| 499 |
}
|
| 500 |
|
| 501 |
-
/* ===== π¨
|
| 502 |
::selection {
|
| 503 |
background: #FACC15;
|
| 504 |
color: #1F2937;
|
| 505 |
}
|
| 506 |
|
| 507 |
-
/* ===== π¨
|
| 508 |
a {
|
| 509 |
color: #3B82F6 !important;
|
| 510 |
text-decoration: none !important;
|
|
@@ -517,7 +502,7 @@ a:hover {
|
|
| 517 |
border-bottom-color: #EF4444 !important;
|
| 518 |
}
|
| 519 |
|
| 520 |
-
/* ===== π¨ Row/Column
|
| 521 |
.gr-row {
|
| 522 |
gap: 1.5rem !important;
|
| 523 |
}
|
|
@@ -526,7 +511,7 @@ a:hover {
|
|
| 526 |
gap: 1rem !important;
|
| 527 |
}
|
| 528 |
|
| 529 |
-
/* ===== π¨
|
| 530 |
.gr-file-upload {
|
| 531 |
border: 3px dashed #1F2937 !important;
|
| 532 |
border-radius: 8px !important;
|
|
@@ -538,7 +523,7 @@ a:hover {
|
|
| 538 |
background: #EFF6FF !important;
|
| 539 |
}
|
| 540 |
|
| 541 |
-
/* =====
|
| 542 |
@media (max-width: 768px) {
|
| 543 |
.header-text h1 {
|
| 544 |
font-size: 2.2rem !important;
|
|
@@ -563,7 +548,7 @@ a:hover {
|
|
| 563 |
}
|
| 564 |
}
|
| 565 |
|
| 566 |
-
/* ===== π¨
|
| 567 |
@media (prefers-color-scheme: dark) {
|
| 568 |
.gradio-container {
|
| 569 |
background-color: #FEF9C3 !important;
|
|
@@ -576,7 +561,10 @@ a:hover {
|
|
| 576 |
# Gradio UI
|
| 577 |
# ============================================
|
| 578 |
|
| 579 |
-
with gr.Blocks(
|
|
|
|
|
|
|
|
|
|
| 580 |
|
| 581 |
# HOME Badge
|
| 582 |
gr.HTML("""
|
|
@@ -597,32 +585,35 @@ with gr.Blocks(fill_height=True, css=css) as demo:
|
|
| 597 |
|
| 598 |
gr.Markdown(
|
| 599 |
"""
|
| 600 |
-
<p class="subtitle">βοΈ
|
| 601 |
""",
|
| 602 |
)
|
| 603 |
|
|
|
|
|
|
|
|
|
|
| 604 |
with gr.Row(equal_height=False):
|
| 605 |
# ============================================
|
| 606 |
# Left Column - Input
|
| 607 |
# ============================================
|
| 608 |
with gr.Column(scale=1, min_width=350):
|
| 609 |
-
#
|
| 610 |
audio_input = gr.File(
|
| 611 |
-
label="π΅
|
| 612 |
file_types=["audio"],
|
| 613 |
)
|
| 614 |
|
| 615 |
-
#
|
| 616 |
audio_info = gr.Textbox(
|
| 617 |
-
label="π
|
| 618 |
-
placeholder="
|
| 619 |
lines=10,
|
| 620 |
interactive=False,
|
| 621 |
elem_classes="info-box"
|
| 622 |
)
|
| 623 |
|
| 624 |
-
#
|
| 625 |
-
gr.Markdown("### π§
|
| 626 |
original_audio = gr.Audio(
|
| 627 |
label="",
|
| 628 |
type="filepath",
|
|
@@ -633,80 +624,100 @@ with gr.Blocks(fill_height=True, css=css) as demo:
|
|
| 633 |
# Right Column - Controls & Output
|
| 634 |
# ============================================
|
| 635 |
with gr.Column(scale=1, min_width=350):
|
| 636 |
-
#
|
| 637 |
-
gr.Markdown("### βοΈ
|
| 638 |
|
| 639 |
-
#
|
| 640 |
-
|
| 641 |
minimum=0,
|
| 642 |
maximum=100,
|
| 643 |
-
value=
|
| 644 |
step=0.1,
|
| 645 |
-
label="β±οΈ
|
| 646 |
-
info="
|
| 647 |
-
elem_classes="range-slider"
|
| 648 |
)
|
| 649 |
|
| 650 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
time_display = gr.Textbox(
|
| 652 |
label="",
|
| 653 |
-
value="π
|
| 654 |
interactive=False,
|
| 655 |
elem_classes="time-display"
|
| 656 |
)
|
| 657 |
|
| 658 |
-
#
|
| 659 |
extract_btn = gr.Button(
|
| 660 |
-
"βοΈ
|
| 661 |
variant="primary",
|
| 662 |
size="lg",
|
| 663 |
elem_classes="extract-btn"
|
| 664 |
)
|
| 665 |
|
| 666 |
-
#
|
| 667 |
extract_status = gr.Textbox(
|
| 668 |
-
label="π
|
| 669 |
-
placeholder="
|
| 670 |
lines=8,
|
| 671 |
interactive=False,
|
| 672 |
elem_classes="status-text"
|
| 673 |
)
|
| 674 |
|
| 675 |
-
#
|
| 676 |
-
gr.Markdown("### π§
|
| 677 |
extracted_audio = gr.Audio(
|
| 678 |
label="",
|
| 679 |
type="filepath",
|
| 680 |
interactive=False,
|
| 681 |
)
|
| 682 |
|
| 683 |
-
#
|
| 684 |
download_file = gr.File(
|
| 685 |
-
label="π₯
|
| 686 |
)
|
| 687 |
|
| 688 |
# ============================================
|
| 689 |
# Event Handlers
|
| 690 |
# ============================================
|
| 691 |
|
| 692 |
-
#
|
| 693 |
audio_input.change(
|
| 694 |
fn=get_audio_info_and_waveform,
|
| 695 |
inputs=[audio_input],
|
| 696 |
-
outputs=[original_audio,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
)
|
| 698 |
|
| 699 |
-
#
|
| 700 |
-
|
| 701 |
fn=update_time_display,
|
| 702 |
-
inputs=[
|
| 703 |
outputs=[time_display]
|
| 704 |
)
|
| 705 |
|
| 706 |
-
#
|
| 707 |
extract_btn.click(
|
| 708 |
fn=extract_audio_segment,
|
| 709 |
-
inputs=[audio_input,
|
| 710 |
outputs=[extracted_audio, extract_status, download_file]
|
| 711 |
)
|
| 712 |
|
|
|
|
| 1 |
"""
|
| 2 |
π΅ Audio Segment Extractor
|
| 3 |
+
- Upload audio file
|
| 4 |
+
- Timeline visualization
|
| 5 |
+
- Start/End time segment extraction
|
| 6 |
+
- Download
|
| 7 |
"""
|
| 8 |
|
| 9 |
import gradio as gr
|
|
|
|
| 14 |
import time
|
| 15 |
|
| 16 |
# ============================================
|
| 17 |
+
# Temp directory for audio processing
|
| 18 |
# ============================================
|
| 19 |
UPLOAD_DIR = tempfile.mkdtemp()
|
| 20 |
|
| 21 |
|
| 22 |
# ============================================
|
| 23 |
+
# π΅ Audio Processing Functions
|
| 24 |
# ============================================
|
| 25 |
|
| 26 |
def get_audio_info_and_waveform(audio_file):
|
| 27 |
+
"""Get audio info and generate waveform data"""
|
| 28 |
if not audio_file:
|
| 29 |
+
return None, 0, 0, 10, "π΅ Please upload an audio file"
|
| 30 |
|
| 31 |
try:
|
| 32 |
path = audio_file.name if hasattr(audio_file, 'name') else audio_file
|
| 33 |
|
| 34 |
+
# Get audio info
|
| 35 |
probe_cmd = ['ffprobe', '-v', 'error',
|
| 36 |
'-show_entries', 'format=duration,bit_rate,format_name:stream=sample_rate,channels,codec_name',
|
| 37 |
'-of', 'json', path]
|
|
|
|
| 48 |
channels = stream_info.get('channels', 'N/A')
|
| 49 |
codec = stream_info.get('codec_name', 'N/A')
|
| 50 |
|
| 51 |
+
# File size
|
| 52 |
file_size = os.path.getsize(path) / (1024 * 1024) # MB
|
| 53 |
|
| 54 |
+
info_text = f"""π΅ Audio Information
|
| 55 |
ββββββββββββββββββββββββββββββ
|
| 56 |
+
β±οΈ Duration: {duration:.2f}s ({duration/60:.1f}min)
|
| 57 |
+
ποΈ Bitrate: {bit_rate} kbps
|
| 58 |
+
π Sample Rate: {sample_rate} Hz
|
| 59 |
+
π’ Channels: {channels}
|
| 60 |
+
ποΈ Codec: {codec}
|
| 61 |
+
π Format: {format_name}
|
| 62 |
+
πΎ Size: {file_size:.2f} MB
|
| 63 |
ββββββββββββββββββββββββββββββ"""
|
| 64 |
|
| 65 |
+
# Return audio preview and update sliders
|
| 66 |
+
return path, 0, min(duration, 10), duration, info_text
|
| 67 |
|
| 68 |
except Exception as e:
|
| 69 |
+
return None, 0, 10, 100, f"β Failed to get info: {str(e)}"
|
| 70 |
|
| 71 |
|
| 72 |
+
def extract_audio_segment(audio_file, start_time, end_time):
|
| 73 |
+
"""Extract audio segment"""
|
| 74 |
if not audio_file:
|
| 75 |
+
return None, "β Please upload an audio file", None
|
| 76 |
|
| 77 |
try:
|
| 78 |
path = audio_file.name if hasattr(audio_file, 'name') else audio_file
|
| 79 |
+
start_time = float(start_time) if start_time else 0
|
| 80 |
+
end_time = float(end_time) if end_time else 10
|
| 81 |
|
| 82 |
if end_time <= start_time:
|
| 83 |
+
return None, "β End time must be greater than start time", None
|
| 84 |
|
| 85 |
duration = end_time - start_time
|
| 86 |
|
| 87 |
+
# Determine output file extension
|
| 88 |
ext = os.path.splitext(path)[1].lower()
|
| 89 |
if ext not in ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac']:
|
| 90 |
ext = '.mp3'
|
| 91 |
|
| 92 |
output_path = os.path.join(UPLOAD_DIR, f"audio_cut_{int(time.time())}{ext}")
|
| 93 |
|
| 94 |
+
# Try stream copy first (fast, lossless)
|
| 95 |
cmd = [
|
| 96 |
'ffmpeg', '-y', '-i', path,
|
| 97 |
'-ss', str(start_time), '-t', str(duration),
|
|
|
|
| 100 |
|
| 101 |
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
| 102 |
|
| 103 |
+
# If copy fails, try re-encoding
|
| 104 |
if not os.path.exists(output_path) or os.path.getsize(output_path) == 0:
|
| 105 |
output_path = os.path.join(UPLOAD_DIR, f"audio_cut_{int(time.time())}.mp3")
|
| 106 |
cmd = [
|
|
|
|
| 113 |
if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
|
| 114 |
file_size = os.path.getsize(output_path) / 1024 # KB
|
| 115 |
|
| 116 |
+
status_text = f"""β
Audio Extraction Complete!
|
| 117 |
ββββββββββββββββββββββββββββββ
|
| 118 |
+
β±οΈ Start: {start_time:.2f}s
|
| 119 |
+
β±οΈ End: {end_time:.2f}s
|
| 120 |
+
β±οΈ Duration: {duration:.2f}s
|
| 121 |
+
π Size: {file_size:.1f} KB
|
| 122 |
ββββββββββββββββββββββββββββββ"""
|
| 123 |
|
| 124 |
return output_path, status_text, output_path
|
| 125 |
else:
|
| 126 |
+
return None, "β Audio extraction failed", None
|
| 127 |
|
| 128 |
except Exception as e:
|
| 129 |
+
return None, f"β Error: {str(e)}", None
|
| 130 |
|
| 131 |
|
| 132 |
def format_time(seconds):
|
| 133 |
+
"""Convert seconds to MM:SS.ms format"""
|
| 134 |
if seconds is None:
|
| 135 |
return "00:00.00"
|
| 136 |
mins = int(seconds // 60)
|
|
|
|
| 138 |
return f"{mins:02d}:{secs:05.2f}"
|
| 139 |
|
| 140 |
|
| 141 |
+
def update_time_display(start_time, end_time):
|
| 142 |
+
"""Update time display based on slider values"""
|
| 143 |
+
if start_time is not None and end_time is not None:
|
| 144 |
+
duration = end_time - start_time
|
| 145 |
+
if duration < 0:
|
| 146 |
+
return "β οΈ End time must be greater than start time"
|
| 147 |
+
return f"π {format_time(start_time)} β {format_time(end_time)} (Duration: {duration:.2f}s)"
|
| 148 |
+
return "π Select a segment"
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def update_end_slider(start_time, max_duration):
|
| 152 |
+
"""Update end slider minimum based on start time"""
|
| 153 |
+
return gr.update(minimum=start_time + 0.1)
|
| 154 |
|
| 155 |
|
| 156 |
# ============================================
|
|
|
|
| 161 |
/* ===== π¨ Google Fonts Import ===== */
|
| 162 |
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
|
| 163 |
|
| 164 |
+
/* ===== π¨ Comic Classic Background ===== */
|
| 165 |
.gradio-container {
|
| 166 |
background-color: #FEF9C3 !important;
|
| 167 |
background-image:
|
|
|
|
| 171 |
font-family: 'Comic Neue', cursive, sans-serif !important;
|
| 172 |
}
|
| 173 |
|
| 174 |
+
/* ===== Hide HuggingFace Elements ===== */
|
| 175 |
.tabs,
|
| 176 |
.tab-nav,
|
| 177 |
.huggingface-space-header,
|
|
|
|
| 203 |
pointer-events: none !important;
|
| 204 |
}
|
| 205 |
|
| 206 |
+
/* ===== Hide Footer ===== */
|
| 207 |
footer,
|
| 208 |
.footer,
|
| 209 |
.gradio-container footer,
|
|
|
|
| 223 |
margin: 0 !important;
|
| 224 |
}
|
| 225 |
|
| 226 |
+
/* ===== Main Container ===== */
|
| 227 |
#col-container {
|
| 228 |
max-width: 1000px;
|
| 229 |
margin: 0 auto;
|
| 230 |
}
|
| 231 |
|
| 232 |
+
/* ===== π¨ Header Title - Comic Style ===== */
|
| 233 |
.header-text h1 {
|
| 234 |
font-family: 'Bangers', cursive !important;
|
| 235 |
color: #1F2937 !important;
|
|
|
|
| 244 |
-webkit-text-stroke: 2px #1F2937 !important;
|
| 245 |
}
|
| 246 |
|
| 247 |
+
/* ===== π¨ Subtitle ===== */
|
| 248 |
.subtitle {
|
| 249 |
text-align: center !important;
|
| 250 |
font-family: 'Comic Neue', cursive !important;
|
|
|
|
| 254 |
font-weight: 700 !important;
|
| 255 |
}
|
| 256 |
|
| 257 |
+
/* ===== π¨ Cards/Panels - Comic Frame Style ===== */
|
| 258 |
.gr-panel,
|
| 259 |
.gr-box,
|
| 260 |
.gr-form,
|
|
|
|
| 273 |
box-shadow: 8px 8px 0px #1F2937 !important;
|
| 274 |
}
|
| 275 |
|
| 276 |
+
/* ===== π¨ Input Fields ===== */
|
| 277 |
textarea,
|
| 278 |
input[type="text"],
|
| 279 |
input[type="number"] {
|
|
|
|
| 300 |
font-weight: 400 !important;
|
| 301 |
}
|
| 302 |
|
| 303 |
+
/* ===== π¨ Primary Button - Comic Blue ===== */
|
| 304 |
.gr-button-primary,
|
| 305 |
button.primary,
|
| 306 |
.gr-button.primary {
|
|
|
|
| 333 |
box-shadow: 2px 2px 0px #1F2937 !important;
|
| 334 |
}
|
| 335 |
|
| 336 |
+
/* ===== π¨ Secondary Button - Comic Red ===== */
|
| 337 |
.gr-button-secondary,
|
| 338 |
button.secondary {
|
| 339 |
background: #EF4444 !important;
|
|
|
|
| 362 |
box-shadow: 2px 2px 0px #1F2937 !important;
|
| 363 |
}
|
| 364 |
|
| 365 |
+
/* ===== π¨ Extract Button - Comic Green ===== */
|
| 366 |
.extract-btn {
|
| 367 |
background: #10B981 !important;
|
| 368 |
border: 3px solid #1F2937 !important;
|
|
|
|
| 388 |
box-shadow: 2px 2px 0px #1F2937 !important;
|
| 389 |
}
|
| 390 |
|
| 391 |
+
/* ===== π¨ Slider ===== */
|
| 392 |
input[type="range"] {
|
| 393 |
+
accent-color: #10B981 !important;
|
| 394 |
height: 8px !important;
|
| 395 |
}
|
| 396 |
|
|
|
|
| 398 |
background: #FACC15 !important;
|
| 399 |
}
|
| 400 |
|
| 401 |
+
/* ===== π¨ Labels ===== */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
label,
|
| 403 |
.gr-input-label,
|
| 404 |
.gr-block-label {
|
|
|
|
| 412 |
color: #1F2937 !important;
|
| 413 |
}
|
| 414 |
|
| 415 |
+
/* ===== π¨ Info Text ===== */
|
| 416 |
.gr-info,
|
| 417 |
.info {
|
| 418 |
color: #6B7280 !important;
|
|
|
|
| 420 |
font-size: 0.9rem !important;
|
| 421 |
}
|
| 422 |
|
| 423 |
+
/* ===== π¨ Status Text Area ===== */
|
| 424 |
.status-text textarea {
|
| 425 |
background: #E0F2FE !important;
|
| 426 |
border: 3px solid #0EA5E9 !important;
|
|
|
|
| 430 |
font-size: 0.95rem !important;
|
| 431 |
}
|
| 432 |
|
| 433 |
+
/* ===== π¨ Info Box ===== */
|
| 434 |
.info-box textarea {
|
| 435 |
background: #FEF3C7 !important;
|
| 436 |
border: 3px solid #F59E0B !important;
|
|
|
|
| 440 |
font-size: 0.9rem !important;
|
| 441 |
}
|
| 442 |
|
| 443 |
+
/* ===== π¨ Time Display ===== */
|
| 444 |
.time-display {
|
| 445 |
font-family: 'Bangers', cursive !important;
|
| 446 |
font-size: 1.5rem !important;
|
|
|
|
| 454 |
letter-spacing: 1px !important;
|
| 455 |
}
|
| 456 |
|
| 457 |
+
/* ===== π¨ Audio Player ===== */
|
| 458 |
audio {
|
| 459 |
width: 100% !important;
|
| 460 |
border-radius: 8px !important;
|
|
|
|
| 462 |
box-shadow: 4px 4px 0px #1F2937 !important;
|
| 463 |
}
|
| 464 |
|
| 465 |
+
/* ===== π¨ Scrollbar - Comic Style ===== */
|
| 466 |
::-webkit-scrollbar {
|
| 467 |
width: 12px;
|
| 468 |
height: 12px;
|
|
|
|
| 483 |
background: #EF4444;
|
| 484 |
}
|
| 485 |
|
| 486 |
+
/* ===== π¨ Selection Highlight ===== */
|
| 487 |
::selection {
|
| 488 |
background: #FACC15;
|
| 489 |
color: #1F2937;
|
| 490 |
}
|
| 491 |
|
| 492 |
+
/* ===== π¨ Links ===== */
|
| 493 |
a {
|
| 494 |
color: #3B82F6 !important;
|
| 495 |
text-decoration: none !important;
|
|
|
|
| 502 |
border-bottom-color: #EF4444 !important;
|
| 503 |
}
|
| 504 |
|
| 505 |
+
/* ===== π¨ Row/Column Spacing ===== */
|
| 506 |
.gr-row {
|
| 507 |
gap: 1.5rem !important;
|
| 508 |
}
|
|
|
|
| 511 |
gap: 1rem !important;
|
| 512 |
}
|
| 513 |
|
| 514 |
+
/* ===== π¨ File Upload Area ===== */
|
| 515 |
.gr-file-upload {
|
| 516 |
border: 3px dashed #1F2937 !important;
|
| 517 |
border-radius: 8px !important;
|
|
|
|
| 523 |
background: #EFF6FF !important;
|
| 524 |
}
|
| 525 |
|
| 526 |
+
/* ===== Responsive Adjustments ===== */
|
| 527 |
@media (max-width: 768px) {
|
| 528 |
.header-text h1 {
|
| 529 |
font-size: 2.2rem !important;
|
|
|
|
| 548 |
}
|
| 549 |
}
|
| 550 |
|
| 551 |
+
/* ===== π¨ Disable Dark Mode ===== */
|
| 552 |
@media (prefers-color-scheme: dark) {
|
| 553 |
.gradio-container {
|
| 554 |
background-color: #FEF9C3 !important;
|
|
|
|
| 561 |
# Gradio UI
|
| 562 |
# ============================================
|
| 563 |
|
| 564 |
+
with gr.Blocks(title="Audio Segment Extractor") as demo:
|
| 565 |
+
|
| 566 |
+
# Inject CSS via HTML
|
| 567 |
+
gr.HTML(f"<style>{css}</style>")
|
| 568 |
|
| 569 |
# HOME Badge
|
| 570 |
gr.HTML("""
|
|
|
|
| 585 |
|
| 586 |
gr.Markdown(
|
| 587 |
"""
|
| 588 |
+
<p class="subtitle">βοΈ Cut any segment from your audio file! πΆ</p>
|
| 589 |
""",
|
| 590 |
)
|
| 591 |
|
| 592 |
+
# State for max duration
|
| 593 |
+
max_duration = gr.State(100)
|
| 594 |
+
|
| 595 |
with gr.Row(equal_height=False):
|
| 596 |
# ============================================
|
| 597 |
# Left Column - Input
|
| 598 |
# ============================================
|
| 599 |
with gr.Column(scale=1, min_width=350):
|
| 600 |
+
# File upload
|
| 601 |
audio_input = gr.File(
|
| 602 |
+
label="π΅ Upload Audio File",
|
| 603 |
file_types=["audio"],
|
| 604 |
)
|
| 605 |
|
| 606 |
+
# Audio info
|
| 607 |
audio_info = gr.Textbox(
|
| 608 |
+
label="π Audio Information",
|
| 609 |
+
placeholder="Upload an audio file to see information...",
|
| 610 |
lines=10,
|
| 611 |
interactive=False,
|
| 612 |
elem_classes="info-box"
|
| 613 |
)
|
| 614 |
|
| 615 |
+
# Original audio preview
|
| 616 |
+
gr.Markdown("### π§ Original Audio Preview")
|
| 617 |
original_audio = gr.Audio(
|
| 618 |
label="",
|
| 619 |
type="filepath",
|
|
|
|
| 624 |
# Right Column - Controls & Output
|
| 625 |
# ============================================
|
| 626 |
with gr.Column(scale=1, min_width=350):
|
| 627 |
+
# Segment selection
|
| 628 |
+
gr.Markdown("### βοΈ Select Extraction Segment")
|
| 629 |
|
| 630 |
+
# Start time slider
|
| 631 |
+
start_time = gr.Slider(
|
| 632 |
minimum=0,
|
| 633 |
maximum=100,
|
| 634 |
+
value=0,
|
| 635 |
step=0.1,
|
| 636 |
+
label="β±οΈ Start Time (seconds)",
|
| 637 |
+
info="Drag to select start time"
|
|
|
|
| 638 |
)
|
| 639 |
|
| 640 |
+
# End time slider
|
| 641 |
+
end_time = gr.Slider(
|
| 642 |
+
minimum=0,
|
| 643 |
+
maximum=100,
|
| 644 |
+
value=10,
|
| 645 |
+
step=0.1,
|
| 646 |
+
label="β±οΈ End Time (seconds)",
|
| 647 |
+
info="Drag to select end time"
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
# Time display
|
| 651 |
time_display = gr.Textbox(
|
| 652 |
label="",
|
| 653 |
+
value="π Select a segment",
|
| 654 |
interactive=False,
|
| 655 |
elem_classes="time-display"
|
| 656 |
)
|
| 657 |
|
| 658 |
+
# Extract button
|
| 659 |
extract_btn = gr.Button(
|
| 660 |
+
"βοΈ EXTRACT AUDIO SEGMENT!",
|
| 661 |
variant="primary",
|
| 662 |
size="lg",
|
| 663 |
elem_classes="extract-btn"
|
| 664 |
)
|
| 665 |
|
| 666 |
+
# Extraction status
|
| 667 |
extract_status = gr.Textbox(
|
| 668 |
+
label="π Extraction Status",
|
| 669 |
+
placeholder="Extraction results will appear here...",
|
| 670 |
lines=8,
|
| 671 |
interactive=False,
|
| 672 |
elem_classes="status-text"
|
| 673 |
)
|
| 674 |
|
| 675 |
+
# Extracted audio preview
|
| 676 |
+
gr.Markdown("### π§ Extracted Audio Preview")
|
| 677 |
extracted_audio = gr.Audio(
|
| 678 |
label="",
|
| 679 |
type="filepath",
|
| 680 |
interactive=False,
|
| 681 |
)
|
| 682 |
|
| 683 |
+
# Download
|
| 684 |
download_file = gr.File(
|
| 685 |
+
label="π₯ Download",
|
| 686 |
)
|
| 687 |
|
| 688 |
# ============================================
|
| 689 |
# Event Handlers
|
| 690 |
# ============================================
|
| 691 |
|
| 692 |
+
# File upload - get info and update sliders
|
| 693 |
audio_input.change(
|
| 694 |
fn=get_audio_info_and_waveform,
|
| 695 |
inputs=[audio_input],
|
| 696 |
+
outputs=[original_audio, start_time, end_time, max_duration, audio_info]
|
| 697 |
+
).then(
|
| 698 |
+
fn=lambda d: (gr.update(maximum=d), gr.update(maximum=d)),
|
| 699 |
+
inputs=[max_duration],
|
| 700 |
+
outputs=[start_time, end_time]
|
| 701 |
+
)
|
| 702 |
+
|
| 703 |
+
# Start time change - update time display
|
| 704 |
+
start_time.change(
|
| 705 |
+
fn=update_time_display,
|
| 706 |
+
inputs=[start_time, end_time],
|
| 707 |
+
outputs=[time_display]
|
| 708 |
)
|
| 709 |
|
| 710 |
+
# End time change - update time display
|
| 711 |
+
end_time.change(
|
| 712 |
fn=update_time_display,
|
| 713 |
+
inputs=[start_time, end_time],
|
| 714 |
outputs=[time_display]
|
| 715 |
)
|
| 716 |
|
| 717 |
+
# Extract button click
|
| 718 |
extract_btn.click(
|
| 719 |
fn=extract_audio_segment,
|
| 720 |
+
inputs=[audio_input, start_time, end_time],
|
| 721 |
outputs=[extracted_audio, extract_status, download_file]
|
| 722 |
)
|
| 723 |
|