"""Shared helpers for the optional media scripts (footage, music, sound effects, voices). Keys come from the environment and are never printed: GEMINI_API_KEY (or GOOGLE_API_KEY) Google AI Studio key, for Nano Banana images and Veo video ELEVENLABS_API_KEY ElevenLabs key with Music, Sound Effects, Text to Speech and Speech to Text permissions (only the ones you use) ffmpeg comes from ffmpeg-static in this folder (`npm install` once), or from $FFMPEG. """ import json import os import subprocess import sys import time import urllib.error import urllib.request HERE = os.path.dirname(os.path.abspath(__file__)) def ffmpeg(): if os.environ.get("FFMPEG"): return os.environ["FFMPEG"] try: return subprocess.run(["node", "-e", "console.log(require('ffmpeg-static'))"], cwd=HERE, capture_output=True, text=True, check=True).stdout.strip() except Exception: sys.exit("ffmpeg not found: run `npm install` in the skill's scripts/ folder, or set FFMPEG") def key(*names): for n in names: if os.environ.get(n): return os.environ[n] sys.exit(f"missing API key: set {' or '.join(names)} (it is read from the environment, never printed)") def http_json(url, body=None, headers=None, timeout=300, retries=4, retry_on=(429, 500, 503), wait=20): """POST (or GET when body is None) and return parsed JSON, retrying on rate limits and outages.""" data = json.dumps(body).encode() if body is not None else None h = {"Content-Type": "application/json", **(headers or {})} for attempt in range(retries + 1): try: with urllib.request.urlopen(urllib.request.Request(url, data=data, headers=h), timeout=timeout) as r: return json.load(r) except urllib.error.HTTPError as e: msg = e.read().decode()[:600] if e.code in retry_on and attempt < retries: print(f" HTTP {e.code}, retrying in {wait}s", flush=True) time.sleep(wait) continue raise RuntimeError(f"HTTP {e.code}: {msg}") from None def http_bytes(url, body=None, headers=None, timeout=600): data = json.dumps(body).encode() if body is not None else None h = {"Content-Type": "application/json", **(headers or {})} try: with urllib.request.urlopen(urllib.request.Request(url, data=data, headers=h), timeout=timeout) as r: return r.read() except urllib.error.HTTPError as e: raise RuntimeError(f"HTTP {e.code}: {e.read().decode()[:600]}") from None def decode(path, sr=44100, mono=True, af=None): """Decode any audio/video file to float32 samples with numpy.""" import numpy as np args = [ffmpeg(), "-v", "error", "-i", path] if af: args += ["-af", af] args += ["-ac", "1" if mono else "2", "-ar", str(sr), "-f", "f32le", "-"] return np.frombuffer(subprocess.run(args, capture_output=True, check=True).stdout, np.float32) def lufs(path): import re err = subprocess.run([ffmpeg(), "-hide_banner", "-i", path, "-af", "apad=pad_dur=1,ebur128", "-f", "null", "-"], capture_output=True, text=True).stderr return float(re.findall(r"I:\s+(-?[\d.]+) LUFS", err)[-1])