영상 분위기 자동 분석 후 AI BGM 생성 & 싱크 맞추기
영상 분위기를 Vision LLM으로 분석하고 MusicGen으로 BGM 생성 후 FFmpeg으로 자동 싱크
2026.04.13
MusicGenerationMusicGenAudioCraftVideoEditingFFmpeg
[미검증]
📌 0. 시리즈
| 응용편 | 제목 | 난이도 | 핵심 기술 |
|---|---|---|---|
| 응용 1 | 사진 10장으로 AI 캐릭터·프로필 이미지 만들기 | ⭐⭐⭐ | FLUX.1 dev · LoRA · ComfyUI |
| 응용 2 | 내 목소리 AI 클론 — 유튜브 내레이터 자동화 | ⭐⭐⭐ | F5-TTS · Kokoro · Sesame CSM-1B |
| 응용 3 | 상품 이미지 1장 → 15초 광고 영상 자동 생성 | ⭐⭐⭐⭐ | Wan2.2 · HunyuanVideo 1.5 · LTX-2 |
| 응용 4 | 공장 불량 자동 검사 — NG/OK 탐지 + 로봇 좌표 추출 | ⭐⭐~⭐⭐⭐⭐⭐ | YOLOv12 · OpenCV · RealSense |
| 응용 5 | 영상 분위기 분석 → BGM 자동 생성 & 싱크 | ⭐⭐⭐ | MusicGen · AudioCraft · Stable Audio Open |
| 응용 6 | 주제 한 줄 입력으로 유튜브 영상 완성 — AI 콘텐츠 자동화 파이프라인 | ⭐⭐⭐⭐ | LangGraph · CrewAI · AutoGen 0.4 |
| 응용 7 | 사진 보고 글 쓰는 AI — Vision LLM 상세페이지 자동 작성 | ⭐⭐⭐⭐ | Qwen2.5-VL · InternVL3 · LLaVA-Next |
| 응용 8 | 내 PDF 문서를 AI가 읽는다 — 사내 지식 RAG 챗봇 구축 | ⭐⭐⭐ | LlamaIndex · ChromaDB · Qdrant |
📌 1. 들어가며
이 포스트에서 만들 것
이 포스트를 끝까지 따라하면 영상 파일 1개만 넣으면 아래 결과물을 자동으로 만들 수 있습니다.
입력: 영상 파일 1개 (광고, 유튜브 영상, 쇼츠 등)
↓
[1단계] 영상 자동 분석
└─ 대표 프레임 추출 → Qwen2.5-VL로 분위기 파악
└─ "어두운 분위기, 긴장감, 빠른 컷" → 음악 프롬프트 자동 생성
[2단계] AI BGM 생성
└─ MusicGen / Stable Audio Open으로 맞춤 BGM 생성
[3단계] 영상 싱크
└─ 영상 길이에 맞게 BGM 자동 조절
└─ Fade-in / Fade-out 처리
출력: BGM이 완벽하게 씌워진 최종 영상 파일
왜 AI BGM인가 — 저작권 문제 해결
기존 BGM 사용의 문제:
├─ 유튜브 저작권 경고 → 수익 박탈
├─ 음원 라이선스 구매 비용 (월 수만원~수십만원)
├─ 원하는 분위기의 음악을 찾는 데 걸리는 시간
└─ 상업적 사용 불가 음원 혼용으로 법적 리스크
AI BGM 생성의 장점:
├─ MusicGen (Meta): MIT 라이선스 → 상업적 사용 가능
├─ Stable Audio Open: Stability AI Open Rail → 상업적 사용 가능
├─ 완전히 새로운 음악 생성 → 저작권 침해 원천 차단
└─ 영상 분위기에 딱 맞는 맞춤 음악 자동 생성
📌 2. 환경 준비
2-1. VRAM 8GB+ 권장
MusicGen 모델별 VRAM:
small (300M) → VRAM 4GB / CPU도 가능 (느림)
medium (1.5B) → VRAM 8GB 권장
large (3.3B) → VRAM 16GB 권장
melody (1.5B) → VRAM 8GB (허밍/멜로디 기반 생성)
Stable Audio Open:
→ VRAM 8GB+
CPU만 있는 경우:
→ MusicGen small 사용
→ 30초 생성에 약 2~5분 소요 (GPU 대비 10배 느림)
2-2. audiocraft, transformers, ffmpeg 설치
bash# Python 환경 생성
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
# PyTorch 설치 (CUDA 12.1)
pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu121
# Meta AudioCraft (MusicGen 포함)
pip install audiocraft
# HuggingFace Transformers (Stable Audio Open, Qwen2.5-VL)
pip install transformers accelerate
# 영상 분석
pip install opencv-python pillow
# 오디오 처리
pip install soundfile scipy numpy
# FFmpeg 설치
# Windows: https://ffmpeg.org/download.html → PATH 등록
# Linux:
sudo apt install ffmpeg
# Mac:
brew install ffmpeg
FFmpeg 설치 확인:
bashffmpeg -version
ffprobe -version
📌 3. 영상 분위기 분석
3-1. 영상에서 대표 프레임 추출 (OpenCV)
영상 전체를 분석하면 시간이 오래 걸리므로, 구간별 대표 프레임을 추출해 분위기를 파악합니다.
pythonimport cv2
import os
import numpy as np
from PIL import Image
def extract_keyframes(video_path, num_frames=6, output_dir="./keyframes"):
"""
영상에서 균등 간격으로 대표 프레임 추출
num_frames: 추출할 프레임 수 (구간 분석용)
"""
os.makedirs(output_dir, exist_ok=True)
cap = cv2.VideoCapture(video_path)
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
duration = total / fps
print(f"영상 정보: {total}프레임 / {fps:.1f}fps / {duration:.1f}초")
# 균등 간격 프레임 위치 계산
positions = [int(total * i / num_frames) for i in range(num_frames)]
saved_paths = []
for i, pos in enumerate(positions):
cap.set(cv2.CAP_PROP_POS_FRAMES, pos)
ret, frame = cap.read()
if not ret:
continue
# BGR → RGB 변환
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = Image.fromarray(frame_rgb)
save_path = os.path.join(output_dir, f"frame_{i:02d}.jpg")
img.save(save_path, quality=90)
saved_paths.append(save_path)
timestamp = pos / fps
print(f"프레임 {i+1}/{num_frames} 추출 완료: {timestamp:.1f}초 지점")
cap.release()
return saved_paths, duration
# 실행
frame_paths, video_duration = extract_keyframes(
video_path = "./my_video.mp4",
num_frames = 6,
output_dir = "./keyframes"
)
장면 변화 감지 기반 추출 (고급):
pythondef extract_scene_changes(video_path, threshold=30.0):
"""
장면 변화가 있는 지점의 프레임 자동 추출
threshold: 프레임 간 차이 임계값 (낮을수록 민감)
"""
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
prev = None
scenes = []
frame_no = 0
while True:
ret, frame = cap.read()
if not ret:
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
if prev is not None:
diff = cv2.absdiff(prev, gray)
score = np.mean(diff)
if score > threshold:
timestamp = frame_no / fps
scenes.append((frame_no, timestamp, frame.copy()))
prev = gray
frame_no += 1
cap.release()
print(f"장면 변화 감지: {len(scenes)}개 씬")
return scenes
3-2. Qwen2.5-VL로 장면 설명 생성
pythonfrom transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
from PIL import Image
import torch
def load_vision_model(model_name="Qwen/Qwen2.5-VL-7B-Instruct"):
"""Qwen2.5-VL 모델 로드"""
print(f"모델 로드 중: {model_name}")
model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
model_name,
torch_dtype = torch.bfloat16,
device_map = "auto",
)
processor = AutoProcessor.from_pretrained(model_name)
return model, processor
def analyze_frame_mood(image_path, model, processor):
"""
단일 프레임 분위기 분석
반환: 분위기 설명 텍스트
"""
image = Image.open(image_path).convert("RGB")
prompt = """이 영상 프레임의 분위기를 분석해주세요.
다음 항목을 간결하게 설명하세요:
1. 전반적 분위기 (예: 긴장감, 평화로움, 역동적, 감성적)
2. 색감 (예: 어두운, 밝은, 따뜻한, 차가운)
3. 속도감 (예: 빠른, 느린, 정적인)
4. 배경 상황 (예: 공장, 자연, 도시, 실내)
한 줄로 요약: """
messages = [{
"role": "user",
"content": [
{"type": "image", "image": image},
{"type": "text", "text": prompt}
]
}]
text = processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = processor(
text=[text], images=[image],
return_tensors="pt"
).to(model.device)
with torch.no_grad():
output_ids = model.generate(
**inputs,
max_new_tokens = 100,
temperature = 0.3,
)
response = processor.decode(
output_ids[0][inputs.input_ids.shape[1]:],
skip_special_tokens=True
)
return response.strip()
def analyze_all_frames(frame_paths, model, processor):
"""모든 프레임 분위기 통합 분석"""
descriptions = []
for i, path in enumerate(frame_paths):
desc = analyze_frame_mood(path, model, processor)
descriptions.append(desc)
print(f"[{i+1}/{len(frame_paths)}] {desc}")
# 전체 영상 분위기 통합 요약
combined = " / ".join(descriptions)
summary_prompt = f"""다음은 영상 각 구간의 분위기 설명입니다:
{combined}
전체 영상의 BGM으로 어울리는 음악을 영어로 설명해주세요.
형식: [장르], [분위기], [BPM 범위], [악기]
예시: cinematic orchestral, tense and dramatic, 80-100 BPM, strings and percussion"""
messages = [{"role": "user", "content": summary_prompt}]
text = processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = processor(text=[text], return_tensors="pt").to(model.device)
with torch.no_grad():
output_ids = model.generate(**inputs, max_new_tokens=80)
summary = processor.decode(
output_ids[0][inputs.input_ids.shape[1]:],
skip_special_tokens=True
)
return descriptions, summary.strip()
3-3. 분위기 텍스트 → 음악 프롬프트 변환 로직
python# 분위기 키워드 → MusicGen 프롬프트 자동 매핑 테이블
MOOD_TO_PROMPT = {
# 분위기 키워드: MusicGen 프롬프트
"긴장감": "tense cinematic music, suspenseful strings, low brass, dark atmosphere",
"평화로움": "peaceful ambient music, soft piano, gentle nature sounds, calm",
"역동적": "energetic electronic music, driving beat, synth bass, powerful",
"감성적": "emotional ballad, soft piano, slow tempo, melancholic",
"어두운": "dark atmospheric music, minor key, deep bass, moody",
"밝은": "upbeat cheerful music, major key, bright strings, optimistic",
"따뜻한": "warm acoustic music, guitar, cozy atmosphere, gentle",
"차가운": "cold ambient soundscape, electronic pads, minimalist, icy",
"빠른": "fast paced electronic, high tempo 140bpm, energetic beat",
"느린": "slow cinematic, 60bpm, long reverb, meditative",
"공장": "industrial ambient, mechanical sounds, rhythmic percussion",
"자연": "nature sounds, acoustic instruments, birds, flowing water",
"도시": "urban electronic, city vibes, modern beat, metropolitan",
"고급스러운": "luxury lounge jazz, sophisticated, smooth, elegant piano",
"광고": "corporate upbeat, inspiring, motivational, clean production",
}
def text_to_music_prompt(mood_description, video_duration):
"""
분위기 설명 → MusicGen 최적화 프롬프트 생성
"""
# 키워드 매핑
matched_prompts = []
for keyword, prompt in MOOD_TO_PROMPT.items():
if keyword in mood_description:
matched_prompts.append(prompt)
# 매핑된 프롬프트 조합
if matched_prompts:
base_prompt = matched_prompts[0]
else:
# 기본값: Qwen2.5-VL이 생성한 설명 그대로 사용
base_prompt = mood_description
# 영상 길이 → 음악 스타일 보완
if video_duration < 15:
base_prompt += ", short punchy, high energy intro"
elif video_duration < 60:
base_prompt += ", full arrangement, verse and chorus structure"
else:
base_prompt += ", long form, gradual build, sustained atmosphere"
print(f"\n🎵 생성될 음악 프롬프트:\n{base_prompt}\n")
return base_prompt
# 전체 파이프라인 실행
model, processor = load_vision_model("Qwen/Qwen2.5-VL-7B-Instruct")
frame_paths, video_duration = extract_keyframes("./my_video.mp4")
descriptions, summary = analyze_all_frames(frame_paths, model, processor)
music_prompt = text_to_music_prompt(summary, video_duration)
📌 4. BGM 생성
4-1. MusicGen 사용법
pythonimport torch
import soundfile as sf
from audiocraft.models import MusicGen
from audiocraft.data.audio import audio_write
def load_musicgen(size="medium"):
"""
size: "small" / "medium" / "large" / "melody"
"""
print(f"MusicGen {size} 모델 로드 중...")
model = MusicGen.get_pretrained(f"facebook/musicgen-{size}")
return model
모델 크기 선택 가이드:
small (300M):
VRAM: 4GB / CPU 가능
품질: ⭐⭐⭐
속도: 매우 빠름
→ 빠른 테스트, 저사양 환경
medium (1.5B):
VRAM: 8GB
품질: ⭐⭐⭐⭐
속도: 빠름
→ 실용적인 균형 (권장)
large (3.3B):
VRAM: 16GB
품질: ⭐⭐⭐⭐⭐
속도: 보통
→ 최고 품질 필요 시
melody (1.5B):
VRAM: 8GB
특징: 허밍/멜로디 입력 → 편곡 가능
→ 특정 멜로디를 기반으로 BGM 생성 시
duration, top_k, temperature 파라미터:
pythondef generate_bgm_musicgen(model, prompt, duration=30.0,
top_k=250, temperature=1.0):
"""
model: MusicGen 모델
prompt: 음악 설명 텍스트
duration: 생성 길이 (초) — 최대 30초 권장
top_k: 샘플링 다양성
낮을수록 → 예측 가능하고 안정적
높을수록 → 창의적이지만 불안정
temperature: 창의성 강도
0.5 → 보수적, 일관성 높음
1.0 → 균형 (권장)
1.5 → 창의적, 예상치 못한 결과
"""
model.set_generation_params(
duration = duration,
top_k = top_k,
temperature = temperature,
cfg_coef = 3.0, # 프롬프트 충실도 (3.0 권장)
)
print(f"BGM 생성 중... ({duration}초)")
with torch.no_grad():
wav = model.generate([prompt]) # 배치 처리 가능
# wav shape: [batch, channels, samples]
audio = wav[0].cpu().numpy() # 첫 번째 결과 선택
return audio, model.sample_rate
# 실행
musicgen_model = load_musicgen("medium")
audio, sample_rate = generate_bgm_musicgen(
model = musicgen_model,
prompt = music_prompt,
duration = 30.0,
top_k = 250,
temperature = 1.0,
)
sf.write("./bgm_raw.wav", audio.T, sample_rate)
print(f"BGM 저장 완료: ./bgm_raw.wav ({sample_rate}Hz)")
프롬프트 예시 모음:
pythonPROMPT_EXAMPLES = {
"광고_고급":
"luxury cinematic music, elegant piano and strings, "
"slow tempo 70bpm, sophisticated, dramatic crescendo, "
"Hollywood style, 4K quality audio",
"광고_역동":
"energetic corporate commercial music, driving synth bass, "
"inspiring brass, upbeat 120bpm, motivational, "
"clean production, modern electronic",
"유튜브_브이로그":
"warm acoustic indie pop, ukulele and guitar, "
"cheerful 90bpm, positive vibes, summer feel, "
"light percussion, whistling melody",
"공장_산업":
"industrial ambient techno, mechanical rhythm, "
"dark electronic, 130bpm, metallic percussion, "
"dystopian atmosphere, low drone bass",
"제품_스킨케어":
"gentle spa ambient music, soft piano, "
"nature sounds birds, slow 60bpm, relaxing, "
"clean minimalist, airy texture",
"게임_액션":
"epic action orchestral, fast drums 160bpm, "
"powerful brass, dramatic strings, intense battle, "
"Hans Zimmer style, aggressive",
"자연_다큐":
"peaceful nature documentary, ambient orchestral, "
"solo cello, soft strings, 50bpm, serene, "
"David Attenborough style, flowing",
}
4-2. Stable Audio Open 사용법
pythonfrom transformers import AutoModel, AutoProcessor
import torch
import soundfile as sf
import numpy as np
def load_stable_audio():
"""Stable Audio Open 모델 로드"""
print("Stable Audio Open 로드 중...")
model = AutoModel.from_pretrained(
"stabilityai/stable-audio-open-1.0",
torch_dtype = torch.float16,
trust_remote_code = True,
).to("cuda")
processor = AutoProcessor.from_pretrained(
"stabilityai/stable-audio-open-1.0",
trust_remote_code = True,
)
return model, processor
def generate_bgm_stable_audio(model, processor, prompt,
duration=30.0, steps=100):
"""
Stable Audio Open으로 BGM 생성
duration: 생성 길이 (초) — 최대 47초
steps: 디노이징 스텝 수 (높을수록 고품질)
"""
inputs = processor(
text = [prompt],
return_tensors = "pt",
).to("cuda")
with torch.no_grad():
audio = model.generate(
**inputs,
seconds_total = duration,
num_inference_steps = steps,
)
audio_np = audio.cpu().numpy().squeeze()
sample_rate = 44100 # Stable Audio: 44.1kHz 스테레오
return audio_np, sample_rate
# 실행
sa_model, sa_processor = load_stable_audio()
audio_sa, sr = generate_bgm_stable_audio(
model = sa_model,
processor = sa_processor,
prompt = music_prompt,
duration = 30.0,
steps = 100,
)
sf.write("./bgm_stable_audio.wav", audio_sa.T, sr)
print("Stable Audio Open BGM 저장 완료")
MusicGen vs Stable Audio Open 비교:
| 항목 | MusicGen | Stable Audio Open |
|---|---|---|
| 음질 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 샘플레이트 | 32kHz | 44.1kHz 스테레오 |
| 최대 길이 | 30초 | 47초 |
| VRAM | 8GB (medium) | 8GB+ |
| 속도 | 빠름 | 보통 |
| 라이선스 | MIT | Stability AI Open Rail |
| 장르 다양성 | 매우 넓음 | 넓음 |
| 권장 상황 | 빠른 생성, 다양한 장르 | 고음질, 스테레오 필요 시 |
📌 5. 영상 싱크 맞추기
5-1. 영상 길이 감지 (FFprobe)
pythonimport subprocess
import json
def get_video_duration(video_path):
"""
FFprobe로 영상 길이 정확히 감지
반환: 초 단위 float
"""
cmd = [
"ffprobe",
"-v", "quiet",
"-print_format", "json",
"-show_streams",
video_path
]
result = subprocess.run(cmd, capture_output=True, text=True)
info = json.loads(result.stdout)
for stream in info["streams"]:
if stream["codec_type"] == "video":
duration = float(stream["duration"])
fps_str = stream["r_frame_rate"] # "24/1" 형식
fps_num, fps_den = map(int, fps_str.split("/"))
fps = fps_num / fps_den
print(f"영상 길이: {duration:.2f}초 / FPS: {fps:.1f}")
return duration, fps
raise ValueError("영상 스트림을 찾을 수 없습니다.")
video_duration, video_fps = get_video_duration("./my_video.mp4")
5-2. BGM 자르기 / 루프 처리
pythonimport numpy as np
import soundfile as sf
def adjust_bgm_duration(bgm_path, target_duration, output_path,
crossfade_sec=2.0):
"""
BGM을 영상 길이에 맞게 자르거나 루프 처리
crossfade_sec: 루프 이음새 크로스페이드 길이 (초)
"""
audio, sr = sf.read(bgm_path)
bgm_length = len(audio) / sr
print(f"BGM 원본 길이: {bgm_length:.2f}초")
print(f"목표 길이: {target_duration:.2f}초")
if bgm_length >= target_duration:
# --- 자르기 ---
target_samples = int(target_duration * sr)
adjusted = audio[:target_samples]
print("→ 자르기 처리")
else:
# --- 루프 처리 ---
crossfade_samples = int(crossfade_sec * sr)
loops_needed = int(np.ceil(target_duration / bgm_length)) + 1
# BGM을 필요한 횟수만큼 이어붙이기
looped = np.tile(audio, (loops_needed, 1)) if audio.ndim == 2 \
else np.tile(audio, loops_needed)
# 크로스페이드 처리 (이음새 자연스럽게)
if audio.ndim == 2:
fade_out = np.linspace(1, 0, crossfade_samples)[:, np.newaxis]
fade_in = np.linspace(0, 1, crossfade_samples)[:, np.newaxis]
else:
fade_out = np.linspace(1, 0, crossfade_samples)
fade_in = np.linspace(0, 1, crossfade_samples)
# 루프 이음새 위치에 크로스페이드 적용
loop_point = len(audio) - crossfade_samples
looped[loop_point:loop_point + crossfade_samples] = (
audio[loop_point:] * fade_out +
audio[:crossfade_samples] * fade_in
)
target_samples = int(target_duration * sr)
adjusted = looped[:target_samples]
print(f"→ 루프 처리 ({loops_needed-1}회 반복)")
sf.write(output_path, adjusted, sr)
print(f"BGM 조정 완료: {output_path}")
return output_path
adjust_bgm_duration(
bgm_path = "./bgm_raw.wav",
target_duration = video_duration,
output_path = "./bgm_adjusted.wav",
crossfade_sec = 2.0,
)
5-3. Fade-in / Fade-out 처리
pythondef apply_fade(audio_path, output_path,
fade_in_sec=1.5, fade_out_sec=2.0):
"""
오디오에 Fade-in / Fade-out 적용
fade_in_sec: 시작 페이드인 길이 (초)
fade_out_sec: 끝 페이드아웃 길이 (초)
"""
audio, sr = sf.read(audio_path)
total = len(audio)
# Fade-in 처리 (선형 증가)
fade_in_samples = int(fade_in_sec * sr)
fade_in_curve = np.linspace(0.0, 1.0, fade_in_samples)
# Fade-out 처리 (선형 감소)
fade_out_samples = int(fade_out_sec * sr)
fade_out_curve = np.linspace(1.0, 0.0, fade_out_samples)
if audio.ndim == 2:
# 스테레오
fade_in_curve = fade_in_curve[:, np.newaxis]
fade_out_curve = fade_out_curve[:, np.newaxis]
audio[:fade_in_samples] *= fade_in_curve
audio[total - fade_out_samples:] *= fade_out_curve
sf.write(output_path, audio, sr)
print(f"Fade 처리 완료: in={fade_in_sec}s / out={fade_out_sec}s")
return output_path
apply_fade(
audio_path = "./bgm_adjusted.wav",
output_path = "./bgm_final.wav",
fade_in_sec = 1.5,
fade_out_sec = 2.0,
)
📌 6. FFmpeg으로 최종 합치기
오디오 볼륨 조절 & 믹싱 커맨드
기본 합치기 (BGM만 있는 경우):
bashffmpeg \
-i ./my_video.mp4 \
-i ./bgm_final.wav \
-c:v copy \
-c:a aac \
-b:a 192k \
-shortest \
-filter:a "volume=0.85" \
./output_with_bgm.mp4
영상 원본 음성 + BGM 믹싱 (목소리 더빙이 있는 경우):
bashffmpeg \
-i ./my_video.mp4 \
-i ./bgm_final.wav \
-filter_complex \
"[0:a]volume=1.0[voice];
[1:a]volume=0.3[bgm];
[voice][bgm]amix=inputs=2:duration=shortest[aout]" \
-map 0:v \
-map "[aout]" \
-c:v copy \
-c:a aac \
-b:a 192k \
./output_mixed.mp4
# 볼륨 조절 가이드:
# [voice] volume=1.0 → 목소리 원본 볼륨 100%
# [bgm] volume=0.3 → BGM은 30% (배경음 역할)
Python으로 전체 파이프라인 자동화:
pythonimport subprocess
def merge_video_bgm(video_path, bgm_path, output_path,
bgm_volume=0.85, has_original_audio=False,
voice_volume=1.0):
"""
영상 + BGM 최종 합치기
has_original_audio: True → 원본 음성 + BGM 믹싱
False → BGM만 사용
"""
if has_original_audio:
# 목소리 + BGM 믹싱
cmd = [
"ffmpeg", "-y",
"-i", video_path,
"-i", bgm_path,
"-filter_complex",
f"[0:a]volume={voice_volume}[voice];"
f"[1:a]volume={bgm_volume}[bgm];"
f"[voice][bgm]amix=inputs=2:duration=shortest[aout]",
"-map", "0:v",
"-map", "[aout]",
"-c:v", "copy",
"-c:a", "aac",
"-b:a", "192k",
output_path
]
else:
# BGM만 추가
cmd = [
"ffmpeg", "-y",
"-i", video_path,
"-i", bgm_path,
"-c:v", "copy",
"-c:a", "aac",
"-b:a", "192k",
"-shortest",
"-filter:a", f"volume={bgm_volume}",
output_path
]
print("FFmpeg 실행 중...")
subprocess.run(cmd, check=True)
print(f"✅ 최종 영상 저장: {output_path}")
# 실행
merge_video_bgm(
video_path = "./my_video.mp4",
bgm_path = "./bgm_final.wav",
output_path = "./final_with_bgm.mp4",
bgm_volume = 0.85,
has_original_audio = False,
)
전체 자동화 파이프라인 한 번에 실행:
pythondef full_pipeline(video_path, output_path,
musicgen_size="medium", bgm_volume=0.85):
"""
영상 입력 → BGM 자동 분석/생성/싱크 → 최종 영상 출력
"""
print("=" * 50)
print("🎬 BGM 자동 생성 파이프라인 시작")
print("=" * 50)
# 1단계: 프레임 추출
print("\n📷 [1/6] 대표 프레임 추출 중...")
frame_paths, duration = extract_keyframes(video_path)
# 2단계: 분위기 분석
print("\n🔍 [2/6] 영상 분위기 분석 중...")
model_vl, processor_vl = load_vision_model()
_, summary = analyze_all_frames(frame_paths, model_vl, processor_vl)
# 3단계: 프롬프트 생성
print("\n✍️ [3/6] 음악 프롬프트 생성 중...")
music_prompt = text_to_music_prompt(summary, duration)
# 4단계: BGM 생성
print("\n🎵 [4/6] BGM 생성 중...")
musicgen = load_musicgen(musicgen_size)
audio, sr = generate_bgm_musicgen(
musicgen, music_prompt, duration=min(duration, 30.0)
)
sf.write("./temp_bgm_raw.wav", audio.T, sr)
# 5단계: 싱크 & 페이드
print("\n✂️ [5/6] BGM 싱크 맞추는 중...")
adjust_bgm_duration("./temp_bgm_raw.wav", duration, "./temp_bgm_adjusted.wav")
apply_fade("./temp_bgm_adjusted.wav", "./temp_bgm_final.wav")
# 6단계: 최종 합치기
print("\n🎬 [6/6] 영상 + BGM 합치는 중...")
merge_video_bgm(video_path, "./temp_bgm_final.wav", output_path, bgm_volume)
print("\n" + "=" * 50)
print(f"✅ 완성! → {output_path}")
print("=" * 50)
# 한 줄 실행
full_pipeline(
video_path = "./my_video.mp4",
output_path = "./final_with_bgm.mp4",
musicgen_size = "medium",
bgm_volume = 0.85,
)
📌 7. 결과 확인 & 트러블슈팅
분위기가 영상과 안 맞을 때
원인 1: Qwen2.5-VL 분석 결과가 부정확
→ 프레임 수 늘리기: num_frames=6 → 12
→ 장면 변화 감지 방식으로 교체
원인 2: 프롬프트 매핑 테이블 미스매치
→ MOOD_TO_PROMPT에 해당 분위기 키워드 추가
→ Qwen2.5-VL 출력을 직접 프롬프트로 사용
원인 3: MusicGen이 프롬프트를 잘못 해석
→ 프롬프트를 더 구체적으로 작성
→ 나쁜 예: "sad music"
→ 좋은 예: "melancholic solo piano, slow 60bpm,
minor key, emotional, no drums"
원인 4: temperature 값이 너무 높음
→ temperature=1.0 → 0.7로 낮추기
→ 낮을수록 프롬프트에 충실한 결과
직접 프롬프트 수동 입력:
music_prompt = "luxury cinematic, elegant piano and strings,
slow 70bpm, sophisticated"
audio, sr = generate_bgm_musicgen(musicgen, music_prompt, duration=30)
음악이 뚝 끊길 때
원인 1: 루프 이음새 크로스페이드 부족
→ crossfade_sec=2.0 → 3.0으로 늘리기
원인 2: Fade-out 처리 미적용
→ apply_fade()의 fade_out_sec 확인
→ 최소 2.0초 이상 권장
원인 3: 영상 길이 > BGM 길이인데 루프 처리 없음
→ adjust_bgm_duration() 함수 적용 확인
→ crossfade_sec=3.0 권장
원인 4: FFmpeg 인코딩 중 오디오 잘림
→ -shortest 옵션 제거 후 -t 옵션으로 길이 명시
ffmpeg -i video.mp4 -i bgm.wav \
-t {video_duration} \ ← 명시적 길이 지정
-c:v copy -c:a aac output.mp4
오디오 품질 확인 (FFprobe):
ffprobe -v quiet -show_streams ./final_with_bgm.mp4 | grep -E "codec|sample|duration"
✅ 완성 체크리스트
- 영상 대표 프레임 추출 확인 (6장 이상)
- Qwen2.5-VL 분위기 분석 결과 확인
- 음악 프롬프트 자동 생성 확인
- MusicGen BGM 생성 성공 (30초 WAV 파일)
- BGM 영상 길이에 맞게 싱크 조정 완료
- Fade-in / Fade-out 적용 확인
- FFmpeg 최종 합치기 완료 및 재생 확인