Kims12 commited on
Commit
47d0ad6
ยท
verified ยท
1 Parent(s): aa66a00

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +146 -173
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import os
2
  import uuid
3
  import moviepy.editor as mp
 
4
  from PIL import Image
5
  import gradio as gr
6
 
@@ -10,16 +11,11 @@ import gradio as gr
10
  global_logs = []
11
 
12
  def add_log(message: str):
13
- """
14
- ๋กœ๊ทธ ๋ฉ”์‹œ์ง€๋ฅผ ์ €์žฅํ•˜๋Š” ํ•จ์ˆ˜
15
- """
16
  global_logs.append(message)
17
  print(message)
18
 
19
  def parse_time_to_seconds(time_str: str):
20
- """
21
- ์‹œ:๋ถ„:์ดˆ ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด(์˜ˆ: '00:00:10' ๋˜๋Š” '00:01:02')์„ ์ดˆ ๋‹จ์œ„(float)๋กœ ๋ณ€ํ™˜
22
- """
23
  try:
24
  h, m, s = time_str.split(":")
25
  total_seconds = int(h) * 3600 + int(m) * 60 + float(s)
@@ -28,9 +24,6 @@ def parse_time_to_seconds(time_str: str):
28
  return 0.0
29
 
30
  def generate_thumbnail(video_clip, time_point):
31
- """
32
- ํŠน์ • ์‹œ์ ์˜ ํ”„๋ ˆ์ž„์„ ์ธ๋„ค์ผ๋กœ ์ƒ์„ฑํ•˜์—ฌ PIL.Image๋กœ ๋ฐ˜ํ™˜
33
- """
34
  try:
35
  frame = video_clip.get_frame(time_point)
36
  thumbnail_img = Image.fromarray(frame)
@@ -42,12 +35,8 @@ def generate_thumbnail(video_clip, time_point):
42
  return thumbnail_img
43
 
44
  def adjust_aspect_ratio(clip, option):
45
- """
46
- ์˜์ƒ ํด๋ฆฝ์„ ์„ ํƒํ•œ ํ”Œ๋žซํผ์˜ ๊ถŒ์žฅ ๋น„์œจ๋กœ ์ค‘์•™ ํฌ๋กญํ•˜์—ฌ ์กฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
47
- """
48
  if option == "์›๋ณธ ์œ ์ง€":
49
  return clip
50
-
51
  if option == "์œ ํŠœ๋ธŒ (16:9)":
52
  target_ratio = 16 / 9
53
  elif option == "์‡ผ์ธ /๋ฆด์Šค (9:16)":
@@ -71,13 +60,18 @@ def adjust_aspect_ratio(clip, option):
71
  new_width = width
72
  new_height = int(width / target_ratio)
73
 
74
- return clip.crop(x_center=width / 2, y_center=height / 2, width=new_width, height=new_height)
 
 
 
 
 
75
 
76
  def process_video(video,
77
  start_time_str,
78
  end_time_str,
79
- platform_option,
80
- frame_rate_factor,
81
  speed_factor,
82
  repeat_count,
83
  resolution_scale):
