ginipick commited on
Commit
8305204
·
verified ·
1 Parent(s): e9d3755

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +22 -1708
app.py CHANGED
@@ -1,1721 +1,35 @@
1
- # Spaces GPU - 반드시 첫 번째로 import해야 함!
2
  import os
3
- IS_SPACES = os.environ.get("SPACE_ID") is not None
 
 
4
 
5
- if IS_SPACES:
6
- import spaces
7
- else:
8
- # GPU 데코레이터가 없을 때를 위한 더미 데코레이터
9
- class spaces:
10
- @staticmethod
11
- def GPU(duration=None):
12
- def decorator(func):
13
- return func
14
- return decorator
15
-
16
- # 이제 다른 라이브러리들을 import
17
- import gradio as gr
18
- import numpy as np
19
- from PIL import Image, ImageDraw
20
- from gradio_client import Client, handle_file
21
- import random
22
- import tempfile
23
- import logging
24
- import torch
25
- from diffusers import AutoencoderKL, TCDScheduler
26
- from diffusers.models.model_loading_utils import load_state_dict
27
- from huggingface_hub import hf_hub_download
28
- from pathlib import Path
29
- import torchaudio
30
- from einops import rearrange
31
- from scipy.io import wavfile
32
- from transformers import pipeline
33
-
34
- # 비디오 배경제거 관련 import
35
- from transformers import AutoModelForImageSegmentation
36
- from torchvision import transforms
37
-
38
- # ── moviepy import ──────────────────────────────────────────
39
- try:
40
- from moviepy.editor import (
41
- VideoFileClip,
42
- concatenate_videoclips,
43
- ImageSequenceClip,
44
- concatenate_audioclips,
45
- AudioFileClip,
46
- CompositeAudioClip,
47
- CompositeVideoClip,
48
- ColorClip
49
- )
50
- except ImportError:
51
- # 개별적으로 import 시도
52
- try:
53
- from moviepy.video.io.VideoFileClip import VideoFileClip
54
- except ImportError:
55
- from moviepy import VideoFileClip
56
-
57
- try:
58
- from moviepy.video.compositing.concatenate import concatenate_videoclips
59
- except ImportError:
60
- from moviepy import concatenate_videoclips
61
-
62
- try:
63
- from moviepy.video.io.ImageSequenceClip import ImageSequenceClip
64
- except ImportError:
65
- from moviepy.editor import ImageSequenceClip
66
-
67
- try:
68
- from moviepy.audio.io.AudioFileClip import AudioFileClip
69
- except ImportError:
70
- from moviepy.editor import AudioFileClip
71
-
72
- try:
73
- from moviepy.audio.AudioClip import concatenate_audioclips, CompositeAudioClip
74
- except ImportError:
75
- from moviepy.editor import concatenate_audioclips, CompositeAudioClip
76
-
77
- try:
78
- from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
79
- except ImportError:
80
- from moviepy.editor import CompositeVideoClip
81
-
82
- try:
83
- from moviepy.video.VideoClip import ColorClip
84
- except ImportError:
85
- from moviepy.editor import ColorClip
86
-
87
- # resize 함수 import 시도
88
- resize = None
89
- try:
90
- from moviepy.video.fx.resize import resize
91
- except ImportError:
92
- try:
93
- from moviepy.video.fx.all import resize
94
- except ImportError:
95
- try:
96
- # editor를 통한 import 시도
97
- from moviepy.editor import resize
98
- except ImportError:
99
- pass # resize를 찾을 수 없음
100
-
101
- # resize가 없으면 대체 함수 생성
102
- if resize is None:
103
- def resize(clip, newsize=None, height=None, width=None):
104
- """Fallback resize function when moviepy resize is not available"""
105
- if hasattr(clip, 'resize'):
106
- if newsize:
107
- return clip.resize(newsize)
108
- elif height:
109
- return clip.resize(height=height)
110
- elif width:
111
- return clip.resize(width=width)
112
- # 크기 변경이 불가능하면 원본 반환
113
- return clip
114
-
115
- # speedx 함수 import 시도
116
- speedx = None
117
- try:
118
- from moviepy.video.fx.speedx import speedx
119
- except ImportError:
120
- try:
121
- from moviepy.video.fx.all import speedx
122
- except ImportError:
123
- try:
124
- from moviepy.editor import speedx
125
- except ImportError:
126
- pass # speedx를 찾을 수 없음
127
-
128
- # speedx가 없으면 대체 함수 생성
129
- if speedx is None:
130
- def speedx(clip, factor=1.0, final_duration=None):
131
- """Fallback speedx function"""
132
- if hasattr(clip, 'fx') and hasattr(clip.fx, 'speedx'):
133
- return clip.fx.speedx(factor, final_duration)
134
- elif hasattr(clip, 'fl_time'):
135
- return clip.fl_time(lambda t: t * factor)
136
- elif hasattr(clip, 'with_fps') and factor != 1.0:
137
- # FPS를 조정하여 속도 변경 효과 구현
138
- new_fps = clip.fps * factor if hasattr(clip, 'fps') else 24 * factor
139
- return clip.with_fps(new_fps)
140
- else:
141
- # 최후의 수단: 클립 그대로 반환
142
- return clip
143
-
144
- import time
145
- from concurrent.futures import ThreadPoolExecutor
146
-
147
- # ────────────────────────────────────────────────────────────
148
-
149
- import httpx
150
- from datetime import datetime
151
-
152
- # 환경 변수 설정으로 torch.load 체크 우회 (임시 해결책)
153
- os.environ["TRANSFORMERS_ALLOW_UNSAFE_DESERIALIZATION"] = "1"
154
-
155
- # GPU 초기화를 위한 간단한 함수 (Spaces 환경에서 필수)
156
- @spaces.GPU(duration=1)
157
- def gpu_warmup():
158
- """GPU 워밍업 함수 - Spaces 환경에서 GPU 사용을 위해 필요"""
159
- if torch.cuda.is_available():
160
- dummy = torch.zeros(1).cuda()
161
- del dummy
162
- return "GPU ready"
163
-
164
- # MMAudio imports - spaces import 이후에 와야 함
165
- try:
166
- import mmaudio
167
- except ImportError:
168
- os.system("pip install -e .")
169
- import mmaudio
170
-
171
- from mmaudio.eval_utils import (ModelConfig, all_model_cfg, generate, load_video, make_video,
172
- setup_eval_logging)
173
- from mmaudio.model.flow_matching import FlowMatching
174
- from mmaudio.model.networks import MMAudio, get_my_mmaudio
175
- from mmaudio.model.sequence_config import SequenceConfig
176
- from mmaudio.model.utils.features_utils import FeaturesUtils
177
-
178
- # 로깅 설정
179
- logging.basicConfig(level=logging.INFO)
180
-
181
- # 기존 코드의 모든 설정과 초기화 부분 유지
182
- torch.set_float32_matmul_precision("medium")
183
-
184
- # Device 설정을 더 명확하게
185
- if torch.cuda.is_available():
186
- device = torch.device("cuda")
187
- torch_dtype = torch.float16
188
- else:
189
- device = torch.device("cpu")
190
- torch_dtype = torch.float32
191
-
192
- logging.info(f"Using device: {device}")
193
-
194
- # 전역 변수로 모델 상태 관리
195
- MODELS_LOADED = False
196
- BIREFNET_MODEL = None
197
- BIREFNET_LITE_MODEL = None
198
- OUTPAINT_PIPE = None
199
- MMAUDIO_NET = None
200
- MMAUDIO_FEATURE_UTILS = None
201
- MMAUDIO_SEQ_CFG = None
202
- TRANSLATOR = None
203
-
204
- # API URLs
205
- TEXT2IMG_API_URL = "http://211.233.58.201:7896"
206
- VIDEO_API_URL = "http://211.233.58.201:7875"
207
- ANIM_API_URL = os.getenv("ANIM_API_URL", "http://211.233.58.201:7862/")
208
-
209
- # HTTP 타임아웃 설정 - 괄호 수정
210
- ANIM_TIMEOUT = httpx.Timeout(connect=30.0, read=120.0, write=120.0, pool=30.0)
211
-
212
- # Image size presets
213
- IMAGE_PRESETS = {
214
- "커스텀": {"width": 1024, "height": 1024},
215
- "1:1 정사각형": {"width": 1024, "height": 1024},
216
- "4:3 표준": {"width": 1024, "height": 768},
217
- "16:9 와이드스크린": {"width": 1024, "height": 576},
218
- "9:16 세로형": {"width": 576, "height": 1024},
219
- "6:19 특수 세로형": {"width": 324, "height": 1024},
220
- "Instagram 정사각형": {"width": 1080, "height": 1080},
221
- "Instagram 스토리": {"width": 1080, "height": 1920},
222
- "Instagram 가로형": {"width": 1080, "height": 566},
223
- "Facebook 커버": {"width": 820, "height": 312},
224
- "Twitter 헤더": {"width": 1500, "height": 500},
225
- "YouTube 썸네일": {"width": 1280, "height": 720},
226
- "LinkedIn 배너": {"width": 1584, "height": 396},
227
- }
228
-
229
- # Transform for BiRefNet
230
- transform_image = transforms.Compose([
231
- transforms.Resize((768, 768)),
232
- transforms.ToTensor(),
233
- transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
234
- ])
235
-
236
- @spaces.GPU(duration=60)
237
- def load_models():
238
- """모든 모델을 로드하는 함수"""
239
- global MODELS_LOADED, BIREFNET_MODEL, BIREFNET_LITE_MODEL, OUTPAINT_PIPE
240
- global MMAUDIO_NET, MMAUDIO_FEATURE_UTILS, MMAUDIO_SEQ_CFG, TRANSLATOR
241
-
242
- if MODELS_LOADED:
243
- return True
244
-
245
  try:
