from __future__ import annotations import argparse import json import os import pathlib import platform import shutil import tempfile import time import generator.audio_assets import generator.catalog import generator.engraving.cache import generator.engraving.fonts import generator.engraving.pipeline import generator.engraving.types import generator.parts import generator.pico_artwork import generator.printing import generator.production import generator.score import generator.score_io import generator.summary import modulus.filesystem RUNTIME_FONT_FILES = ("PetalumaScript.otf", "OFL.txt") def copy_runtime_fonts(static_assets_dir: pathlib.Path, output_dir: pathlib.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=pathlib.Path) parser.add_argument("--output-dir", type=pathlib.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=pathlib.Path, default=pathlib.Path("package/assets")) parser.add_argument("--check", action="store_true") parser.add_argument("--engraving-cache", type=pathlib.Path) parser.add_argument("--jobs", type=int, default=1, choices=range(1, 17), metavar="1-16") parser.add_argument("--composition-jobs", type=int, default=2, choices=range(1, 17), metavar="1-16") parser.add_argument("--composition-cache", type=pathlib.Path) parser.add_argument("--trace-level", choices=("summary", "full"), default="summary", help="Diagnostic detail only; does not seed composition.") parser.add_argument("--profile-output", type=pathlib.Path, help="Write build and per-role elapsed timings (not a search timeout).") 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') composition_cache = getattr(args, 'composition_cache', None) if composition_cache: composition_cache = composition_cache.resolve() if composition_cache.is_relative_to(output_dir) or output_dir.is_relative_to(composition_cache): raise ValueError('composition cache and generated assets must not overlap') progress = getattr(args, 'progress', False) started = time.monotonic() timings = [] stages = [] last_stage = started def report(message): nonlocal last_stage now = time.monotonic() stages.append({'completed_before': message, 'seconds': now - last_stage}) last_stage = now if progress: print(f'Assets [{time.monotonic() - started:.0f}s]: {message}', flush=True) report('checking source paths') generator.parts.source_paths(data_dir / 'songs') output_dir.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix='.vamp_assets_', dir=output_dir.parent) as temporary: staged = pathlib.Path(temporary) / 'assets' staged.mkdir(parents=True, exist_ok=True) copy_runtime_fonts(static_assets_dir, staged) report('generating performance parts') performance_count = generator.production.generate_tree(data_dir / 'songs', staged / 'songs', jobs=getattr(args, 'composition_jobs', 2), cache_dir=getattr(args, 'composition_cache', None), progress=progress, timings=timings) if getattr(args, 'trace_level', 'summary') == 'summary': for path in (staged / 'analysis').glob('*.roles'): roles = json.loads(path.read_text()) for role in roles.values(): trace = role.get('details', {}).get('selection_trace', {}) trace.pop('decisions', None) path.write_text(json.dumps(roles, sort_keys=True, indent=2)) generator.score_io.validate_generated_tree(staged) if args.check: generator.summary.validate_expected_performance_summary(pathlib.Path(__file__).resolve().parents[1] / 'tests' / 'fixtures' / 'performance_summary.json', staged / 'songs') report('copying supplied recordings') song_ids = {generator.score.slugify(path.stem) for path in (data_dir / 'songs').glob('*.xml')} audio_assets = generator.audio_assets.copy_audio(static_assets_dir, staged, song_ids) generator.audio_assets.validate_recording_lengths(staged, audio_assets) report('engraving pages') validated_pages = generator.engraving.pipeline.engrave_tree(staged, static_assets_dir / 'fonts' / 'petaluma', cache_dir=cache_dir, jobs=getattr(args, 'jobs', 1), progress=progress) report('generating printable parts') generator.printing.generate_tree(staged, static_assets_dir / 'fonts' / 'petaluma', cache_dir=cache_dir, progress=progress) report('rendering Pico titles') generator.pico_artwork.generate_titles(staged, static_assets_dir / 'fonts' / 'petaluma') report('validating score, page, and audio assets') generator.score_io.validate_generated_tree(staged, validated_pages) report('building catalog database') generator.catalog.build_catalog(staged, audio_assets=audio_assets) with modulus.filesystem.publish_directories([(staged, output_dir)]): pass report('publishing complete') profile_path = getattr(args, 'profile_output', None) if profile_path: profile_path.parent.mkdir(parents=True, exist_ok=True) profile_path.write_text(json.dumps({'total_seconds': time.monotonic() - started, 'stages': stages, 'roles': timings, 'platform': platform.platform(), 'cpus': os.cpu_count(), 'workers': getattr(args, 'composition_jobs', 2)}, indent=2)) print(f'Generated Product Vamp assets: {performance_count} performance scores.') if args.check: print('Generator check passed.') def main() -> int: args = parse_args() if args.renderer_fingerprint: print(generator.engraving.cache.renderer_fingerprint(generator.engraving.fonts.Fonts(args.static_assets_dir / 'fonts' / 'petaluma'), generator.engraving.types.Style())) else: generate_assets(args) return 0 if __name__ == "__main__": raise SystemExit(main())