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:
kihong.kim
2026-01-03 21:38:34 +09:00
commit c3795138da
64 changed files with 13059 additions and 0 deletions

View 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,
}