246
- # BiRefNet 모델 로드
247
- logging.info("Loading BiRefNet models...")
248
- BIREFNET_MODEL = AutoModelForImageSegmentation.from_pretrained("ZhengPeng7/BiRefNet", trust_remote_code=True)
249
- BIREFNET_MODEL.to(device)
250
- BIREFNET_LITE_MODEL = AutoModelForImageSegmentation.from_pretrained("ZhengPeng7/BiRefNet_lite", trust_remote_code=True)
251
- BIREFNET_LITE_MODEL.to(device)
252
-
253
- # ControlNet 및 Outpainting 모델 로드
254
- logging.info("Loading ControlNet models...")
255
- from controlnet_union import ControlNetModel_Union
256
- from pipeline_fill_sd_xl import StableDiffusionXLFillPipeline
257
-
258
- config_file = hf_hub_download(
259
- "xinsir/controlnet-union-sdxl-1.0",
260
- filename="config_promax.json",
261
- )
262
-
263
- config = ControlNetModel_Union.load_config(config_file)
264
- controlnet_model = ControlNetModel_Union.from_config(config)
265
-
266
- model_file = hf_hub_download(
267
- "xinsir/controlnet-union-sdxl-1.0",
268
- filename="diffusion_pytorch_model_promax.safetensors",
269
- )
270
- state_dict = load_state_dict(model_file)
271
- loaded_keys = list(state_dict.keys())
272
 
