6-1_gif / app.py
Kims12's picture
Update app.py
a03d759 verified
import os
import uuid
import moviepy.editor as mp
from PIL import Image
import gradio as gr
# -------------------------------
# ๋‚ด๋ถ€ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜๋“ค
# -------------------------------
global_logs = []
def add_log(message: str):
global_logs.append(message)
print(message)
def parse_time_to_seconds(time_str: str):
try:
h, m, s = time_str.split(":")
return int(h)*3600 + int(m)*60 + float(s)
except Exception:
return 0.0
def generate_thumbnail(video_clip, time_point):
try:
frame = video_clip.get_frame(time_point)
return Image.fromarray(frame)
except Exception as e:
add_log(f"[ERROR] ์ธ๋„ค์ผ ์ƒ์„ฑ ์‹คํŒจ: {e}")
frame = video_clip.get_frame(0)
return Image.fromarray(frame)
def adjust_aspect_ratio(clip, option):
if option == "์›๋ณธ ์œ ์ง€":
return clip
if option == "์œ ํŠœ๋ธŒ (16:9)":
target_ratio = 16/9
elif option == "์‡ผ์ธ /๋ฆด์Šค (9:16)":
target_ratio = 9/16
elif option == "์ •์‚ฌ๊ฐํ˜• (1:1)":
target_ratio = 1.0
elif option == "์ธ์Šคํƒ€๊ทธ๋žจ (4:5)":
target_ratio = 4/5
elif option == "ํด๋ž˜์‹ (4:3)":
target_ratio = 4/3
else:
return clip
width, height = clip.size
current_ratio = width/height
if current_ratio > target_ratio:
new_width = int(height * target_ratio)
new_height = height
else:
new_width = width
new_height = int(width / target_ratio)
return clip.crop(x_center=width/2, y_center=height/2, width=new_width, height=new_height)
def process_video(video,
start_time_str,
end_time_str,
platform_option,
frame_rate_factor,
speed_factor,
repeat_count,
resolution_scale):
global global_logs
global_logs = []
add_log("๐ŸŽฅ [LOG] ์˜์ƒ ์ฒ˜๋ฆฌ ์‹œ์ž‘")
video_path = video if isinstance(video, str) else video.name
try:
input_video = mp.VideoFileClip(video_path)
except Exception as e:
add_log(f"[ERROR] ๋น„๋””์˜ค ๋กœ๋“œ ์‹คํŒจ: {e}")
return None, None, "\n".join(global_logs)
duration = input_video.duration
add_log(f"[LOG] ์˜์ƒ ์žฌ์ƒ์‹œ๊ฐ„: {duration:.2f}์ดˆ")
start_sec = parse_time_to_seconds(start_time_str)
end_sec = parse_time_to_seconds(end_time_str)
if start_sec < 0:
start_sec = 0
if end_sec <= 0 or end_sec > duration:
end_sec = duration
if start_sec >= end_sec:
start_sec, end_sec = 0, duration
add_log(f"[LOG] ์‹œ๊ฐ„ ์„ค์ •: {start_sec}์ดˆ ~ {end_sec}์ดˆ")
clip = input_video.subclip(start_sec, end_sec)
add_log(f"[LOG] ํ”Œ๋žซํผ ๋น„์œจ ์ ์šฉ: {platform_option}")
clip = adjust_aspect_ratio(clip, platform_option)
if abs(resolution_scale - 1.0) > 1e-3:
add_log(f"[LOG] ํ•ด์ƒ๋„ ์ถ•์†Œ: {resolution_scale*100:.0f}%")
clip = clip.resize(resolution_scale)
if abs(speed_factor - 1.0) > 1e-3:
add_log(f"[LOG] ์žฌ์ƒ์†๋„ {speed_factor}๋ฐฐ ์ ์šฉ")
clip = clip.fx(mp.vfx.speedx, speed_factor)
original_fps = clip.fps
target_fps = original_fps * frame_rate_factor
add_log(f"[LOG] FPS: {target_fps:.2f} (์›๋ณธ {original_fps})")
clip = clip.set_fps(target_fps)
add_log(f"[LOG] GIF ๋ฐ˜๋ณต ํšŸ์ˆ˜: {repeat_count} (0: ๋ฌดํ•œ)")
output_filename = f"temp_{uuid.uuid4().hex}.gif"
try:
loop_param = 0 if int(repeat_count) == 0 else int(repeat_count)
clip.write_gif(output_filename, fps=target_fps, loop=loop_param)
add_log(f"[LOG] GIF ์ƒ์„ฑ ์™„๋ฃŒ: {output_filename}, loop={loop_param}")
except Exception as e:
add_log(f"[ERROR] GIF ์ƒ์„ฑ ์‹คํŒจ: {e}")
return None, None, "\n".join(global_logs)
return output_filename, output_filename, "\n".join(global_logs)
def update_thumbnails(video, start_time_str, end_time_str):
video_path = video if isinstance(video, str) else video.name
try:
input_video = mp.VideoFileClip(video_path)
except Exception as e:
add_log(f"[ERROR] ๋น„๋””์˜ค ๋กœ๋“œ ์‹คํŒจ: {e}")
return None, None
duration = input_video.duration
start_sec = parse_time_to_seconds(start_time_str)
end_sec = parse_time_to_seconds(end_time_str)
if start_sec < 0:
start_sec = 0
if end_sec <= 0 or end_sec > duration:
end_sec = duration
if start_sec >= end_sec:
start_sec, end_sec = 0, duration
start_thumb = generate_thumbnail(input_video, start_sec)
end_thumb = generate_thumbnail(input_video, end_sec)
return start_thumb, end_thumb
# -------------------------------
# Custom CSS (๊ธ€์ž ํฌ๊ธฐ ํฌ๊ฒŒ, ํšŒ์ƒ‰ ์ œ๊ฑฐ, ํฌ์ธํŠธ ๋ฒ„ํŠผ ๋“ฑ)
# -------------------------------
custom_css = """
body {
font-size: 1.6em; /* ๊ธ€์ž ํฌ๊ธฐ ์ฆ๊ฐ€ */
background-color: #ffffff;
font-family: 'Arial', sans-serif;
}
.gradio-container {
width: 80% !important;
margin: 0 auto;
background-color: #ffffff; /* ํšŒ์ƒ‰ ์ œ๊ฑฐ */
}
/* ์ œ๋ชฉ ๋ฐ ์‚ฌ์šฉ๊ฐ€์ด๋“œ (์™ผ์ชฝ ์ •๋ ฌ, ํฌ๊ฒŒ) */
.custom-title {
font-size: 3.5em; /* ๊ธ€์ž ํฌ๊ธฐ ์ฆ๊ฐ€ */
font-weight: bold;
margin: 20px 0;
color: #2c3e50;
text-align: left;
}
.custom-user-guide {
font-size: 1.8em; /* ๊ธ€์ž ํฌ๊ธฐ ์ฆ๊ฐ€ */
margin-bottom: 20px;
color: #2c3e50;
text-align: left;
}
/* ์‚ฌ์šฉ๊ฐ€์ด๋“œ ๋ฐ•์Šค ์Šคํƒ€์ผ (ํšŒ์ƒ‰ ์ œ๊ฑฐ, ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ) */
.guide-box {
border: 3px solid #3498db; /* ํ…Œ๋‘๋ฆฌ ๋‘๊ป˜ ์ฆ๊ฐ€ */
border-radius: 10px;
padding: 20px; /* ํŒจ๋”ฉ ์ฆ๊ฐ€ */
background-color: #ffffff;
margin: 20px 0;
text-align: left;
font-size: 1.2em; /* ๊ธ€์ž ํฌ๊ธฐ ์ฆ๊ฐ€ */
}
/* ํ”„๋ ˆ์ž„ ์Šคํƒ€์ผ */
.frame {
border: 3px solid #3498db; /* ํ…Œ๋‘๋ฆฌ ๋‘๊ป˜ ์ฆ๊ฐ€ */
border-radius: 20px;
padding: 25px; /* ํŒจ๋”ฉ ์ฆ๊ฐ€ */
background-color: #ffffff;
margin: 15px;
box-shadow: 3px 3px 10px rgba(0,0,0,0.1);
}
/* ๊ฐ ํ”„๋ ˆ์ž„ ์ œ๋ชฉ ํฌ๊ฒŒ */
.frame h3 {
font-size: 2.2em; /* ๊ธ€์ž ํฌ๊ธฐ ์ฆ๊ฐ€ */
margin-bottom: 20px;
text-align: left;
}
/* ํ–‰ ๋ฐ ์ปฌ๋Ÿผ ๋ ˆ์ด์•„์›ƒ */
.row-container {
display: flex;
justify-content: space-between;
}
.column {
flex: 1;
margin: 10px;
}
/* ํฌ์ธํŠธ ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
.gif-button {
margin-top: 35px;
padding: 20px 35px; /* ํŒจ๋”ฉ ์ฆ๊ฐ€ */
font-size: 1.8em; /* ๊ธ€์ž ํฌ๊ธฐ ์ฆ๊ฐ€ */
background-color: #e67e22;
color: #fff;
border: none;
border-radius: 12px;
cursor: pointer;
}
/* ์Šฌ๋ผ์ด๋” ์Šคํƒ€์ผ (๊ธ€์ž์™€ ๋ฐ” ํฌ๊ฒŒ) */
input[type=range] {
height: 30px; /* ๋†’์ด ์ฆ๊ฐ€ */
}
input[type=range]::-webkit-slider-thumb {
height: 30px; /* ๋†’์ด ์ฆ๊ฐ€ */
width: 30px; /* ๋„ˆ๋น„ ์ฆ๊ฐ€ */
}
.slider-label {
font-size: 1.6em; /* ๊ธ€์ž ํฌ๊ธฐ ์ฆ๊ฐ€ */
margin-bottom: 8px;
}
/* ๋ชจ๋“  input, select, button ๊ธ€์ž ํฌ๊ธฐ ํ‚ค์›€ */
input, button, select, textarea {
font-size: 1.5em; /* ๊ธ€์ž ํฌ๊ธฐ ์ฆ๊ฐ€ */
}
/* ๋ผ๋ฒจ ๊ธ€์ž ํฌ๊ธฐ ํ‚ค์›€ */
label, .label-wrap span {
font-size: 1.3em !important; /* ๊ธ€์ž ํฌ๊ธฐ ์ฆ๊ฐ€ */
font-weight: bold;
}
/* ํšŒ์ƒ‰ ๋ฐฐ๊ฒฝ ์ œ๊ฑฐ */
.gradio-container .prose,
.gradio-container .gr-box,
.gradio-container .gr-form,
.gradio-container .gr-panel {
background-color: #ffffff !important;
}
.gradio-container .gr-form,
.gradio-container .gr-group {
border-color: #3498db;
background-color: #ffffff !important;
}
.gradio-container .gr-input,
.gradio-container .gr-checkbox,
.gradio-container .gr-radio,
.gradio-container .gr-dropdown {
font-size: 1.4em; /* ๊ธ€์ž ํฌ๊ธฐ ์ฆ๊ฐ€ */
}
"""
# -------------------------------
# Gradio UI ๊ตฌ์„ฑ (HTML/CSS ์ปค์Šคํ„ฐ๋งˆ์ด์ง•)
# -------------------------------
with gr.Blocks(css=custom_css, title="์˜์ƒ -> GIF ๋ณ€ํ™˜ ์„œ๋น„์Šค") as demo:
# ์ œ๋ชฉ ๋ฐ ์‚ฌ์šฉ๊ฐ€์ด๋“œ (์™ผ์ชฝ ์ •๋ ฌ)
gr.HTML("<div class='custom-title'>๐ŸŽฌ ์˜์ƒ์„ GIF๋ณ€ํ™˜ํ•˜๊ธฐ</div>")
gr.HTML("<div class='guide-box'><strong>์‚ฌ์šฉ๊ฐ€์ด๋“œ:</strong><br>"
"1. ์ขŒ์ธก ์ž…๋ ฅ๋ถ€์—์„œ ์˜์ƒ์„ ์—…๋กœ๋“œํ•˜๊ณ  ์˜ต์…˜์„ ์„ ํƒํ•˜์„ธ์š”.<br>"
"2. ํ•˜๋‹จ ์ž‘์—… ์ธ๋„ค์ผ๋ฏธ๋ฆฌ๋ณด๊ธฐ์—์„œ ์‹œ์ž‘/์ข…๋ฃŒ ์‹œ๊ฐ„์„ ์ž…๋ ฅํ•ด ์ธ๋„ค์ผ์„ ํ™•์ธํ•˜์„ธ์š”.<br>"
"3. โœจ GIF ์ƒ์„ฑํ•˜๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ GIF๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์šฐ์ธก ์ถœ๋ ฅ๋ถ€์—์„œ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธ ๋ฐ ๋‹ค์šด๋กœ๋“œํ•˜์„ธ์š”. ๐Ÿ˜Š</div>")
# ์ฒซ๋ฒˆ์งธ ํ–‰: ์ž…๋ ฅ๋ถ€ (์ขŒ์ธก) / ์ถœ๋ ฅ๋ถ€ (์šฐ์ธก)
with gr.Row(elem_classes="row-container"):
with gr.Column(elem_classes="column"):
with gr.Group(elem_classes="frame"):
gr.Markdown("### ๐Ÿ“ฅ ์ž…๋ ฅ๋ถ€")
video_input = gr.Video(label="์˜์ƒ ์—…๋กœ๋“œ", show_label=True)
platform_option = gr.Radio(
label="ํ•ด์ƒ๋„/๋น„์œจ ์„ ํƒ",
choices=["์›๋ณธ ์œ ์ง€", "์œ ํŠœ๋ธŒ (16:9)", "์‡ผ์ธ /๋ฆด์Šค (9:16)", "์ •์‚ฌ๊ฐํ˜• (1:1)", "์ธ์Šคํƒ€๊ทธ๋žจ (4:5)", "ํด๋ž˜์‹ (4:3)"],
value="์›๋ณธ ์œ ์ง€"
)
resolution_scale_slider = gr.Slider(label="์ถœ๋ ฅ ํ•ด์ƒ๋„ ์ถ•์†Œ ๋น„์œจ (0.1 ~ 1.0)",
minimum=0.1, maximum=1.0, step=0.1, value=1.0)
frame_rate_slider = gr.Slider(label="ํ”„๋ ˆ์ž„ ๋ ˆ์ดํŠธ ๋ฐฐ์œจ ์กฐ์ ˆ (0.1 ~ 1.0)",
minimum=0.1, maximum=1.0, step=0.1, value=1.0)
speed_slider = gr.Slider(label="์žฌ์ƒ ์†๋„ ์กฐ์ ˆ (0.5 ~ 5.0)",
minimum=0.5, maximum=5.0, step=0.1, value=1.0)
repeat_slider = gr.Slider(label="GIF ๋ฐ˜๋ณต ํšŸ์ˆ˜ (0: ๋ฌดํ•œ, 1~10)",
minimum=0, maximum=10, step=1, value=0)
with gr.Column(elem_classes="column"):
with gr.Group(elem_classes="frame"):
gr.Markdown("### ๐Ÿ“ค ์ถœ๋ ฅ๋ถ€")
gif_preview_output = gr.Image(label="GIF ๊ฒฐ๊ณผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ")
download_output = gr.File(label="GIF ๋‹ค์šด๋กœ๋“œ")
# ๋‘๋ฒˆ์งธ ํ–‰: ์ž‘์—… ์ธ๋„ค์ผ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ „์ฒด ๋„ˆ๋น„)
with gr.Group(elem_classes="frame"):
gr.Markdown("### ๐Ÿ–ผ๏ธ ์ž‘์—… ์ธ๋„ค์ผ๋ฏธ๋ฆฌ๋ณด๊ธฐ")
with gr.Row():
start_time_tb = gr.Textbox(label="์‹œ์ž‘ ์‹œ๊ฐ„ (์˜ˆ: 00:00:05)", value="00:00:00")
end_time_tb = gr.Textbox(label="์ข…๋ฃŒ ์‹œ๊ฐ„ (์˜ˆ: 00:00:10)", value="00:00:05")
with gr.Row():
start_thumb_output = gr.Image(label="์‹œ์ž‘ ์ธ๋„ค์ผ")
end_thumb_output = gr.Image(label="์ข…๋ฃŒ ์ธ๋„ค์ผ")
generate_button = gr.Button("โœจ GIF ์ƒ์„ฑํ•˜๊ธฐ", elem_classes="gif-button")
# ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ
start_time_tb.change(fn=update_thumbnails,
inputs=[video_input, start_time_tb, end_time_tb],
outputs=[start_thumb_output, end_thumb_output])
end_time_tb.change(fn=update_thumbnails,
inputs=[video_input, start_time_tb, end_time_tb],
outputs=[start_thumb_output, end_thumb_output])
video_input.change(fn=update_thumbnails,
inputs=[video_input, start_time_tb, end_time_tb],
outputs=[start_thumb_output, end_thumb_output])
generate_button.click(fn=process_video,
inputs=[video_input, start_time_tb, end_time_tb,
platform_option, frame_rate_slider, speed_slider, repeat_slider,
resolution_scale_slider],
outputs=[gif_preview_output, download_output])
demo.launch()