Initial commit: YouTube Shorts maker application
Features: - Video download from TikTok/Douyin using yt-dlp - Audio transcription with OpenAI Whisper - GPT-4 translation (direct/summarize/rewrite modes) - Subtitle generation with ASS format - Video trimming with frame-accurate preview - BGM integration with volume control - Intro text overlay support - Thumbnail generation with text overlay Tech stack: - Backend: FastAPI, Python 3.11+ - Frontend: React, Vite, TailwindCSS - Video processing: FFmpeg - AI: OpenAI Whisper, GPT-4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
297
backend/app/services/default_bgm.py
Normal file
297
backend/app/services/default_bgm.py
Normal file
@@ -0,0 +1,297 @@
|
||||
"""
|
||||
Default BGM Initializer
|
||||
|
||||
Downloads pre-selected royalty-free BGM tracks on first startup.
|
||||
Tracks are from Kevin MacLeod (incompetech.com) - CC-BY 4.0 License.
|
||||
Free for commercial use with attribution: "Kevin MacLeod (incompetech.com)"
|
||||
"""
|
||||
|
||||
import os
|
||||
import httpx
|
||||
import aiofiles
|
||||
import asyncio
|
||||
from typing import List, Tuple, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class DefaultBGM(BaseModel):
|
||||
"""Default BGM track info."""
|
||||
id: str
|
||||
name: str
|
||||
url: str
|
||||
category: str
|
||||
description: str
|
||||
|
||||
|
||||
# Curated list of royalty-free BGM from Kevin MacLeod (incompetech.com)
|
||||
# CC-BY 4.0 License - Free for commercial use with attribution
|
||||
# Attribution: "Kevin MacLeod (incompetech.com)"
|
||||
DEFAULT_BGM_TRACKS: List[DefaultBGM] = [
|
||||
# === 활기찬/에너지 (Upbeat/Energetic) ===
|
||||
DefaultBGM(
|
||||
id="upbeat_energetic",
|
||||
name="Upbeat Energetic",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Vivacity.mp3",
|
||||
category="upbeat",
|
||||
description="활기차고 에너지 넘치는 BGM - 피트니스, 챌린지 영상",
|
||||
),
|
||||
DefaultBGM(
|
||||
id="happy_pop",
|
||||
name="Happy Pop",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Carefree.mp3",
|
||||
category="upbeat",
|
||||
description="밝고 경쾌한 팝 BGM - 제품 소개, 언박싱",
|
||||
),
|
||||
DefaultBGM(
|
||||
id="upbeat_fun",
|
||||
name="Upbeat Fun",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Happy%20Happy%20Game%20Show.mp3",
|
||||
category="upbeat",
|
||||
description="신나는 게임쇼 비트 - 트렌디한 쇼츠",
|
||||
),
|
||||
|
||||
# === 차분한/편안한 (Chill/Relaxing) ===
|
||||
DefaultBGM(
|
||||
id="chill_lofi",
|
||||
name="Chill Lo-Fi",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Gymnopedie%20No%201.mp3",
|
||||
category="chill",
|
||||
description="차분하고 편안한 피아노 BGM - 일상, 브이로그",
|
||||
),
|
||||
DefaultBGM(
|
||||
id="calm_piano",
|
||||
name="Calm Piano",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Prelude%20No.%201.mp3",
|
||||
category="chill",
|
||||
description="잔잔한 피아노 BGM - 감성적인 콘텐츠",
|
||||
),
|
||||
DefaultBGM(
|
||||
id="soft_ambient",
|
||||
name="Soft Ambient",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Dreamlike.mp3",
|
||||
category="chill",
|
||||
description="부드러운 앰비언트 - ASMR, 수면 콘텐츠",
|
||||
),
|
||||
|
||||
# === 유머/코미디 (Funny/Comedy) ===
|
||||
DefaultBGM(
|
||||
id="funny_comedy",
|
||||
name="Funny Comedy",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Sneaky%20Snitch.mp3",
|
||||
category="funny",
|
||||
description="유쾌한 코미디 BGM - 코미디, 밈 영상",
|
||||
),
|
||||
DefaultBGM(
|
||||
id="quirky_playful",
|
||||
name="Quirky Playful",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Monkeys%20Spinning%20Monkeys.mp3",
|
||||
category="funny",
|
||||
description="장난스럽고 귀여운 BGM - 펫, 키즈 콘텐츠",
|
||||
),
|
||||
|
||||
# === 드라마틱/시네마틱 (Cinematic) ===
|
||||
DefaultBGM(
|
||||
id="cinematic_epic",
|
||||
name="Cinematic Epic",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Epic%20Unease.mp3",
|
||||
category="cinematic",
|
||||
description="웅장한 시네마틱 BGM - 리뷰, 소개 영상",
|
||||
),
|
||||
DefaultBGM(
|
||||
id="inspirational",
|
||||
name="Inspirational",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Hero%20Theme.mp3",
|
||||
category="cinematic",
|
||||
description="영감을 주는 BGM - 동기부여, 성장 콘텐츠",
|
||||
),
|
||||
|
||||
# === 생활용품/제품 리뷰 (Lifestyle/Product) ===
|
||||
DefaultBGM(
|
||||
id="lifestyle_modern",
|
||||
name="Lifestyle Modern",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Acoustic%20Breeze.mp3",
|
||||
category="lifestyle",
|
||||
description="모던한 라이프스타일 BGM - 제품 리뷰",
|
||||
),
|
||||
DefaultBGM(
|
||||
id="shopping_bright",
|
||||
name="Shopping Bright",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Pleasant%20Porridge.mp3",
|
||||
category="lifestyle",
|
||||
description="밝은 쇼핑 BGM - 하울, 추천 영상",
|
||||
),
|
||||
DefaultBGM(
|
||||
id="soft_corporate",
|
||||
name="Soft Corporate",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Laid%20Back%20Guitars.mp3",
|
||||
category="lifestyle",
|
||||
description="부드러운 기업형 BGM - 정보성 콘텐츠",
|
||||
),
|
||||
|
||||
# === 어쿠스틱/감성 (Acoustic/Emotional) ===
|
||||
DefaultBGM(
|
||||
id="soft_acoustic",
|
||||
name="Soft Acoustic",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Peaceful.mp3",
|
||||
category="acoustic",
|
||||
description="따뜻한 어쿠스틱 BGM - 요리, 일상 브이로그",
|
||||
),
|
||||
DefaultBGM(
|
||||
id="gentle_guitar",
|
||||
name="Gentle Guitar",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Sunflower%20Slow%20Drag.mp3",
|
||||
category="acoustic",
|
||||
description="잔잔한 기타 BGM - 여행, 풍경 영상",
|
||||
),
|
||||
|
||||
# === 트렌디/일렉트로닉 (Trendy/Electronic) ===
|
||||
DefaultBGM(
|
||||
id="electronic_chill",
|
||||
name="Electronic Chill",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Digital%20Lemonade.mp3",
|
||||
category="electronic",
|
||||
description="일렉트로닉 칠아웃 - 테크, 게임 콘텐츠",
|
||||
),
|
||||
DefaultBGM(
|
||||
id="driving_beat",
|
||||
name="Driving Beat",
|
||||
url="https://incompetech.com/music/royalty-free/mp3-royaltyfree/Cipher.mp3",
|
||||
category="electronic",
|
||||
description="드라이빙 비트 - 스포츠, 액션 영상",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def download_bgm_file(
|
||||
url: str,
|
||||
output_path: str,
|
||||
timeout: int = 60,
|
||||
) -> Tuple[bool, str]:
|
||||
"""
|
||||
Download a single BGM file.
|
||||
|
||||
Args:
|
||||
url: Download URL
|
||||
output_path: Full path to save the file
|
||||
timeout: Download timeout in seconds
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message)
|
||||
"""
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Accept": "audio/mpeg,audio/*;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(follow_redirects=True, headers=headers) as client:
|
||||
response = await client.get(url, timeout=timeout)
|
||||
|
||||
if response.status_code != 200:
|
||||
return False, f"HTTP {response.status_code}"
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
# Save file
|
||||
async with aiofiles.open(output_path, 'wb') as f:
|
||||
await f.write(response.content)
|
||||
|
||||
return True, "Downloaded successfully"
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return False, "Download timeout"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
async def initialize_default_bgm(
|
||||
bgm_dir: str,
|
||||
force: bool = False,
|
||||
) -> Tuple[int, int, List[str]]:
|
||||
"""
|
||||
Initialize default BGM tracks.
|
||||
|
||||
Downloads default BGM tracks if not already present.
|
||||
|
||||
Args:
|
||||
bgm_dir: Directory to save BGM files
|
||||
force: Force re-download even if files exist
|
||||
|
||||
Returns:
|
||||
Tuple of (downloaded_count, skipped_count, error_messages)
|
||||
"""
|
||||
os.makedirs(bgm_dir, exist_ok=True)
|
||||
|
||||
downloaded = 0
|
||||
skipped = 0
|
||||
errors = []
|
||||
|
||||
for track in DEFAULT_BGM_TRACKS:
|
||||
output_path = os.path.join(bgm_dir, f"{track.id}.mp3")
|
||||
|
||||
# Skip if already exists (unless force=True)
|
||||
if os.path.exists(output_path) and not force:
|
||||
skipped += 1
|
||||
print(f"[BGM] Skipping {track.name} (already exists)")
|
||||
continue
|
||||
|
||||
print(f"[BGM] Downloading {track.name}...")
|
||||
success, message = await download_bgm_file(track.url, output_path)
|
||||
|
||||
if success:
|
||||
downloaded += 1
|
||||
print(f"[BGM] Downloaded {track.name}")
|
||||
else:
|
||||
errors.append(f"{track.name}: {message}")
|
||||
print(f"[BGM] Failed to download {track.name}: {message}")
|
||||
|
||||
return downloaded, skipped, errors
|
||||
|
||||
|
||||
async def get_default_bgm_list() -> List[dict]:
|
||||
"""
|
||||
Get list of default BGM tracks with metadata.
|
||||
|
||||
Returns:
|
||||
List of BGM info dictionaries
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"id": track.id,
|
||||
"name": track.name,
|
||||
"category": track.category,
|
||||
"description": track.description,
|
||||
}
|
||||
for track in DEFAULT_BGM_TRACKS
|
||||
]
|
||||
|
||||
|
||||
def check_default_bgm_status(bgm_dir: str) -> dict:
|
||||
"""
|
||||
Check which default BGM tracks are installed.
|
||||
|
||||
Args:
|
||||
bgm_dir: BGM directory path
|
||||
|
||||
Returns:
|
||||
Status dictionary with installed/missing tracks
|
||||
"""
|
||||
installed = []
|
||||
missing = []
|
||||
|
||||
for track in DEFAULT_BGM_TRACKS:
|
||||
file_path = os.path.join(bgm_dir, f"{track.id}.mp3")
|
||||
if os.path.exists(file_path):
|
||||
installed.append(track.id)
|
||||
else:
|
||||
missing.append(track.id)
|
||||
|
||||
return {
|
||||
"total": len(DEFAULT_BGM_TRACKS),
|
||||
"installed": len(installed),
|
||||
"missing": len(missing),
|
||||
"installed_ids": installed,
|
||||
"missing_ids": missing,
|
||||
}
|
||||
Reference in New Issue
Block a user