seawolf2357 commited on
Commit
45ffb76
ยท
verified ยท
1 Parent(s): b5c39ce

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +510 -0
app.py ADDED
@@ -0,0 +1,510 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = """
74
+ /* ===== ๐ŸŽจ Google Fonts Import ===== */
75
+ @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
76
+
77
+ /* ===== ๐ŸŽจ Comic Classic ๋ฐฐ๊ฒฝ - ๋นˆํ‹ฐ์ง€ ํŽ˜์ดํผ + ๋„ํŠธ ํŒจํ„ด ===== */
78
+ .gradio-container {
79
+ background-color: #FEF9C3 !important;
80
+ background-image:
81
+ radial-gradient(#1F2937 1px, transparent 1px) !important;
82
+ background-size: 20px 20px !important;
83
+ min-height: 100vh !important;
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 {
99
+ display: none !important;
100
+ visibility: hidden !important;
101
+ height: 0 !important;
102
+ width: 0 !important;
103
+ overflow: hidden !important;
104
+ opacity: 0 !important;
105
+ pointer-events: none !important;
106
+ }
107
+
108
+ /* ===== Footer ์™„์ „ ์ˆจ๊น€ ===== */
109
+ footer,
110
+ .footer,
111
+ .gradio-container footer,
112
+ .built-with,
113
+ [class*="footer"],
114
+ .gradio-footer,
115
+ .main-footer,
116
+ div[class*="footer"],
117
+ .show-api,
118
+ .built-with-gradio,
119
+ a[href*="gradio.app"],
120
+ a[href*="huggingface.co/spaces"] {
121
+ display: none !important;
122
+ visibility: hidden !important;
123
+ height: 0 !important;
124
+ padding: 0 !important;
125
+ margin: 0 !important;
126
+ }
127
+
128
+ /* ===== ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ ===== */
129
+ #col-container {
130
+ max-width: 1000px;
131
+ margin: 0 auto;
132
+ }
133
+
134
+ /* ===== ๐ŸŽจ ํ—ค๋” ํƒ€์ดํ‹€ - ์ฝ”๋ฏน ์Šคํƒ€์ผ ===== */
135
+ .header-text h1 {
136
+ font-family: 'Bangers', cursive !important;
137
+ color: #1F2937 !important;
138
+ font-size: 3.5rem !important;
139
+ font-weight: 400 !important;
140
+ text-align: center !important;
141
+ margin-bottom: 0.5rem !important;
142
+ text-shadow:
143
+ 4px 4px 0px #FACC15,
144
+ 6px 6px 0px #1F2937 !important;
145
+ letter-spacing: 3px !important;
146
+ -webkit-text-stroke: 2px #1F2937 !important;
147
+ }
148
+
149
+ /* ===== ๐ŸŽจ ์„œ๋ธŒํƒ€์ดํ‹€ ===== */
150
+ .subtitle {
151
+ text-align: center !important;
152
+ font-family: 'Comic Neue', cursive !important;
153
+ font-size: 1.2rem !important;
154
+ color: #1F2937 !important;
155
+ margin-bottom: 1.5rem !important;
156
+ font-weight: 700 !important;
157
+ }
158
+
159
+ /* ===== ๐ŸŽจ ์นด๋“œ/ํŒจ๋„ - ๋งŒํ™” ํ”„๋ ˆ์ž„ ์Šคํƒ€์ผ ===== */
160
+ .gr-panel,
161
+ .gr-box,
162
+ .gr-form,
163
+ .block,
164
+ .gr-group {
165
+ background: #FFFFFF !important;
166
+ border: 3px solid #1F2937 !important;
167
+ border-radius: 8px !important;
168
+ box-shadow: 6px 6px 0px #1F2937 !important;
169
+ transition: all 0.2s ease !important;
170
+ }
171
+
172
+ .gr-panel:hover,
173
+ .block:hover {
174
+ transform: translate(-2px, -2px) !important;
175
+ box-shadow: 8px 8px 0px #1F2937 !important;
176
+ }
177
+
178
+ /* ===== ๐ŸŽจ ์ž…๋ ฅ ํ•„๋“œ (Textbox) ===== */
179
+ textarea,
180
+ input[type="text"],
181
+ input[type="number"] {
182
+ background: #FFFFFF !important;
183
+ border: 3px solid #1F2937 !important;
184
+ border-radius: 8px !important;
185
+ color: #1F2937 !important;
186
+ font-family: 'Comic Neue', cursive !important;
187
+ font-size: 1rem !important;
188
+ font-weight: 700 !important;
189
+ transition: all 0.2s ease !important;
190
+ }
191
+
192
+ textarea:focus,
193
+ input[type="text"]:focus,
194
+ input[type="number"]:focus {
195
+ border-color: #3B82F6 !important;
196
+ box-shadow: 4px 4px 0px #3B82F6 !important;
197
+ outline: none !important;
198
+ }
199
+
200
+ textarea::placeholder {
201
+ color: #9CA3AF !important;
202
+ font-weight: 400 !important;
203
+ }
204
+
205
+ /* ===== ๐ŸŽจ Primary ๋ฒ„ํŠผ - ์ฝ”๋ฏน ๋ธ”๋ฃจ ===== */
206
+ .gr-button-primary,
207
+ button.primary,
208
+ .gr-button.primary {
209
+ background: #3B82F6 !important;
210
+ border: 3px solid #1F2937 !important;
211
+ border-radius: 8px !important;
212
+ color: #FFFFFF !important;
213
+ font-family: 'Bangers', cursive !important;
214
+ font-weight: 400 !important;
215
+ font-size: 1.3rem !important;
216
+ letter-spacing: 2px !important;
217
+ padding: 14px 28px !important;
218
+ box-shadow: 5px 5px 0px #1F2937 !important;
219
+ transition: all 0.1s ease !important;
220
+ text-shadow: 1px 1px 0px #1F2937 !important;
221
+ }
222
+
223
+ .gr-button-primary:hover,
224
+ button.primary:hover,
225
+ .gr-button.primary:hover {
226
+ background: #2563EB !important;
227
+ transform: translate(-2px, -2px) !important;
228
+ box-shadow: 7px 7px 0px #1F2937 !important;
229
+ }
230
+
231
+ .gr-button-primary:active,
232
+ button.primary:active,
233
+ .gr-button.primary:active {
234
+ transform: translate(3px, 3px) !important;
235
+ box-shadow: 2px 2px 0px #1F2937 !important;
236
+ }
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;
245
+ color: #FFFFFF !important;
246
+ font-family: 'Bangers', cursive !important;
247
+ font-weight: 400 !important;
248
+ font-size: 1.1rem !important;
249
+ letter-spacing: 1px !important;
250
+ box-shadow: 4px 4px 0px #1F2937 !important;
251
+ transition: all 0.1s ease !important;
252
+ text-shadow: 1px 1px 0px #1F2937 !important;
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
+ /* ===== ๐ŸŽจ ์•„์ฝ”๋””์–ธ - ๋งํ’์„  ์Šคํƒ€์ผ ===== */
296
+ .gr-accordion {
297
+ background: #FACC15 !important;
298
+ border: 3px solid #1F2937 !important;
299
+ border-radius: 8px !important;
300
+ box-shadow: 4px 4px 0px #1F2937 !important;
301
+ }
302
+
303
+ .gr-accordion-header {
304
+ color: #1F2937 !important;
305
+ font-family: 'Comic Neue', cursive !important;
306
+ font-weight: 700 !important;
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,
323
+ .gr-block-label {
324
+ color: #1F2937 !important;
325
+ font-family: 'Comic Neue', cursive !important;
326
+ font-weight: 700 !important;
327
+ font-size: 1rem !important;
328
+ }
329
+
330
+ span.gr-label {
331
+ color: #1F2937 !important;
332
+ }
333
+
334
+ /* ===== ๐ŸŽจ ์ •๋ณด ํ…์ŠคํŠธ ===== */
335
+ .gr-info,
336
+ .info {
337
+ color: #6B7280 !important;
338
+ font-family: 'Comic Neue', cursive !important;
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
+ /* ===== ๐ŸŽจ ์Šคํฌ๋กค๋ฐ” - ์ฝ”๋ฏน ์Šคํƒ€์ผ ===== */
351
+ ::-webkit-scrollbar {
352
+ width: 12px;
353
+ height: 12px;
354
+ }
355
+
356
+ ::-webkit-scrollbar-track {
357
+ background: #FEF9C3;
358
+ border: 2px solid #1F2937;
359
+ }
360
+
361
+ ::-webkit-scrollbar-thumb {
362
+ background: #3B82F6;
363
+ border: 2px solid #1F2937;
364
+ border-radius: 0px;
365
+ }
366
+
367
+ ::-webkit-scrollbar-thumb:hover {
368
+ background: #EF4444;
369
+ }
370
+
371
+ /* ===== ๐ŸŽจ ์„ ํƒ ํ•˜์ด๋ผ์ดํŠธ ===== */
372
+ ::selection {
373
+ background: #FACC15;
374
+ color: #1F2937;
375
+ }
376
+
377
+ /* ===== ๐ŸŽจ ๋งํฌ ์Šคํƒ€์ผ ===== */
378
+ 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 ๊ฐ„๊ฒฉ ===== */
389
+ .gr-row {
390
+ gap: 1.5rem !important;
391
+ }
392
+
393
+ .gr-column {
394
+ gap: 1rem !important;
395
+ }
396
+
397
+ /* ===== ๋ฐ˜์‘ํ˜• ์กฐ์ • ===== */
398
+ @media (max-width: 768px) {
399
+ .header-text h1 {
400
+ font-size: 2.2rem !important;
401
+ text-shadow:
402
+ 3px 3px 0px #FACC15,
403
+ 4px 4px 0px #1F2937 !important;
404
+ }
405
+
406
+ .gr-button-primary,
407
+ button.primary {
408
+ padding: 12px 20px !important;
409
+ font-size: 1.1rem !important;
410
+ }
411
+
412
+ .gr-panel,
413
+ .block {
414
+ box-shadow: 4px 4px 0px #1F2937 !important;
415
+ }
416
+ }
417
+
418
+ /* ===== ๐ŸŽจ ๋‹คํฌ๋ชจ๋“œ ๋น„ํ™œ์„ฑํ™” (์ฝ”๋ฏน์€ ๋ฐ์•„์•ผ ํ•จ) ===== */
419
+ @media (prefers-color-scheme: dark) {
420
+ .gradio-container {
421
+ background-color: #FEF9C3 !important;
422
+ }
423
+ }
424
+ """
425
+
426
+ # Build the Gradio interface
427
+ with gr.Blocks(fill_height=True, css=css) as demo:
428
+
429
+ # HOME Badge
430
+ gr.HTML("""
431
+ <div style="text-align: center; margin: 20px 0 10px 0;">
432
+ <a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;">
433
+ <img src="https://img.shields.io/static/v1?label=๐Ÿ  HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
434
+ </a>
435
+ </div>
436
+ """)
437
+
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()