seawolf2357 commited on
Commit
a8f165b
ยท
verified ยท
1 Parent(s): 6c5a8fc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +341 -136
app.py CHANGED
@@ -1,73 +1,155 @@
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
- import cv2
3
- import tempfile
4
  import os
5
- from PIL import Image
6
-
7
- def extract_last_frame(video_file):
8
- """
9
- ๋น„๋””์˜ค ํŒŒ์ผ์—์„œ ๋งˆ์ง€๋ง‰ ํ”„๋ ˆ์ž„์„ ์ถ”์ถœํ•˜์—ฌ ์ด๋ฏธ์ง€๋กœ ๋ฐ˜ํ™˜
10
- """
11
- if video_file is None:
12
- return None, "โš ๏ธ Please upload a video file first!"
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  try:
15
- # OpenCV๋กœ ๋น„๋””์˜ค ์—ด๊ธฐ
16
- cap = cv2.VideoCapture(video_file)
 
 
 
 
 
 
 
 
 
17
 
18
- if not cap.isOpened():
19
- return None, "โŒ Error: Cannot open video file!"
 
 
 
 
20
 
21
- # ๋น„๋””์˜ค ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
22
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
23
- fps = cap.get(cv2.CAP_PROP_FPS)
24
- width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
25
- height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
26
- duration = total_frames / fps if fps > 0 else 0
27
 
28
- if total_frames <= 0:
29
- cap.release()
30
- return None, "โŒ Error: Video has no frames!"
 
 
 
 
 
 
 
31
 
32
- # ๋งˆ์ง€๋ง‰ ํ”„๋ ˆ์ž„์œผ๋กœ ์ด๋™
33
- cap.set(cv2.CAP_PROP_POS_FRAMES, total_frames - 1)
34
 
35
- # ํ”„๋ ˆ์ž„ ์ฝ๊ธฐ
36
- ret, frame = cap.read()
37
- cap.release()
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- if not ret:
40
- return None, "โŒ Error: Cannot read the last frame!"
41
 
42
- # BGR to RGB ๋ณ€ํ™˜
43
- frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
 
 
44
 
45
- # PIL Image๋กœ ๋ณ€ํ™˜
46
- image = Image.fromarray(frame_rgb)
47
 
48
- # ์ •๋ณด ๋กœ๊ทธ ์ƒ์„ฑ
49
- info_log = f"""โœ… EXTRACTION COMPLETE!
50
- {'=' * 50}
51
- ๐Ÿ“น Video Info:
52
- โ€ข Total Frames: {total_frames:,}
53
- โ€ข FPS: {fps:.2f}
54
- โ€ข Duration: {duration:.2f} seconds
55
- โ€ข Resolution: {width} x {height}
56
- {'=' * 50}
57
- ๐Ÿ–ผ๏ธ Extracted Frame:
58
- โ€ข Frame Number: {total_frames} (Last Frame)
59
- โ€ข Image Size: {width} x {height}
60
- {'=' * 50}
61
- ๐Ÿ’พ Ready to download!"""
62
 
63
- return image, info_log
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  except Exception as e:
66
- return None, f"โŒ Error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
 
69
  # ============================================
70
- # ๐ŸŽจ Comic Classic Theme - Toon Playground
71
  # ============================================
72
 
73
  css = """
@@ -84,15 +166,26 @@ css = """
84
  font-family: 'Comic Neue', cursive, sans-serif !important;
85
  }
86
 
87
- /* ===== ํ—ˆ๊น…ํŽ˜์ด์Šค ์ƒ๋‹จ ์š”์†Œ ์ˆจ๊น€ ===== */
 
 
88
  .huggingface-space-header,
89
  #space-header,
90
  .space-header,
91
  [class*="space-header"],
 
 
 
92
  .svelte-1ed2p3z,
93
  .space-header-badge,
94
  .header-badge,
95
  [data-testid="space-header"],
 
 
 
 
 
 
96
  .svelte-kqij2n,
97
  .svelte-1ax1toq,
