115 lines
4.1 KiB
Python
115 lines
4.1 KiB
Python
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 |