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()