98
  .embed-container > div:first-child {
@@ -237,8 +330,7 @@ button.primary:active,
237
 
238
  /* ===== ๐ŸŽจ Secondary ๋ฒ„ํŠผ - ์ฝ”๋ฏน ๋ ˆ๋“œ ===== */
239
  .gr-button-secondary,
240
- button.secondary,
241
- .extract-btn {
242
  background: #EF4444 !important;
243
  border: 3px solid #1F2937 !important;
244
  border-radius: 8px !important;
@@ -253,43 +345,57 @@ button.secondary,
253
  }
254
 
255
  .gr-button-secondary:hover,
256
- button.secondary:hover,
257
- .extract-btn:hover {
258
  background: #DC2626 !important;
259
  transform: translate(-2px, -2px) !important;
260
  box-shadow: 6px 6px 0px #1F2937 !important;
261
  }
262
 
263
  .gr-button-secondary:active,
264
- button.secondary:active,
265
- .extract-btn:active {
266
  transform: translate(2px, 2px) !important;
267
  box-shadow: 2px 2px 0px #1F2937 !important;
268
  }
269
 
270
- /* ===== ๐ŸŽจ ๋กœ๊ทธ ์ถœ๋ ฅ ์˜์—ญ ===== */
271
- .info-log textarea {
272
- background: #1F2937 !important;
273
- color: #10B981 !important;
274
- font-family: 'Courier New', monospace !important;
275
- font-size: 0.9rem !important;
276
- font-weight: 400 !important;
277
- border: 3px solid #10B981 !important;
278
  border-radius: 8px !important;
279
- box-shadow: 4px 4px 0px #10B981 !important;
 
 
 
 
 
 
 
280
  }
281
 
282
- /* ===== ๏ฟฝ๏ฟฝ ๋น„๋””์˜ค ์—…๋กœ๋“œ ์˜์—ญ ===== */
283
- .video-upload {
284
- border: 4px dashed #3B82F6 !important;
285
- border-radius: 12px !important;
286
- background: #EFF6FF !important;
287
- transition: all 0.2s ease !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  }
289
 
290
- .video-upload:hover {
291
- border-color: #EF4444 !important;
292
- background: #FEF2F2 !important;
293
  }
294
 
295
  /* ===== ๐ŸŽจ ์•„์ฝ”๋””์–ธ - ๋งํ’์„  ์Šคํƒ€์ผ ===== */
@@ -307,16 +413,6 @@ button.secondary:active,
307
  font-size: 1.1rem !important;
308
  }
309
 
310
- /* ===== ๐ŸŽจ ์ด๋ฏธ์ง€ ์ถœ๋ ฅ ์˜์—ญ ===== */
311
- .gr-image,
312
- .image-container {
313
- border: 4px solid #1F2937 !important;
314
- border-radius: 8px !important;
315
- box-shadow: 8px 8px 0px #1F2937 !important;
316
- overflow: hidden !important;
317
- background: #FFFFFF !important;
318
- }
319
-
320
  /* ===== ๐ŸŽจ ๋ผ๋ฒจ ์Šคํƒ€์ผ ===== */
321
  label,
322
  .gr-input-label,
@@ -339,12 +435,46 @@ span.gr-label {
339
  font-size: 0.9rem !important;
340
  }
341
 
342
- /* ===== ๐ŸŽจ ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” ===== */
343
- .progress-bar,
344
- .gr-progress-bar {
345
- background: #3B82F6 !important;
346
- border: 2px solid #1F2937 !important;
347
- border-radius: 4px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  }
349
 
350
  /* ===== ๐ŸŽจ ์Šคํฌ๋กค๋ฐ” - ์ฝ”๋ฏน ์Šคํƒ€์ผ ===== */
@@ -379,10 +509,12 @@ a {
379
  color: #3B82F6 !important;
380
  text-decoration: none !important;
381
  font-weight: 700 !important;
 
382
  }
383
 
384
  a:hover {
385
  color: #EF4444 !important;
 
386
  }
387
 
388
  /* ===== ๐ŸŽจ Row/Column ๊ฐ„๊ฒฉ ===== */
@@ -394,6 +526,18 @@ a:hover {
394
  gap: 1rem !important;
395
  }
