import os import subprocess from .db import get_compile_job, get_track_file_path, get_track_by_id, update_compile_job VIDEO_DATA_DIR = os.getenv("VIDEO_DATA_DIR", "/app/data/videos") def _build_concat_cmd(file_paths: list[str], crossfade_sec: float, output_path: str) -> list: """FFmpeg command: N audio files → single audio with acrossfade.""" n = len(file_paths) if n == 1: # Single track: just copy return ["ffmpeg", "-y", "-i", file_paths[0], "-c:a", "libmp3lame", "-q:a", "2", output_path] cmd = [] for fp in file_paths: cmd += ["-i", fp] # Build acrossfade filter chain filter_parts = [] prev = "0" for i in range(1, n): out_label = f"a{i:02d}" filter_parts.append(f"[{prev}][{i}]acrossfade=d={crossfade_sec}:c1=tri:c2=tri[{out_label}]") prev = out_label filter_str = ";".join(filter_parts) return ( ["ffmpeg", "-y"] + cmd + ["-filter_complex", filter_str, "-map", f"[{prev}]", "-c:a", "libmp3lame", "-q:a", "2", output_path] ) def _make_gradient_bg(width: int, height: int, output_path: str) -> None: """Simple dark gradient background image via FFmpeg.""" subprocess.run( [ "ffmpeg", "-y", "-f", "lavfi", "-i", f"color=c=0x111827:size={width}x{height}:rate=1", "-frames:v", "1", output_path, ], check=True, capture_output=True, ) def _get_audio_duration(path: str) -> float: """Return duration in seconds via ffprobe.""" try: result = subprocess.run( ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path], capture_output=True, text=True, check=True, ) return float(result.stdout.strip()) except Exception: return 0.0 def run_compile(job_id: int) -> None: """Main compile worker — called as BackgroundTask.""" job = get_compile_job(job_id) if not job: return update_compile_job(job_id, status="rendering") try: track_ids = job["track_ids"] if not track_ids: raise ValueError("트랙이 선택되지 않았습니다") # Resolve file paths file_paths = [] for tid in track_ids: fp = get_track_file_path(tid) if not fp or not os.path.exists(fp): raise ValueError(f"트랙 파일 없음 (id={tid})") file_paths.append(fp) out_dir = os.path.join(VIDEO_DATA_DIR, f"compile_{job_id}") os.makedirs(out_dir, exist_ok=True) # Step 1: concat audio audio_path = os.path.join(out_dir, "audio.mp3") concat_cmd = _build_concat_cmd(file_paths, job["crossfade_sec"], audio_path) subprocess.run(concat_cmd, check=True, capture_output=True) duration = _get_audio_duration(audio_path) # Step 2: background image bg_path = os.path.join(out_dir, "bg.jpg") _make_gradient_bg(1920, 1080, bg_path) # Step 3: audio + bg → MP4 output_path = os.path.join(out_dir, "output.mp4") subprocess.run( [ "ffmpeg", "-y", "-loop", "1", "-i", bg_path, "-i", audio_path, "-c:v", "libx264", "-tune", "stillimage", "-preset", "fast", "-c:a", "aac", "-b:a", "192k", "-pix_fmt", "yuv420p", "-shortest", output_path, ], check=True, capture_output=True, ) update_compile_job(job_id, status="done", output_path=output_path, duration_sec=duration) except subprocess.CalledProcessError as e: err = (e.stderr or b"").decode()[-300:] update_compile_job(job_id, status="failed", error=err) except Exception as e: update_compile_job(job_id, status="failed", error=str(e))