from moviepy.editor import AudioFileClip, ImageClip, VideoFileClip, AudioFileClip, concatenate_videoclips from moviepy.video.fx.fadein import fadein from moviepy.video.fx.fadeout import fadeout import os from app.shared.logger import setup_logger logger = setup_logger(__name__) class MoviepyService: def __init__(self): self.save_dir = "./uploads/" def create_video_from_existing_images( self, image_paths: list[str], music_path: str = None, output_filename: str = "final_video.mp4", progress_id: str = None ) -> str: try: if not image_paths: raise ValueError("image_paths가 비어 있습니다.") os.makedirs(self.save_dir, exist_ok=True) logger.info(f"[moviepy] {len(image_paths)}장의 이미지로 70초 영상 생성 시작") # ✅ 영상 전체 길이 고정 total_duration = 70.0 # 1분 10초 image_duration = total_duration / len(image_paths) fade_duration = min(1.0, image_duration * 0.15) # ✅ 오디오 로드 (선택 사항) audio = None if music_path and os.path.exists(music_path): try: audio = AudioFileClip(music_path) logger.info(f"[moviepy] 음악 로드 성공 (길이: {audio.duration:.2f}s)") except Exception as e: logger.warning(f"[moviepy] 음악 로드 실패 → 무음 처리됨: {e}") audio = None # ✅ 이미지 클립 생성 clips = [] for i, path in enumerate(image_paths): clip = ( ImageClip(path) .set_duration(image_duration) .resize(height=1080) .fx(fadein, fade_duration) .fx(fadeout, fade_duration) ) clips.append(clip) logger.info(f"[moviepy] 이미지 클립 {i+1}/{len(image_paths)} 생성 완료") if progress_id: from app.shared.progress import set_progress set_progress(progress_id, int(60 + (i + 1) / len(image_paths) * 30)) # ✅ 비디오 합치기 final_clip = concatenate_videoclips(clips, method="compose") if audio: final_clip = final_clip.set_audio(audio) # ✅ 저장 output_path = os.path.join(self.save_dir, output_filename) final_clip.write_videofile( output_path, fps=24, codec="libx264", audio_codec="aac" if audio else None, threads=4, verbose=False, logger=None ) logger.info(f"[moviepy] 비디오 생성 완료: {output_path}") return output_path except Exception as e: logger.error(f"[moviepy] 비디오 생성 실패: {e}") raise def merge_video_and_audio( self, video_path: str, audio_path: str, output_filename: str = "final_merged_video.mp4" ) -> str: try: if not os.path.exists(video_path): raise FileNotFoundError(f"비디오 파일이 존재하지 않습니다: {video_path}") if not os.path.exists(audio_path): raise FileNotFoundError(f"오디오 파일이 존재하지 않습니다: {audio_path}") video = VideoFileClip(video_path) audio = AudioFileClip(audio_path) final_clip = video.set_audio(audio) output_path = os.path.join(self.save_dir, output_filename) final_clip.write_videofile( output_path, codec="libx264", audio_codec="aac", threads=4, fps=24, verbose=False, logger=None ) logger.info(f"[moviepy] 오디오 합성 완료: {output_path}") return output_path except Exception as e: logger.error(f"[moviepy] 영상과 오디오 합성 실패: {e}") raise