396
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  /* ===== ๋ฐ˜์‘ํ˜• ์กฐ์ • ===== */
398
  @media (max-width: 768px) {
399
  .header-text h1 {
@@ -413,6 +557,10 @@ a:hover {
413
  .block {
414
  box-shadow: 4px 4px 0px #1F2937 !important;
415
  }
 
 
 
 
416
  }
417
 
418
  /* ===== ๐ŸŽจ ๋‹คํฌ๋ชจ๋“œ ๋น„ํ™œ์„ฑํ™” (์ฝ”๋ฏน์€ ๋ฐ์•„์•ผ ํ•จ) ===== */
@@ -423,7 +571,11 @@ a:hover {
423
  }
424
  """
425
 
426
- # Build the Gradio interface
 
 
 
 
427
  with gr.Blocks(fill_height=True, css=css) as demo:
428
 
429
  # HOME Badge
@@ -438,73 +590,126 @@ with gr.Blocks(fill_height=True, css=css) as demo:
438
  # Header Title
439
  gr.Markdown(
440
  """
441
- # ๐ŸŽฌ VIDEO LAST FRAME EXTRACTOR ๐Ÿ–ผ๏ธ
442
  """,
443
  elem_classes="header-text"
444
  )
445
 
446
  gr.Markdown(
447
  """
448
- <p class="subtitle">๐Ÿ“น Upload a video and extract the LAST FRAME instantly! ๐Ÿ’พ</p>
449
  """,
450
  )
451
 
452
  with gr.Row(equal_height=False):
453
- # Left column - Input
454
- with gr.Column(scale=1, min_width=320):
455
- video_input = gr.Video(
456
- label="๐Ÿ“น Upload Your Video",
457
- sources=["upload"],
458
- elem_classes="video-upload"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  )
460
 
 
461
  extract_btn = gr.Button(
462
- "๐ŸŽฌ EXTRACT LAST FRAME! ๐Ÿ–ผ๏ธ",
463
  variant="primary",
464
  size="lg",
465
  elem_classes="extract-btn"
466
  )
467
 
468
- with gr.Accordion("๐Ÿ“œ Extraction Info", open=True):
469
- info_log = gr.Textbox(
470
- label="",
471
- placeholder="Upload a video and click extract to see info...",
472
- lines=12,
473
- max_lines=20,
474
- interactive=False,
475
- elem_classes="info-log"
476
- )
477
-
478
- # Right column - Output
479
- with gr.Column(scale=1, min_width=320):
480
- output_image = gr.Image(
481
- label="๐Ÿ–ผ๏ธ Last Frame",
482
- type="pil",
483
- show_label=True,
484
- height=500,
485
  )
486
 
487
- gr.Markdown(
488
- """
489
- <p style="text-align: center; margin-top: 10px; font-weight: 700; color: #1F2937;">
490
- ๐Ÿ’ก Right-click on the image to save, or use the download button!
491
- </p>
492
- """
493
  )
494
-
495
- # Connect the extract button
496
- extract_btn.click(
497
- fn=extract_last_frame,
498
- inputs=[video_input],
499
- outputs=[output_image, info_log],
 
 
 
 
 
 
 
 
 
500
  )
501
 
502
- # Auto-extract when video is uploaded
503
- video_input.change(
504
- fn=extract_last_frame,
505
- inputs=[video_input],
506
- outputs=[output_image, info_log],
507
  )
 
 
 
 
 
 
 
 
508
 
509
  if __name__ == "__main__":
510
  demo.launch()
 
1
+ """
2
+ ๐ŸŽต Audio Segment Extractor
3
+ - ์˜ค๋””์˜ค ํŒŒ์ผ ์—…๋กœ๋“œ
4
+ - ํƒ€์ž„๋ผ์ธ ์‹œ๊ฐํ™”
5
+ - ์‹œ์ž‘~์ข…๋ฃŒ ์‹œ๊ฐ„ ๊ตฌ๊ฐ„ ์ถ”์ถœ
6
+ - ๋‹ค์šด๋กœ๋“œ
7
+ """
8
+
9
  import gradio as gr
 
 
10
  import os
11
+ import json
12
+ import subprocess
13
+ 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]
38
+ result = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=30)
39
+ info = json.loads(result.stdout)
40
+
41
+ format_info = info.get('format', {})
42
+ stream_info = info.get('streams', [{}])[0] if info.get('streams') else {}
43
 
44
+ duration = float(format_info.get('duration', 0))
45
+ bit_rate = int(format_info.get('bit_rate', 0)) // 1000 if format_info.get('bit_rate') else 0
46
+ format_name = format_info.get('format_name', 'N/A')
47
+ sample_rate = stream_info.get('sample_rate', 'N/A')
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),
98
+ '-c', 'copy', output_path
99
+ ]
 
 
 
 
 
 
 
 
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 = [
107
+ 'ffmpeg', '-y', '-i', path,
108
+ '-ss', str(start_time), '-t', str(duration),
109
+ '-acodec', 'libmp3lame', '-b:a', '192k', output_path
110
+ ]
111
+ subprocess.run(cmd, capture_output=True, timeout=120)
112
+
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)
137
+ secs = seconds % 60
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
  # ============================================
152
+ # ๐ŸŽจ Comic Classic Theme CSS
153
  # ============================================
154
 
155
  css = """
 
166
  font-family: 'Comic Neue', cursive, sans-serif !important;
167
  }
168
 
169
+ /* ===== ํ—ˆ๊น…ํŽ˜์ด์Šค ์ƒ๋‹จ ์š”์†Œ ์ˆจ๊น€ (๊ฐ•ํ™”) ===== */
170
+ .tabs,
171
+ .tab-nav,
172
  .huggingface-space-header,
173
  #space-header,
174
  .space-header,
175
  [class*="space-header"],
176
+ .h-header,
177
+ .gradio-container > header,
178
+ .contain > header,
179
  .svelte-1ed2p3z,
180
  .space-header-badge,
181
  .header-badge,
182
  [data-testid="space-header"],
183
+ .gr-prose > div:first-child,
184
+ .contain > div:first-child > div:first-child,
185
+ .app > div:first-child > header,
186
+ iframe[src*="huggingface"],
187
+ .huggingface-logo,
188
+ .hf-logo,
189
  .svelte-kqij2n,
190
  .svelte-1ax1toq,
191
  .embed-container > div:first-child {
 
330
 
331
  /* ===== ๐ŸŽจ Secondary ๋ฒ„ํŠผ - ์ฝ”๋ฏน ๋ ˆ๋“œ ===== */
332
  .gr-button-secondary,
333
+ button.secondary {
 
334
  background: #EF4444 !important;
335
  border: 3px solid #1F2937 !important;
336
  border-radius: 8px !important;
 
345
  }
346
 
347
  .gr-button-secondary:hover,
348
+ button.secondary:hover {
 
349
  background: #DC2626 !important;
350
  transform: translate(-2px, -2px) !important;
351
  box-shadow: 6px 6px 0px #1F2937 !important;
352
  }
353
 
354
  .gr-button-secondary:active,
355
+ button.secondary:active {
 
356
  transform: translate(2px, 2px) !important;
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;
 
 
 
 
364
  border-radius: 8px !important;
365
+ color: #FFFFFF !important;
366
+ font-family: 'Bangers', cursive !important;
367
+ font-weight: 400 !important;
368
+ font-size: 1.3rem !important;
369
+ letter-spacing: 2px !important;
370
+ box-shadow: 5px 5px 0px #1F2937 !important;
371
+ transition: all 0.1s ease !important;
372
+ text-shadow: 1px 1px 0px #1F2937 !important;
373
  }
374
 
375
+ .extract-btn:hover {
376
+ background: #059669 !important;
377
+ transform: translate(-2px, -2px) !important;
378
+ box-shadow: 7px 7px 0px #1F2937 !important;
379
+ }
380
+
381
+ .extract-btn:active {
382
+ transform: translate(3px, 3px) !important;
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
+
392
+ .gr-slider {
393
+ background: #FACC15 !important;
394
  }
395
 
396
+ /* ===== ๐ŸŽจ Range Slider ํŠน๋ณ„ ์Šคํƒ€์ผ ===== */
397
+ .range-slider input[type="range"] {
398
+ accent-color: #10B981 !important;
399
  }
400
 
401
  /* ===== ๐ŸŽจ ์•„์ฝ”๋””์–ธ - ๋งํ’์„  ์Šคํƒ€์ผ ===== */
 
413
  font-size: 1.1rem !important;
414
  }
415
 
 
 
 
 
 
 
 
 
 
 
416
  /* ===== ๐ŸŽจ ๋ผ๋ฒจ ์Šคํƒ€์ผ ===== */
417
  label,
418
  .gr-input-label,
 
435
  font-size: 0.9rem !important;
436
  }
437
 
438
+ /* ===== ๐ŸŽจ ์ƒํƒœ ํ…์ŠคํŠธ ์˜์—ญ ===== */
439
+ .status-text textarea {
440
+ background: #E0F2FE !important;
441
+ border: 3px solid #0EA5E9 !important;
442
+ color: #0C4A6E !important;
443
+ font-family: 'Comic Neue', cursive !important;
444
+ font-weight: 700 !important;
445
+ font-size: 0.95rem !important;
446
+ }
447
+
448
+ /* ===== ๐ŸŽจ ์ •๋ณด ๋ฐ•์Šค ===== */
449
+ .info-box textarea {
450
+ background: #FEF3C7 !important;
451
+ border: 3px solid #F59E0B !important;
452
+ color: #92400E !important;
453
+ font-family: 'Courier New', monospace !important;
454
+ font-weight: 600 !important;
455
+ font-size: 0.9rem !important;
456
+ }
457
+
458
+ /* ===== ๐ŸŽจ ์‹œ๊ฐ„ ํ‘œ์‹œ ===== */
459
+ .time-display {
460
+ font-family: 'Bangers', cursive !important;
461
+ font-size: 1.5rem !important;
462
+ color: #1F2937 !important;
463
+ text-align: center !important;
464
+ padding: 15px !important;
465
+ background: linear-gradient(135deg, #FACC15 0%, #FDE68A 100%) !important;
466
+ border: 3px solid #1F2937 !important;
467
+ border-radius: 8px !important;
468
+ box-shadow: 4px 4px 0px #1F2937 !important;
469
+ letter-spacing: 1px !important;
470
+ }
471
+
472
+ /* ===== ๐ŸŽจ ์˜ค๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ===== */
473
+ audio {
474
+ width: 100% !important;
475
+ border-radius: 8px !important;
476
+ border: 3px solid #1F2937 !important;
477
+ box-shadow: 4px 4px 0px #1F2937 !important;
478
  }
479
 
480
  /* ===== ๐ŸŽจ ์Šคํฌ๋กค๋ฐ” - ์ฝ”๋ฏน ์Šคํƒ€์ผ ===== */
 
509
  color: #3B82F6 !important;
510
  text-decoration: none !important;
511
  font-weight: 700 !important;
512
+ border-bottom: 2px solid #3B82F6 !important;
513
  }
514
 
515
  a:hover {
516
  color: #EF4444 !important;
517
+ border-bottom-color: #EF4444 !important;
518
  }
519
 
520
  /* ===== ๐ŸŽจ Row/Column ๊ฐ„๊ฒฉ ===== */
 
526
  gap: 1rem !important;
527
  }
