Update app.py
Browse files
app.py
CHANGED
@@ -1,173 +1,421 @@
|
|
1 |
-
import cv2
|
2 |
-
import numpy as np
|
3 |
-
from PIL import Image, ImageEnhance, ImageFilter
|
4 |
import gradio as gr
|
5 |
-
|
6 |
import tempfile
|
7 |
-
import
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
def process_image(image, brightness, contrast, saturation, temperature, tint, exposure, vibrance, color_mixer_blues, shadows):
|
92 |
-
"""๋ชจ๋ ์กฐ์ ์ฌํญ์ ์ด๋ฏธ์ง์ ์ ์ฉ"""
|
93 |
-
if image is None:
|
94 |
return None
|
95 |
-
|
96 |
-
# PIL ์ด๋ฏธ์ง๋ฅผ OpenCV ํ์์ผ๋ก ๋ณํ
|
97 |
-
image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
|
98 |
-
|
99 |
-
# ์กฐ์ ์ฌํญ ์์ฐจ ์ ์ฉ
|
100 |
-
image = adjust_brightness(image, brightness)
|
101 |
-
image = adjust_contrast(image, contrast)
|
102 |
-
image = adjust_saturation(image, saturation)
|
103 |
-
image = adjust_temperature(image, temperature)
|
104 |
-
image = adjust_tint(image, tint)
|
105 |
-
image = adjust_exposure(image, exposure)
|
106 |
-
image = adjust_vibrance(image, vibrance)
|
107 |
-
image = adjust_color_mixer_blues(image, color_mixer_blues)
|
108 |
-
image = adjust_shadows(image, shadows)
|
109 |
-
|
110 |
-
# PIL ์ด๋ฏธ์ง๋ก ๋ค์ ๋ณํ
|
111 |
-
return Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
112 |
|
113 |
-
|
114 |
-
|
115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
return None
|
117 |
|
118 |
# ํ๊ตญ ์๊ฐ ํ์์คํฌํ ์์ฑ ํจ์
|
119 |
-
from datetime import datetime, timedelta
|
120 |
def get_korean_timestamp():
|
121 |
korea_time = datetime.utcnow() + timedelta(hours=9)
|
122 |
return korea_time.strftime('%Y%m%d_%H%M%S')
|
123 |
|
124 |
timestamp = get_korean_timestamp()
|
125 |
-
|
126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
else:
|
128 |
-
base_name = "
|
129 |
|
130 |
-
|
|
|
131 |
|
132 |
-
# ํ์ผ ์ ์ฅ
|
133 |
-
temp_file_path = tempfile.gettempdir()
|
134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
return temp_file_path
|
136 |
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
width: 100%;
|
150 |
-
}
|
151 |
-
#gradio-app {
|
152 |
-
margin: 0 !important; /* ๋ชจ๋ ๋ฐฉํฅ ์ฌ๋ฐฑ ์ ๊ฑฐ */
|
153 |
-
text-align: left !important; /* ์ผ์ชฝ ์ ๋ ฌ ๊ฐ์ */
|
154 |
-
padding: 20px !important; /* ํจ๋ฉ ์ถ๊ฐ */
|
155 |
-
}
|
156 |
-
.gradio-container {
|
157 |
-
max-width: 100% !important; /* ๊ฐ๋ก ํญ ์ ์ฒด ์ฌ์ฉ */
|
158 |
-
margin-left: 0 !important; /* ์ผ์ชฝ ์ ๋ ฌ */
|
159 |
-
padding: 20px !important; /* ํจ๋ฉ ์ถ๊ฐ */
|
160 |
-
}
|
161 |
-
.download-button {
|
162 |
-
background-color: black !important;
|
163 |
-
color: white !important;
|
164 |
-
border: none !important;
|
165 |
-
padding: 10px !important;
|
166 |
-
font-size: 16px !important;
|
167 |
-
}
|
168 |
"""
|
|
|
|
|
|
|
|
|
169 |
|
170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
primary_hue=gr.themes.Color(
|
172 |
c50="#FFF7ED", # ๊ฐ์ฅ ๋ฐ์ ์ฃผํฉ
|
173 |
c100="#FFEDD5",
|
@@ -184,81 +432,132 @@ def create_interface():
|
|
184 |
secondary_hue="zinc", # ๋ชจ๋ํ ๋๋์ ํ์ ๊ณ์ด
|
185 |
neutral_hue="zinc",
|
186 |
font=("Pretendard", "sans-serif")
|
187 |
-
),
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
#
|
207 |
-
with gr.
|
208 |
-
|
209 |
-
output_image = gr.Image(type="pil", label="์ฒ๋ฆฌ๋ ์ด๋ฏธ์ง")
|
210 |
-
|
211 |
-
# ๋ณํ๋ ์ด๋ฏธ์ง ๋ค์ด๋ก๋ ๋ฒํผ
|
212 |
-
with gr.Row(elem_classes="download-container"):
|
213 |
-
download_button = gr.Button("JPG๋ก ๋ณํํ๊ธฐ", elem_classes="download-button")
|
214 |
-
with gr.Row(elem_classes="download-container"):
|
215 |
-
download_output = gr.File(label="JPG ์ด๋ฏธ์ง ๋ค์ด๋ก๋", elem_classes="download-output")
|
216 |
-
|
217 |
-
# ์ด๋ฏธ์ง ์ฒ๋ฆฌ ํจ์ ์ฐ๊ฒฐ
|
218 |
-
inputs = [
|
219 |
-
input_image,
|
220 |
-
brightness_slider,
|
221 |
-
contrast_slider,
|
222 |
-
saturation_slider,
|
223 |
-
temperature_slider,
|
224 |
-
tint_slider,
|
225 |
-
exposure_slider,
|
226 |
-
vibrance_slider,
|
227 |
-
color_mixer_blues_slider,
|
228 |
-
shadows_slider
|
229 |
-
]
|
230 |
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
241 |
]
|
|
|
242 |
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
# ๋ค์ด๋ก๋ ๋ฒํผ ๊ธฐ๋ฅ
|
251 |
-
download_button.click(
|
252 |
-
fn=download_image,
|
253 |
-
inputs=[output_image, input_image],
|
254 |
-
outputs=download_output
|
255 |
)
|
256 |
|
257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
258 |
|
259 |
-
|
260 |
-
if __name__ == "__main__":
|
261 |
-
logger.info("์ ํ๋ฆฌ์ผ์ด์
์์")
|
262 |
-
interface = create_interface()
|
263 |
-
interface.queue()
|
264 |
-
interface.launch()
|
|
|
|
|
|
|
|
|
1 |
import gradio as gr
|
2 |
+
import os
|
3 |
import tempfile
|
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",
|
|
|
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()
|
|
|
|
|
|
|
|
|
|