ssboost commited on
Commit
bbf28e1
ยท
verified ยท
1 Parent(s): 1ed87be

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +522 -223
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
- from io import BytesIO
6
  import tempfile
7
- import logging
8
-
9
- # ๋กœ๊น… ์„ค์ • - INFO ๋ ˆ๋ฒจ๋กœ ๋ณ€๊ฒฝ
10
- logging.basicConfig(level=logging.INFO)
11
- logger = logging.getLogger(__name__)
12
-
13
- def adjust_brightness(image, value):
14
- """์ด๋ฏธ์ง€ ๋ฐ๊ธฐ ์กฐ์ ˆ"""
15
- value = float(value - 1) * 100 # 0-2 ๋ฒ”์œ„๋ฅผ -100์—์„œ +100์œผ๋กœ ๋ณ€ํ™˜
16
- hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
17
- h, s, v = cv2.split(hsv)
18
- v = cv2.add(v, value)
19
- v = np.clip(v, 0, 255)
20
- final_hsv = cv2.merge((h, s, v))
21
- return cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)
22
-
23
- def adjust_contrast(image, value):
24
- """์ด๋ฏธ์ง€ ๋Œ€๋น„ ์กฐ์ ˆ"""
25
- value = float(value)
26
- return np.clip(image * value, 0, 255).astype(np.uint8)
27
-
28
- def adjust_saturation(image, value):
29
- """์ด๋ฏธ์ง€ ์ฑ„๋„ ์กฐ์ ˆ"""
30
- value = float(value - 1) * 100 # 0-2 ๋ฒ”์œ„๋ฅผ -100์—์„œ +100์œผ๋กœ ๋ณ€ํ™˜
31
- hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
32
- h, s, v = cv2.split(hsv)
33
- s = cv2.add(s, value)
34
- s = np.clip(s, 0, 255)
35
- final_hsv = cv2.merge((h, s, v))
36
- return cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)
37
-
38
- def adjust_temperature(image, value):
39
- """์ด๋ฏธ์ง€ ์ƒ‰์˜จ๋„ ์กฐ์ ˆ (์ƒ‰์ƒ ๋ฐธ๋Ÿฐ์Šค)"""
40
- value = float(value) * 30 # ํšจ๊ณผ ์Šค์ผ€์ผ ์กฐ์ ˆ
41
- b, g, r = cv2.split(image)
42
- if value > 0: # ๋”ฐ๋œปํ•˜๊ฒŒ
43
- r = cv2.add(r, value)
44
- b = cv2.subtract(b, value)
45
- else: # ์ฐจ๊ฐ‘๊ฒŒ
46
- r = cv2.add(r, value)
47
- b = cv2.subtract(b, value)
48
-
49
- r = np.clip(r, 0, 255)
50
- b = np.clip(b, 0, 255)
51
- return cv2.merge([b, g, r])
52
-
53
- def adjust_tint(image, value):
54
- """์ด๋ฏธ์ง€ ์ƒ‰์กฐ ์กฐ์ ˆ"""
55
- hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
56
- h, s, v = cv2.split(hsv_image)
57
- h = cv2.add(h, int(value))
58
- h = np.clip(h, 0, 179) # Hue ๊ฐ’์€ 0-179 ๋ฒ”์œ„
59
- final_hsv = cv2.merge((h, s, v))
60
- return cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)
61
-
62
- def adjust_exposure(image, value):
63
- """์ด๋ฏธ์ง€ ๋…ธ์ถœ ์กฐ์ ˆ"""
64
- enhancer = ImageEnhance.Brightness(Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)))
65
- img_enhanced = enhancer.enhance(1 + float(value) / 5.0)
66
- return cv2.cvtColor(np.array(img_enhanced), cv2.COLOR_RGB2BGR)
67
-
68
- def adjust_vibrance(image, value):
69
- """์ด๋ฏธ์ง€ ํ™œ๊ธฐ ์กฐ์ ˆ"""
70
- img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
71
- converter = ImageEnhance.Color(img)
72
- factor = 1 + (float(value) / 100.0)
73
- img = converter.enhance(factor)
74
- return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
75
-
76
- def adjust_color_mixer_blues(image, value):
77
- """์ด๋ฏธ์ง€ ์ปฌ๋Ÿฌ ๋ฏน์„œ (๋ธ”๋ฃจ) ์กฐ์ ˆ"""
78
- b, g, r = cv2.split(image)
79
- b = cv2.add(b, float(value))
80
- b = np.clip(b, 0, 255)
81
- return cv2.merge([b, g, r])
82
-
83
- def adjust_shadows(image, value):
84
- """์ด๋ฏธ์ง€ ๊ทธ๋ฆผ์ž ์กฐ์ ˆ"""
85
- pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
86
- enhancer = ImageEnhance.Brightness(pil_image)
87
- factor = 1 + (float(value) / 100.0)
88
- pil_image = enhancer.enhance(factor)
89
- return cv2.cvtColor(np.array(pil_image), cv2.COLOR_BGR2RGB)
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
- def download_image(image, input_image_name):
114
- """์ด๋ฏธ์ง€๋ฅผ JPG ํ˜•์‹์œผ๋กœ ์ €์žฅํ•˜๊ณ  ๊ฒฝ๋กœ ๋ฐ˜ํ™˜"""
115
- if image is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- if input_image_name and hasattr(input_image_name, 'name'):
126
- base_name = input_image_name.name.split('.')[0] # ํŒŒ์ผ ๊ฐ์ฒด์—์„œ ์ด๋ฆ„ ์ถ”์ถœ
 
 
 
 
 
 
 
 
127
  else:
