#!/usr/bin/env python3 """Batch FLP generator — produces 50 unique reggaeton FLP+JSON pairs. Usage: python scripts/batch_generate.py [--count 50] [--out-dir output/batch] Output structure: output/batch_{timestamp}/ reggaeton_000_95bpm_Am_i-VII-VI-VII.json reggaeton_000_95bpm_Am_i-VII-VI-VII.flp reggaeton_001_90bpm_Dm_i-iv-VII-III.json ... manifest.json ← list of all generated songs with metadata """ import argparse import json import re import sys from datetime import datetime from pathlib import Path sys.path.insert(0, str(Path(__file__).parents[1])) from src.composer.variation import generate_batch from src.flp_builder.builder import FLPBuilder from src.flp_builder.schema import SongDefinition # --------------------------------------------------------------------------- # Filename helpers # --------------------------------------------------------------------------- _UNSAFE_RE = re.compile(r'[^\w\-]') def sanitize_filename(s: str) -> str: """Replace unsafe filename chars with _.""" return _UNSAFE_RE.sub('_', s) def make_filename(idx: int, song: SongDefinition) -> str: """Build stem like ``reggaeton_000_95bpm_Am_i_VII_VI_VII`` (no extension).""" prog_safe = sanitize_filename(song.progression_name) return f"reggaeton_{idx:03d}_{song.meta.bpm}bpm_{song.meta.key}_{prog_safe}" # --------------------------------------------------------------------------- # Manifest # --------------------------------------------------------------------------- def build_manifest(songs: list[SongDefinition], filenames: list[str]) -> dict: """Build manifest dict with per-song metadata.""" entries = [] for idx, (song, stem) in enumerate(zip(songs, filenames)): bar_count = int(max(item.bar + item.bars for item in song.items)) entries.append({ "idx": idx, "filename": stem, "bpm": song.meta.bpm, "key": song.meta.key, "progression": song.progression_name, "title": song.meta.title, "bars": bar_count, }) return { "generated_at": datetime.now().isoformat(), "count": len(songs), "songs": entries, } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main(): parser = argparse.ArgumentParser(description="Batch FLP generator") parser.add_argument("--count", type=int, default=50, help="Number of songs to generate (default: 50)") parser.add_argument("--out-dir", default="", help="Output directory (default: output/batch_{timestamp})") args = parser.parse_args() timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") out_dir = Path(args.out_dir) if args.out_dir else Path("output") / f"batch_{timestamp}" out_dir.mkdir(parents=True, exist_ok=True) print(f"Generating {args.count} songs -> {out_dir}") songs = generate_batch(args.count) builder = FLPBuilder() filenames: list[str] = [] for idx, song in enumerate(songs): stem = make_filename(idx, song) filenames.append(stem) # Write JSON json_path = out_dir / f"{stem}.json" json_path.write_text(song.to_json(), encoding="utf-8") # Write FLP flp_path = out_dir / f"{stem}.flp" flp_bytes = builder.build(song) flp_path.write_bytes(flp_bytes) bar_count = int(max(item.bar + item.bars for item in song.items)) print(f" [{idx+1:>3}/{args.count}] {stem}.flp {len(flp_bytes):>9,}b {bar_count}bars") # Write manifest manifest = build_manifest(songs, filenames) (out_dir / "manifest.json").write_text( json.dumps(manifest, indent=2), encoding="utf-8" ) total_size = sum((out_dir / f"{f}.flp").stat().st_size for f in filenames) print(f"\nDone. {args.count} FLPs in {out_dir}") print(f" Total size: {total_size:,} bytes") if __name__ == "__main__": main()