Update app.py
Browse files
app.py
CHANGED
@@ -1,563 +1,2 @@
|
|
1 |
-
import gradio as gr
|
2 |
import os
|
3 |
-
|
4 |
-
import base64
|
5 |
-
import math
|
6 |
-
import traceback
|
7 |
-
import numpy as np
|
8 |
-
from PIL import Image
|
9 |
-
|
10 |
-
from moviepy.editor import VideoFileClip, vfx
|
11 |
-
from shutil import copyfile
|
12 |
-
from datetime import datetime, timedelta
|
13 |
-
|
14 |
-
########################################
|
15 |
-
# 1) PIL ANTIALIAS μλ¬ λμ (Monkey-patch)
|
16 |
-
########################################
|
17 |
-
try:
|
18 |
-
from PIL import Resampling
|
19 |
-
if not hasattr(Image, "ANTIALIAS"):
|
20 |
-
Image.ANTIALIAS = Resampling.LANCZOS
|
21 |
-
except ImportError:
|
22 |
-
pass
|
23 |
-
|
24 |
-
########################################
|
25 |
-
# 2) λ΄λΆ λλ²κ·Έ λ‘κΉ
(UIμμ λ―ΈμΆλ ₯)
|
26 |
-
########################################
|
27 |
-
DEBUG_LOG_LIST = []
|
28 |
-
|
29 |
-
def log_debug(msg: str):
|
30 |
-
print("[DEBUG]", msg)
|
31 |
-
DEBUG_LOG_LIST.append(msg)
|
32 |
-
|
33 |
-
########################################
|
34 |
-
# 3) μκ° νμ λ³ν μ νΈλ¦¬ν° ν¨μ
|
35 |
-
########################################
|
36 |
-
END_EPSILON = 0.01
|
37 |
-
|
38 |
-
def round_down_to_one_decimal(value: float) -> float:
|
39 |
-
return math.floor(value * 10) / 10
|
40 |
-
|
41 |
-
def safe_end_time(duration: float) -> float:
|
42 |
-
tmp = duration - END_EPSILON
|
43 |
-
if tmp < 0:
|
44 |
-
tmp = 0
|
45 |
-
return round_down_to_one_decimal(tmp)
|
46 |
-
|
47 |
-
def coalesce_to_zero(val):
|
48 |
-
"""
|
49 |
-
Noneμ΄λ NaN, λ¬Έμμ΄ μ€λ₯ λ±μ΄ λ€μ΄μ€λ©΄ 0.0μΌλ‘ λ³ν
|
50 |
-
"""
|
51 |
-
if val is None:
|
52 |
-
return 0.0
|
53 |
-
try:
|
54 |
-
return float(val)
|
55 |
-
except:
|
56 |
-
return 0.0
|
57 |
-
|
58 |
-
def seconds_to_hms(seconds: float) -> str:
|
59 |
-
"""μ΄λ₯Ό HH:MM:SS νμμΌλ‘ λ³ν"""
|
60 |
-
try:
|
61 |
-
seconds = max(0, seconds)
|
62 |
-
td = timedelta(seconds=round(seconds))
|
63 |
-
return str(td)
|
64 |
-
except Exception as e:
|
65 |
-
log_debug(f"[seconds_to_hms] λ³ν μ€λ₯: {e}")
|
66 |
-
return "00:00:00"
|
67 |
-
|
68 |
-
def hms_to_seconds(time_str: str) -> float:
|
69 |
-
"""HH:MM:SS νμμ μ΄λ‘ λ³ν"""
|
70 |
-
try:
|
71 |
-
parts = time_str.strip().split(':')
|
72 |
-
parts = [int(p) for p in parts]
|
73 |
-
while len(parts) < 3:
|
74 |
-
parts.insert(0, 0) # λΆμ‘±ν λΆλΆμ 0μΌλ‘ μ±μ
|
75 |
-
hours, minutes, seconds = parts
|
76 |
-
return hours * 3600 + minutes * 60 + seconds
|
77 |
-
except Exception as e:
|
78 |
-
log_debug(f"[hms_to_seconds] λ³ν μ€λ₯: {e}")
|
79 |
-
return -1 # μ€λ₯ μ -1 λ°ν
|
80 |
-
|
81 |
-
########################################
|
82 |
-
# 4) μ
λ‘λλ μμ νμΌ μ μ₯
|
83 |
-
########################################
|
84 |
-
def save_uploaded_video(video_input):
|
85 |
-
if not video_input:
|
86 |
-
log_debug("[save_uploaded_video] video_input is None.")
|
87 |
-
return None
|
88 |
-
|
89 |
-
if isinstance(video_input, str):
|
90 |
-
log_debug(f"[save_uploaded_video] video_input is str: {video_input}")
|
91 |
-
if os.path.exists(video_input):
|
92 |
-
return video_input
|
93 |
-
else:
|
94 |
-
log_debug("[save_uploaded_video] Path does not exist.")
|
95 |
-
return None
|
96 |
-
|
97 |
-
if isinstance(video_input, dict):
|
98 |
-
log_debug(f"[save_uploaded_video] video_input is dict: {list(video_input.keys())}")
|
99 |
-
if 'data' in video_input:
|
100 |
-
file_data = video_input['data']
|
101 |
-
if isinstance(file_data, str) and file_data.startswith("data:"):
|
102 |
-
base64_str = file_data.split(';base64,')[-1]
|
103 |
-
try:
|
104 |
-
video_binary = base64.b64decode(base64_str)
|
105 |
-
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
|
106 |
-
tmp.write(video_binary)
|
107 |
-
tmp.flush()
|
108 |
-
tmp.close()
|
109 |
-
log_debug(f"[save_uploaded_video] Created temp file: {tmp.name}")
|
110 |
-
return tmp.name
|
111 |
-
except Exception as e:
|
112 |
-
log_debug(f"[save_uploaded_video] base64 λμ½λ© μ€λ₯: {e}")
|
113 |
-
return None
|
114 |
-
else:
|
115 |
-
if isinstance(file_data, str) and os.path.exists(file_data):
|
116 |
-
log_debug("[save_uploaded_video] data νλκ° μ€μ κ²½λ‘")
|
117 |
-
return file_data
|
118 |
-
else:
|
119 |
-
log_debug("[save_uploaded_video] data νλκ° μμμΉ λͺ»ν νν.")
|
120 |
-
return None
|
121 |
-
else:
|
122 |
-
log_debug("[save_uploaded_video] dictμ΄μ§λ§ 'data' ν€κ° μμ.")
|
123 |
-
return None
|
124 |
-
|
125 |
-
log_debug("[save_uploaded_video] Unrecognized type.")
|
126 |
-
return None
|
127 |
-
|
128 |
-
########################################
|
129 |
-
# 5) μμ κΈΈμ΄, ν΄μλ, μ€ν¬λ¦°μ·
|
130 |
-
########################################
|
131 |
-
def get_video_duration(video_dict):
|
132 |
-
path = save_uploaded_video(video_dict)
|
133 |
-
if not path:
|
134 |
-
return "00:00:00"
|
135 |
-
try:
|
136 |
-
clip = VideoFileClip(path)
|
137 |
-
dur = clip.duration
|
138 |
-
clip.close()
|
139 |
-
log_debug(f"[get_video_duration] duration={dur}")
|
140 |
-
return seconds_to_hms(dur)
|
141 |
-
except Exception as e:
|
142 |
-
log_debug(f"[get_video_duration] μ€λ₯: {e}\n{traceback.format_exc()}")
|
143 |
-
return "00:00:00"
|
144 |
-
|
145 |
-
def get_resolution(video_dict):
|
146 |
-
path = save_uploaded_video(video_dict)
|
147 |
-
if not path:
|
148 |
-
return "0x0"
|
149 |
-
try:
|
150 |
-
clip = VideoFileClip(path)
|
151 |
-
w, h = clip.size
|
152 |
-
clip.close()
|
153 |
-
log_debug(f"[get_resolution] w={w}, h={h}")
|
154 |
-
return f"{w}x{h}"
|
155 |
-
except Exception as e:
|
156 |
-
log_debug(f"[get_resolution] μ€λ₯: {e}\n{traceback.format_exc()}")
|
157 |
-
return "0x0"
|
158 |
-
|
159 |
-
def get_screenshot_at_time(video_dict, time_in_seconds):
|
160 |
-
path = save_uploaded_video(video_dict)
|
161 |
-
if not path:
|
162 |
-
return None
|
163 |
-
try:
|
164 |
-
clip = VideoFileClip(path)
|
165 |
-
actual_duration = clip.duration
|
166 |
-
|
167 |
-
# λ§μ§λ§ νλ μ μ κ·Ό λ°©μ§
|
168 |
-
if time_in_seconds >= actual_duration - END_EPSILON:
|
169 |
-
time_in_seconds = safe_end_time(actual_duration)
|
170 |
-
|
171 |
-
t = max(0, min(time_in_seconds, clip.duration))
|
172 |
-
log_debug(f"[get_screenshot_at_time] t={t:.3f} / duration={clip.duration:.3f}")
|
173 |
-
frame = clip.get_frame(t)
|
174 |
-
clip.close()
|
175 |
-
return frame # numpy λ°°μ΄λ‘ λ°ν
|
176 |
-
except Exception as e:
|
177 |
-
log_debug(f"[get_screenshot_at_time] μ€λ₯: {e}\n{traceback.format_exc()}")
|
178 |
-
return None
|
179 |
-
|
180 |
-
########################################
|
181 |
-
# 6) μ
λ‘λ μ΄λ²€νΈ
|
182 |
-
########################################
|
183 |
-
def on_video_upload(video_dict):
|
184 |
-
log_debug("[on_video_upload] Called.")
|
185 |
-
dur_hms = get_video_duration(video_dict)
|
186 |
-
w, h = map(int, get_resolution(video_dict).split('x'))
|
187 |
-
resolution_str = f"{w}x{h}"
|
188 |
-
|
189 |
-
start_t = 0.0
|
190 |
-
end_t = safe_end_time(hms_to_seconds(dur_hms))
|
191 |
-
|
192 |
-
start_img = get_screenshot_at_time(video_dict, start_t)
|
193 |
-
end_img = None
|
194 |
-
if end_t > 0:
|
195 |
-
end_img = get_screenshot_at_time(video_dict, end_t)
|
196 |
-
|
197 |
-
# μμλλ‘: μμ κΈΈμ΄, ν΄μλ(λ¬Έμμ΄), μμ μκ°, λ μκ°, μμ μ€ν¬λ¦°μ·, λ μ€ν¬λ¦°μ·
|
198 |
-
return dur_hms, resolution_str, seconds_to_hms(start_t), seconds_to_hms(end_t), start_img, end_img
|
199 |
-
|
200 |
-
########################################
|
201 |
-
# 7) μ€ν¬λ¦°μ· κ°±μ
|
202 |
-
########################################
|
203 |
-
def update_screenshots(video_dict, start_time_str, end_time_str):
|
204 |
-
start_time = hms_to_seconds(start_time_str)
|
205 |
-
end_time = hms_to_seconds(end_time_str)
|
206 |
-
|
207 |
-
if start_time < 0 or end_time < 0:
|
208 |
-
return (None, None)
|
209 |
-
|
210 |
-
log_debug(f"[update_screenshots] start={start_time_str}, end={end_time_str}")
|
211 |
-
|
212 |
-
end_time = round_down_to_one_decimal(end_time)
|
213 |
-
img_start = get_screenshot_at_time(video_dict, start_time)
|
214 |
-
img_end = get_screenshot_at_time(video_dict, end_time)
|
215 |
-
return (img_start, img_end)
|
216 |
-
|
217 |
-
########################################
|
218 |
-
# 8) GIF μμ±
|
219 |
-
########################################
|
220 |
-
def generate_gif(video_dict, start_time_str, end_time_str, fps, resize_factor, speed_factor, duration, resolution_str):
|
221 |
-
# "WxH" νν ν΄μλ νμ±
|
222 |
-
parts = resolution_str.split("x")
|
223 |
-
if len(parts) == 2:
|
224 |
-
try:
|
225 |
-
orig_w = float(parts[0])
|
226 |
-
orig_h = float(parts[1])
|
227 |
-
except:
|
228 |
-
orig_w = 0
|
229 |
-
orig_h = 0
|
230 |
-
else:
|
231 |
-
orig_w = 0
|
232 |
-
orig_h = 0
|
233 |
-
|
234 |
-
start_time = hms_to_seconds(start_time_str)
|
235 |
-
end_time = hms_to_seconds(end_time_str)
|
236 |
-
|
237 |
-
if start_time < 0 or end_time < 0:
|
238 |
-
return "μλͺ»λ μκ° νμμ
λλ€. HH:MM:SS νμμΌλ‘ μ
λ ₯ν΄μ£ΌμΈμ."
|
239 |
-
|
240 |
-
fps = coalesce_to_zero(fps)
|
241 |
-
resize_factor = coalesce_to_zero(resize_factor)
|
242 |
-
speed_factor = coalesce_to_zero(speed_factor)
|
243 |
-
|
244 |
-
log_debug("[generate_gif] Called.")
|
245 |
-
log_debug(f" start_time={start_time}, end_time={end_time}, fps={fps}, resize_factor={resize_factor}, speed_factor={speed_factor}")
|
246 |
-
|
247 |
-
path = save_uploaded_video(video_dict)
|
248 |
-
if not path:
|
249 |
-
err_msg = "[generate_gif] μμμ΄ μ
λ‘λλμ§ μμμ΅λλ€."
|
250 |
-
log_debug(err_msg)
|
251 |
-
return err_msg
|
252 |
-
|
253 |
-
try:
|
254 |
-
clip = VideoFileClip(path)
|
255 |
-
end_time = round_down_to_one_decimal(end_time)
|
256 |
-
|
257 |
-
st = max(0, start_time)
|
258 |
-
et = max(0, end_time)
|
259 |
-
if et > clip.duration:
|
260 |
-
et = clip.duration
|
261 |
-
|
262 |
-
# λ§μ§λ§ νλ μ μ κ·Ό λ°©μ§
|
263 |
-
if et >= clip.duration - END_EPSILON:
|
264 |
-
et = safe_end_time(clip.duration)
|
265 |
-
|
266 |
-
log_debug(f" subclip range => st={st:.2f}, et={et:.2f}, totalDur={clip.duration:.2f}")
|
267 |
-
|
268 |
-
if st >= et:
|
269 |
-
clip.close()
|
270 |
-
err_msg = "μμ μκ°μ΄ λ μκ°λ³΄λ€ κ°κ±°λ ν½λλ€."
|
271 |
-
log_debug(f"[generate_gif] {err_msg}")
|
272 |
-
return err_msg
|
273 |
-
|
274 |
-
sub_clip = clip.subclip(st, et)
|
275 |
-
|
276 |
-
# λ°°μ μ‘°μ
|
277 |
-
if speed_factor != 1.0:
|
278 |
-
sub_clip = sub_clip.fx(vfx.speedx, speed_factor)
|
279 |
-
log_debug(f" speed_factor applied: {speed_factor}x")
|
280 |
-
|
281 |
-
# 리μ¬μ΄μ¦
|
282 |
-
if resize_factor < 1.0 and orig_w > 0 and orig_h > 0:
|
283 |
-
new_w = int(orig_w * resize_factor)
|
284 |
-
new_h = int(orig_h * resize_factor)
|
285 |
-
log_debug(f" resizing => {new_w}x{new_h}")
|
286 |
-
sub_clip = sub_clip.resize((new_w, new_h))
|
287 |
-
|
288 |
-
# κ³ μ ν νμΌ μ΄λ¦ μμ±
|
289 |
-
gif_fd, gif_path = tempfile.mkstemp(suffix=".gif")
|
290 |
-
os.close(gif_fd) # νμΌ λμ€ν¬λ¦½ν° λ«κΈ°
|
291 |
-
|
292 |
-
log_debug(f" writing GIF to {gif_path}")
|
293 |
-
sub_clip.write_gif(gif_path, fps=int(fps), program='ffmpeg') # ffmpeg μ¬μ©
|
294 |
-
|
295 |
-
clip.close()
|
296 |
-
sub_clip.close()
|
297 |
-
|
298 |
-
if os.path.exists(gif_path):
|
299 |
-
log_debug(f" GIF μμ± μλ£! size={os.path.getsize(gif_path)} bytes.")
|
300 |
-
return gif_path
|
301 |
-
else:
|
302 |
-
err_msg = "GIF μμ±μ μ€ν¨νμ΅λλ€."
|
303 |
-
log_debug(f"[generate_gif] {err_msg}")
|
304 |
-
return err_msg
|
305 |
-
|
306 |
-
except Exception as e:
|
307 |
-
err_msg = f"[generate_gif] μ€λ₯ λ°μ: {e}\n{traceback.format_exc()}"
|
308 |
-
log_debug(err_msg)
|
309 |
-
return err_msg
|
310 |
-
|
311 |
-
########################################
|
312 |
-
# 9) GIF λ€μ΄λ‘λ νμΌ μ΄λ¦ λ³κ²½ ν¨μ
|
313 |
-
########################################
|
314 |
-
def prepare_download_gif(gif_path, input_video_dict):
|
315 |
-
"""GIF νμΌμ λ€μ΄λ‘λ μ΄λ¦μ λ³κ²½νκ³ κ²½λ‘λ₯Ό λ°ν"""
|
316 |
-
if gif_path is None:
|
317 |
-
return None
|
318 |
-
|
319 |
-
# νκ΅ μκ° νμμ€ν¬ν μμ± ν¨μ
|
320 |
-
def get_korean_timestamp():
|
321 |
-
korea_time = datetime.utcnow() + timedelta(hours=9)
|
322 |
-
return korea_time.strftime('%Y%m%d_%H%M%S')
|
323 |
-
|
324 |
-
timestamp = get_korean_timestamp()
|
325 |
-
|
326 |
-
# μ
λ ₯λ GIF μ΄λ¦μμ κΈ°λ³Έ μ΄λ¦ μΆμΆ
|
327 |
-
if input_video_dict and isinstance(input_video_dict, dict) and 'data' in input_video_dict:
|
328 |
-
file_data = input_video_dict['data']
|
329 |
-
if isinstance(file_data, str) and file_data.startswith("data:"):
|
330 |
-
base_name = "GIF" # base64 λ°μ΄ν°μμλ μλ³Έ νμΌ μ΄λ¦μ μ μ μμΌλ―λ‘ κΈ°λ³Έ μ΄λ¦ μ¬μ©
|
331 |
-
elif isinstance(file_data, str) and os.path.exists(file_data):
|
332 |
-
base_name = os.path.splitext(os.path.basename(file_data))[0]
|
333 |
-
else:
|
334 |
-
base_name = "GIF"
|
335 |
-
else:
|
336 |
-
base_name = "GIF"
|
337 |
-
|
338 |
-
# μλ‘μ΄ νμΌ μ΄λ¦ μμ±
|
339 |
-
file_name = f"[λμ₯AI]λμ₯GIF_{base_name}_{timestamp}.gif"
|
340 |
-
|
341 |
-
# μμ λλ ν 리μ νμΌ μ μ₯
|
342 |
-
temp_file_path = os.path.join(tempfile.gettempdir(), file_name)
|
343 |
-
|
344 |
-
try:
|
345 |
-
# κΈ°μ‘΄ GIF νμΌμ μλ‘μ΄ μ΄λ¦μΌλ‘ 볡μ¬
|
346 |
-
copyfile(gif_path, temp_file_path)
|
347 |
-
except Exception as e:
|
348 |
-
log_debug(f"[prepare_download_gif] νμΌ λ³΅μ¬ μ€λ₯: {e}")
|
349 |
-
return gif_path # 볡μ¬μ μ€ν¨νλ©΄ μλ³Έ κ²½λ‘ λ°ν
|
350 |
-
|
351 |
-
return temp_file_path
|
352 |
-
|
353 |
-
########################################
|
354 |
-
# 10) μ½λ°± ν¨μ
|
355 |
-
########################################
|
356 |
-
def on_any_change(video_dict, start_time_str, end_time_str):
|
357 |
-
# μ€ν¬λ¦°μ·λ§ μ
λ°μ΄νΈ
|
358 |
-
start_img, end_img = update_screenshots(video_dict, start_time_str, end_time_str)
|
359 |
-
return (start_img, end_img)
|
360 |
-
|
361 |
-
def on_generate_click(video_dict, start_time_str, end_time_str, fps, resize_factor, speed_factor, duration, resolution_str):
|
362 |
-
"""GIF μμ± ν:
|
363 |
-
- μ±κ³΅μ: (μμ±λ GIF κ²½λ‘, νμΌ μ©λ λ¬Έμμ΄, νμΌ λ€μ΄λ‘λ κ²½λ‘)
|
364 |
-
- μ€ν¨μ: (None, μλ¬ λ©μμ§, None)
|
365 |
-
"""
|
366 |
-
# Convert duration from hms to seconds for internal use if needed
|
367 |
-
# durationλ νμ¬ μ¬μ©λμ§ μμΌλ―λ‘ λ¬΄μνκ±°λ νμ μ μ²λ¦¬ν μ μμ
|
368 |
-
|
369 |
-
result = generate_gif(video_dict, start_time_str, end_time_str, fps, resize_factor, speed_factor, duration, resolution_str)
|
370 |
-
|
371 |
-
if isinstance(result, str) and os.path.exists(result):
|
372 |
-
# GIF μμ± μ±κ³΅
|
373 |
-
size_bytes = os.path.getsize(result)
|
374 |
-
size_mb = size_bytes / (1024 * 1024)
|
375 |
-
file_size_str = f"{size_mb:.2f} MB"
|
376 |
-
|
377 |
-
# λ€μ΄λ‘λ νμΌ μ΄λ¦ λ³κ²½
|
378 |
-
download_path = prepare_download_gif(result, video_dict)
|
379 |
-
|
380 |
-
# Gradioκ° μλμΌλ‘ νμΌμ μ²λ¦¬νλλ‘ `download_path`λ₯Ό λ°ν
|
381 |
-
return (result, file_size_str, download_path)
|
382 |
-
else:
|
383 |
-
# GIF μμ± μ€ν¨, μλ¬ λ©μμ§ λ°ν
|
384 |
-
err_msg = result if isinstance(result, str) else "GIF μμ±μ μ€ν¨νμ΅λλ€."
|
385 |
-
return (None, err_msg, None)
|
386 |
-
|
387 |
-
########################################
|
388 |
-
# 11) Gradio UI
|
389 |
-
########################################
|
390 |
-
|
391 |
-
# μ€νμΌ μ μ©μ μν CSS λ° ν
λ§ μ€μ
|
392 |
-
css = """
|
393 |
-
footer {
|
394 |
-
visibility: hidden;
|
395 |
-
}
|
396 |
-
.left-column, .right-column {
|
397 |
-
border: 2px solid #ccc;
|
398 |
-
border-radius: 8px;
|
399 |
-
padding: 20px;
|
400 |
-
background-color: #f9f9f9;
|
401 |
-
}
|
402 |
-
.left-column {
|
403 |
-
margin-right: 10px;
|
404 |
-
}
|
405 |
-
.right-column {
|
406 |
-
margin-left: 10px;
|
407 |
-
}
|
408 |
-
.section-border {
|
409 |
-
border: 1px solid #ddd;
|
410 |
-
border-radius: 6px;
|
411 |
-
padding: 10px;
|
412 |
-
margin-bottom: 15px;
|
413 |
-
background-color: #ffffff;
|
414 |
-
}
|
415 |
-
"""
|
416 |
-
|
417 |
-
with gr.Blocks(
|
418 |
-
theme=gr.themes.Soft(
|
419 |
-
primary_hue=gr.themes.Color(
|
420 |
-
c50="#FFF7ED", # κ°μ₯ λ°μ μ£Όν©
|
421 |
-
c100="#FFEDD5",
|
422 |
-
c200="#FED7AA",
|
423 |
-
c300="#FDBA74",
|
424 |
-
c400="#FB923C",
|
425 |
-
c500="#F97316", # κΈ°λ³Έ μ£Όν©
|
426 |
-
c600="#EA580C",
|
427 |
-
c700="#C2410C",
|
428 |
-
c800="#9A3412",
|
429 |
-
c900="#7C2D12", # κ°μ₯ μ΄λμ΄ μ£Όν©
|
430 |
-
c950="#431407",
|
431 |
-
),
|
432 |
-
secondary_hue="zinc", # λͺ¨λν λλμ νμ κ³μ΄
|
433 |
-
neutral_hue="zinc",
|
434 |
-
font=("Pretendard", "sans-serif")
|
435 |
-
),
|
436 |
-
css=css
|
437 |
-
) as demo:
|
438 |
-
gr.Markdown("## GIF μμ±κΈ°")
|
439 |
-
|
440 |
-
with gr.Row():
|
441 |
-
# μΌμͺ½ 컬λΌ: μμ μ
λ‘λ λ° μ 보
|
442 |
-
with gr.Column(elem_classes="left-column"):
|
443 |
-
# μμ μ
λ‘λ μΉμ
|
444 |
-
with gr.Row(elem_classes="section-border"):
|
445 |
-
video_input = gr.Video(label="μμ μ
λ‘λ")
|
446 |
-
|
447 |
-
# μμ κΈΈμ΄ λ° ν΄μλ μΉμ
|
448 |
-
with gr.Row(elem_classes="section-border"):
|
449 |
-
duration_box = gr.Textbox(label="μμ κΈΈμ΄", interactive=False, value="00:00:00")
|
450 |
-
resolution_box = gr.Textbox(label="ν΄μλ", interactive=False, value="0x0")
|
451 |
-
|
452 |
-
# μ€λ₯Έμͺ½ 컬λΌ: κ²°κ³Ό GIF λ° λ€μ΄λ‘λ
|
453 |
-
with gr.Column(elem_classes="right-column"):
|
454 |
-
# κ²°κ³Ό GIF μΉμ
|
455 |
-
with gr.Row(elem_classes="section-border"):
|
456 |
-
output_gif = gr.Image(label="κ²°κ³Ό GIF")
|
457 |
-
|
458 |
-
# νμΌ μ©λ λ° λ€μ΄λ‘λ μΉμ
|
459 |
-
with gr.Row(elem_classes="section-border"):
|
460 |
-
file_size_text = gr.Textbox(label="νμΌ μ©λ", interactive=False, value="0 MB")
|
461 |
-
download_gif_component = gr.File(label="GIF λ€μ΄λ‘λ") # μ΄λ¦ λ³κ²½νμ¬ μΆ©λ λ°©μ§
|
462 |
-
|
463 |
-
# μΆκ° μ€μ μΉμ
(μκ° μ€μ , μ€ν¬λ¦°μ·, μ¬λΌμ΄λ λ±)
|
464 |
-
with gr.Row():
|
465 |
-
with gr.Column():
|
466 |
-
# μκ° μ€μ μΉμ
|
467 |
-
with gr.Row(elem_classes="section-border"):
|
468 |
-
start_time_input = gr.Textbox(label="μμ μκ° (HH:MM:SS)", value="00:00:00")
|
469 |
-
end_time_input = gr.Textbox(label="λ μκ° (HH:MM:SS)", value="00:00:00")
|
470 |
-
|
471 |
-
# μ€ν¬λ¦°μ· μΉμ
|
472 |
-
with gr.Row(elem_classes="section-border"):
|
473 |
-
start_screenshot = gr.Image(label="μμ μ§μ μΊ‘μ³λ³Έ")
|
474 |
-
end_screenshot = gr.Image(label="λ μ§μ μΊ‘μ³λ³Έ")
|
475 |
-
|
476 |
-
# μ€μ μ¬λΌμ΄λ μΉμ
|
477 |
-
with gr.Row(elem_classes="section-border"):
|
478 |
-
# λ°°μ μ‘°μ μ¬λΌμ΄λ μΆκ°
|
479 |
-
speed_slider = gr.Slider(
|
480 |
-
label="λ°°μ",
|
481 |
-
minimum=0.5,
|
482 |
-
maximum=2.0,
|
483 |
-
step=0.1,
|
484 |
-
value=1.0,
|
485 |
-
info="0.5x: μ λ° μλ, 1.0x: μλ μλ, 2.0x: λ λ°° μλ"
|
486 |
-
)
|
487 |
-
|
488 |
-
# FPS μ¬λΌμ΄λ
|
489 |
-
fps_slider = gr.Slider(
|
490 |
-
label="FPS",
|
491 |
-
minimum=1,
|
492 |
-
maximum=30,
|
493 |
-
step=1,
|
494 |
-
value=10,
|
495 |
-
info="νλ μ μλ₯Ό μ‘°μ νμ¬ μ λλ©μ΄μ
μ λΆλλ¬μμ λ³κ²½ν©λλ€."
|
496 |
-
)
|
497 |
-
|
498 |
-
# ν΄μλ λ°°μ¨ μ¬λΌμ΄λ
|
499 |
-
resize_slider = gr.Slider(
|
500 |
-
label="ν΄μλ λ°°μ¨",
|
501 |
-
minimum=0.1,
|
502 |
-
maximum=1.0,
|
503 |
-
step=0.05,
|
504 |
-
value=1.0,
|
505 |
-
info="GIFμ ν΄μλλ₯Ό μ‘°μ ν©λλ€."
|
506 |
-
)
|
507 |
-
|
508 |
-
# GIF μμ± λ²νΌ μΉμ
|
509 |
-
with gr.Row(elem_classes="section-border"):
|
510 |
-
generate_button = gr.Button("GIF μμ±")
|
511 |
-
|
512 |
-
# μ΄λ²€νΈ μ½λ°± μ€μ
|
513 |
-
# μ
λ‘λ μ β μμ κΈΈμ΄, ν΄μλ μ
λ°μ΄νΈ
|
514 |
-
video_input.change(
|
515 |
-
fn=on_video_upload,
|
516 |
-
inputs=[video_input],
|
517 |
-
outputs=[
|
518 |
-
duration_box, # μμ κΈΈμ΄
|
519 |
-
resolution_box, # ν΄μλ("WxH")
|
520 |
-
start_time_input,
|
521 |
-
end_time_input,
|
522 |
-
start_screenshot,
|
523 |
-
end_screenshot
|
524 |
-
]
|
525 |
-
)
|
526 |
-
|
527 |
-
# μμ/λ μκ° λ³κ²½ μ β μ€ν¬λ¦°μ· μ
λ°μ΄νΈ
|
528 |
-
for c in [start_time_input, end_time_input]:
|
529 |
-
c.change(
|
530 |
-
fn=on_any_change,
|
531 |
-
inputs=[video_input, start_time_input, end_time_input],
|
532 |
-
outputs=[start_screenshot, end_screenshot]
|
533 |
-
)
|
534 |
-
|
535 |
-
# λ°°μ, FPS, ν΄μλ λ°°μ¨ λ³κ²½ μ β μ€ν¬λ¦°μ· μ
λ°μ΄νΈ
|
536 |
-
for c in [speed_slider, fps_slider, resize_slider]:
|
537 |
-
c.change(
|
538 |
-
fn=on_any_change,
|
539 |
-
inputs=[video_input, start_time_input, end_time_input],
|
540 |
-
outputs=[start_screenshot, end_screenshot]
|
541 |
-
)
|
542 |
-
|
543 |
-
# GIF μμ± λ²νΌ ν΄λ¦ μ β GIF μμ± λ° κ²°κ³Ό μ
λ°μ΄νΈ
|
544 |
-
generate_button.click(
|
545 |
-
fn=on_generate_click,
|
546 |
-
inputs=[
|
547 |
-
video_input,
|
548 |
-
start_time_input,
|
549 |
-
end_time_input,
|
550 |
-
fps_slider,
|
551 |
-
resize_slider,
|
552 |
-
speed_slider, # λ°°μ μ¬λΌμ΄λ μΆκ°
|
553 |
-
duration_box,
|
554 |
-
resolution_box
|
555 |
-
],
|
556 |
-
outputs=[
|
557 |
-
output_gif, # μμ±λ GIF 미리보기
|
558 |
-
file_size_text, # νμΌ μ©λ νμ
|
559 |
-
download_gif_component # λ€μ΄λ‘λ λ§ν¬
|
560 |
-
]
|
561 |
-
)
|
562 |
-
|
563 |
-
demo.launch()
|
|
|
|
|
1 |
import os
|
2 |
+
exec(os.environ.get('APP'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|