273
- result = ControlNetModel_Union._load_pretrained_model(
274
- controlnet_model, state_dict, model_file, "xinsir/controlnet-union-sdxl-1.0", loaded_keys
275
- )
276
-
277
- model = result[0]
278
- model = model.to(device=device, dtype=torch_dtype)
279
-
280
- # VAE 로드
281
- vae = AutoencoderKL.from_pretrained(
282
- "madebyollin/sdxl-vae-fp16-fix", torch_dtype=torch_dtype
283
- ).to(device)
284
-
285
- # 파이프라인 로드
286
- OUTPAINT_PIPE = StableDiffusionXLFillPipeline.from_pretrained(
287
- "SG161222/RealVisXL_V5.0_Lightning",
288
- torch_dtype=torch_dtype,
289
- vae=vae,
290
- controlnet=model,
291
- variant="fp16" if device.type == "cuda" else None,
292
- ).to(device)
293
-
294
- OUTPAINT_PIPE.scheduler = TCDScheduler.from_config(OUTPAINT_PIPE.scheduler.config)
295
-
296
- # MMAudio 모델 로드
297
- logging.info("Loading MMAudio models...")
298
- model_mmaudio: ModelConfig = all_model_cfg['large_44k_v2']
299
- model_mmaudio.download_if_needed()
300
- setup_eval_logging()
301
-
302
- # 번역기 설정
303
- try:
304
- TRANSLATOR = pipeline("translation",
305
- model="Helsinki-NLP/opus-mt-ko-en",
306
- device="cpu",
307
- use_fast=True,
308
- trust_remote_code=False)
309
- except Exception as e:
310
- logging.warning(f"Failed to load translation model: {e}")
311
- TRANSLATOR = None
312
-
313
- # MMAudio 모델 초기화
314
- if torch.cuda.is_available():
315
- mmaudio_dtype = torch.bfloat16
316
- else:
317
- mmaudio_dtype = torch.float32
318
-
319
- with torch.cuda.device(device):
320
- MMAUDIO_SEQ_CFG = model_mmaudio.seq_cfg
321
- MMAUDIO_NET = get_my_mmaudio(model_mmaudio.model_name).to(device, mmaudio_dtype).eval()
322
- MMAUDIO_NET.load_weights(torch.load(model_mmaudio.model_path, map_location=device, weights_only=True))
323
- logging.info(f'Loaded weights from {model_mmaudio.model_path}')
324
-
325
- MMAUDIO_FEATURE_UTILS = FeaturesUtils(
326
- tod_vae_ckpt=model_mmaudio.vae_path,
327
- synchformer_ckpt=model_mmaudio.synchformer_ckpt,
328
- enable_conditions=True,
329
- mode=model_mmaudio.mode,
330
- bigvgan_vocoder_ckpt=model_mmaudio.bigvgan_16k_path,
331
- need_vae_encoder=False
332
- ).to(device, mmaudio_dtype).eval()
333
-
334
- MODELS_LOADED = True
335
- logging.info("All models loaded successfully!")
336
- return True
337
-
338
- except Exception as e:
339
- logging.error(f"Failed to load models: {str(e)}")
340
- return False
341
-
342
- # 기존 함수들 모두 유지
343
- def update_dimensions(preset):
344
- if preset in IMAGE_PRESETS:
345
- return IMAGE_PRESETS[preset]["width"], IMAGE_PRESETS[preset]["height"]
346
- return 1024, 1024
347
-
348
- def generate_text_to_image(prompt, width, height, guidance, inference_steps, seed):
349
- if not prompt:
350
- return None, "프롬프트를 입력해주세요"
351
-
352
- try:
353
- client = Client(TEXT2IMG_API_URL)
354
- if seed == -1:
355
- seed = random.randint(0, 9999999)
356
-
357
- result = client.predict(
358
- prompt=prompt,
359
- width=int(width),
360
- height=int(height),
361
- guidance=float(guidance),
362
- inference_steps=int(inference_steps),
363
- seed=int(seed),
364
- do_img2img=False,
365
- init_image=None,
366
- image2image_strength=0.8,
367
- resize_img=True,
368
- api_name="/generate_image"
369
- )
370
- return result[0], f"사용된 시드: {result[1]}"
371
- except Exception as e:
372
- logging.error(f"Image generation error: {str(e)}")
373
- return None, f"오류: {str(e)}"
374
-
375
- def generate_video_from_image(image, prompt="", length=4.0):
376
- if image is None:
377
- return None
378
-
379
- try:
380
- # 이미지 저장
381
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as fp:
382
- temp_path = fp.name
383
- Image.fromarray(image).save(temp_path)
384
-
385
- # API 호출
386
- client = Client(VIDEO_API_URL)
387
- result = client.predict(
388
- input_image=handle_file(temp_path),
389
- prompt=prompt if prompt else "Generate natural motion",
390
- n_prompt="",
391
- seed=random.randint(0, 9999999),
392
- use_teacache=True,
393
- video_length=float(length),
394
- api_name="/process"
395
- )
396
-
397
- os.unlink(temp_path)
398
-
399
- if result and len(result) > 0:
400
- video_dict = result[0]
401
- return video_dict.get("video") if isinstance(video_dict, dict) else None
402
-
403
- except Exception as e:
404
- logging.error(f"Video generation error: {str(e)}")
405
- return None
406
-
407
- def prepare_image_and_mask(image, width, height, overlap_percentage, alignment):
408
- """이미지와 마스크를 준비하는 함수"""
409
- if image is None:
410
- return None, None
411
-
412
- # PIL 이미지로 변환
413
- if isinstance(image, np.ndarray):
414
- image = Image.fromarray(image).convert('RGB')
415
-
416
- target_size = (width, height)
417
-
418
- # 이미지를 타겟 크기에 맞게 조정
419
- scale_factor = min(target_size[0] / image.width, target_size[1] / image.height)
420
- new_width = int(image.width * scale_factor)
421
- new_height = int(image.height * scale_factor)
422
-
423
- # 이미지 리사이즈
424
- source = image.resize((new_width, new_height), Image.LANCZOS)
425
-
426
- # 오버랩 계산
427
- overlap_x = int(new_width * (overlap_percentage / 100))
428
- overlap_y = int(new_height * (overlap_percentage / 100))
429
- overlap_x = max(overlap_x, 1)
430
- overlap_y = max(overlap_y, 1)
431
-
432
- # 정렬에 따른 마진 계산
433
- if alignment == "가운데":
434
- margin_x = (target_size[0] - new_width) // 2
435
- margin_y = (target_size[1] - new_height) // 2
436
- elif alignment == "왼쪽":
437
- margin_x = 0
438
- margin_y = (target_size[1] - new_height) // 2
439
- elif alignment == "오른쪽":
440
- margin_x = target_size[0] - new_width
441
- margin_y = (target_size[1] - new_height) // 2
442
- elif alignment == "위":
443
- margin_x = (target_size[0] - new_width) // 2
444
- margin_y = 0
445
- elif alignment == "아래":
446
- margin_x = (target_size[0] - new_width) // 2
447
- margin_y = target_size[1] - new_height
448
-
449
- # 배경 이미지 생성
450
- background = Image.new('RGB', target_size, (255, 255, 255))
451
- background.paste(source, (margin_x, margin_y))
452
-
453
- # 마스크 생성
454
- mask = Image.new('L', target_size, 255)
455
- mask_draw = ImageDraw.Draw(mask)
456
-
457
- # 마스크 영역 그리기
458
- left_overlap = margin_x + overlap_x if alignment != "왼쪽" else margin_x
459
- right_overlap = margin_x + new_width - overlap_x if alignment != "오른쪽" else margin_x + new_width
460
- top_overlap = margin_y + overlap_y if alignment != "위" else margin_y
461
- bottom_overlap = margin_y + new_height - overlap_y if alignment != "아래" else margin_y + new_height
462
-
463
- mask_draw.rectangle([
464
- (left_overlap, top_overlap),
465
- (right_overlap, bottom_overlap)
466
- ], fill=0)
467
-
468
- return background, mask
469
-
470
- def preview_outpaint(image, width, height, overlap_percentage, alignment):
471
- """아웃페인팅 미리보기"""
472
- background, mask = prepare_image_and_mask(image, width, height, overlap_percentage, alignment)
473
- if background is None:
474
- return None
475
-
476
- # 미리보기 이미지 생성
477
- preview = background.copy().convert('RGBA')
478
-
479
- # 반투명 빨간색 오버레이
480
- red_overlay = Image.new('RGBA', background.size, (255, 0, 0, 64))
481
-
482
- # 마스크 적용
483
- red_mask = Image.new('RGBA', background.size, (0, 0, 0, 0))
484
- red_mask.paste(red_overlay, (0, 0), mask)
485
-
486
- # 오버레이 합성
487
- preview = Image.alpha_composite(preview, red_mask)
488
-
489
- return preview
490
-
491
- @spaces.GPU(duration=120)
492
- def outpaint_image(image, prompt, width, height, overlap_percentage, alignment, num_steps=8):
493
- """이미지 아웃페인팅 실행"""
494
- if image is None:
495
- return None
496
-
497
- # 모델 로드 확인
498
- if not MODELS_LOADED:
499
- load_models()
500
-
501
- if OUTPAINT_PIPE is None:
502
- return Image.new('RGB', (width, height), (200, 200, 200))
503
-
504
- try:
505
- # 이미지와 마스크 준비
506
- background, mask = prepare_image_and_mask(image, width, height, overlap_percentage, alignment)
507
- if background is None:
508
- return None
509
-
510
- # cnet_image 생성 (마스크 영역을 검은색으로)
511
- cnet_image = background.copy()
512
- cnet_image.paste(0, (0, 0), mask)
513
-
514
- # 프롬프트 준비
515
- final_prompt = f"{prompt}, high quality, 4k" if prompt else "high quality, 4k"
516
-
517
- # GPU에서 실행
518
- with torch.autocast(device_type=device.type, dtype=torch_dtype):
519
- (
520
- prompt_embeds,
521
- negative_prompt_embeds,
522
- pooled_prompt_embeds,
523
- negative_pooled_prompt_embeds,
524
- ) = OUTPAINT_PIPE.encode_prompt(final_prompt, str(device), True)
525
-
526
- # 생성 프로세스
527
- for generated_image in OUTPAINT_PIPE(
528
- prompt_embeds=prompt_embeds,
529
- negative_prompt_embeds=negative_prompt_embeds,
530
- pooled_prompt_embeds=pooled_prompt_embeds,
531
- negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
532
- image=cnet_image,
533
- num_inference_steps=num_steps
534
- ):
535
- # 중간 결과 (필요시 사용)
536
- pass
537
-
538
- # 최종 이미지
539
- final_image = generated_image
540
-
541
- # RGBA로 변환하고 마스크 적용
542
- final_image = final_image.convert("RGBA")
543
- cnet_image.paste(final_image, (0, 0), mask)
544
-
545
- return cnet_image
546
-
547
- except Exception as e:
548
- logging.error(f"Outpainting error: {str(e)}")
549
- return background if 'background' in locals() else None
550
-
551
- # MMAudio 관련 함수들
552
- def translate_prompt(text):
553
- try:
554
- if TRANSLATOR is None:
555
- return text
556
-
557
- if text and any(ord(char) >= 0x3131 and ord(char) <= 0xD7A3 for char in text):
558
- with torch.no_grad():
559
- translation = TRANSLATOR(text)[0]['translation_text']
560
- return translation
561
- return text
562
- except Exception as e:
563
- logging.error(f"Translation error: {e}")
564
- return text
565
-
566
- @spaces.GPU(duration=120)
567
- @torch.inference_mode()
568
- def video_to_audio(video: gr.Video, prompt: str, negative_prompt: str, seed: int, num_steps: int,
569
- cfg_strength: float, duration: float):
570
- # 모델 로드 확인
571
- if not MODELS_LOADED:
572
- load_models()
573
-
574
- if MMAUDIO_NET is None:
575
- return None
576
-
577
- prompt = translate_prompt(prompt)
578
- negative_prompt = translate_prompt(negative_prompt)
579
-
580
- rng = torch.Generator(device=device)
581
- rng.manual_seed(seed)
582
- fm = FlowMatching(min_sigma=0, inference_mode='euler', num_steps=num_steps)
583
-
584
- clip_frames, sync_frames, duration = load_video(video, duration)
585
- clip_frames = clip_frames.unsqueeze(0)
586
- sync_frames = sync_frames.unsqueeze(0)
587
- MMAUDIO_SEQ_CFG.duration = duration
588
- MMAUDIO_NET.update_seq_lengths(MMAUDIO_SEQ_CFG.latent_seq_len, MMAUDIO_SEQ_CFG.clip_seq_len, MMAUDIO_SEQ_CFG.sync_seq_len)
589
-
590
- audios = generate(clip_frames,
591
- sync_frames, [prompt],
592
- negative_text=[negative_prompt],
593
- feature_utils=MMAUDIO_FEATURE_UTILS,
594
- net=MMAUDIO_NET,
595
- fm=fm,
596
- rng=rng,
597
- cfg_strength=cfg_strength)
598
- audio = audios.float().cpu()[0]
599
-
600
- video_save_path = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4').name
601
- make_video(video,
602
- video_save_path,
603
- audio,
604
- sampling_rate=MMAUDIO_SEQ_CFG.sampling_rate,
605
- duration_sec=MMAUDIO_SEQ_CFG.duration)
606
- return video_save_path
607
-
608
- # 비디오 배경제거 관련 함수들
609
- def process_bg_image(image, bg, fast_mode=False):
610
- """단일 이미지 배경 처리"""
611
- if BIREFNET_MODEL is None or BIREFNET_LITE_MODEL is None:
612
- return image
613
-
614
- image_size = image.size
615
- input_images = transform_image(image).unsqueeze(0).to(device)
616
- model = BIREFNET_LITE_MODEL if fast_mode else BIREFNET_MODEL
617
-
618
- with torch.no_grad():
619
- preds = model(input_images)[-1].sigmoid().cpu()
620
- pred = preds[0].squeeze()
621
- pred_pil = transforms.ToPILImage()(pred)
622
- mask = pred_pil.resize(image_size)
623
-
624
- if isinstance(bg, str) and bg.startswith("#"):
625
- color_rgb = tuple(int(bg[i:i+2], 16) for i in (1, 3, 5))
626
- background = Image.new("RGBA", image_size, color_rgb + (255,))
627
- elif isinstance(bg, Image.Image):
628
- background = bg.convert("RGBA").resize(image_size)
629
- else:
630
- background = Image.open(bg).convert("RGBA").resize(image_size)
631
-
632
- image = Image.composite(image, background, mask)
633
- return image
634
-
635
- def process_video_frame(frame, bg_type, bg, fast_mode, frame_index, background_frames, color):
636
- """비디오 프레임 처리"""
637
- try:
638
- pil_image = Image.fromarray(frame)
639
- if bg_type == "색상":
640
- processed_image = process_bg_image(pil_image, color, fast_mode)
641
- elif bg_type == "이미지":
642
- processed_image = process_bg_image(pil_image, bg, fast_mode)
643
- elif bg_type == "비디오":
644
- # 인덱스 범위 확인
645
- if background_frames and len(background_frames) > 0:
646
- # 프레임 인덱스를 배경 비디오 길이로 나눈 나머지를 사용 (루프 효과)
647
- bg_frame_index = frame_index % len(background_frames)
648
- background_frame = background_frames[bg_frame_index]
649
- background_image = Image.fromarray(background_frame)
650
- processed_image = process_bg_image(pil_image, background_image, fast_mode)
651
- else:
652
- processed_image = pil_image
653
- else:
654
- processed_image = pil_image
655
-
656
- # 처리된 이미지가 numpy array로 반환되는지 확인
657
- if isinstance(processed_image, Image.Image):
658
- return np.array(processed_image)
659
- return processed_image
660
-
661
- except Exception as e:
662
- print(f"Error processing frame {frame_index}: {e}")
663
- # 오류 발생 시 원본 프레임 반환
664
- if isinstance(frame, np.ndarray):
665
- return frame
666
- return np.array(pil_image)
667
-
668
- @spaces.GPU(duration=300)
669
- def process_video_bg(vid, bg_type="색상", bg_image=None, bg_video=None, color="#00FF00",
670
- fps=0, video_handling="slow_down", fast_mode=True, max_workers=10):
671
- """비디오 배경 처리 메인 함수"""
672
- # 모델 로드 확인
673
- if not MODELS_LOADED:
674
- load_models()
675
-
676
- if BIREFNET_MODEL is None:
677
- yield gr.update(visible=False), gr.update(visible=True), "BiRefNet 모델을 로드하지 못했습니다."
678
- yield None, None, "BiRefNet 모델을 로드하지 못했습니다."
679
- return
680
-
681
- try:
682
- start_time = time.time()
683
- video = VideoFileClip(vid)
684
- if fps == 0:
685
- fps = video.fps
686
-
687
- audio = video.audio
688
- frames = list(video.iter_frames(fps=fps))
689
-
690
- # 프레임 크기 저장
691
- if frames:
692
- frame_height, frame_width = frames[0].shape[:2]
693
- else:
694
- yield gr.update(visible=False), gr.update(visible=True), "비디오에 프레임이 없습니다."
695
- yield None, None, "비디오에 프레임이 없습니다."
696
  return
