"""Build background-music loops from FreePD (public-domain / CC0). FreePD (Kevin MacLeod) releases its catalogue as 100% public-domain music, mirrored on the Internet Archive (item `freepd`). We pull four mood tracks, trim each to a ~60s segment, normalise, fade, and save a compact mono OGG loop used by tts/music.py. No attribution is required (public domain), but FreePD is credited in the README. Run from the repo root: python scripts/build_music_loops.py OGG files are tracked via Git LFS (see .gitattributes). """ from __future__ import annotations import io import os import urllib.parse import urllib.request import numpy as np import soundfile as sf IA_BASE = "https://archive.org/download/freepd" TARGET_SR = 44100 SEG_START = 8.0 # skip any intro SEG_LEN = 60.0 OUT_DIR = os.path.join("tts", "music_loops") # bed id -> FreePD track (path within the IA item) TRACKS = { "ambient_drift": "Page2/Ambient C Motion.mp3", "lofi_pulse": "Page2/Chill Beat.mp3", "cinematic_rise": "Page2/Wonder Flow.mp3", "newsroom_bed": "Page2/Action Investigation.mp3", } def _fetch(track: str) -> bytes: url = IA_BASE + "/" + urllib.parse.quote(track) req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=120) as r: return r.read() def _resample(x: np.ndarray, sr: int, target: int) -> np.ndarray: if sr == target: return x n = int(round(len(x) * target / sr)) xp = np.linspace(0, 1, len(x), dtype=np.float64) fp = np.linspace(0, 1, n, dtype=np.float64) return np.interp(fp, xp, x).astype(np.float32) def build(): os.makedirs(OUT_DIR, exist_ok=True) for bed_id, track in TRACKS.items(): audio, sr = sf.read(io.BytesIO(_fetch(track)), dtype="float32") if audio.ndim > 1: audio = audio.mean(axis=1) audio = _resample(audio, sr, TARGET_SR) start = int(SEG_START * TARGET_SR) if start + int(5 * TARGET_SR) >= len(audio): start = 0 seg = audio[start:start + int(SEG_LEN * TARGET_SR)] peak = float(np.abs(seg).max()) or 1.0 seg = (seg / peak * 0.9).astype(np.float32) f = int(1.0 * TARGET_SR) if len(seg) > 2 * f: seg[:f] *= np.linspace(0, 1, f, dtype=np.float32) seg[-f:] *= np.linspace(1, 0, f, dtype=np.float32) # FLAC: stable libsndfile write + lossless/compact. Loops are read server-side # only (mixed into the speech), so browser codec support is irrelevant. out = os.path.join(OUT_DIR, f"{bed_id}.flac") sf.write(out, seg, TARGET_SR, format="FLAC", subtype="PCM_16") print(f" {bed_id:16s} <- {track} {len(seg)/TARGET_SR:.1f}s {os.path.getsize(out)//1024}KB") if __name__ == "__main__": build() print("done.")