from __future__ import annotations import argparse import shutil import tempfile from pathlib import Path from time import monotonic from generator.arrange.generate import generate_performance_scores from generator.practice.generate import generate_practice_scores from generator.score import count_json_files, validate_generated_tree from generator.summary import validate_expected_performance_summary, validate_practice_parts from generator.parts import source_paths from generator.engraving import Fonts, Style, engrave_tree from generator.engraving.cache import renderer_fingerprint from generator.playback import compile_tree from generator.catalog import build_catalog RUNTIME_FONT_FILES = ("PetalumaScript.otf", "OFL.txt") def copy_runtime_fonts(static_assets_dir: Path, output_dir: Path) -> None: source = static_assets_dir / "fonts" / "petaluma" destination = output_dir / "fonts" / "petaluma" destination.mkdir(parents=True, exist_ok=True) for name in RUNTIME_FONT_FILES: shutil.copy2(source / name, destination / name) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Generate Product Vamp package score assets.") parser.add_argument("--data-dir", type=Path) parser.add_argument("--output-dir", type=Path) parser.add_argument("--renderer-fingerprint", action="store_true", help="Print the engraving cache identity without generating assets.") parser.add_argument("--static-assets-dir", type=Path, default=Path("package/assets")) parser.add_argument("--check", action="store_true") parser.add_argument("--engraving-cache", type=Path) parser.add_argument("--jobs", type=int, default=1, choices=range(1, 17), metavar="1-16") parser.add_argument("--progress", action="store_true") args = parser.parse_args() if not args.renderer_fingerprint and (args.data_dir is None or args.output_dir is None): parser.error('--data-dir and --output-dir are required for generation') return args def generate_assets(args: argparse.Namespace) -> None: data_dir = args.data_dir.resolve() output_dir = args.output_dir.resolve() static_assets_dir = args.static_assets_dir.resolve() cache_dir = getattr(args, 'engraving_cache', None) cache_dir = cache_dir.resolve() if cache_dir else None if cache_dir and (cache_dir.is_relative_to(output_dir) or output_dir.is_relative_to(cache_dir)): raise ValueError('engraving cache and generated assets must not overlap') progress = getattr(args, 'progress', False) started = monotonic() def report(message): if progress: print(f'Assets [{monotonic() - started:.0f}s]: {message}', flush=True) report('checking source paths') for source in sorted(data_dir.rglob('*.xml')): if source.is_relative_to(data_dir / 'rhythms'): continue if source.parent not in (data_dir / 'practice', data_dir / 'songs'): raise ValueError(f'{source}: MusicXML sources belong directly under data/practice or data/songs') source_paths(data_dir / 'songs') source_paths(data_dir / 'practice', practice=True) output_dir.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix='.vamp-assets-', dir=output_dir.parent) as temporary: staged = Path(temporary) / 'assets' staged.mkdir(parents=True, exist_ok=True) copy_runtime_fonts(static_assets_dir, staged) report('generating performance and practice parts') performance_count = generate_performance_scores(data_dir / 'songs', staged / 'songs') practice_count = generate_practice_scores(data_dir / 'practice', staged / 'practice') validate_generated_tree(staged) if args.check: validate_expected_performance_summary(data_dir / 'expected' / 'performance_summary.json', staged / 'songs') validate_practice_parts(staged / 'practice') report('compiling playback') compile_tree(staged) report('engraving pages') engrave_tree(staged, static_assets_dir / 'fonts' / 'petaluma', cache_dir=cache_dir, jobs=getattr(args, 'jobs', 1), progress=progress) report('validating score, page, and audio assets') validate_generated_tree(staged) report('building catalog database') build_catalog(staged) backup = Path(temporary) / 'previous' if output_dir.exists(): output_dir.rename(backup) try: staged.rename(output_dir) except OSError: if backup.exists(): backup.rename(output_dir) raise report('publishing complete') print(f'Generated Product Vamp assets: {performance_count} performance scores, ' f'{practice_count} practice parts, {count_json_files(output_dir)} total JSON files.') if args.check: print('Generator check passed.') def main() -> int: args = parse_args() if args.renderer_fingerprint: print(renderer_fingerprint(Fonts(args.static_assets_dir / 'fonts' / 'petaluma'), Style())) else: generate_assets(args) return 0 if __name__ == "__main__": raise SystemExit(main())