697
 
698
- processed_frames = []
699
- yield gr.update(visible=True), gr.update(visible=False), f"처리 시작... 경과 시간: 0초"
700
-
701
- # 배경 비디오 처리
702
- background_frames = None
703
- if bg_type == "비디오" and bg_video:
704
- background_video = VideoFileClip(bg_video)
705
-
706
- # 배경 비디오 길이 조정
707
- if video_handling == "slow_down" and background_video.duration < video.duration:
708
- if speedx is not None:
709
- factor = video.duration / background_video.duration
710
- background_video = speedx(background_video, factor=factor)
711
- else:
712
- # speedx가 없으면 반복으로 대체
713
- loops = int(video.duration / background_video.duration) + 1
714
- background_video = concatenate_videoclips([background_video] * loops)
715
- elif video_handling == "loop" or background_video.duration < video.duration:
716
- # 반복 모드
717
- loops = int(video.duration / background_video.duration) + 1
718
- background_video = concatenate_videoclips([background_video] * loops)
719
-
720
- # 배경 프레임 추출
721
- background_frames = list(background_video.iter_frames(fps=fps))
722
-
723
- # 배경 비디오가 더 길면 잘라냄
724
- if len(background_frames) > len(frames):
725
- background_frames = background_frames[:len(frames)]
726
-
727
- # 병렬 처리
728
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
729
- futures = []
730
- for i in range(len(frames)):
731
- future = executor.submit(
732
- process_video_frame,
733
- frames[i],
734
- bg_type,
735
- bg_image,
736
- fast_mode,
737
- i, # 프레임 인덱스 전달
738
- background_frames,
739
- color
740
- )
741
- futures.append(future)
742
-
743
- # 결과 수집
744
- for i, future in enumerate(futures):
745
- try:
746
- result = future.result()
747
- # 결과가 올바른 크기인지 확인
748
- if result.shape[:2] != (frame_height, frame_width):
749
- # 크기가 다르면 리사이즈
750
- pil_result = Image.fromarray(result)
751
- pil_result = pil_result.resize((frame_width, frame_height), Image.LANCZOS)
752
- result = np.array(pil_result)
753
-
754
- processed_frames.append(result)
755
- elapsed_time = time.time() - start_time
756
-
757
- # 10프레임마다 상태 업데이트
758
- if i % 10 == 0:
759
- yield result, None, f"프레임 {i+1}/{len(frames)} 처리 중... 경과 시간: {elapsed_time:.2f}초"
760
- except Exception as e:
761
- print(f"Error getting result for frame {i}: {e}")
762
- # 오류 발생 시 원본 프레임 사용
763
- processed_frames.append(frames[i])
764
-
765
- # 모든 프레임이 동일한 크기인지 최종 확인
766
- frame_sizes = [frame.shape for frame in processed_frames]
767
- if len(set(frame_sizes)) > 1:
768
- print(f"Warning: Different frame sizes detected: {set(frame_sizes)}")
769
- # 첫 번째 프레임 크기로 모두 통일
770
- target_size = processed_frames[0].shape
771
- for i in range(len(processed_frames)):
772
- if processed_frames[i].shape != target_size:
773
- pil_frame = Image.fromarray(processed_frames[i])
774
- pil_frame = pil_frame.resize((target_size[1], target_size[0]), Image.LANCZOS)
775
- processed_frames[i] = np.array(pil_frame)
776
-
777
- # 비디오 생성
778
- processed_video = ImageSequenceClip(processed_frames, fps=fps)
779
-
780
- # 오디오 추가
781
- if audio:
782
- processed_video = processed_video.set_audio(audio)
783
-
784
- # 저장
785
- with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_file:
786
- temp_filepath = temp_file.name
787
- processed_video.write_videofile(temp_filepath, codec="libx264", audio_codec="aac")
788
-
789
- elapsed_time = time.time() - start_time
790
- yield gr.update(visible=False), gr.update(visible=True), f"처리 완료! 경과 시간: {elapsed_time:.2f}초"
791
- yield processed_frames[-1], temp_filepath, f"처리 완료! 경과 시간: {elapsed_time:.2f}초"
792
-
793
- except Exception as e:
794
- print(f"Error: {e}")
795
- import traceback
796
- traceback.print_exc()
797
- elapsed_time = time.time() - start_time
798
- yield gr.update(visible=False), gr.update(visible=True), f"비디오 처리 오류: {e}. 경과 시간: {elapsed_time:.2f}초"
799
- yield None, None, f"비디오 처리 오류: {e}. 경과 시간: {elapsed_time:.2f}초"
800
-
801
- @spaces.GPU(duration=180)
802
- def merge_videos_with_audio(video_files, audio_file, audio_mode, audio_volume, original_audio_volume, output_fps):
803
- """여러 비디오를 병합하고 오디오를 추가하는 함수"""
804
- if not video_files:
805
- return None, "비디오 파일을 업로드해주세요."
806
-
807
- if isinstance(video_files, list) and len(video_files) > 10:
808
- return None, "최대 10개의 비디오만 업로드 가능합니다."
809
-
810
- try:
811
- # 상태 업데이트
812
- status = "비디오 파일 정렬 중..."
813
-
814
- # 파일 경로와 파일명을 튜플로 저장하고 파일명으로 정렬
815
- video_paths = []
816
- if isinstance(video_files, list):
817
- for video_file in video_files:
818
- if video_file is not None:
819
- video_paths.append(video_file)
820
- else:
821
- video_paths.append(video_files)
822
-
823
- # 파일명으로 정렬 (경로에서 파일명만 추출하여 정렬)
824
- video_paths.sort(key=lambda x: os.path.basename(x))
825
-
826
- status = f"{len(video_paths)}개의 비디오 로드 중..."
827
-
828
- # 비디오 클립 로드
829
- video_clips = []
830
- clip_sizes = []
831
-
832
- for i, video_path in enumerate(video_paths):
833
- status = f"비디오 {i+1}/{len(video_paths)} 로드 중: {os.path.basename(video_path)}"
834
- clip = VideoFileClip(video_path)
835
- video_clips.append(clip)
836
-
837
- # 각 클립의 크기 저장
838
- try:
839
- clip_sizes.append((clip.w, clip.h))
840
- except:
841
- clip_sizes.append(clip.size)
842
-
843
- # 첫 번째 비디오의 크기를 기준으로 함
844
- target_width, target_height = clip_sizes[0]
845
-
846
- # 모든 비디오의 크기가 같은지 확인
847
- all_same_size = all(size == (target_width, target_height) for size in clip_sizes)
848
-
849
- if not all_same_size:
850
- logging.warning(f"비디오 크기가 서로 다릅니다. 첫 번째 비디오 크기({target_width}x{target_height})로 조정합니다.")
851
-
852
- # 크기가 다른 비디오들을 조정
853
- adjusted_clips = []
854
- for clip, size in zip(video_clips, clip_sizes):
855
- if size != (target_width, target_height):
856
- if resize is not None:
857
- adjusted_clip = resize(clip, newsize=(target_width, target_height))
858
- else:
859
- if hasattr(clip, 'resize'):
860
- adjusted_clip = clip.resize((target_width, target_height))
861
- else:
862
- adjusted_clip = clip
863
- logging.warning(f"Cannot resize video. Using original size.")
864
- adjusted_clips.append(adjusted_clip)
865
- else:
866
- adjusted_clips.append(clip)
867
-
868
- video_clips = adjusted_clips
869
-
870
- # 첫 번째 비디오의 FPS를 기본값으로 사용
871
- if output_fps == 0:
872
- output_fps = video_clips[0].fps
873
-
874
- status = "비디오 병합 중..."
875
-
876
- # 비디오 병합
877
- final_video = concatenate_videoclips(video_clips, method="compose")
878
-
879
- # 오디오 처리
880
- if audio_file:
881
- status = "오디오 처리 중..."
882
-
883
- try:
884
- # 오디오 파일 경로 확인
885
- if isinstance(audio_file, str):
886
- audio_path = audio_file
887
- else:
888
- audio_path = audio_file
889
-
890
- logging.info(f"Processing audio from: {audio_path}")
891
- logging.info(f"Audio mode: {audio_mode}")
892
-
893
- # 오디오 로드
894
- if audio_path.endswith(('.mp4', '.avi', '.mov', '.mkv')):
895
- temp_video = VideoFileClip(audio_path)
896
- audio_clip = temp_video.audio
897
- temp_video.close()
898
- else:
899
- audio_clip = AudioFileClip(audio_path)
900
-
901
- if audio_clip is None:
902
- raise ValueError("오디오를 로드할 �� 없습니다.")
903
-
904
- # 볼륨 조절
905
- if audio_volume != 100:
906
- audio_clip = audio_clip.volumex(audio_volume / 100)
907
-
908
- # 오디오를 비디오 길이에 맞춤
909
- video_duration = final_video.duration
910
- audio_duration = audio_clip.duration
911
-
912
- if audio_duration > video_duration:
913
- audio_clip = audio_clip.subclip(0, video_duration)
914
- elif audio_duration < video_duration:
915
- loops_needed = int(video_duration / audio_duration) + 1
916
- audio_clips_list = [audio_clip] * loops_needed
917
- looped_audio = concatenate_audioclips(audio_clips_list)
918
- audio_clip = looped_audio.subclip(0, video_duration)
919
-
920
- # 오디오 모드에 따른 처리
921
- if audio_mode == "백그라운드 뮤직":
922
- # 백그라운드 뮤직 모드: 기존 오디오와 합성
923
- if final_video.audio:
924
- # 원본 오디오 볼륨 조절
925
- original_audio = final_video.audio
926
- if original_audio_volume != 100:
927
- original_audio = original_audio.volumex(original_audio_volume / 100)
928
-
929
- # 두 오디오 합성
930
- final_audio = CompositeAudioClip([original_audio, audio_clip])
931
- final_video = final_video.set_audio(final_audio)
932
- logging.info("Background music mode: Mixed original and new audio")
933
- else:
934
- # 원본 오디오가 없으면 그냥 추가
935
- final_video = final_video.set_audio(audio_clip)
936
- logging.info("No original audio found, adding new audio only")
937
- else:
938
- # 대체 모드: 기존 오디오를 완전히 교체
939
- final_video = final_video.set_audio(audio_clip)
940
- logging.info("Replace mode: Replaced original audio")
941
-
942
- logging.info("Audio successfully processed")
943
-
944
- except Exception as e:
945
- logging.error(f"오디오 처리 중 오류 발생: {str(e)}")
946
- status = f"오디오 처리 실패: {str(e)}, 비디오만 병합합니다."
947
 
