#!/usr/bin/env python3 """Compose an instrumental score with ElevenLabs Music, section by section, timed to the edit. python3 music.py score.json --out music/ [--seeds 11,29] one take per seed (max 2 run at once) python3 music.py --plan "prompt" --seconds 61 print the plan ElevenLabs suggests score.json: {"name": "cinematic", "bpm": 120, "model": "music_v2_5", "negative": ["vocals", "singing", "lyrics", "lo-fi", "muddy mix"], "sections": [{"tag": "[Intro]", "seconds": 6.5, "styles": ["staccato strings on eighth notes", "building momentum"]}, {"tag": "[Impact and Breath]", "seconds": 3.0, "styles": ["huge hit on the downbeat", "sudden quiet"]}, ...]} Each section is a composition-plan chunk with duration_ms = its seconds (min 3 s, max 120 s), so the music changes where the film changes. " BPM" is added to every section's styles. The total should equal the film's length. Needs ELEVENLABS_API_KEY with the Music Generation permission (a key without it fails with "missing the permission music_generation"); plans allow ~2 requests at once. """ import argparse, concurrent.futures, json, os, sys from common import key, http_json, http_bytes ap = argparse.ArgumentParser() ap.add_argument("score", nargs="?"); ap.add_argument("--out", default="music"); ap.add_argument("--seeds", default="") ap.add_argument("--plan"); ap.add_argument("--seconds", type=float, default=60); ap.add_argument("--format", default="mp3_44100_192") a = ap.parse_args() K = key("ELEVENLABS_API_KEY") H = {"xi-api-key": K} if a.plan: print(json.dumps(http_json("https://api.elevenlabs.io/v1/music/plan", {"prompt": a.plan, "music_length_ms": int(a.seconds * 1000), "model_id": "music_v2_5"}, H), indent=1)) sys.exit() S = json.load(open(a.score)) os.makedirs(a.out, exist_ok=True) total = sum(s["seconds"] for s in S["sections"]) chunks = [{"text": s["tag"], "duration_ms": int(round(s["seconds"] * 1000)), "positive_styles": [f"{S.get('bpm', 120)} BPM"] + s["styles"], "negative_styles": S.get("negative", []), "context_adherence": "high"} for s in S["sections"]] bad = [c["text"] for c in chunks if not 3000 <= c["duration_ms"] <= 120000] if bad: sys.exit(f"sections must be 3-120 s long: {bad}. Merge a short section into its neighbour and shape it in the mix.") def take(seed): name = f"{S.get('name', 'score')}{'-' + str(seed) if seed else ''}" out = f"{a.out}/{name}.mp3" if os.path.exists(out): return f"{name}: exists" body = {"composition_plan": {"chunks": chunks}, "model_id": S.get("model", "music_v2_5"), "respect_sections_durations": True} if seed: body["seed"] = int(seed) try: data = http_bytes(f"https://api.elevenlabs.io/v1/music?output_format={a.format}", body, H, timeout=600) except RuntimeError as e: return f"{name}: {e}" open(out, "wb").write(data) return f"{name}: ok {len(data) // 1024} KB ({total:.1f} s planned)" seeds = [s for s in a.seeds.split(",") if s] or [None] with concurrent.futures.ThreadPoolExecutor(2) as ex: for r in ex.map(take, seeds): print(r, flush=True)