Files
Conal Mullan fde30d3f45 Initial release of claude-code-video-toolkit
AI-native video production workspace for Claude Code featuring:
- 4 skills: remotion, elevenlabs, ffmpeg, playwright-recording
- 3 commands: /new-sprint-video, /record-demo, /generate-voiceover
- Sprint review template with theme system
- Brand profiles for visual identity
- Python tools for audio generation (voiceover, music, sfx)
- 2 example projects: sprint-review-cho-oyu, digital-samba-skill-demo

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 20:38:32 +00:00

183 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""
Generate background music using ElevenLabs Music API.
Usage:
# Generate background music
python tools/music.py --prompt "Upbeat tech corporate" --duration 120 --output music.mp3
# JSON output for machine parsing
python tools/music.py --prompt "Calm ambient" --duration 60 --output bg.mp3 --json
"""
import argparse
import json
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
from elevenlabs.client import ElevenLabs
# Add parent to path for local imports
sys.path.insert(0, str(Path(__file__).parent))
from config import get_elevenlabs_api_key
def parse_args():
parser = argparse.ArgumentParser(
description="Generate background music using ElevenLabs",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python tools/music.py --prompt "Subtle corporate tech" --duration 150 --output public/audio/bg-music.mp3
python tools/music.py --prompt "Upbeat indie rock" --duration 60 --output music.mp3 --json
Prompt tips:
Include genre, mood, instruments, tempo, and use case.
Example: "Subtle corporate technology background music, soft synth pads, minimal beats, professional presentation"
""",
)
parser.add_argument(
"--prompt",
"-p",
type=str,
required=True,
help="Music description prompt (genre, mood, instruments, tempo)",
)
parser.add_argument(
"--duration",
"-d",
type=int,
required=True,
help="Music duration in seconds (10-300)",
)
parser.add_argument(
"--output",
"-o",
type=str,
required=True,
help="Output audio file path (.mp3)",
)
parser.add_argument(
"--vocals",
action="store_true",
help="Allow vocals (default: instrumental only)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output result as JSON (for machine parsing)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be done without making API calls",
)
return parser.parse_args()
def get_audio_duration(file_path: str) -> float | None:
"""Get audio duration using ffprobe if available."""
import subprocess
try:
result = subprocess.run(
[
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"csv=p=0",
file_path,
],
capture_output=True,
text=True,
)
if result.returncode == 0:
return float(result.stdout.strip())
except (FileNotFoundError, ValueError):
pass
return None
def main():
load_dotenv()
args = parse_args()
# Validate duration
if args.duration < 10 or args.duration > 300:
print("Error: Duration must be between 10 and 300 seconds", file=sys.stderr)
sys.exit(1)
# Get API key
api_key = get_elevenlabs_api_key()
if not api_key:
print("Error: ELEVENLABS_API_KEY not found in environment", file=sys.stderr)
sys.exit(1)
# Prepare output directory
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
# Dry run mode
if args.dry_run:
result = {
"dry_run": True,
"prompt": args.prompt,
"duration_seconds": args.duration,
"instrumental": not args.vocals,
"output": str(output_path),
}
if args.json:
print(json.dumps(result, indent=2))
else:
print("Would generate music:")
print(f" Prompt: {args.prompt}")
print(f" Duration: {args.duration}s")
print(f" Instrumental: {not args.vocals}")
print(f" Output: {output_path}")
return
# Generate music
if not args.json:
print(f"Generating {args.duration}s of music...", file=sys.stderr)
client = ElevenLabs(api_key=api_key)
music = client.music.compose(
prompt=args.prompt,
music_length_ms=args.duration * 1000,
force_instrumental=not args.vocals,
)
with open(output_path, "wb") as f:
for chunk in music:
f.write(chunk)
# Get actual duration if ffprobe available
actual_duration = get_audio_duration(str(output_path))
# Output result
result = {
"success": True,
"output": str(output_path),
"prompt": args.prompt,
"requested_duration": args.duration,
}
if actual_duration:
result["actual_duration_seconds"] = round(actual_duration, 2)
if args.json:
print(json.dumps(result, indent=2))
else:
print(f"Music saved to: {output_path}", file=sys.stderr)
if actual_duration:
print(f"Duration: {actual_duration:.2f}s", file=sys.stderr)
if __name__ == "__main__":
main()