gif-1 / app.py
ssboost's picture
Update app.py
7eb0a25 verified
raw
history blame
19.9 kB
import gradio as gr
import os
import tempfile
import base64
import math
import traceback
import numpy as np
from PIL import Image
from moviepy.editor import VideoFileClip, vfx
from shutil import copyfile
from datetime import datetime, timedelta
########################################
# 1) PIL ANTIALIAS μ—λŸ¬ λŒ€μ‘ (Monkey-patch)
########################################
try:
from PIL import Resampling
if not hasattr(Image, "ANTIALIAS"):
Image.ANTIALIAS = Resampling.LANCZOS
except ImportError:
pass
########################################
# 2) λ‚΄λΆ€ 디버그 λ‘œκΉ… (UIμ—μ„œ 미좜λ ₯)
########################################
DEBUG_LOG_LIST = []
def log_debug(msg: str):
print("[DEBUG]", msg)
DEBUG_LOG_LIST.append(msg)
########################################
# 3) μ‹œκ°„ ν˜•μ‹ λ³€ν™˜ μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜
########################################
END_EPSILON = 0.01
def round_down_to_one_decimal(value: float) -> float:
return math.floor(value * 10) / 10
def safe_end_time(duration: float) -> float:
tmp = duration - END_EPSILON
if tmp < 0:
tmp = 0
return round_down_to_one_decimal(tmp)
def coalesce_to_zero(val):
"""
Noneμ΄λ‚˜ NaN, λ¬Έμžμ—΄ 였λ₯˜ 등이 λ“€μ–΄μ˜€λ©΄ 0.0으둜 λ³€ν™˜
"""
if val is None:
return 0.0
try:
return float(val)
except:
return 0.0
def seconds_to_hms(seconds: float) -> str:
"""초λ₯Ό HH:MM:SS ν˜•μ‹μœΌλ‘œ λ³€ν™˜"""
try:
seconds = max(0, seconds)
td = timedelta(seconds=round(seconds))
return str(td)
except Exception as e:
log_debug(f"[seconds_to_hms] λ³€ν™˜ 였λ₯˜: {e}")
return "00:00:00"
def hms_to_seconds(time_str: str) -> float:
"""HH:MM:SS ν˜•μ‹μ„ 초둜 λ³€ν™˜"""
try:
parts = time_str.strip().split(':')
parts = [int(p) for p in parts]
while len(parts) < 3:
parts.insert(0, 0) # λΆ€μ‘±ν•œ 뢀뢄은 0으둜 채움
hours, minutes, seconds = parts
return hours * 3600 + minutes * 60 + seconds
except Exception as e:
log_debug(f"[hms_to_seconds] λ³€ν™˜ 였λ₯˜: {e}")
return -1 # 였λ₯˜ μ‹œ -1 λ°˜ν™˜
########################################
# 4) μ—…λ‘œλ“œλœ μ˜μƒ 파일 μ €μž₯
########################################
def save_uploaded_video(video_input):
if not video_input:
log_debug("[save_uploaded_video] video_input is None.")
return None
if isinstance(video_input, str):
log_debug(f"[save_uploaded_video] video_input is str: {video_input}")
if os.path.exists(video_input):
return video_input
else:
log_debug("[save_uploaded_video] Path does not exist.")
return None
if isinstance(video_input, dict):
log_debug(f"[save_uploaded_video] video_input is dict: {list(video_input.keys())}")
if 'data' in video_input:
file_data = video_input['data']
if isinstance(file_data, str) and file_data.startswith("data:"):
base64_str = file_data.split(';base64,')[-1]
try:
video_binary = base64.b64decode(base64_str)
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
tmp.write(video_binary)
tmp.flush()
tmp.close()
log_debug(f"[save_uploaded_video] Created temp file: {tmp.name}")
return tmp.name
except Exception as e:
log_debug(f"[save_uploaded_video] base64 λ””μ½”λ”© 였λ₯˜: {e}")
return None
else:
if isinstance(file_data, str) and os.path.exists(file_data):
log_debug("[save_uploaded_video] data ν•„λ“œκ°€ μ‹€μ œ 경둜")
return file_data
else:
log_debug("[save_uploaded_video] data ν•„λ“œκ°€ μ˜ˆμƒμΉ˜ λͺ»ν•œ ν˜•νƒœ.")
return None
else:
log_debug("[save_uploaded_video] dictμ΄μ§€λ§Œ 'data' ν‚€κ°€ μ—†μŒ.")
return None
log_debug("[save_uploaded_video] Unrecognized type.")
return None
########################################
# 5) μ˜μƒ 길이, 해상도, μŠ€ν¬λ¦°μƒ·
########################################
def get_video_duration(video_dict):
path = save_uploaded_video(video_dict)
if not path:
return "00:00:00"
try:
clip = VideoFileClip(path)
dur = clip.duration
clip.close()
log_debug(f"[get_video_duration] duration={dur}")
return seconds_to_hms(dur)
except Exception as e:
log_debug(f"[get_video_duration] 였λ₯˜: {e}\n{traceback.format_exc()}")
return "00:00:00"
def get_resolution(video_dict):
path = save_uploaded_video(video_dict)
if not path:
return "0x0"
try:
clip = VideoFileClip(path)
w, h = clip.size
clip.close()
log_debug(f"[get_resolution] w={w}, h={h}")
return f"{w}x{h}"
except Exception as e:
log_debug(f"[get_resolution] 였λ₯˜: {e}\n{traceback.format_exc()}")
return "0x0"
def get_screenshot_at_time(video_dict, time_in_seconds):
path = save_uploaded_video(video_dict)
if not path:
return None
try:
clip = VideoFileClip(path)
actual_duration = clip.duration
# λ§ˆμ§€λ§‰ ν”„λ ˆμž„ μ ‘κ·Ό λ°©μ§€
if time_in_seconds >= actual_duration - END_EPSILON:
time_in_seconds = safe_end_time(actual_duration)
t = max(0, min(time_in_seconds, clip.duration))
log_debug(f"[get_screenshot_at_time] t={t:.3f} / duration={clip.duration:.3f}")
frame = clip.get_frame(t)
clip.close()
return frame # numpy λ°°μ—΄λ‘œ λ°˜ν™˜
except Exception as e:
log_debug(f"[get_screenshot_at_time] 였λ₯˜: {e}\n{traceback.format_exc()}")
return None
########################################
# 6) μ—…λ‘œλ“œ 이벀트
########################################
def on_video_upload(video_dict):
log_debug("[on_video_upload] Called.")
dur_hms = get_video_duration(video_dict)
w, h = map(int, get_resolution(video_dict).split('x'))
resolution_str = f"{w}x{h}"
start_t = 0.0
end_t = safe_end_time(hms_to_seconds(dur_hms))
start_img = get_screenshot_at_time(video_dict, start_t)
end_img = None
if end_t > 0:
end_img = get_screenshot_at_time(video_dict, end_t)
# μˆœμ„œλŒ€λ‘œ: μ˜μƒ 길이, 해상도(λ¬Έμžμ—΄), μ‹œμž‘ μ‹œκ°„, 끝 μ‹œκ°„, μ‹œμž‘ μŠ€ν¬λ¦°μƒ·, 끝 μŠ€ν¬λ¦°μƒ·
return dur_hms, resolution_str, seconds_to_hms(start_t), seconds_to_hms(end_t), start_img, end_img
########################################
# 7) μŠ€ν¬λ¦°μƒ· κ°±μ‹ 
########################################
def update_screenshots(video_dict, start_time_str, end_time_str):
start_time = hms_to_seconds(start_time_str)
end_time = hms_to_seconds(end_time_str)
if start_time < 0 or end_time < 0:
return (None, None)
log_debug(f"[update_screenshots] start={start_time_str}, end={end_time_str}")
end_time = round_down_to_one_decimal(end_time)
img_start = get_screenshot_at_time(video_dict, start_time)
img_end = get_screenshot_at_time(video_dict, end_time)
return (img_start, img_end)
########################################
# 8) GIF 생성
########################################
def generate_gif(video_dict, start_time_str, end_time_str, fps, resize_factor, speed_factor, duration, resolution_str):
# "WxH" ν˜•νƒœ 해상도 νŒŒμ‹±
parts = resolution_str.split("x")
if len(parts) == 2:
try:
orig_w = float(parts[0])
orig_h = float(parts[1])
except:
orig_w = 0
orig_h = 0
else:
orig_w = 0
orig_h = 0
start_time = hms_to_seconds(start_time_str)
end_time = hms_to_seconds(end_time_str)
if start_time < 0 or end_time < 0:
return "잘λͺ»λœ μ‹œκ°„ ν˜•μ‹μž…λ‹ˆλ‹€. HH:MM:SS ν˜•μ‹μœΌλ‘œ μž…λ ₯ν•΄μ£Όμ„Έμš”."
fps = coalesce_to_zero(fps)
resize_factor = coalesce_to_zero(resize_factor)
speed_factor = coalesce_to_zero(speed_factor)
log_debug("[generate_gif] Called.")
log_debug(f" start_time={start_time}, end_time={end_time}, fps={fps}, resize_factor={resize_factor}, speed_factor={speed_factor}")
path = save_uploaded_video(video_dict)
if not path:
err_msg = "[generate_gif] μ˜μƒμ΄ μ—…λ‘œλ“œλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."
log_debug(err_msg)
return err_msg
try:
clip = VideoFileClip(path)
end_time = round_down_to_one_decimal(end_time)
st = max(0, start_time)
et = max(0, end_time)
if et > clip.duration:
et = clip.duration
# λ§ˆμ§€λ§‰ ν”„λ ˆμž„ μ ‘κ·Ό λ°©μ§€
if et >= clip.duration - END_EPSILON:
et = safe_end_time(clip.duration)
log_debug(f" subclip range => st={st:.2f}, et={et:.2f}, totalDur={clip.duration:.2f}")
if st >= et:
clip.close()
err_msg = "μ‹œμž‘ μ‹œκ°„μ΄ 끝 μ‹œκ°„λ³΄λ‹€ κ°™κ±°λ‚˜ ν½λ‹ˆλ‹€."
log_debug(f"[generate_gif] {err_msg}")
return err_msg
sub_clip = clip.subclip(st, et)
# 배속 μ‘°μ •
if speed_factor != 1.0:
sub_clip = sub_clip.fx(vfx.speedx, speed_factor)
log_debug(f" speed_factor applied: {speed_factor}x")
# λ¦¬μ‚¬μ΄μ¦ˆ
if resize_factor < 1.0 and orig_w > 0 and orig_h > 0:
new_w = int(orig_w * resize_factor)
new_h = int(orig_h * resize_factor)
log_debug(f" resizing => {new_w}x{new_h}")
sub_clip = sub_clip.resize((new_w, new_h))
# κ³ μœ ν•œ 파일 이름 생성
gif_fd, gif_path = tempfile.mkstemp(suffix=".gif")
os.close(gif_fd) # 파일 λ””μŠ€ν¬λ¦½ν„° λ‹«κΈ°
log_debug(f" writing GIF to {gif_path}")
sub_clip.write_gif(gif_path, fps=int(fps), program='ffmpeg') # ffmpeg μ‚¬μš©
clip.close()
sub_clip.close()
if os.path.exists(gif_path):
log_debug(f" GIF 생성 μ™„λ£Œ! size={os.path.getsize(gif_path)} bytes.")
return gif_path
else:
err_msg = "GIF 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."
log_debug(f"[generate_gif] {err_msg}")
return err_msg
except Exception as e:
err_msg = f"[generate_gif] 였λ₯˜ λ°œμƒ: {e}\n{traceback.format_exc()}"
log_debug(err_msg)
return err_msg
########################################
# 9) GIF λ‹€μš΄λ‘œλ“œ 파일 이름 λ³€κ²½ ν•¨μˆ˜
########################################
def prepare_download_gif(gif_path, input_video_dict):
"""GIF 파일의 λ‹€μš΄λ‘œλ“œ 이름을 λ³€κ²½ν•˜κ³  경둜λ₯Ό λ°˜ν™˜"""
if gif_path is None:
return None
# ν•œκ΅­ μ‹œκ°„ νƒ€μž„μŠ€νƒ¬ν”„ 생성 ν•¨μˆ˜
def get_korean_timestamp():
korea_time = datetime.utcnow() + timedelta(hours=9)
return korea_time.strftime('%Y%m%d_%H%M%S')
timestamp = get_korean_timestamp()
# μž…λ ₯된 GIF μ΄λ¦„μ—μ„œ κΈ°λ³Έ 이름 μΆ”μΆœ
if input_video_dict and isinstance(input_video_dict, dict) and 'data' in input_video_dict:
file_data = input_video_dict['data']
if isinstance(file_data, str) and file_data.startswith("data:"):
base_name = "GIF" # base64 λ°μ΄ν„°μ—μ„œλŠ” 원본 파일 이름을 μ•Œ 수 μ—†μœΌλ―€λ‘œ κΈ°λ³Έ 이름 μ‚¬μš©
elif isinstance(file_data, str) and os.path.exists(file_data):
base_name = os.path.splitext(os.path.basename(file_data))[0]
else:
base_name = "GIF"
else:
base_name = "GIF"
# μƒˆλ‘œμš΄ 파일 이름 생성
file_name = f"[끝μž₯AI]끝μž₯GIF_{base_name}_{timestamp}.gif"
# μž„μ‹œ 디렉토리에 파일 μ €μž₯
temp_file_path = os.path.join(tempfile.gettempdir(), file_name)
try:
# κΈ°μ‘΄ GIF νŒŒμΌμ„ μƒˆλ‘œμš΄ μ΄λ¦„μœΌλ‘œ 볡사
copyfile(gif_path, temp_file_path)
except Exception as e:
log_debug(f"[prepare_download_gif] 파일 볡사 였λ₯˜: {e}")
return gif_path # 볡사에 μ‹€νŒ¨ν•˜λ©΄ 원본 경둜 λ°˜ν™˜
return temp_file_path
########################################
# 10) 콜백 ν•¨μˆ˜
########################################
def on_any_change(video_dict, start_time_str, end_time_str):
# μŠ€ν¬λ¦°μƒ·λ§Œ μ—…λ°μ΄νŠΈ
start_img, end_img = update_screenshots(video_dict, start_time_str, end_time_str)
return (start_img, end_img)
def on_generate_click(video_dict, start_time_str, end_time_str, fps, resize_factor, speed_factor, duration, resolution_str):
"""GIF 생성 ν›„:
- μ„±κ³΅μ‹œ: (μƒμ„±λœ GIF 경둜, 파일 μš©λŸ‰ λ¬Έμžμ—΄, 파일 λ‹€μš΄λ‘œλ“œ 경둜)
- μ‹€νŒ¨μ‹œ: (None, μ—λŸ¬ λ©”μ‹œμ§€, None)
"""
# Convert duration from hms to seconds for internal use if needed
# durationλŠ” ν˜„μž¬ μ‚¬μš©λ˜μ§€ μ•ŠμœΌλ―€λ‘œ λ¬΄μ‹œν•˜κ±°λ‚˜ ν•„μš” μ‹œ μ²˜λ¦¬ν•  수 있음
result = generate_gif(video_dict, start_time_str, end_time_str, fps, resize_factor, speed_factor, duration, resolution_str)
if isinstance(result, str) and os.path.exists(result):
# GIF 생성 성곡
size_bytes = os.path.getsize(result)
size_mb = size_bytes / (1024 * 1024)
file_size_str = f"{size_mb:.2f} MB"
# λ‹€μš΄λ‘œλ“œ 파일 이름 λ³€κ²½
download_path = prepare_download_gif(result, video_dict)
# Gradioκ°€ μžλ™μœΌλ‘œ νŒŒμΌμ„ μ²˜λ¦¬ν•˜λ„λ‘ `download_path`λ₯Ό λ°˜ν™˜
return (result, file_size_str, download_path)
else:
# GIF 생성 μ‹€νŒ¨, μ—λŸ¬ λ©”μ‹œμ§€ λ°˜ν™˜
err_msg = result if isinstance(result, str) else "GIF 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."
return (None, err_msg, None)
########################################
# 11) Gradio UI
########################################
# μŠ€νƒ€μΌ μ μš©μ„ μœ„ν•œ CSS 및 ν…Œλ§ˆ μ„€μ •
css = """
footer {
visibility: hidden;
}
.left-column, .right-column {
border: 2px solid #ccc;
border-radius: 8px;
padding: 20px;
background-color: #f9f9f9;
}
.left-column {
margin-right: 10px;
}
.right-column {
margin-left: 10px;
}
.section-border {
border: 1px solid #ddd;
border-radius: 6px;
padding: 10px;
margin-bottom: 15px;
background-color: #ffffff;
}
"""
with gr.Blocks(
theme=gr.themes.Soft(
primary_hue=gr.themes.Color(
c50="#FFF7ED", # κ°€μž₯ 밝은 μ£Όν™©
c100="#FFEDD5",
c200="#FED7AA",
c300="#FDBA74",
c400="#FB923C",
c500="#F97316", # κΈ°λ³Έ μ£Όν™©
c600="#EA580C",
c700="#C2410C",
c800="#9A3412",
c900="#7C2D12", # κ°€μž₯ μ–΄λ‘μš΄ μ£Όν™©
c950="#431407",
),
secondary_hue="zinc", # λͺ¨λ˜ν•œ λŠλ‚Œμ˜ νšŒμƒ‰ 계열
neutral_hue="zinc",
font=("Pretendard", "sans-serif")
),
css=css
) as demo:
with gr.Row():
# μ™Όμͺ½ 컬럼: μ˜μƒ μ—…λ‘œλ“œ 및 정보
with gr.Column(elem_classes="left-column"):
# μ˜μƒ μ—…λ‘œλ“œ μ„Ήμ…˜
with gr.Row(elem_classes="section-border"):
video_input = gr.Video(label="μ˜μƒ μ—…λ‘œλ“œ")
# μ˜μƒ 길이 및 해상도 μ„Ήμ…˜
with gr.Row(elem_classes="section-border"):
duration_box = gr.Textbox(label="μ˜μƒ 길이", interactive=False, value="00:00:00")
resolution_box = gr.Textbox(label="해상도", interactive=False, value="0x0")
# 였λ₯Έμͺ½ 컬럼: κ²°κ³Ό GIF 및 λ‹€μš΄λ‘œλ“œ
with gr.Column(elem_classes="right-column"):
# κ²°κ³Ό GIF μ„Ήμ…˜
with gr.Row(elem_classes="section-border"):
output_gif = gr.Image(label="κ²°κ³Ό GIF")
# 파일 μš©λŸ‰ 및 λ‹€μš΄λ‘œλ“œ μ„Ήμ…˜
with gr.Row(elem_classes="section-border"):
file_size_text = gr.Textbox(label="파일 μš©λŸ‰", interactive=False, value="0 MB")
download_gif_component = gr.File(label="GIF λ‹€μš΄λ‘œλ“œ") # 이름 λ³€κ²½ν•˜μ—¬ 좩돌 λ°©μ§€
# μΆ”κ°€ μ„€μ • μ„Ήμ…˜ (μ‹œκ°„ μ„€μ •, μŠ€ν¬λ¦°μƒ·, μŠ¬λΌμ΄λ” λ“±)
with gr.Row():
with gr.Column():
# μ‹œκ°„ μ„€μ • μ„Ήμ…˜
with gr.Row(elem_classes="section-border"):
start_time_input = gr.Textbox(label="μ‹œμž‘ μ‹œκ°„ (HH:MM:SS)", value="00:00:00")
end_time_input = gr.Textbox(label="끝 μ‹œκ°„ (HH:MM:SS)", value="00:00:00")
# μŠ€ν¬λ¦°μƒ· μ„Ήμ…˜
with gr.Row(elem_classes="section-border"):
start_screenshot = gr.Image(label="μ‹œμž‘ 지점 캑쳐본")
end_screenshot = gr.Image(label="끝 지점 캑쳐본")
# μ„€μ • μŠ¬λΌμ΄λ” μ„Ήμ…˜
with gr.Row(elem_classes="section-border"):
# 배속 쑰절 μŠ¬λΌμ΄λ” μΆ”κ°€
speed_slider = gr.Slider(
label="배속",
minimum=0.5,
maximum=2.0,
step=0.1,
value=1.0,
info="0.5x: 절반 속도, 1.0x: μ›λž˜ 속도, 2.0x: 두 λ°° 속도"
)
# FPS μŠ¬λΌμ΄λ”
fps_slider = gr.Slider(
label="FPS",
minimum=1,
maximum=30,
step=1,
value=10,
info="ν”„λ ˆμž„ 수λ₯Ό μ‘°μ ˆν•˜μ—¬ μ• λ‹ˆλ©”μ΄μ…˜μ˜ λΆ€λ“œλŸ¬μ›€μ„ λ³€κ²½ν•©λ‹ˆλ‹€."
)
# 해상도 배율 μŠ¬λΌμ΄λ”
resize_slider = gr.Slider(
label="해상도 배율",
minimum=0.1,
maximum=1.0,
step=0.05,
value=1.0,
info="GIF의 해상도λ₯Ό μ‘°μ ˆν•©λ‹ˆλ‹€."
)
# GIF 생성 λ²„νŠΌ μ„Ήμ…˜
with gr.Row(elem_classes="section-border"):
generate_button = gr.Button("GIF 생성")
# 이벀트 콜백 μ„€μ •
# μ—…λ‘œλ“œ μ‹œ β†’ μ˜μƒ 길이, 해상도 μ—…λ°μ΄νŠΈ
video_input.change(
fn=on_video_upload,
inputs=[video_input],
outputs=[
duration_box, # μ˜μƒ 길이
resolution_box, # 해상도("WxH")
start_time_input,
end_time_input,
start_screenshot,
end_screenshot
]
)
# μ‹œμž‘/끝 μ‹œκ°„ λ³€κ²½ μ‹œ β†’ μŠ€ν¬λ¦°μƒ· μ—…λ°μ΄νŠΈ
for c in [start_time_input, end_time_input]:
c.change(
fn=on_any_change,
inputs=[video_input, start_time_input, end_time_input],
outputs=[start_screenshot, end_screenshot]
)
# 배속, FPS, 해상도 배율 λ³€κ²½ μ‹œ β†’ μŠ€ν¬λ¦°μƒ· μ—…λ°μ΄νŠΈ
for c in [speed_slider, fps_slider, resize_slider]:
c.change(
fn=on_any_change,
inputs=[video_input, start_time_input, end_time_input],
outputs=[start_screenshot, end_screenshot]
)
# GIF 생성 λ²„νŠΌ 클릭 μ‹œ β†’ GIF 생성 및 κ²°κ³Ό μ—…λ°μ΄νŠΈ
generate_button.click(
fn=on_generate_click,
inputs=[
video_input,
start_time_input,
end_time_input,
fps_slider,
resize_slider,
speed_slider, # 배속 μŠ¬λΌμ΄λ” μΆ”κ°€
duration_box,
resolution_box
],
outputs=[
output_gif, # μƒμ„±λœ GIF 미리보기
file_size_text, # 파일 μš©λŸ‰ ν‘œμ‹œ
download_gif_component # λ‹€μš΄λ‘œλ“œ 링크
]
)
demo.launch()