Frame / app.py
seawolf2357's picture
Update app.py
0d375d2 verified
import gradio as gr
import cv2
import tempfile
import os
from PIL import Image
def extract_last_frame(video_file):
"""
๋น„๋””์˜ค ํŒŒ์ผ์—์„œ ๋งˆ์ง€๋ง‰ ํ”„๋ ˆ์ž„์„ ์ถ”์ถœํ•˜์—ฌ ์ด๋ฏธ์ง€๋กœ ๋ฐ˜ํ™˜
"""
if video_file is None:
return None, "โš ๏ธ Please upload a video file first!"
try:
# OpenCV๋กœ ๋น„๋””์˜ค ์—ด๊ธฐ
cap = cv2.VideoCapture(video_file)
if not cap.isOpened():
return None, "โŒ Error: Cannot open video file!"
# ๋น„๋””์˜ค ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
duration = total_frames / fps if fps > 0 else 0
if total_frames <= 0:
cap.release()
return None, "โŒ Error: Video has no frames!"
# ๋งˆ์ง€๋ง‰ ํ”„๋ ˆ์ž„์œผ๋กœ ์ด๋™
cap.set(cv2.CAP_PROP_POS_FRAMES, total_frames - 1)
# ํ”„๋ ˆ์ž„ ์ฝ๊ธฐ
ret, frame = cap.read()
cap.release()
if not ret:
return None, "โŒ Error: Cannot read the last frame!"
# BGR to RGB ๋ณ€ํ™˜
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# PIL Image๋กœ ๋ณ€ํ™˜
image = Image.fromarray(frame_rgb)
# ์ •๋ณด ๋กœ๊ทธ ์ƒ์„ฑ
info_log = f"""โœ… EXTRACTION COMPLETE!
{'=' * 50}
๐Ÿ“น Video Info:
โ€ข Total Frames: {total_frames:,}
โ€ข FPS: {fps:.2f}
โ€ข Duration: {duration:.2f} seconds
โ€ข Resolution: {width} x {height}
{'=' * 50}
๐Ÿ–ผ๏ธ Extracted Frame:
โ€ข Frame Number: {total_frames} (Last Frame)
โ€ข Image Size: {width} x {height}
{'=' * 50}
๐Ÿ’พ Ready to download!"""
return image, info_log
except Exception as e:
return None, f"โŒ Error: {str(e)}"
# ============================================
# ๐ŸŽจ Comic Classic Theme - Toon Playground
# ============================================
css = """
/* ===== ๐ŸŽจ Google Fonts Import ===== */
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
/* ===== ๐ŸŽจ Comic Classic ๋ฐฐ๊ฒฝ - ๋นˆํ‹ฐ์ง€ ํŽ˜์ดํผ + ๋„ํŠธ ํŒจํ„ด ===== */
.gradio-container {
background-color: #FEF9C3 !important;
background-image:
radial-gradient(#1F2937 1px, transparent 1px) !important;
background-size: 20px 20px !important;
min-height: 100vh !important;
font-family: 'Comic Neue', cursive, sans-serif !important;
}
/* ===== ํ—ˆ๊น…ํŽ˜์ด์Šค ์ƒ๋‹จ ์š”์†Œ ์ˆจ๊น€ ===== */
.huggingface-space-header,
#space-header,
.space-header,
[class*="space-header"],
.svelte-1ed2p3z,
.space-header-badge,
.header-badge,
[data-testid="space-header"],
.svelte-kqij2n,
.svelte-1ax1toq,
.embed-container > div:first-child {
display: none !important;
visibility: hidden !important;
height: 0 !important;
width: 0 !important;
overflow: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
/* ===== Footer ์™„์ „ ์ˆจ๊น€ ===== */
footer,
.footer,
.gradio-container footer,
.built-with,
[class*="footer"],
.gradio-footer,
.main-footer,
div[class*="footer"],
.show-api,
.built-with-gradio,
a[href*="gradio.app"],
a[href*="huggingface.co/spaces"] {
display: none !important;
visibility: hidden !important;
height: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
/* ===== ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ ===== */
#col-container {
max-width: 1000px;
margin: 0 auto;
}
/* ===== ๐ŸŽจ ํ—ค๋” ํƒ€์ดํ‹€ - ์ฝ”๋ฏน ์Šคํƒ€์ผ ===== */
.header-text h1 {
font-family: 'Bangers', cursive !important;
color: #1F2937 !important;
font-size: 3.5rem !important;
font-weight: 400 !important;
text-align: center !important;
margin-bottom: 0.5rem !important;
text-shadow:
4px 4px 0px #FACC15,
6px 6px 0px #1F2937 !important;
letter-spacing: 3px !important;
-webkit-text-stroke: 2px #1F2937 !important;
}
/* ===== ๐ŸŽจ ์„œ๋ธŒํƒ€์ดํ‹€ ===== */
.subtitle {
text-align: center !important;
font-family: 'Comic Neue', cursive !important;
font-size: 1.2rem !important;
color: #1F2937 !important;
margin-bottom: 1.5rem !important;
font-weight: 700 !important;
}
/* ===== ๐ŸŽจ ์นด๋“œ/ํŒจ๋„ - ๋งŒํ™” ํ”„๋ ˆ์ž„ ์Šคํƒ€์ผ ===== */
.gr-panel,
.gr-box,
.gr-form,
.block,
.gr-group {
background: #FFFFFF !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 6px 6px 0px #1F2937 !important;
transition: all 0.2s ease !important;
}
.gr-panel:hover,
.block:hover {
transform: translate(-2px, -2px) !important;
box-shadow: 8px 8px 0px #1F2937 !important;
}
/* ===== ๐ŸŽจ ์ž…๋ ฅ ํ•„๋“œ (Textbox) ===== */
textarea,
input[type="text"],
input[type="number"] {
background: #FFFFFF !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
color: #1F2937 !important;
font-family: 'Comic Neue', cursive !important;
font-size: 1rem !important;
font-weight: 700 !important;
transition: all 0.2s ease !important;
}
textarea:focus,
input[type="text"]:focus,
input[type="number"]:focus {
border-color: #3B82F6 !important;
box-shadow: 4px 4px 0px #3B82F6 !important;
outline: none !important;
}
textarea::placeholder {
color: #9CA3AF !important;
font-weight: 400 !important;
}
/* ===== ๐ŸŽจ Primary ๋ฒ„ํŠผ - ์ฝ”๋ฏน ๋ธ”๋ฃจ ===== */
.gr-button-primary,
button.primary,
.gr-button.primary {
background: #3B82F6 !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
color: #FFFFFF !important;
font-family: 'Bangers', cursive !important;
font-weight: 400 !important;
font-size: 1.3rem !important;
letter-spacing: 2px !important;
padding: 14px 28px !important;
box-shadow: 5px 5px 0px #1F2937 !important;
transition: all 0.1s ease !important;
text-shadow: 1px 1px 0px #1F2937 !important;
}
.gr-button-primary:hover,
button.primary:hover,
.gr-button.primary:hover {
background: #2563EB !important;
transform: translate(-2px, -2px) !important;
box-shadow: 7px 7px 0px #1F2937 !important;
}
.gr-button-primary:active,
button.primary:active,
.gr-button.primary:active {
transform: translate(3px, 3px) !important;
box-shadow: 2px 2px 0px #1F2937 !important;
}
/* ===== ๐ŸŽจ Secondary ๋ฒ„ํŠผ - ์ฝ”๋ฏน ๋ ˆ๋“œ ===== */
.gr-button-secondary,
button.secondary,
.extract-btn {
background: #EF4444 !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
color: #FFFFFF !important;
font-family: 'Bangers', cursive !important;
font-weight: 400 !important;
font-size: 1.1rem !important;
letter-spacing: 1px !important;
box-shadow: 4px 4px 0px #1F2937 !important;
transition: all 0.1s ease !important;
text-shadow: 1px 1px 0px #1F2937 !important;
}
.gr-button-secondary:hover,
button.secondary:hover,
.extract-btn:hover {
background: #DC2626 !important;
transform: translate(-2px, -2px) !important;
box-shadow: 6px 6px 0px #1F2937 !important;
}
.gr-button-secondary:active,
button.secondary:active,
.extract-btn:active {
transform: translate(2px, 2px) !important;
box-shadow: 2px 2px 0px #1F2937 !important;
}
/* ===== ๐ŸŽจ ๋กœ๊ทธ ์ถœ๋ ฅ ์˜์—ญ ===== */
.info-log textarea {
background: #1F2937 !important;
color: #10B981 !important;
font-family: 'Courier New', monospace !important;
font-size: 0.9rem !important;
font-weight: 400 !important;
border: 3px solid #10B981 !important;
border-radius: 8px !important;
box-shadow: 4px 4px 0px #10B981 !important;
}
/* ===== ๐ŸŽจ ๋น„๋””์˜ค ์—…๋กœ๋“œ ์˜์—ญ ===== */
.video-upload {
border: 4px dashed #3B82F6 !important;
border-radius: 12px !important;
background: #EFF6FF !important;
transition: all 0.2s ease !important;
}
.video-upload:hover {
border-color: #EF4444 !important;
background: #FEF2F2 !important;
}
/* ===== ๐ŸŽจ ์•„์ฝ”๋””์–ธ - ๋งํ’์„  ์Šคํƒ€์ผ ===== */
.gr-accordion {
background: #FACC15 !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 4px 4px 0px #1F2937 !important;
}
.gr-accordion-header {
color: #1F2937 !important;
font-family: 'Comic Neue', cursive !important;
font-weight: 700 !important;
font-size: 1.1rem !important;
}
/* ===== ๐ŸŽจ ์ด๋ฏธ์ง€ ์ถœ๋ ฅ ์˜์—ญ ===== */
.gr-image,
.image-container {
border: 4px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 8px 8px 0px #1F2937 !important;
overflow: hidden !important;
background: #FFFFFF !important;
}
/* ===== ๐ŸŽจ ๋ผ๋ฒจ ์Šคํƒ€์ผ ===== */
label,
.gr-input-label,
.gr-block-label {
color: #1F2937 !important;
font-family: 'Comic Neue', cursive !important;
font-weight: 700 !important;
font-size: 1rem !important;
}
span.gr-label {
color: #1F2937 !important;
}
/* ===== ๐ŸŽจ ์ •๋ณด ํ…์ŠคํŠธ ===== */
.gr-info,
.info {
color: #6B7280 !important;
font-family: 'Comic Neue', cursive !important;
font-size: 0.9rem !important;
}
/* ===== ๐ŸŽจ ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” ===== */
.progress-bar,
.gr-progress-bar {
background: #3B82F6 !important;
border: 2px solid #1F2937 !important;
border-radius: 4px !important;
}
/* ===== ๐ŸŽจ ์Šคํฌ๋กค๋ฐ” - ์ฝ”๋ฏน ์Šคํƒ€์ผ ===== */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: #FEF9C3;
border: 2px solid #1F2937;
}
::-webkit-scrollbar-thumb {
background: #3B82F6;
border: 2px solid #1F2937;
border-radius: 0px;
}
::-webkit-scrollbar-thumb:hover {
background: #EF4444;
}
/* ===== ๐ŸŽจ ์„ ํƒ ํ•˜์ด๋ผ์ดํŠธ ===== */
::selection {
background: #FACC15;
color: #1F2937;
}
/* ===== ๐ŸŽจ ๋งํฌ ์Šคํƒ€์ผ ===== */
a {
color: #3B82F6 !important;
text-decoration: none !important;
font-weight: 700 !important;
}
a:hover {
color: #EF4444 !important;
}
/* ===== ๐ŸŽจ Row/Column ๊ฐ„๊ฒฉ ===== */
.gr-row {
gap: 1.5rem !important;
}
.gr-column {
gap: 1rem !important;
}
/* ===== ๋ฐ˜์‘ํ˜• ์กฐ์ • ===== */
@media (max-width: 768px) {
.header-text h1 {
font-size: 2.2rem !important;
text-shadow:
3px 3px 0px #FACC15,
4px 4px 0px #1F2937 !important;
}
.gr-button-primary,
button.primary {
padding: 12px 20px !important;
font-size: 1.1rem !important;
}
.gr-panel,
.block {
box-shadow: 4px 4px 0px #1F2937 !important;
}
}
/* ===== ๐ŸŽจ ๋‹คํฌ๋ชจ๋“œ ๋น„ํ™œ์„ฑํ™” (์ฝ”๋ฏน์€ ๋ฐ์•„์•ผ ํ•จ) ===== */
@media (prefers-color-scheme: dark) {
.gradio-container {
background-color: #FEF9C3 !important;
}
}
"""
# Build the Gradio interface
with gr.Blocks(fill_height=True, css=css) as demo:
# HOME Badge
gr.HTML("""
<div style="text-align: center; margin: 20px 0 10px 0;">
<a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;">
<img src="https://img.shields.io/static/v1?label=๐Ÿ  HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
</a>
</div>
""")
# Header Title
gr.Markdown(
"""
# ๐ŸŽฌ VIDEO LAST FRAME EXTRACTOR ๐Ÿ–ผ๏ธ
""",
elem_classes="header-text"
)
gr.Markdown(
"""
<p class="subtitle">๐Ÿ“น Upload a video and extract the LAST FRAME instantly! ๐Ÿ’พ</p>
""",
)
with gr.Row(equal_height=False):
# Left column - Input
with gr.Column(scale=1, min_width=320):
video_input = gr.Video(
label="๐Ÿ“น Upload Your Video",
sources=["upload"],
elem_classes="video-upload"
)
extract_btn = gr.Button(
"๐ŸŽฌ EXTRACT LAST FRAME! ๐Ÿ–ผ๏ธ",
variant="primary",
size="lg",
elem_classes="extract-btn"
)
with gr.Accordion("๐Ÿ“œ Extraction Info", open=True):
info_log = gr.Textbox(
label="",
placeholder="Upload a video and click extract to see info...",
lines=12,
max_lines=20,
interactive=False,
elem_classes="info-log"
)
# Right column - Output
with gr.Column(scale=1, min_width=320):
output_image = gr.Image(
label="๐Ÿ–ผ๏ธ Last Frame",
type="pil",
show_label=True,
height=500,
)
gr.Markdown(
"""
<p style="text-align: center; margin-top: 10px; font-weight: 700; color: #1F2937;">
๐Ÿ’ก Right-click on the image to save, or use the download button!
</p>
"""
)
# Connect the extract button
extract_btn.click(
fn=extract_last_frame,
inputs=[video_input],
outputs=[output_image, info_log],
)
# Auto-extract when video is uploaded
video_input.change(
fn=extract_last_frame,
inputs=[video_input],
outputs=[output_image, info_log],
)
if __name__ == "__main__":
demo.launch()