import os import uuid import moviepy.editor as mp from PIL import Image import gradio as gr # ------------------------------- # 내부 처리 함수들 # ------------------------------- global_logs = [] def add_log(message: str): global_logs.append(message) print(message) def parse_time_to_seconds(time_str: str): try: h, m, s = time_str.split(":") return int(h)*3600 + int(m)*60 + float(s) except Exception: return 0.0 def generate_thumbnail(video_clip, time_point): try: frame = video_clip.get_frame(time_point) return Image.fromarray(frame) except Exception as e: add_log(f"[ERROR] 썸네일 생성 실패: {e}") frame = video_clip.get_frame(0) return Image.fromarray(frame) def adjust_aspect_ratio(clip, option): if option == "원본 유지": return clip if option == "유튜브 (16:9)": target_ratio = 16/9 elif option == "쇼츠/릴스 (9:16)": target_ratio = 9/16 elif option == "정사각형 (1:1)": target_ratio = 1.0 elif option == "인스타그램 (4:5)": target_ratio = 4/5 elif option == "클래식 (4:3)": target_ratio = 4/3 else: return clip width, height = clip.size current_ratio = width/height if current_ratio > target_ratio: new_width = int(height * target_ratio) new_height = height else: new_width = width new_height = int(width / target_ratio) return clip.crop(x_center=width/2, y_center=height/2, width=new_width, height=new_height) def process_video(video, start_time_str, end_time_str, platform_option, frame_rate_factor, speed_factor, repeat_count, resolution_scale): global global_logs global_logs = [] add_log("🎥 [LOG] 영상 처리 시작") video_path = video if isinstance(video, str) else video.name try: input_video = mp.VideoFileClip(video_path) except Exception as e: add_log(f"[ERROR] 비디오 로드 실패: {e}") return None, None, "\n".join(global_logs) duration = input_video.duration add_log(f"[LOG] 영상 재생시간: {duration:.2f}초") start_sec = parse_time_to_seconds(start_time_str) end_sec = parse_time_to_seconds(end_time_str) if start_sec < 0: start_sec = 0 if end_sec <= 0 or end_sec > duration: end_sec = duration if start_sec >= end_sec: start_sec, end_sec = 0, duration add_log(f"[LOG] 시간 설정: {start_sec}초 ~ {end_sec}초") clip = input_video.subclip(start_sec, end_sec) add_log(f"[LOG] 플랫폼 비율 적용: {platform_option}") clip = adjust_aspect_ratio(clip, platform_option) if abs(resolution_scale - 1.0) > 1e-3: add_log(f"[LOG] 해상도 축소: {resolution_scale*100:.0f}%") clip = clip.resize(resolution_scale) if abs(speed_factor - 1.0) > 1e-3: add_log(f"[LOG] 재생속도 {speed_factor}배 적용") clip = clip.fx(mp.vfx.speedx, speed_factor) original_fps = clip.fps target_fps = original_fps * frame_rate_factor add_log(f"[LOG] FPS: {target_fps:.2f} (원본 {original_fps})") clip = clip.set_fps(target_fps) add_log(f"[LOG] GIF 반복 횟수: {repeat_count} (0: 무한)") output_filename = f"temp_{uuid.uuid4().hex}.gif" try: loop_param = 0 if int(repeat_count) == 0 else int(repeat_count) clip.write_gif(output_filename, fps=target_fps, loop=loop_param) add_log(f"[LOG] GIF 생성 완료: {output_filename}, loop={loop_param}") except Exception as e: add_log(f"[ERROR] GIF 생성 실패: {e}") return None, None, "\n".join(global_logs) return output_filename, output_filename, "\n".join(global_logs) def update_thumbnails(video, start_time_str, end_time_str): video_path = video if isinstance(video, str) else video.name try: input_video = mp.VideoFileClip(video_path) except Exception as e: add_log(f"[ERROR] 비디오 로드 실패: {e}") return None, None duration = input_video.duration start_sec = parse_time_to_seconds(start_time_str) end_sec = parse_time_to_seconds(end_time_str) if start_sec < 0: start_sec = 0 if end_sec <= 0 or end_sec > duration: end_sec = duration if start_sec >= end_sec: start_sec, end_sec = 0, duration start_thumb = generate_thumbnail(input_video, start_sec) end_thumb = generate_thumbnail(input_video, end_sec) return start_thumb, end_thumb # ------------------------------- # Custom CSS (글자 크기 크게, 회색 제거, 포인트 버튼 등) # ------------------------------- custom_css = """ body { font-size: 1.6em; /* 글자 크기 증가 */ background-color: #ffffff; font-family: 'Arial', sans-serif; } .gradio-container { width: 80% !important; margin: 0 auto; background-color: #ffffff; /* 회색 제거 */ } /* 제목 및 사용가이드 (왼쪽 정렬, 크게) */ .custom-title { font-size: 3.5em; /* 글자 크기 증가 */ font-weight: bold; margin: 20px 0; color: #2c3e50; text-align: left; } .custom-user-guide { font-size: 1.8em; /* 글자 크기 증가 */ margin-bottom: 20px; color: #2c3e50; text-align: left; } /* 사용가이드 박스 스타일 (회색 제거, 흰색 배경) */ .guide-box { border: 3px solid #3498db; /* 테두리 두께 증가 */ border-radius: 10px; padding: 20px; /* 패딩 증가 */ background-color: #ffffff; margin: 20px 0; text-align: left; font-size: 1.2em; /* 글자 크기 증가 */ } /* 프레임 스타일 */ .frame { border: 3px solid #3498db; /* 테두리 두께 증가 */ border-radius: 20px; padding: 25px; /* 패딩 증가 */ background-color: #ffffff; margin: 15px; box-shadow: 3px 3px 10px rgba(0,0,0,0.1); } /* 각 프레임 제목 크게 */ .frame h3 { font-size: 2.2em; /* 글자 크기 증가 */ margin-bottom: 20px; text-align: left; } /* 행 및 컬럼 레이아웃 */ .row-container { display: flex; justify-content: space-between; } .column { flex: 1; margin: 10px; } /* 포인트 버튼 스타일 */ .gif-button { margin-top: 35px; padding: 20px 35px; /* 패딩 증가 */ font-size: 1.8em; /* 글자 크기 증가 */ background-color: #e67e22; color: #fff; border: none; border-radius: 12px; cursor: pointer; } /* 슬라이더 스타일 (글자와 바 크게) */ input[type=range] { height: 30px; /* 높이 증가 */ } input[type=range]::-webkit-slider-thumb { height: 30px; /* 높이 증가 */ width: 30px; /* 너비 증가 */ } .slider-label { font-size: 1.6em; /* 글자 크기 증가 */ margin-bottom: 8px; } /* 모든 input, select, button 글자 크기 키움 */ input, button, select, textarea { font-size: 1.5em; /* 글자 크기 증가 */ } /* 라벨 글자 크기 키움 */ label, .label-wrap span { font-size: 1.3em !important; /* 글자 크기 증가 */ font-weight: bold; } /* 회색 배경 제거 */ .gradio-container .prose, .gradio-container .gr-box, .gradio-container .gr-form, .gradio-container .gr-panel { background-color: #ffffff !important; } .gradio-container .gr-form, .gradio-container .gr-group { border-color: #3498db; background-color: #ffffff !important; } .gradio-container .gr-input, .gradio-container .gr-checkbox, .gradio-container .gr-radio, .gradio-container .gr-dropdown { font-size: 1.4em; /* 글자 크기 증가 */ } """ # ------------------------------- # Gradio UI 구성 (HTML/CSS 커스터마이징) # ------------------------------- with gr.Blocks(css=custom_css, title="영상 -> GIF 변환 서비스") as demo: # 제목 및 사용가이드 (왼쪽 정렬) gr.HTML("