528
 
529
+ /* ===== ๐ŸŽจ ํŒŒ์ผ ์—…๋กœ๋“œ ์˜์—ญ ===== */
530
+ .gr-file-upload {
531
+ border: 3px dashed #1F2937 !important;
532
+ border-radius: 8px !important;
533
+ background: #FEF9C3 !important;
534
+ }
535
+
536
+ .gr-file-upload:hover {
537
+ border-color: #3B82F6 !important;
538
+ background: #EFF6FF !important;
539
+ }
540
+
541
  /* ===== ๋ฐ˜์‘ํ˜• ์กฐ์ • ===== */
542
  @media (max-width: 768px) {
543
  .header-text h1 {
 
557
  .block {
558
  box-shadow: 4px 4px 0px #1F2937 !important;
559
  }
560
+
561
+ .time-display {
562
+ font-size: 1.2rem !important;
563
+ }
564
  }
565
 
566
  /* ===== ๐ŸŽจ ๋‹คํฌ๋ชจ๋“œ ๋น„ํ™œ์„ฑํ™” (์ฝ”๋ฏน์€ ๋ฐ์•„์•ผ ํ•จ) ===== */
 
571
  }
572
  """
573
 
574
+
575
+ # ============================================
576
+ # Gradio UI
577
+ # ============================================
578
+
579
  with gr.Blocks(fill_height=True, css=css) as demo:
580
 
581
  # HOME Badge
 
590
  # Header Title
591
  gr.Markdown(
592
  """
593
+ # ๐ŸŽต AUDIO SEGMENT EXTRACTOR ๐ŸŽต
594
  """,
595
  elem_classes="header-text"
596
  )
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",
629
+ interactive=False,
630
+ )
631
+
632
+ # ============================================
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
+
713
 
714
  if __name__ == "__main__":
715
  demo.launch()