"""Animily Music - 영상 렌더링 이미지 + 음악 + 워터마크 → MP4 영상. Ken Burns 효과 + ANIMILY 워터마크 (우측 상단, 영상 전체). """ import os import subprocess from config import ( OUTPUT_DIR, WATERMARK_TEXT, WATERMARK_FONT, WATERMARK_SIZE, ) def render_video(image_path, audio_path, output_path, title=None): """이미지 + 오디오 → 워터마크 포함 영상 렌더링 Args: image_path: 배경 이미지 (1920x1080) audio_path: 오디오 파일 output_path: 출력 MP4 경로 title: (미사용, 향후 텍스트 오버레이용) Returns: bool: 성공 여부 """ # 오디오 길이 측정 probe = subprocess.run( ["ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", audio_path], capture_output=True, text=True, ) try: duration = float(probe.stdout.strip()) except ValueError: print(f" [RENDER] 오디오 길이 측정 실패", flush=True) return False print(f" [RENDER] 영상 렌더링 시작 (길이: {duration/60:.1f}분)", flush=True) # Ken Burns: 매우 느린 줌인 (119분 동안 1.0 → 1.08 정도) # zoompan fps=30, duration=전체 프레임 수 total_frames = int(duration * 30) # 워터마크 필터 watermark_filter = ( f"drawtext=text='{WATERMARK_TEXT}':" f"fontfile={WATERMARK_FONT}:" f"fontsize={WATERMARK_SIZE}:" f"fontcolor=white@0.4:" f"x=w-tw-80:y=40:" f"shadowcolor=black@0.3:shadowx=1:shadowy=1" ) # Ken Burns + 워터마크 결합 # zoompan으로 미세 줌인 효과 video_filter = ( f"zoompan=z='min(zoom+0.00001,1.08)':" f"d={total_frames}:s=1920x1080:fps=30," f"{watermark_filter}" ) cmd = [ "ffmpeg", "-y", "-loop", "1", "-i", image_path, "-i", audio_path, "-vf", video_filter, "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-tune", "stillimage", "-c:a", "aac", "-b:a", "192k", "-shortest", "-pix_fmt", "yuv420p", output_path, ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=7200) if result.returncode != 0: print(f" [RENDER] ffmpeg 에러: {result.stderr[:500]}", flush=True) # 폴백: Ken Burns 없이 단순 정지 이미지 cmd_simple = [ "ffmpeg", "-y", "-loop", "1", "-i", image_path, "-i", audio_path, "-vf", watermark_filter, "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-tune", "stillimage", "-c:a", "aac", "-b:a", "192k", "-shortest", "-pix_fmt", "yuv420p", output_path, ] result = subprocess.run(cmd_simple, capture_output=True, text=True, timeout=7200) if result.returncode != 0: print(f" [RENDER] 폴백도 실패: {result.stderr[:300]}", flush=True) return False size_mb = os.path.getsize(output_path) / (1024 * 1024) print(f" [RENDER] 완료: {output_path} ({size_mb:.0f}MB)", flush=True) return True def extend_video(short_video, target_hours, output_path): """��은 영상을 반복하여 긴 영상 생성 Args: short_video: 원본 영상 (1h or 2h) target_hours: 목표 시간 output_path: 출력 경로 Returns: bool: 성공 여부 """ # 원본 길이 측정 probe = subprocess.run( ["ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", short_video], capture_output=True, text=True, ) src_duration = float(probe.stdout.strip()) target_seconds = target_hours * 3600 # 필요 반복 횟�� repeats = int(target_seconds / src_duration) + 1 print(f" [RENDER] 영상 반복: {repeats}회 → {target_hours}시간", flush=True) # concat 파일 생성 concat_file = os.path.join(OUTPUT_DIR, "_concat_list.txt") with open(concat_file, "w") as f: for _ in range(repeats): f.write(f"file '{short_video}'\n") cmd = [ "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, "-t", str(int(target_seconds)), "-c", "copy", output_path, ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600) os.remove(concat_file) if result.returncode != 0: print(f" [RENDER] 반복 영상 실패: {result.stderr[:300]}", flush=True) return False size_mb = os.path.getsize(output_path) / (1024 * 1024) print(f" [RENDER] 반복 영상 완료: {size_mb:.0f}MB ({target_hours}h)", flush=True) return True def concat_videos(video_paths, output_path): """여러 영상을 순서대로 이어붙이기 (12시간용: 1h+2h 반복) Args: video_paths: 이어붙일 영상 리스트 output_path: 출력 경로 Returns: bool: 성공 여부 """ concat_file = os.path.join(OUTPUT_DIR, "_concat_mix.txt") with open(concat_file, "w") as f: for vp in video_paths: f.write(f"file '{vp}'\n") cmd = [ "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, "-c", "copy", output_path, ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600) os.remove(concat_file) if result.returncode != 0: print(f" [RENDER] concat 실패: {result.stderr[:300]}", flush=True) return False size_mb = os.path.getsize(output_path) / (1024 * 1024) print(f" [RENDER] concat 완료: {size_mb:.0f}MB", flush=True) return True if __name__ == "__main__": print("render_video.py - 직접 실행은 scheduler.py를 사용하세요")