|
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 = """ |
|
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; /* ๊ธ์ ํฌ๊ธฐ ์ฆ๊ฐ */ |
|
} |
|
""" |
|
|
|
|
|
|
|
|
|
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() |