@@ -85,11 +79,9 @@ def process_video(video,
85
  ๋™์˜์ƒ์„ GIF๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜
86
  """
87
  global global_logs
88
- global_logs = [] # ํ˜ธ์ถœ ์‹œ๋งˆ๋‹ค ๋กœ๊ทธ ์ดˆ๊ธฐํ™”
89
-
90
  add_log("[LOG 1] ๋น„๋””์˜ค ์—…๋กœ๋“œ ๋ฐ ์ฒ˜๋ฆฌ ์‹œ์ž‘")
91
 
92
- # video ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ํŒŒ์ผ ๊ฒฝ๋กœ ์ถ”์ถœ
93
  video_path = video if isinstance(video, str) else video.name
94
 
95
  add_log("[LOG 2] ๋น„๋””์˜ค ๋กœ๋“œ ์ค‘...")
@@ -106,7 +98,6 @@ def process_video(video,
106
  start_sec = parse_time_to_seconds(start_time_str)
107
  end_sec = parse_time_to_seconds(end_time_str)
108
 
109
- # ์‹œ๊ฐ„ ๋ฒ”์œ„ ๋ณด์ •
110
  if start_sec < 0:
111
  start_sec = 0
112
  if end_sec <= 0 or end_sec > duration:
@@ -123,12 +114,10 @@ def process_video(video,
123
  add_log(f"[LOG 7] ํ”Œ๋žซํผ ๋น„์œจ ์กฐ์ •: {platform_option}")
124
  clip = adjust_aspect_ratio(clip, platform_option)
125
 
126
- # ์ถœ๋ ฅ ํ•ด์ƒ๋„ ์ถ•์†Œ
127
  if abs(resolution_scale - 1.0) > 1e-3:
128
  add_log(f"[LOG 7-1] ์ถœ๋ ฅ ํ•ด์ƒ๋„ ์ถ•์†Œ: {resolution_scale*100:.0f}%")
129
  clip = clip.resize(resolution_scale)
130
 
131
- # ์žฌ์ƒ์†๋„ ์กฐ์ ˆ
132
  if abs(speed_factor - 1.0) > 1e-3:
133
  add_log(f"[LOG 8] ์žฌ์ƒ์†๋„ {speed_factor}๋ฐฐ๋กœ ์กฐ์ ˆ ์ค‘...")
134
  clip = clip.fx(mp.vfx.speedx, speed_factor)
@@ -138,7 +127,6 @@ def process_video(video,
138
  add_log(f"[LOG 9] ์ตœ์ข… ์ถœ๋ ฅ FPS: {target_fps:.2f} (์›๋ณธ FPS: {original_fps})")
139
  clip = clip.set_fps(target_fps)
140
 
141
- # ๋ฐ˜๋ณต ํšŸ์ˆ˜ ์„ค์ •
142
  add_log(f"[LOG 10] GIF ๋ฐ˜๋ณต ํšŸ์ˆ˜ ์„ค์ •: {repeat_count} (0์ด๋ฉด ๋ฌดํ•œ๋ฐ˜๋ณต)")
143
  final_clip = clip
144
 
@@ -146,6 +134,7 @@ def process_video(video,
146
  output_filename = f"temp_{uuid.uuid4().hex}.gif"
147
  try:
148
  loop_param = 0 if int(repeat_count) == 0 else int(repeat_count)
 
149
  final_clip.write_gif(output_filename, fps=target_fps, loop=loop_param)
150
  add_log(f"[LOG 12] GIF ์ƒ์„ฑ ์™„๋ฃŒ! ํŒŒ์ผ๋ช…: {output_filename}, loop={loop_param}")
151
  except Exception as e:
@@ -155,11 +144,7 @@ def process_video(video,
155
  return output_filename, output_filename, "\n".join(global_logs)
156
 
157
  def update_thumbnails(video, start_time_str, end_time_str):
158
- """
159
- ์‹œ์ž‘/๋ ์ธ๋„ค์ผ์„ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•œ ํ•จ์ˆ˜
160
- """
161
  video_path = video if isinstance(video, str) else video.name
162
-
163
  try:
164
  input_video = mp.VideoFileClip(video_path)
165
  except Exception as e:
@@ -180,129 +165,141 @@ def update_thumbnails(video, start_time_str, end_time_str):
180
 
181
  start_thumb = generate_thumbnail(input_video, start_sec)
182
  end_thumb = generate_thumbnail(input_video, end_sec)
183
-
184
  return start_thumb, end_thumb
185
 
186
-
187
- # ---------- ์ปค์Šคํ…€ CSS ----------
 
188
  custom_css = """
189
- /* ์ „์ฒด ๋ฐฐ๊ฒฝ */
190
  body {
191
- background-color: #fdfdfd;
192
- margin: 0;
193
- padding: 0;
194
- font-family: 'Noto Sans KR', sans-serif;
195
  }
196
 
197
- /* ์ƒ๋‹จ ์ œ๋ชฉ ์˜์—ญ */
198
- .title-container {
199
- text-align: center;
200
- margin-top: 20px;
201
- margin-bottom: 15px;
 
 
 
202
  }
203
- .title-container h1 {
204
- font-size: 2.2rem;
205
- font-weight: bold;
206
- color: #333;
 
 
 
 
207
  }
208
- .title-container h1 .emoji {
209
- font-size: 1.8rem;
 
 
210
  }
211
 
212
- /* ์‚ฌ์šฉ ๊ฐ€์ด๋“œ ์˜์—ญ */
213
- .guide-container {
214
- text-align: center;
215
- padding: 0 20px;
216
- margin-bottom: 20px;
217
  }
218
- .guide-container p {
219
- font-size: 1rem;
220
- color: #444;
221
- line-height: 1.5;
 
 
 
222
  }
223
 
224
- /* ํ”„๋ ˆ์ž„(ํ…Œ๋‘๋ฆฌ) ๊ณตํ†ต ์Šคํƒ€์ผ */
225
- .frame {
226
- border: 2px solid #ccc;
227
- border-radius: 15px;
228
- padding: 15px;
229
- margin: 10px;
230
- background-color: #fff;
 
 
 
231
  }
232
- .frame h3 {
233
- margin-top: 0;
 
 
 
234
  }
235
 
236
- /* ๊ทธ๋ฆฌ๋””์˜ค ์ปดํฌ๋„ŒํŠธ์˜ ๊ธฐ๋ณธ ์—ฌ๋ฐฑ ๋“ฑ์„ ์ œ๊ฑฐ/์กฐ์ • */
237
- .gradio-container {
238
- padding: 0 !important;
 
 
 
 
 
 
 
 
 
 
239
  }
240
 
241
- /* ์ž…๋ ฅ๋ถ€์™€ ์ถœ๋ ฅ๋ถ€ ๊ฐ™์€ ๋ผ์ธ ๋ฐฐ์น˜ */
242
- .flex-row {
243
- display: flex;
244
- justify-content: space-around;
245
- gap: 10px;
246
- flex-wrap: wrap; /* ํ™”๋ฉด ์ข์„ ๋•Œ ์ž๋™ ์ค„๋ฐ”๊ฟˆ */
247
  }
248
- .flex-column {
249
- flex: 1;
250
- min-width: 300px;
 
 
 
 
251
  }
252
 
253
- /* ํƒ€์›ํ˜• ๋ฒ„ํŠผ */
254
- .custom-button {
255
- border-radius: 25px !important;
256
- background-color: #fa8072 !important;
257
- color: #fff !important;
258
- font-weight: bold !important;
259
  }
260
 
261
- /* ํ•˜๋‹จ ์ธ๋„ค์ผ ์˜์—ญ์€ ํ•œ ์ค„(์ „์ฒด ํญ)๋กœ ๋ฐฐ์น˜ */
262
- .full-width-row {
263
- display: flex;
264
- justify-content: center;
265
- margin-top: 10px;
266
- gap: 20px;
267
- flex-wrap: wrap; /* ํ™”๋ฉด ์ข์•„์ง€๋ฉด ์ค„๋ฐ”๊ฟˆ */
268
  }
269
  """
270
 
271
  # ------------------------------
272
  # Gradio ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌ์„ฑ (Blocks)
273
  # ------------------------------
274
- with gr.Blocks(css=custom_css, theme=None) as demo:
275
  # ์ƒ๋‹จ ์ œ๋ชฉ
276
- with gr.HTML():
277
- html_title = """
278
- <div class="title-container">
279
- <h1><span class="emoji">๐ŸŽž๏ธ</span> ์˜์ƒ์„ GIF๋กœ ๋ณ€ํ™”ํ•˜๊ธฐ <span class="emoji">โœจ</span></h1>
280
- </div>
281
- """
282
- print(html_title) # ์ด ์ถœ๋ ฅ์€ ์‹ค์ œ UI์—๋Š” ์˜ํ–ฅ ์ฃผ์ง€ ์•Š์Œ
283
- gr.HTML(html_title)
284
 
285
  # ์‚ฌ์šฉ ๊ฐ€์ด๋“œ
286
- with gr.HTML():
287
- html_guide = """
288
- <div class="guide-container">
289
- <p>๐ŸŒŸ ์›ํ•˜๋Š” ์˜์ƒ์„ ์—…๋กœ๋“œํ•˜๊ณ <br>
290
- ์›ํ•˜๋Š” ํ•ด์ƒ๋„/๋น„์œจ, ์†๋„, ํ”„๋ ˆ์ž„ ๋“ฑ์„ ์กฐ์ •ํ•˜์„ธ์š”.<br>
291
- <strong>GIF ์ƒ์„ฑํ•˜๊ธฐ</strong> ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋ณ€ํ™˜์ด ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค.</p>
 
 
 
292
  </div>
293
  """
294
- print(html_guide)
295
- gr.HTML(html_guide)
296
-
297
- # 1) ์ž…๋ ฅ๋ถ€ - ์ถœ๋ ฅ๋ถ€ (๊ฐ™์€ ํ–‰์— ๋ฐฐ์น˜)
298
- with gr.Row(elem_id="row-input-output"):
299
- with gr.Column(elem_id="col-input", variant="panel"):
300
- custom_input_html = """
301
- <div class="frame">
302
- <h3>์ž…๋ ฅ๋ถ€ ๐Ÿš€</h3>
303
- </div>
304
- """
305
- gr.HTML(custom_input_html)
306
 
307
  video_input = gr.Video(label="์˜์ƒ ์—…๋กœ๋“œ")
308
  platform_option = gr.Radio(
@@ -310,53 +307,42 @@ with gr.Blocks(css=custom_css, theme=None) as demo:
310
  choices=["์›๋ณธ ์œ ์ง€", "์œ ํŠœ๋ธŒ (16:9)", "์‡ผ์ธ /๋ฆด์Šค (9:16)", "์ •์‚ฌ๊ฐํ˜• (1:1)", "์ธ์Šคํƒ€๊ทธ๋žจ (4:5)", "ํด๋ž˜์‹ (4:3)"],
311
  value="์›๋ณธ ์œ ์ง€"
312
  )
313
- resolution_scale_slider = gr.Slider(
314
- label="์ถœ๋ ฅ ํ•ด์ƒ๋„ ์ถ•์†Œ ๋น„์œจ (0.1 ~ 1.0)",
315
- minimum=0.1, maximum=1.0, step=0.1, value=1.0
316
- )
317
- frame_rate_slider = gr.Slider(
318
- label="ํ”„๋ ˆ์ž„ ๋ ˆ์ดํŠธ ๋ฐฐ์œจ ์กฐ์ ˆ (0.1 ~ 1.0)",
319
- minimum=0.1, maximum=1.0, step=0.1, value=1.0
320
- )
321
- speed_slider = gr.Slider(
322
- label="์žฌ์ƒ ์†๋„ ์กฐ์ ˆ (0.5 ~ 5.0)",
323
- minimum=0.5, maximum=5.0, step=0.1, value=1.0
324
- )
325
- repeat_slider = gr.Slider(
326
- label="GIF ๋ฐ˜๋ณต ํšŸ์ˆ˜ (0: ๋ฌดํ•œ๋ฐ˜๋ณต, 1~10: ๋ฐ˜๋ณต ํšŸ์ˆ˜)",
327
- minimum=0, maximum=10, step=1, value=0
328
- )
329
 
330
- with gr.Column(elem_id="col-output", variant="panel"):
331
- custom_output_html = """
332
- <div class="frame">
333
- <h3>์ถœ๋ ฅ๋ถ€ ๐ŸŽ‰</h3>
334
- </div>
335
- """
336
- gr.HTML(custom_output_html)
337
 
338
  gif_preview_output = gr.Image(label="GIF ๊ฒฐ๊ณผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ")
339
- download_output = gr.File(label="GIF ๋‹ค์šด๋กœ๋“œ")
340
 
341
- # 2) ์ธ๋„ค์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (๋‘ ๋ฒˆ์งธ ์ค„ ์ „์ฒด)
342
- with gr.Row(elem_id="row-thumbnail"):
343
- custom_thumb_html = """
344
- <div class="frame">
345
- <h3>์ž‘์—… ์ธ๋„ค์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๐Ÿ”</h3>
346
- </div>
347
- """
348
- gr.HTML(custom_thumb_html)
349
 
350
  start_thumb_output = gr.Image(label="์‹œ์ž‘ ์ธ๋„ค์ผ")
351
  end_thumb_output = gr.Image(label="์ข…๋ฃŒ ์ธ๋„ค์ผ")
352
- generate_button = gr.Button("GIF ์ƒ์„ฑํ•˜๊ธฐ", elem_classes="custom-button")
353
- logs_output = gr.Textbox(label="๋กœ๊ทธ ์ถœ๋ ฅ", lines=8)
354
 
355
- # ์ด๋ฒคํŠธ ๋กœ์ง
356
- start_time = gr.Textbox(label="์‹œ์ž‘ ์‹œ๊ฐ„ (์˜ˆ: 00:00:05)", value="00:00:00", visible=False)
357
- end_time = gr.Textbox(label="์ข…๋ฃŒ ์‹œ๊ฐ„ (์˜ˆ: 00:00:10)", value="00:00:05", visible=False)
 
 
 
 
 
358
 
359
- # ์ธ๋„ค์ผ ์—…๋ฐ์ดํŠธ
360
  start_time.change(
361
  fn=update_thumbnails,
362
  inputs=[video_input, start_time, end_time],
@@ -373,17 +359,16 @@ with gr.Blocks(css=custom_css, theme=None) as demo:
373
  outputs=[start_thumb_output, end_thumb_output]
374
  )
375
 
376
- # GIF ์ƒ์„ฑ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
377
  generate_button.click(
378
  fn=process_video,
379
  inputs=[
380
- video_input,
381
- start_time,
382
  end_time,
383
- platform_option,
384
- frame_rate_slider,
385
- speed_slider,
386
- repeat_slider,
387
  resolution_scale_slider
388
  ],
389
  outputs=[
@@ -393,18 +378,6 @@ with gr.Blocks(css=custom_css, theme=None) as demo:
393
  ]
394
  )
395
 
396
- # ๊ฐ„๋‹จํ•œ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ (HTML๋กœ ํ‘œ์‹œ, ์‹ค์ œ ์•ฑ ๋‚ด๋ถ€์— ํ‘œ์‹œํ•˜๋ ค๋ฉด gr.HTML(...)์— ๋„ฃ์–ด๋„ ๋จ)
397
- guide_text = """
398
- <h2>๊ฐ„๋‹จ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ ๐Ÿ“</h2>
399
- <ol>
400
- <li>์˜์ƒ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜์„ธ์š”.</li>
401
- <li>์›ํ•˜๋Š” ํ•ด์ƒ๋„/๋น„์œจ ์˜ต์…˜์„ ์„ ํƒํ•˜์„ธ์š”.</li>
402
- <li>์ถœ๋ ฅ ํ•ด์ƒ๋„ ์ถ•์†Œ ๋น„์œจ, ํ”„๋ ˆ์ž„ ๋ ˆ์ดํŠธ, ์žฌ์ƒ ์†๋„, GIF ๋ฐ˜๋ณต ํšŸ์ˆ˜๋ฅผ ์กฐ์ ˆํ•˜์„ธ์š”.</li>
403
- <li><strong>GIF ์ƒ์„ฑํ•˜๊ธฐ</strong> ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋ณ€ํ™˜์ด ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค.</li>
404
- <li>์ถœ๋ ฅ๋œ GIF๋ฅผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋กœ ํ™•์ธํ•˜๊ณ , ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</li>
405
- </ol>
406
- """
407
- print(guide_text)
408
 
409
  if __name__ == "__main__":
410
  demo.launch()
 
1
  import os
2
  import uuid
3
  import moviepy.editor as mp
4
+ import numpy as np
5
  from PIL import Image
6
  import gradio as gr
7
 
 
11
  global_logs = []
12
 
13
  def add_log(message: str):
14
+ global global_logs
 
 
15
  global_logs.append(message)
16
  print(message)
17
 
18
  def parse_time_to_seconds(time_str: str):
 
 
 
19
  try:
20
  h, m, s = time_str.split(":")
21
  total_seconds = int(h) * 3600 + int(m) * 60 + float(s)
 
24
  return 0.0
25
 
26
  def generate_thumbnail(video_clip, time_point):
 
 
 
27
  try:
28
  frame = video_clip.get_frame(time_point)
29
  thumbnail_img = Image.fromarray(frame)
 
35
  return thumbnail_img
36
 
37
  def adjust_aspect_ratio(clip, option):
 
 
 
38
  if option == "์›๋ณธ ์œ ์ง€":
39
  return clip
 
40
  if option == "์œ ํŠœ๋ธŒ (16:9)":
41
  target_ratio = 16 / 9
42
  elif option == "์‡ผ์ธ /๋ฆด์Šค (9:16)":
 
60
  new_width = width
61
  new_height = int(width / target_ratio)
62
 
63
+ return clip.crop(
64
+ x_center=width / 2,
65
+ y_center=height / 2,
66
+ width=new_width,
67
+ height=new_height
68
+ )
69
 
70
  def process_video(video,
71
  start_time_str,
72
  end_time_str,
73
+ platform_option,
74
+ frame_rate_factor,
75
  speed_factor,
76
  repeat_count,
77
  resolution_scale):
 
79
  ๋™์˜์ƒ์„ GIF๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜
80
  """
81
  global global_logs
82
+ global_logs = []
 
83
  add_log("[LOG 1] ๋น„๋””์˜ค ์—…๋กœ๋“œ ๋ฐ ์ฒ˜๋ฆฌ ์‹œ์ž‘")
84
 
 
85
  video_path = video if isinstance(video, str) else video.name
86
 
87
  add_log("[LOG 2] ๋น„๋””์˜ค ๋กœ๋“œ ์ค‘...")
 
98
  start_sec = parse_time_to_seconds(start_time_str)
99
  end_sec = parse_time_to_seconds(end_time_str)
100
 
 
101
  if start_sec < 0:
102
  start_sec = 0
103
  if end_sec <= 0 or end_sec > duration:
 
114
  add_log(f"[LOG 7] ํ”Œ๋žซํผ ๋น„์œจ ์กฐ์ •: {platform_option}")
115
  clip = adjust_aspect_ratio(clip, platform_option)
116
 
 
117
  if abs(resolution_scale - 1.0) > 1e-3:
118
  add_log(f"[LOG 7-1] ์ถœ๋ ฅ ํ•ด์ƒ๋„ ์ถ•์†Œ: {resolution_scale*100:.0f}%")
119
  clip = clip.resize(resolution_scale)
120
 
 
121
  if abs(speed_factor - 1.0) > 1e-3:
122
  add_log(f"[LOG 8] ์žฌ์ƒ์†๋„ {speed_factor}๋ฐฐ๋กœ ์กฐ์ ˆ ์ค‘...")
123
  clip = clip.fx(mp.vfx.speedx, speed_factor)
 
127
  add_log(f"[LOG 9] ์ตœ์ข… ์ถœ๋ ฅ FPS: {target_fps:.2f} (์›๋ณธ FPS: {original_fps})")
128
  clip = clip.set_fps(target_fps)
129
 
 
130
  add_log(f"[LOG 10] GIF ๋ฐ˜๋ณต ํšŸ์ˆ˜ ์„ค์ •: {repeat_count} (0์ด๋ฉด ๋ฌดํ•œ๋ฐ˜๋ณต)")
131
  final_clip = clip
132
 
 
134
  output_filename = f"temp_{uuid.uuid4().hex}.gif"
135
  try:
136
  loop_param = 0 if int(repeat_count) == 0 else int(repeat_count)
137
+ # ImageMagick๋กœ ์ƒ์„ฑ (๊ธฐ๋ณธ๊ฐ’), loop=0 => ๋ฌดํ•œ
138
  final_clip.write_gif(output_filename, fps=target_fps, loop=loop_param)
139
  add_log(f"[LOG 12] GIF ์ƒ์„ฑ ์™„๋ฃŒ! ํŒŒ์ผ๋ช…: {output_filename}, loop={loop_param}")
140
  except Exception as e:
 
144
  return output_filename, output_filename, "\n".join(global_logs)
145
 
146
  def update_thumbnails(video, start_time_str, end_time_str):
 
 
 
147
  video_path = video if isinstance(video, str) else video.name
 
148
  try:
149
  input_video = mp.VideoFileClip(video_path)
150
  except Exception as e:
 
165
 
166
  start_thumb = generate_thumbnail(input_video, start_sec)
167
  end_thumb = generate_thumbnail(input_video, end_sec)
 
168
  return start_thumb, end_thumb
169
 
170
+ # ------------------------------
171
+ # ์ปค์Šคํ…€ CSS (Gradio ๊ธฐ๋ณธ ์Šคํƒ€์ผ ๊ฐ์ถ”๊ณ , ๋ชจ์„œ๋ฆฌ๋ฅผ ๋‘ฅ๊ธ€๊ฒŒ, ๋ฐฐ๊ฒฝ์ƒ‰ ๋“ฑ)
172
+ # ------------------------------
173
  custom_css = """
174
+ /* ์ „์ฒด ๋ฐฐ๊ฒฝ ๋ฐ ๊ธฐ๋ณธ ๊ธ€๏ฟฝ๏ฟฝ๏ฟฝ ์„ค์ • */
175
  body {
176
+ background: linear-gradient(145deg, #fcfcfc, #eef4f7);
177
+ font-family: 'Helvetica', 'Arial', sans-serif;
178
+ margin: 0;
179
+ padding: 0;
180
  }
181
 
182
+ /* ์ˆจ๊ฒจ์•ผ ํ•˜๋Š” Gradio์˜ ๊ธฐ๋ณธ ์š”์†Œ๋“ค */
183
+ .gradio-container {
184
+ max-width: 1100px; /* ํญ ์กฐ์ ˆ */
185
+ margin: 0 auto;
186
+ padding: 0px;
187
+ box-shadow: none !important;
188
+ border-radius: 0 !important;
189
+ background-color: transparent !important;
190
  }
191
+
192
+ /* ๊ฐ ์„น์…˜์„ ๊ฐ์Œ€ ์นด๋“œ ๋””์ž์ธ */
193
+ .my-frame {
194
+ background-color: #ffffffcc;
195
+ border-radius: 25px;
196
+ padding: 20px;
197
+ margin: 10px;
198
+ box-shadow: 0 0 10px rgba(0,0,0,0.1);
199
  }
200
+
201
+ .my-frame h2 {
202
+ font-size: 1.5em;
203
+ margin-bottom: 10px;
204
  }
205
 
206
+ .my-frame h3 {
207
+ font-size: 1.3em;
208
+ margin-bottom: 8px;
 
 
209
  }
210
+
211
+ /* ์ด๋ชจํ‹ฐ์ฝ˜ ํฌํ•จํ•œ ํƒ€์ดํ‹€ */
212
+ #title {
213
+ text-align: center;
214
+ font-size: 2rem;
215
+ font-weight: bold;
216
+ margin: 20px 0 10px 0;
217
  }
218
 
219
+ /* ์‚ฌ์šฉ ๊ฐ€์ด๋“œ */
220
+ #guide {
221
+ text-align: center;
222
+ margin-bottom: 25px;
223
+ font-size: 1rem;
224
+ line-height: 1.5;
225
+ background-color: #fffcf7cc;
226
+ padding: 15px;
227
+ border-radius: 15px;
228
+ display: inline-block;
229
  }
230
+
231
+ /* ๋ผ๋ฒจ ํ…์ŠคํŠธ ์‚ด์ง ํฌ๊ฒŒ */
232
+ label {
233
+ font-size: 1rem;
234
+ font-weight: 500;
235
  }
236
 
237
+ /* ๋ฒ„ํŠผ ๋””์ž์ธ */
238
+ button {
239
+ font-size: 1rem;
240
+ font-weight: bold;
241
+ padding: 10px 15px;
242
+ border-radius: 25px;
243
+ background: linear-gradient(135deg, #81ecec, #74b9ff);
244
+ color: #fff;
245
+ border: none;
246
+ cursor: pointer;
247
+ }
248
+ button:hover {
249
+ opacity: 0.9;
250
  }
251
 
252
+ /* ์ด๋ฏธ์ง€(์ธ๋„ค์ผ, ๊ฒฐ๊ณผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ) ๋””์ž์ธ */
253
+ img {
254
+ border-radius: 15px;
255
+ max-width: 100%;
 
 
256
  }
257
+
258
+ /* ๊ทธ๋ฆฌ๋“œ ๋ฐฐ์น˜ ์ปจํ…Œ์ด๋„ˆ */
259
+ .row-container {
260
+ display: flex;
261
+ flex-direction: row;
262
+ justify-content: space-between;
263
+ gap: 10px;
264
  }
265
 
266
+ .column-container {
267
+ flex: 1;
 
 
 
 
268
  }
269
 
270
+ /* ์ด๋ชจํ‹ฐ์ฝ˜์šฉ ๊ฐ•์กฐ */
271
+ .emoji {
272
+ font-size: 1.4em;
273
+ margin-right: 8px;
 
 
 
274
  }
275
  """
276
 
277
  # ------------------------------
278
  # Gradio ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌ์„ฑ (Blocks)
279
  # ------------------------------
280
+ with gr.Blocks(css=custom_css) as demo:
281
  # ์ƒ๋‹จ ์ œ๋ชฉ
282
+ gr.HTML("<div id='title'>๐ŸŽ‰ ์˜์ƒ์„ GIF๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ๐ŸŽ‰</div>")
 
 
 
 
 
 
 
283
 
284
  # ์‚ฌ์šฉ ๊ฐ€์ด๋“œ
285
+ gr.HTML(
286
+ """
287
+ <div id='guide'>
288
+ <strong>๐Ÿ“Œ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ</strong><br><br>
289
+ 1) ์˜์ƒ์„ ์—…๋กœ๋“œ ํ›„, ์›ํ•˜๋Š” ํ•ด์ƒ๋„์™€ ์˜ต์…˜์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.<br>
290
+ 2) [GIF ์ƒ์„ฑํ•˜๊ธฐ] ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ๋ณ€ํ™˜์ด ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค.<br>
291
+ 3) ์ธ๋„ค์ผ(์‹œ์ž‘/๋), ์™„์„ฑ๋œ GIF ๋ฏธ๋ฆฌ๋ณด๊ธฐ, ๋‹ค์šด๋กœ๋“œ ๋งํฌ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.<br>
292
+ <br>
293
+ ๐Ÿ† Tips: ํ”„๋ ˆ์ž„ ๋ ˆ์ดํŠธ/ํ•ด์ƒ๋„๋ฅผ ๋‚ฎ์ถ”๋ฉด GIF ์šฉ๋Ÿ‰์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
294
  </div>
295
  """
296
+ )
297
+
298
+ # 1) ์ž…๋ ฅ๋ถ€ + ์ถœ๋ ฅ๋ถ€ (ํ•œ ์ค„)
299
+ with gr.Row():
300
+ with gr.Column():
301
+ # ์ž…๋ ฅ๋ถ€
302
+ gr.HTML("<div class='my-frame'><h2 class='emoji'>๐Ÿ’ป ์ž…๋ ฅ๋ถ€</h2>", elem_id="input-start")
 
 
 
 
 
303
 
304
  video_input = gr.Video(label="์˜์ƒ ์—…๋กœ๋“œ")
305
  platform_option = gr.Radio(
 
307
  choices=["์›๋ณธ ์œ ์ง€", "์œ ํŠœ๋ธŒ (16:9)", "์‡ผ์ธ /๋ฆด์Šค (9:16)", "์ •์‚ฌ๊ฐํ˜• (1:1)", "์ธ์Šคํƒ€๊ทธ๋žจ (4:5)", "ํด๋ž˜์‹ (4:3)"],
308
  value="์›๋ณธ ์œ ์ง€"
309
  )
310
+ resolution_scale_slider = gr.Slider(label="์ถœ๋ ฅ ํ•ด์ƒ๋„ ์ถ•์†Œ ๋น„์œจ (0.1 ~ 1.0)",
311
+ minimum=0.1, maximum=1.0, step=0.1, value=1.0)
312
+ frame_rate_slider = gr.Slider(label="ํ”„๋ ˆ์ž„ ๋ ˆ์ดํŠธ ๋ฐฐ์œจ ์กฐ์ ˆ (0.1 ~ 1.0)",
313
+ minimum=0.1, maximum=1.0, step=0.1, value=1.0)
314
+ speed_slider = gr.Slider(label="์žฌ์ƒ ์†๋„ ์กฐ์ ˆ (0.5 ~ 5.0)",
315
+ minimum=0.5, maximum=5.0, step=0.1, value=1.0)
316
+ repeat_slider = gr.Slider(label="GIF ๋ฐ˜๋ณต ํšŸ์ˆ˜ (0: ๋ฌดํ•œ๋ฐ˜๋ณต, 1~10: ๋ฐ˜๋ณต ํšŸ์ˆ˜)",
317
+ minimum=0, maximum=10, step=1, value=0)
 
 
 
 
 
 
 
 
318
 
319
+ gr.HTML("</div>", elem_id="input-end")
320
+
321
+ with gr.Column():
322
+ # ์ถœ๋ ฅ๋ถ€
323
+ gr.HTML("<div class='my-frame'><h2 class='emoji'>โœ… ์ถœ๋ ฅ๋ถ€</h2>", elem_id="output-start")
 
 
324
 
325
  gif_preview_output = gr.Image(label="GIF ๊ฒฐ๊ณผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ")
326
+ download_output = gr.File(label="GIF ๋‹ค์šด๋กœ๋“œ ๋งํฌ")
327
 
328
+ gr.HTML("</div>", elem_id="output-end")
329
+
330
+ # 2) ์ž‘์—… ์ธ๋„ค์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (ํ•˜๋‹จ ํ•œ ์ค„)
331
+ with gr.Column():
332
+ gr.HTML("<div class='my-frame'><h2 class='emoji'>๐Ÿ” ์ž‘์—… ์ธ๋„ค์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ</h2>", elem_id="thumb-start")
 
 
 
333
 
334
  start_thumb_output = gr.Image(label="์‹œ์ž‘ ์ธ๋„ค์ผ")
335
  end_thumb_output = gr.Image(label="์ข…๋ฃŒ ์ธ๋„ค์ผ")
 
 
336
 
337
+ generate_button = gr.Button("GIF ์ƒ์„ฑํ•˜๊ธฐ")
338
+ logs_output = gr.Textbox(label="์ฒ˜๋ฆฌ ๋กœ๊ทธ", lines=6)
339
+
340
+ gr.HTML("</div>", elem_id="thumb-end")
341
+
342
+ # ๋กœ์ง ์—ฐ๊ฒฐ
343
+ start_time = gr.Textbox(label="", visible=False, value="00:00:00") # ์ˆจ๊น€
344
+ end_time = gr.Textbox(label="", visible=False, value="00:00:05") # ์ˆจ๊น€
345
 
 
346
  start_time.change(
347
  fn=update_thumbnails,
348
  inputs=[video_input, start_time, end_time],
 
359
  outputs=[start_thumb_output, end_thumb_output]
360
  )
361
 
 
362
  generate_button.click(
363
  fn=process_video,
364
  inputs=[
365
+ video_input,
366
+ start_time,
367
  end_time,
368
+ platform_option,
369
+ frame_rate_slider,
370
+ speed_slider,
371
+ repeat_slider,
372
  resolution_scale_slider
373
  ],
374
  outputs=[
 
378
  ]
379
  )
380
 
 
 
 
 
 
 
 
 
 
 
 
 
381
 
382
  if __name__ == "__main__":
383
  demo.launch()