seawolf2357 commited on
Commit
c93f126
Β·
verified Β·
1 Parent(s): a8f165b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +138 -127
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, "🎡 νŒŒμΌμ„ μ—…λ‘œλ“œν•˜μ„Έμš”", None
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
- ⏱️ 길이: {duration:.2f}초 ({duration/60:.1f}λΆ„)
57
- 🎚️ λΉ„νŠΈλ ˆμ΄νŠΈ: {bit_rate} kbps
58
- πŸ”Š μƒ˜ν”Œλ ˆμ΄νŠΈ: {sample_rate} Hz
59
- πŸ“’ 채널: {channels}
60
- πŸŽ›οΈ 코덱: {codec}
61
- πŸ“ 포맷: {format_name}
62
- πŸ’Ύ 크기: {file_size:.2f} MB
63
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"""
64
 
65
- # μ˜€λ””μ˜€ λ―Έλ¦¬λ“£κΈ°μš© λ°˜ν™˜
66
- return path, 0, duration, info_text, gr.update(maximum=duration, value=[0, min(duration, 10)])
67
 
68
  except Exception as e:
69
- return None, 0, 10, f"❌ 정보 쑰회 μ‹€νŒ¨: {str(e)}", gr.update(maximum=100, value=[0, 10])
70
 
71
 
72
- def extract_audio_segment(audio_file, time_range):
73
- """μ˜€λ””μ˜€ ꡬ간 μΆ”μΆœ"""
74
  if not audio_file:
75
- return None, "❌ μ˜€λ””μ˜€ νŒŒμΌμ„ μ—…λ‘œλ“œν•˜μ„Έμš”", None
76
 
77
  try:
78
  path = audio_file.name if hasattr(audio_file, 'name') else audio_file
79
- start_time = float(time_range[0]) if time_range else 0
80
- end_time = float(time_range[1]) if time_range else 10
81
 
82
  if end_time <= start_time:
83
- return None, "❌ μ’…λ£Œ μ‹œκ°„μ΄ μ‹œμž‘ μ‹œκ°„λ³΄λ‹€ 컀야 ν•©λ‹ˆλ‹€", 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
- ⏱️ μ‹œμž‘: {start_time:.2f}초
119
- ⏱️ μ’…λ£Œ: {end_time:.2f}초
120
- ⏱️ 길이: {duration:.2f}초
121
- πŸ“ 크기: {file_size:.1f} KB
122
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"""
123
 
124
  return output_path, status_text, output_path
125
  else:
126
- return None, "❌ μ˜€λ””μ˜€ μΆ”μΆœ μ‹€νŒ¨", None
127
 
128
  except Exception as e:
129
- return None, f"❌ 였λ₯˜: {str(e)}", None
130
 
131
 
132
  def format_time(seconds):
133
- """초λ₯Ό MM:SS.ms ν˜•μ‹μœΌλ‘œ λ³€ν™˜"""
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(time_range):
142
- """μŠ¬λΌμ΄λ” 값에 λ”°λ₯Έ μ‹œκ°„ ν‘œμ‹œ μ—…λ°μ΄νŠΈ"""
143
- if time_range:
144
- start = time_range[0]
145
- end = time_range[1]
146
- duration = end - start
147
- return f"πŸ• {format_time(start)} β†’ {format_time(end)} (길이: {duration:.2f}초)"
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
- /* ===== 🎨 μž…λ ₯ ν•„λ“œ (Textbox) ===== */
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: #EF4444 !important;
389
  height: 8px !important;
390
  }
391
 
@@ -393,27 +398,7 @@ input[type="range"] {
393
  background: #FACC15 !important;
394
  }
395
 
396
- /* ===== 🎨 Range Slider νŠΉλ³„ μŠ€νƒ€μΌ ===== */
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(fill_height=True, css=css) as demo:
 
 
 
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">βœ‚οΈ μ˜€λ””μ˜€ νŒŒμΌμ—μ„œ μ›ν•˜λŠ” κ΅¬κ°„λ§Œ μž˜λΌλ‚΄κΈ°! 🎢</p>
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
- # νƒ€μž„λΌμΈ μŠ¬λΌμ΄λ” (Range Slider)
640
- time_range = gr.Slider(
641
  minimum=0,
642
  maximum=100,
643
- value=[0, 10],
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, gr.State(), gr.State(), audio_info, time_range]
 
 
 
 
 
 
 
 
 
 
 
697
  )
698
 
699
- # μŠ¬λΌμ΄λ” λ³€κ²½ μ‹œ μ‹œκ°„ ν‘œμ‹œ μ—…λ°μ΄νŠΈ
700
- time_range.change(
701
  fn=update_time_display,
702
- inputs=[time_range],
703
  outputs=[time_display]
704
  )
705
 
706
- # μΆ”μΆœ λ²„νŠΌ 클릭
707
  extract_btn.click(
708
  fn=extract_audio_segment,
709
- inputs=[audio_input, time_range],
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