Files
animily_music/daily_precache.py
javamon c3f8d6b288 feat: v2 파이프라인 — 매일 2곡 다양한 장르 BGM 자동 생성/업로드
- daily_precache.py: Claude Code CLI 프리캐시 (날짜/계절/기념일 테마)
- daily_scheduler.py: ACE-Step 음악 → FLUX 이미지 → 영상 렌더 → 큐
- upload_scheduled.py: auto_shorts 동일 큐 방식 업로드
- PRECACHE_GUIDE_MUSIC.md: 19개 장르, 감성 제목, 재생목록 자유 생성
- generate_image.py: --lowvram 제거, GPU VRAM 확인 추가
- config.py: @animily-music 토큰 경로 변경

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 19:24:50 +09:00

224 lines
7.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""
daily_precache.py — Claude Code CLI로 음악 트랙 2개 기획 JSON 생성
Usage:
python daily_precache.py [--date 2026-05-25]
Output:
precache/{date}.json
"""
import os
import sys
import json
import subprocess
import logging
from datetime import datetime, date
from pathlib import Path
# Project paths
PROJECT_DIR = Path("/home/javamon/project/animily_music")
PRECACHE_DIR = PROJECT_DIR / "precache"
GUIDE_PATH = PROJECT_DIR / "PRECACHE_GUIDE_MUSIC.md"
LOG_DIR = PROJECT_DIR / "logs"
# Logging
LOG_DIR.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(LOG_DIR / "precache.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def get_target_date(args=None):
"""Parse target date from args or use tomorrow."""
if args and "--date" in args:
idx = args.index("--date")
if idx + 1 < len(args):
return args[idx + 1]
# Default: tomorrow
from datetime import timedelta
tomorrow = date.today() + timedelta(days=1)
return tomorrow.strftime("%Y-%m-%d")
def build_prompt(target_date: str) -> str:
"""Build the prompt for Claude Code CLI."""
d = datetime.strptime(target_date, "%Y-%m-%d")
weekday_kr = ["", "", "", "", "", "", ""][d.weekday()]
month = d.month
# Season
if month in (3, 4, 5):
season = ""
elif month in (6, 7, 8):
season = "여름"
elif month in (9, 10, 11):
season = "가을"
else:
season = "겨울"
prompt = f"""You are a music curator for the ANIMILY YouTube channel.
Generate a JSON plan for 2 BGM tracks to be published on {target_date} ({weekday_kr}요일).
Season: {season}
Date context: {target_date} ({weekday_kr}요일)
Read the guide at: {GUIDE_PATH}
CRITICAL RULES:
1. Output ONLY valid JSON (no markdown, no explanation, no code blocks)
2. The 2 tracks MUST be different genres
3. Caption focuses on style/mood/instruments only (NOT tempo/key info)
4. BPM must be within the genre's valid range per the guide
5. Duration: 180-300 seconds
6. image_prompt must include "makoto shinkai style" and "no text"
7. youtube_title must include emoji + Korean + English
8. playlist_category must be one of the 8 categories in the guide
Output format:
{{
"date": "{target_date}",
"theme": "<한국어로 날짜/계절/분위기 한줄 요약>",
"tracks": [
{{
"id": "track_001",
"caption": "<english style/mood/instruments description>",
"lyrics": "[Instrumental]",
"bpm": <number>,
"key_scale": "<key> <Major/Minor>",
"time_signature": "4",
"duration": <180-300>,
"image_prompt": "makoto shinkai style, <scene description>, no text, no watermark",
"youtube_title": "<emoji> <한국어 제목> | <English Title> BGM",
"youtube_tags": ["<tag1>", "<tag2>", ...],
"playlist_category": "<one of 8 categories>"
}},
{{
"id": "track_002",
...
}}
]
}}
"""
return prompt
def run_claude_code_cli(prompt: str, target_date: str) -> dict:
"""Run Claude Code CLI and parse JSON output."""
logger.info("Running Claude Code CLI for precache generation...")
try:
result = subprocess.run(
["claude", "-p", prompt, "--output-format", "text"],
capture_output=True,
text=True,
timeout=120,
cwd=str(PROJECT_DIR)
)
if result.returncode != 0:
logger.error(f"Claude CLI failed (rc={result.returncode}): {result.stderr[:500]}")
return None
output = result.stdout.strip()
# Try to extract JSON from output (handle potential wrapping)
json_start = output.find("{")
json_end = output.rfind("}") + 1
if json_start == -1 or json_end == 0:
logger.error(f"No JSON found in output: {output[:300]}")
return None
json_str = output[json_start:json_end]
data = json.loads(json_str)
# Validate structure
if "tracks" not in data or len(data["tracks"]) != 2:
logger.error(f"Invalid structure: expected 2 tracks, got {len(data.get('tracks', []))}")
return None
# Validate each track
required_fields = ["id", "caption", "lyrics", "bpm", "key_scale",
"time_signature", "duration", "image_prompt",
"youtube_title", "youtube_tags", "playlist_category"]
valid_categories = ["신나는 음악", "감성 음악", "잔잔한 음악", "집중 음악",
"수면 음악", "카페 음악", "운동 음악", "드라이브 음악"]
for i, track in enumerate(data["tracks"]):
for field in required_fields:
if field not in track:
logger.error(f"Track {i} missing field: {field}")
return None
if track["playlist_category"] not in valid_categories:
logger.warning(f"Track {i} invalid category '{track['playlist_category']}', fixing...")
track["playlist_category"] = "감성 음악"
if not (180 <= track["duration"] <= 300):
logger.warning(f"Track {i} duration {track['duration']} out of range, clamping")
track["duration"] = max(180, min(300, track["duration"]))
return data
except subprocess.TimeoutExpired:
logger.error("Claude CLI timed out (120s)")
return None
except json.JSONDecodeError as e:
logger.error(f"JSON parse error: {e}")
return None
except Exception as e:
logger.error(f"Unexpected error: {e}")
return None
def save_precache(data: dict, target_date: str) -> Path:
"""Save precache JSON to file."""
PRECACHE_DIR.mkdir(parents=True, exist_ok=True)
output_path = PRECACHE_DIR / f"{target_date}.json"
with open(output_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"Precache saved: {output_path}")
return output_path
def main():
target_date = get_target_date(sys.argv[1:])
logger.info(f"=== Daily Precache: {target_date} ===")
# Check if already exists
output_path = PRECACHE_DIR / f"{target_date}.json"
if output_path.exists():
logger.info(f"Precache already exists: {output_path}")
with open(output_path, "r") as f:
data = json.load(f)
print(json.dumps(data, ensure_ascii=False, indent=2))
return 0
# Generate with Claude Code CLI (up to 3 retries)
prompt = build_prompt(target_date)
data = None
for attempt in range(3):
logger.info(f"Attempt {attempt + 1}/3...")
data = run_claude_code_cli(prompt, target_date)
if data:
break
logger.warning(f"Attempt {attempt + 1} failed, retrying...")
if not data:
logger.error("All attempts failed. Precache generation failed.")
return 1
# Save
save_precache(data, target_date)
print(json.dumps(data, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main())