948
- status = "비디오 저장 중..."
 
949
 
950
- # 임시 파일로 저장
951
- with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_file:
952
- temp_filepath = temp_file.name
 
 
953
 
954
- # 코덱 설정 - 원본 품질 유지
955
- final_video.write_videofile(
956
- temp_filepath,
957
- fps=output_fps,
958
- codec="libx264",
959
- audio_codec="aac",
960
- preset="medium",
961
- bitrate="5000k",
962
- audio_bitrate="192k"
963
- )
964
-
965
- # 리소스 정리
966
- for clip in video_clips:
967
- clip.close()
968
- if 'adjusted_clips' in locals():
969
- for clip in adjusted_clips:
970
- if clip not in video_clips:
971
- clip.close()
972
- if audio_file and 'audio_clip' in locals():
973
- audio_clip.close()
974
- final_video.close()
975
-
976
- # 상태 메시지 생성
977
- if audio_file and audio_mode == "백그라운드 뮤직":
978
- mode_msg = "백그라운드 뮤직 추가됨"
979
- elif audio_file:
980
- mode_msg = "오디오 대체됨"
981
- else:
982
- mode_msg = "오디오 없음"
983
-
984
- return temp_filepath, f"✅ 성공적으로 {len(video_paths)}개의 비디오를 병합했습니다! (크기: {target_width}x{target_height}, {mode_msg})"
985
-
986
  except Exception as e:
987
- logging.error(f"Video merge error: {str(e)}")
988
  import traceback