128
- base_name = "์ด๋ฏธ์ง€"
129
 
130
- file_name = f"[๋์žฅAI]๋์žฅํ•„ํ„ฐ_{base_name}_{timestamp}.jpg"
 
131
 
132
- # ํŒŒ์ผ ์ €์žฅ
133
- temp_file_path = tempfile.gettempdir() + "/" + file_name
134
- image.save(temp_file_path, format="JPEG")
 
 
 
 
 
 
 
135
  return temp_file_path
136
 
137
- def create_interface():
138
- css = """
139
- footer {
140
- visibility: hidden;
141
- }
142
- .download-button, .download-output {
143
- width: 100%;
144
- }
145
- .download-container {
146
- display: flex;
147
- flex-direction: column;
148
- align-items: center;
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
- with gr.Blocks(theme=gr.themes.Soft(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- ), css=css) as interface:
188
-
189
- with gr.Row():
190
- # ์™ผ์ชฝ ์—ด: ๋น„์œจ 3
191
- with gr.Column(scale=3):
192
- # ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
193
- input_image = gr.Image(type="pil", label="์ด๋ฏธ์ง€ ์—…๋กœ๋“œ")
194
-
195
- # ์กฐ์ • ์Šฌ๋ผ์ด๋”
196
- brightness_slider = gr.Slider(0.0, 2.0, value=1.0, step=0.1, label="๋ฐ๊ธฐ ์กฐ์ ˆ")
197
- contrast_slider = gr.Slider(0.5, 1.5, value=1.0, step=0.1, label="๋Œ€๋น„ ์กฐ์ ˆ")
198
- saturation_slider = gr.Slider(0.0, 2.0, value=1.0, step=0.1, label="์ฑ„๋„ ์กฐ์ ˆ")
199
- temperature_slider = gr.Slider(-1.0, 1.0, value=0.0, step=0.1, label="์ƒ‰์˜จ๋„ ์กฐ์ ˆ")
200
- tint_slider = gr.Slider(-100, 100, value=0, step=1, label="์ƒ‰์กฐ ์กฐ์ ˆ")
201
- exposure_slider = gr.Slider(-5.0, 5.0, value=0.0, step=0.1, label="๋…ธ์ถœ ์กฐ์ ˆ")
202
- vibrance_slider = gr.Slider(-100.0, 100.0, value=0.0, step=1.0, label="ํ™œ๊ธฐ ์กฐ์ ˆ")
203
- color_mixer_blues_slider = gr.Slider(-100.0, 100.0, value=0.0, step=1.0, label="์ปฌ๋Ÿฌ ๋ฏน์„œ (๋ธ”๋ฃจ)")
204
- shadows_slider = gr.Slider(-100.0, 100.0, value=0.0, step=1.0, label="๊ทธ๋ฆผ์ž ์กฐ์ ˆ")
205
-
206
- # ์˜ค๋ฅธ์ชฝ ์—ด: ๋น„์œจ 7
207
- with gr.Column(scale=7):
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
- input_components = [
232
- brightness_slider,
233
- contrast_slider,
234
- saturation_slider,
235
- temperature_slider,
236
- tint_slider,
237
- exposure_slider,
238
- vibrance_slider,
239
- color_mixer_blues_slider,
240
- shadows_slider
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  ]
 
242
 
243
- for input_component in input_components:
244
- input_component.change(
245
- fn=process_image,
246
- inputs=inputs,
247
- outputs=output_image
248
- )
249
-
250
- # ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ ๊ธฐ๋Šฅ
251
- download_button.click(
252
- fn=download_image,
253
- inputs=[output_image, input_image],
254
- outputs=download_output
255
  )
256
 
257
- return interface
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()