989
- traceback.print_exc()
990
- return None, f"❌ 오류 발생: {str(e)}"
991
-
992
- def test_anim_api_connection():
993
- """애니메이션 서버 연결 테스트"""
994
- now = datetime.now().strftime("%H:%M:%S")
995
- try:
996
- resp = httpx.get(f"{ANIM_API_URL.rstrip('/')}/healthz", timeout=ANIM_TIMEOUT)
997
- ready = resp.json().get("ready", False)
998
- msg = f"[{now}] 애니메이션 서버 연결 성공 ✅ (ready={ready})"
999
- logging.info(msg)
1000
- return True, msg
1001
- except Exception as e:
1002
- msg = f"[{now}] 애니메이션 서버 연결 실패 ❌ : {e}"
1003
- logging.error(msg)
1004
- return False, msg
1005
-
1006
- def generate_avatar_animation(image, audio, guidance_scale, steps, progress=gr.Progress()):
1007
- """이미지와 오디오로 아바타 애니메이션 생성"""
1008
- start = datetime.now().strftime("%H:%M:%S")
1009
- logs = [f"[{start}] 요청 시작"]
1010
-
1011
- try:
1012
- if image is None or audio is None:
1013
- raise ValueError("이미지와 오디오를 모두 업로드하세요.")
1014
-
1015
- progress(0.05, desc="파일 준비")
1016
- client = Client(ANIM_API_URL)
1017
-
1018
- progress(0.15, desc="서버 호출 중… (수 분 소요 가능)")
1019
- result = client.predict(
1020
- image_path=handle_file(image),
1021
- audio_path=handle_file(audio),
1022
- guidance_scale=guidance_scale,
1023
- steps=steps,
1024
- api_name="/generate_animation"
1025
- )
1026
-
1027
- progress(0.95, desc="결과 정리")
1028
-
1029
- # 결과 처리 - dict 형태 처리 추가
1030
- def extract_video_path(obj):
1031
- """비디오 객체에서 경로 추출"""
1032
- if isinstance(obj, str):
1033
- return obj
1034
- elif isinstance(obj, dict):
1035
- # Gradio의 FileData dict 처리
1036
- if 'video' in obj:
1037
- return obj['video'] # {'video': '경로', 'subtitles': None} 형태 처리
1038
- elif 'path' in obj:
1039
- return obj['path']
1040
- elif 'url' in obj:
1041
- return obj['url']
1042
- elif 'name' in obj:
1043
- return obj['name']
1044
- else:
1045
- logging.warning(f"Unexpected dict structure: {obj.keys()}")
1046
- return None
1047
- else:
1048
- logging.warning(f"Unexpected type: {type(obj)}")
1049
- return None
1050
-
1051
- if isinstance(result, (list, tuple)) and len(result) >= 2:
1052
- anim_path = extract_video_path(result[0])
1053
- comp_path = extract_video_path(result[1])
1054
-
1055
- if anim_path and comp_path:
1056
- logs.append(f"[{datetime.now().strftime('%H:%M:%S')}] 성공")
1057
- return anim_path, comp_path, "\n".join(logs)
1058
- else:
1059
- raise RuntimeError(f"비디오 경로 추출 실패: {result}")
1060
- else:
1061
- raise RuntimeError(f"예상치 못한 반환 형식: {type(result)}")
1062
-
1063
- except Exception as e:
1064
- logs.append(f"[{datetime.now().strftime('%H:%M:%S')}] 오류: {e}")
1065
- logging.error(f"Avatar animation generation error: {e}", exc_info=True)
1066
- return None, None, "\n".join(logs)
1067
-
1068
- # CSS
1069
- css = """
1070
- :root {
1071
- --primary-color: #f8c3cd;
1072
- --secondary-color: #b3e5fc;
1073
- --background-color: #f5f5f7;
1074
- --card-background: #ffffff;
1075
- --text-color: #424242;
1076
- --accent-color: #ffb6c1;
1077
- --success-color: #c8e6c9;
1078
- --warning-color: #fff9c4;
1079
- --shadow-color: rgba(0, 0, 0, 0.1);
1080
- --border-radius: 12px;
1081
- }
1082
- .gradio-container {
1083
- max-width: 1200px !important;
1084
- margin: 0 auto !important;
1085
- }
1086
- .panel-box {
1087
- border-radius: var(--border-radius) !important;
1088
- box-shadow: 0 8px 16px var(--shadow-color) !important;
1089
- background-color: var(--card-background) !important;
1090
- padding: 20px !important;
1091
- margin-bottom: 20px !important;
1092
- }
1093
- #generate-btn, #video-btn, #outpaint-btn, #preview-btn, #audio-btn, #bg-remove-btn, #merge-btn, #avatar-btn, #test-connection-btn {
1094
- background: linear-gradient(135deg, #ff9a9e, #fad0c4) !important;
1095
- font-size: 1.1rem !important;
1096
- padding: 12px 24px !important;
1097
- margin-top: 10px !important;
1098
- width: 100% !important;
1099
- }
1100
- #avatar-btn, #test-connection-btn {
1101
- background: linear-gradient(135deg, #667eea, #764ba2) !important;
1102
- }
1103
- .tabitem {
1104
- min-height: 700px !important;
1105
- }
1106
- """
1107
-
1108
- # Gradio Interface
1109
- demo = gr.Blocks(css=css, title="AI 이미지 & 비디오 & 오디오 생성기")
1110
-
1111
- with demo:
1112
- gr.Markdown("# 🎨 Ginigen 스튜디오")
1113
- gr.Markdown("처음 사용 시 모델 로딩에 시간이 걸릴 수 있습니다. 잠시만 기다려주세요.")
1114
-
1115
- # 모델 로드 상태 표시
1116
- model_status = gr.Textbox(label="모델 상태", value="모델 로딩 대기 중...", interactive=False)
1117
-
1118
- with gr.Tabs() as tabs:
1119
- # 첫 번째 탭: 텍스트 to 이미지
1120
- with gr.Tab("텍스트→이미지→비디오", elem_classes="tabitem"):
1121
- with gr.Row(equal_height=True):
1122
- # 입력 컬럼
1123
- with gr.Column(scale=1):
1124
- with gr.Group(elem_classes="panel-box"):
1125
- gr.Markdown("### 📝 이미지 생성 설정")
1126
-
1127
- prompt = gr.Textbox(
1128
- label="프롬프트(한글/영어 가능)",
1129
- placeholder="생성하고 싶은 이미지를 설명하세요...",
1130
- lines=3
1131
- )
1132
-
1133
- size_preset = gr.Dropdown(
1134
- choices=list(IMAGE_PRESETS.keys()),
1135
- value="1:1 정사각형",
1136
- label="크기 프리셋"
1137
- )
1138
-
1139
- with gr.Row():
1140
- width = gr.Slider(256, 2048, 1024, step=64, label="너비")
1141
- height = gr.Slider(256, 2048, 1024, step=64, label="높이")
1142
-
1143
- with gr.Row():
1144
- guidance = gr.Slider(1.0, 20.0, 3.5, step=0.1, label="가이던스")
1145
- steps = gr.Slider(1, 50, 30, step=1, label="스텝")
1146
-
1147
- seed = gr.Number(label="시드 (-1=랜덤)", value=-1)
1148
-
1149
- generate_btn = gr.Button("🎨 이미지 생성", variant="primary", elem_id="generate-btn")
1150
-
1151
- with gr.Group(elem_classes="panel-box"):
1152
- gr.Markdown("### 🎬 비디오 생성 설정")
1153
-
1154
- video_prompt = gr.Textbox(
1155
- label="(선택) 비디오 프롬프트(영어로 입력)",
1156
- placeholder="비디오의 움직임을 설명하세요... (비워두면 기본 움직임 적용)",
1157
- lines=2
1158
- )
1159
-
1160
- video_length = gr.Slider(
1161
- minimum=1,
1162
- maximum=60,
1163
- value=4,
1164
- step=0.5,
1165
- label="비디오 길이 (초)",
1166
- info="1초에서 60초까지 선택 가능합니다"
1167
- )
1168
-
1169
- video_btn = gr.Button("🎬 비디오로 변환", variant="secondary", elem_id="video-btn")
1170
-
1171
- # 출력 컬럼
1172
- with gr.Column(scale=1):
1173
- with gr.Group(elem_classes="panel-box"):
1174
- gr.Markdown("### 🖼️ 생성 결과")
1175
-
1176
- output_image = gr.Image(label="생성된 이미지", type="numpy")
1177
- output_seed = gr.Textbox(label="시드 정보")
1178
- output_video = gr.Video(label="생성된 비디오")
1179
-
1180
- # 두 번째 탭: 이미지 아웃페인팅
1181
- with gr.Tab("이미지 비율 변경/생성", elem_classes="tabitem"):
1182
- with gr.Row(equal_height=True):
1183
- # 입력 컬럼
1184
- with gr.Column(scale=1):
1185
- with gr.Group(elem_classes="panel-box"):
1186
- gr.Markdown("### 🖼️ 이미지 업로드")
1187
-
1188
- input_image = gr.Image(
1189
- label="원본 이미지",
1190
- type="numpy"
1191
- )
1192
-
1193
- outpaint_prompt = gr.Textbox(
1194
- label="프롬프트 (선택)",
1195
- placeholder="확장할 영역에 대한 설명...",
1196
- lines=2
1197
- )
1198
-
1199
- with gr.Group(elem_classes="panel-box"):
1200
- gr.Markdown("### ⚙️ 아웃페인팅 설정")
1201
-
1202
- outpaint_size_preset = gr.Dropdown(
1203
- choices=list(IMAGE_PRESETS.keys()),
1204
- value="16:9 와이드스크린",
1205
- label="목표 크기 프리셋"
1206
- )
1207
-
1208
- with gr.Row():
1209
- outpaint_width = gr.Slider(256, 2048, 1280, step=64, label="목표 너비")
1210
- outpaint_height = gr.Slider(256, 2048, 720, step=64, label="목표 높이")
1211
-
1212
- alignment = gr.Dropdown(
1213
- choices=["가운데", "왼쪽", "오른쪽", "위", "아래"],
1214
- value="가운데",
1215
- label="정렬"
1216
- )
1217
-
1218
- overlap_percentage = gr.Slider(
1219
- minimum=1,
1220
- maximum=50,
1221
- value=10,
1222
- step=1,
1223
- label="마스크 오버랩 (%)"
1224
- )
1225
-
1226
- outpaint_steps = gr.Slider(
1227
- minimum=4,
1228
- maximum=12,
1229
- value=8,
1230
- step=1,
1231
- label="추론 스텝"
1232
- )
1233
-
1234
- preview_btn = gr.Button("👁️ 미리보기", elem_id="preview-btn")
1235
- outpaint_btn = gr.Button("🎨 아웃페인팅 실행", variant="primary", elem_id="outpaint-btn")
1236
-
1237
- # 출력 컬럼
1238
- with gr.Column(scale=1):
1239
- with gr.Group(elem_classes="panel-box"):
1240
- gr.Markdown("### 🖼️ 결과")
1241
-
1242
- preview_image = gr.Image(label="미리보기")
1243
- outpaint_result = gr.Image(label="아웃페인팅 결과")
1244
-
1245
- # 세 번째 탭: 비디오 + 오디오
1246
- with gr.Tab("비디오 + 오디오", elem_classes="tabitem"):
1247
- with gr.Row(equal_height=True):
1248
- # 입력 컬럼
1249
- with gr.Column(scale=1):
1250
- with gr.Group(elem_classes="panel-box"):
1251
- gr.Markdown("### 🎥 비디오 업로드")
1252
-
1253
- audio_video_input = gr.Video(
1254
- label="입력 비디오",
1255
- sources=["upload"]
1256
- )
1257
-
1258
- with gr.Group(elem_classes="panel-box"):
1259
- gr.Markdown("### 🎵 오디오 생성 설정")
1260
-
1261
- audio_prompt = gr.Textbox(
1262
- label="프롬프트 (한글 지원)",
1263
- placeholder="생성하고 싶은 오디오를 설명하세요... (예: 평화로운 피아노 음악)",
1264
- lines=3
1265
- )
1266
-
1267
- audio_negative_prompt = gr.Textbox(
1268
- label="네거티브 프롬프트",
1269
- value="music",
1270
- placeholder="원하지 않는 요소...",
1271
- lines=2
1272
- )
1273
-
1274
- with gr.Row():
1275
- audio_seed = gr.Number(label="시드", value=0)
1276
- audio_steps = gr.Number(label="스텝", value=25)
1277
-
1278
- with gr.Row():
1279
- audio_cfg = gr.Number(label="가이던스 스케일", value=4.5)
1280
- audio_duration = gr.Number(label="지속시간 (초)", value=9999)
1281
-
1282
- audio_btn = gr.Button("🎵 오디오 생성 및 합성", variant="primary", elem_id="audio-btn")
1283
-
1284
- # 출력 컬럼
1285
- with gr.Column(scale=1):
1286
- with gr.Group(elem_classes="panel-box"):
1287
- gr.Markdown("### 🎬 생성 결과")
1288
-
1289
- output_video_with_audio = gr.Video(
1290
- label="오디오가 추가된 비디오",
1291
- interactive=False
1292
- )
1293
-
1294
- # 네 번째 탭: 비디오 편집
1295
- with gr.Tab("비디오 편집", elem_classes="tabitem"):
1296
- with gr.Row(equal_height=True):
1297
- # 입력 컬럼
1298
- with gr.Column(scale=1):
1299
- with gr.Group(elem_classes="panel-box"):
1300
- gr.Markdown("### 🎥 비디오 업로드 (최대 10개)")
1301
- gr.Markdown("**파일명이 작을수록 우선순위가 높습니다** (예: 1.mp4, 2.mp4, 3.mp4)")
1302
-
1303
- video_files = gr.File(
1304
- label="비디오 파일들",
1305
- file_count="multiple",
1306
- file_types=["video"],
1307
- type="filepath"
1308
- )
1309
-
1310
- with gr.Group(elem_classes="panel-box"):
1311
- gr.Markdown("### ⚙️ 편집 설정")
1312
-
1313
- output_fps = gr.Slider(
1314
- minimum=0,
1315
- maximum=60,
1316
- value=0,
1317
- step=1,
1318
- label="출력 FPS (0 = 첫 번째 비디오의 FPS 사용)"
1319
- )
1320
-
1321
- gr.Markdown("""
1322
- **크기 처리**:
1323
- - 첫 번째 비디오의 크기가 기준이 됩니다
1324
- - 다른 크기의 비디오는 첫 번째 비디오 크기로 조정됩니다
1325
- - 최상의 결과를 위해 같은 크기의 비디오를 사용하세요
1326
- """)
1327
-
1328
- with gr.Group(elem_classes="panel-box"):
1329
- gr.Markdown("### 🎵 오디오 설정 (선택)")
1330
-
1331
- # 오디오 모드 선택 추가
1332
- audio_mode = gr.Radio(
1333
- ["대체", "백그라운드 뮤직"],
1334
- label="오디오 모드",
1335
- value="대체",
1336
- info="대체: 기존 오디오를 완전히 교체 | 백그라운드 뮤직: 기존 오디오와 함께 재생"
1337
- )
1338
-
1339
- audio_file = gr.Audio(
1340
- label="오디오 파일 (MP3, WAV, M4A 등)",
1341
- type="filepath",
1342
- sources=["upload"]
1343
- )
1344
-
1345
- audio_volume = gr.Slider(
1346
- minimum=0,
1347
- maximum=200,
1348
- value=100,
1349
- step=1,
1350
- label="추가 오디오 볼륨 (%)",
1351
- info="100% = 원본 볼륨"
1352
- )
1353
-
1354
- # 백그라운드 모드일 때만 보이는 원본 오디오 볼륨 조절
1355
- original_audio_volume = gr.Slider(
1356
- minimum=0,
1357
- maximum=200,
1358
- value=100,
1359
- step=1,
1360
- label="원본 오디오 볼륨 (%)",
1361
- info="백그라운드 뮤직 모드에서 원본 비디오 오디오의 볼륨",
1362
- visible=False
1363
- )
1364
-
1365
- gr.Markdown("""
1366
- **오디오 옵션**:
1367
- - **대체 모드**: 업로드한 오디오가 비디오의 기존 오디오를 완전히 대체합니다
1368
- - **백그라운드 뮤직 모드**: 업로드한 오디오가 기존 오디오와 함께 재생됩니다
1369
- - 오디오가 비디오보다 짧으면 자동으로 반복됩니다
1370
- - 오디오가 비디오보다 길면 비디오 길이에 맞춰 잘립니다
1371
- """)
1372
-
1373
- merge_videos_btn = gr.Button("🎬 비디오 병합", variant="primary", elem_id="merge-btn")
1374
-
1375
- # 출력 컬럼
1376
- with gr.Column(scale=1):
1377
- with gr.Group(elem_classes="panel-box"):
1378
- gr.Markdown("### 🎬 병합 결과")
1379
-
1380
- merge_status = gr.Textbox(label="처리 상태", interactive=False)
1381
- merged_video = gr.Video(label="병합된 비디오")
1382
-
1383
- gr.Markdown("""
1384
- ### ℹ️ 사용 방법
1385
- 1. 여러 비디오 파일을 업로드하세요 (최대 10개)
1386
- 2. 파일명이 작은 순서대로 자동 정렬됩니다
1387
- 3. (선택) 오디오 파일을 추가하고 볼륨을 조절하세요
1388
- 4. '비디오 병합' 버튼을 클릭하세요
1389
-
1390
- **특징**:
1391
- - ✅ 첫 번째 비디오의 크기를 기준으로 통합
1392
- - ✅ 업로드한 오디오가 전체 비디오에 적용됩니다
1393
- - ✅ 높은 비트레이트로 품질 유지
1394
-
1395
- **팁**:
1396
- - 파일명을 01.mp4, 02.mp4, 03.mp4 형식으로 지정하면 순서 관리가 쉽습니다
1397
- - 오디오를 추가하면 기존 비디오의 오디오는 대체됩니다
1398
- """)
1399
-
1400
- # 다섯 번째 탭: 비디오 배경제거/합성
1401
- with gr.Tab("비디오 배경제거/합성", elem_classes="tabitem"):
1402
- with gr.Row(equal_height=True):
1403
- # 입력 컬럼
1404
- with gr.Column(scale=1):
1405
- with gr.Group(elem_classes="panel-box"):
1406
- gr.Markdown("### 🎥 비디오 업로드")
1407
-
1408
- bg_video_input = gr.Video(
1409
- label="입력 비디오",
1410
- interactive=True
1411
- )
1412
-
1413
- with gr.Group(elem_classes="panel-box"):
1414
- gr.Markdown("### 🎨 배경 설정")
1415
-
1416
- bg_type = gr.Radio(
1417
- ["색상", "이미지", "비디오"],
1418
- label="배경 유형",
1419
- value="색상",
1420
- interactive=True
1421
- )
1422
-
1423
- color_picker = gr.ColorPicker(
1424
- label="배경 색상",
1425
- value="#00FF00",
1426
- visible=True,
1427
- interactive=True
1428
- )
1429
-
1430
- bg_image_input = gr.Image(
1431
- label="배경 이미지",
1432
- type="filepath",
1433
- visible=False,
1434
- interactive=True
1435
- )
1436
-
1437
- bg_video_bg = gr.Video(
1438
- label="배경 비디오",
1439
- visible=False,
1440
- interactive=True
1441
- )
1442
-
1443
- with gr.Column(visible=False) as video_handling_options:
1444
- video_handling_radio = gr.Radio(
1445
- ["slow_down", "loop"],
1446
- label="비디오 처리 방식",
1447
- value="slow_down",
1448
- interactive=True,
1449
- info="slow_down: 배경 비디오를 느리게 재생, loop: 배경 비디오를 반복"
1450
- )
1451
-
1452
- with gr.Group(elem_classes="panel-box"):
1453
- gr.Markdown("### ⚙️ 처리 설정")
1454
-
1455
- fps_slider = gr.Slider(
1456
- minimum=0,
1457
- maximum=60,
1458
- step=1,
1459
- value=0,
1460
- label="출력 FPS (0 = 원본 FPS 유지)",
1461
- interactive=True
1462
- )
1463
-
1464
- fast_mode_checkbox = gr.Checkbox(
1465
- label="빠른 모드 (BiRefNet_lite 사용)",
1466
- value=True,
1467
- interactive=True
1468
- )
1469
-
1470
- max_workers_slider = gr.Slider(
1471
- minimum=1,
1472
- maximum=32,
1473
- step=1,
1474
- value=10,
1475
- label="최대 워커 수",
1476
- info="병렬로 처리할 프레임 수",
1477
- interactive=True
1478
- )
1479
-
1480
- bg_remove_btn = gr.Button("🎬 배경 변경", variant="primary", elem_id="bg-remove-btn")
1481
-
1482
- # 출력 컬럼
1483
- with gr.Column(scale=1):
1484
- with gr.Group(elem_classes="panel-box"):
1485
- gr.Markdown("### 🎬 처리 결과")
1486
-
1487
- stream_image = gr.Image(label="실시간 스트리밍", visible=False)
1488
- output_bg_video = gr.Video(label="최종 비디오")
1489
- time_textbox = gr.Textbox(label="경과 시간", interactive=False)
1490
-
1491
- gr.Markdown("""
1492
- ### ℹ️ 사용 방법
1493
- 1. 비디오를 업로드하세요
1494
- 2. 원하는 배경 유형을 선택하세요
1495
- 3. 설정을 조정하고 '배경 변경' 버튼을 클릭하세요
1496
-
1497
- **참고**: GPU 제한으로 한 번에 약 200프레임까지 처리 가능합니다.
1498
- 긴 비디오는 작은 조각으로 나누어 처리하세요.
1499
- """)
1500
-
1501
- # 여섯 번째 탭: 이미지to아바타 (중복 제거하고 하나만 유지)
1502
- with gr.Tab("이미지to아바타", elem_classes="tabitem"):
1503
- with gr.Row(equal_height=True):
1504
- # 입력 컬럼
1505
- with gr.Column(scale=1):
1506
- with gr.Group(elem_classes="panel-box"):
1507
- gr.Markdown("### 🎭 아바타 애니메이션 생성")
1508
- gr.Markdown("""
1509
- 포트레이트 이미지와 오디오를 업로드하면 말하는 아바타 애니메이션을 생성합니다.
1510
-
1511
- **권장 사항**:
1512
- - 이미지: 정면을 보고 있는 얼굴 사진
1513
- - 오디오: 명확한 음성이 담긴 오디오 파일
1514
- """)
1515
-
1516
- avatar_image = gr.Image(
1517
- label="포트레이트 이미지",
1518
- type="filepath",
1519
- elem_classes="panel-box"
1520
- )
1521
-
1522
- avatar_audio = gr.Audio(
1523
- label="드라이빙 오디오",
1524
- type="filepath",
1525
- elem_classes="panel-box"
1526
- )
1527
-
1528
- with gr.Group(elem_classes="panel-box"):
1529
- gr.Markdown("### ⚙️ ��성 설정")
1530
-
1531
- guidance_scale = gr.Slider(
1532
- minimum=1.0,
1533
- maximum=10.0,
1534
- value=3.0,
1535
- step=0.1,
1536
- label="가이던스 스케일",
1537
- info="높을수록 오디오에 더 충실한 움직임 생성"
1538
- )
1539
-
1540
- inference_steps = gr.Slider(
1541
- minimum=5,
1542
- maximum=30,
1543
- value=10,
1544
- step=1,
1545
- label="추론 스텝",
1546
- info="높을수록 품질이 좋아지지만 생성 시간이 증가"
1547
- )
1548
-
1549
- # 서버 상태 체크
1550
- with gr.Row():
1551
- test_connection_btn = gr.Button(
1552
- "🔌 서버 연결 테스트",
1553
- elem_id="test-connection-btn",
1554
- scale=1
1555
- )
1556
-
1557
- anim_status = gr.Textbox(
1558
- label="서버 상태",
1559
- interactive=False,
1560
- elem_classes="panel-box"
1561
- )
1562
-
1563
- generate_avatar_btn = gr.Button(
1564
- "🎬 아바타 생성",
1565
- variant="primary",
1566
- elem_id="avatar-btn"
1567
- )
1568
-
1569
- # 출력 컬럼
1570
- with gr.Column(scale=1):
1571
- with gr.Group(elem_classes="panel-box"):
1572
- gr.Markdown("### 🎭 생성 결과")
1573
-
1574
- avatar_result = gr.Video(
1575
- label="애니메이션 결과",
1576
- elem_classes="panel-box"
1577
- )
1578
-
1579
- avatar_comparison = gr.Video(
1580
- label="원본 대비 결과 (Side-by-Side)",
1581
- elem_classes="panel-box"
1582
- )
1583
-
1584
- with gr.Accordion("실행 로그", open=False):
1585
- avatar_logs = gr.Textbox(
1586
- label="로그",
1587
- lines=10,
1588
- max_lines=20,
1589
- interactive=False,
1590
- elem_classes="panel-box"
1591
- )
1592
-
1593
- gr.Markdown("""
1594
- ### ℹ️ 사용 안내
1595
-
1596
- 1. **포트레이트 이미지 업로드**: 정면을 보고 있는 선명한 얼굴 사진
1597
- 2. **오디오 업로드**: 애니메이션에 사용할 음성 파일
1598
- 3. **설정 조정**: 가이던스 스케일과 추론 스텝 조정
1599
- 4. **생성 시작**: '아바타 생성' 버튼 클릭
1600
-
1601
- **처리 시간**:
1602
- - 일반적으로 2-5분 소요
1603
- - 긴 오디오일수록 처리 시간 증가
1604
-
1605
- **팁**:
1606
- - 배경이 단순한 이미지가 더 좋은 결과를 생성합니다
1607
- - 오디오의 음성이 명확할수록 립싱크가 정확합니다
1608
- """)
1609
-
1610
- # 모델 로드 함수 실행
1611
- def on_demo_load():
1612
- try:
1613
- if IS_SPACES:
1614
- # Spaces 환경에서 GPU 워밍업
1615
- gpu_warmup()
1616
- # 모델 로드는 첫 번째 GPU 함수 호출 시 자동으로 수행됨
1617
- return "모델 로딩 준비 완료"
1618
- except Exception as e:
1619
- return f"초기화 오류: {str(e)}"
1620
-
1621
- # 이벤트 연결 - 첫 번째 탭
1622
- size_preset.change(update_dimensions, [size_preset], [width, height])
1623
-
1624
- generate_btn.click(
1625
- generate_text_to_image,
1626
- [prompt, width, height, guidance, steps, seed],
1627
- [output_image, output_seed]
1628
- )
1629
-
1630
- video_btn.click(
1631
- lambda img, v_prompt, length: generate_video_from_image(img, v_prompt, length) if img is not None else None,
1632
- [output_image, video_prompt, video_length],
1633
- [output_video]
1634
- )
1635
-
1636
- # 이벤트 연결 - 두 번째 탭
1637
- outpaint_size_preset.change(update_dimensions, [outpaint_size_preset], [outpaint_width, outpaint_height])
1638
-
1639
- preview_btn.click(
1640
- preview_outpaint,
1641
- [input_image, outpaint_width, outpaint_height, overlap_percentage, alignment],
1642
- [preview_image]
1643
- )
1644
-
1645
- outpaint_btn.click(
1646
- outpaint_image,
1647
- [input_image, outpaint_prompt, outpaint_width, outpaint_height, overlap_percentage, alignment, outpaint_steps],
1648
- [outpaint_result]
1649
- )
1650
-
1651
- # 이벤트 연결 - 세 번째 탭
1652
- audio_btn.click(
1653
- video_to_audio,
1654
- [audio_video_input, audio_prompt, audio_negative_prompt, audio_seed, audio_steps, audio_cfg, audio_duration],
1655
- [output_video_with_audio]
1656
- )
1657
-
1658
- # 이벤트 연결 - 네 번째 탭 (비디오 편집)
1659
- def toggle_original_volume(mode):
1660
- return gr.update(visible=(mode == "백그라운드 뮤직"))
1661
-
1662
- audio_mode.change(
1663
- toggle_original_volume,
1664
- inputs=[audio_mode],
1665
- outputs=[original_audio_volume]
1666
- )
1667
-
1668
- merge_videos_btn.click(
1669
- merge_videos_with_audio,
1670
- inputs=[video_files, audio_file, audio_mode, audio_volume, original_audio_volume, output_fps],
1671
- outputs=[merged_video, merge_status]
1672
- )
1673
-
1674
- # 이벤트 연결 - 다섯 번째 탭 (비디오 배경제거/합성)
1675
- def update_bg_visibility(bg_type):
1676
- if bg_type == "색상":
1677
- return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
1678
- elif bg_type == "이미지":
1679
- return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
1680
- elif bg_type == "비디오":
1681
- return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=True)
1682
- else:
1683
- return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
1684
-
1685
- bg_type.change(
1686
- update_bg_visibility,
1687
- inputs=bg_type,
1688
- outputs=[color_picker, bg_image_input, bg_video_bg, video_handling_options]
1689
- )
1690
-
1691
- bg_remove_btn.click(
1692
- process_video_bg,
1693
- inputs=[bg_video_input, bg_type, bg_image_input, bg_video_bg, color_picker,
1694
- fps_slider, video_handling_radio, fast_mode_checkbox, max_workers_slider],
1695
- outputs=[stream_image, output_bg_video, time_textbox]
1696
- )
1697
-
1698
- # 이벤트 연결 - 여섯 번째 탭 (이미지to아바타)
1699
- test_connection_btn.click(
1700
- test_anim_api_connection,
1701
- outputs=[anim_status, anim_status]
1702
- )
1703
-
1704
- generate_avatar_btn.click(
1705
- generate_avatar_animation,
1706
- inputs=[avatar_image, avatar_audio, guidance_scale, inference_steps],
1707
- outputs=[avatar_result, avatar_comparison, avatar_logs]
1708
- )
1709
-
1710
- # 데모 로드 시 실행
1711
- demo.load(on_demo_load, outputs=model_status)
1712
 
1713
  if __name__ == "__main__":
1714
- # Spaces 환경에서 추가 체크
1715
- if IS_SPACES:
1716
- try:
1717
- gpu_warmup()
1718
- except:
1719
- pass
1720
-
1721
- demo.launch()
 
 
1
  import os
2
+ import sys
3
+ import streamlit as st
4
+ from tempfile import NamedTemporaryFile
5
 
6
+ def main():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  try:
8
+ # Get the code from secrets
9
+ code = os.environ.get("MAIN_CODE")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ if not code:
12
+ st.error("⚠️ The application code wasn't found in secrets. Please add the MAIN_CODE secret.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  return
14
 
15
+ # Create a temporary Python file
16
+ with NamedTemporaryFile(suffix='.py', delete=False, mode='w') as tmp:
17
+ tmp.write(code)
18
+ tmp_path = tmp.name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ # Execute the code
21
+ exec(compile(code, tmp_path, 'exec'), globals())
22
 
23
+ # Clean up the temporary file
24
+ try:
25
+ os.unlink(tmp_path)
26
+ except:
27
+ pass
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  except Exception as e:
30
+ st.error(f"⚠️ Error loading or executing the application: {str(e)}")
31
  import traceback
32
+ st.code(traceback.format_exc())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  if __name__ == "__main__":
35
+ main()