Files
product-vamp/generator/__main__.py
T
khodowany 0e68f5364b Cleanup
2026-10-04 18:48:02 -05:00

147 lines
7.2 KiB
Python

from __future__ import annotations
import argparse
import json
import pathlib
import shutil
import tempfile
import time
import modulus.filesystem
import generator.arrange.generate
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.playback
import generator.rhythm
import generator.scales.library
import generator.score
import generator.summary
import generator.solo.sequences
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("--export-solo-xml", action="store_true", help="Export completed solo roles as comparison MusicXML, without recomposing.")
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')
for source in sorted(data_dir.rglob('*.xml')):
if any(source.is_relative_to(data_dir / name) for name in ('rhythms', 'scales', 'sequences')):
continue
if source.parent != data_dir / 'songs':
raise ValueError(f'{source}: MusicXML sources belong directly under data/songs')
generator.parts.source_paths(data_dir / 'songs')
report('loading rhythm snippets')
rhythms = generator.rhythm.load_library(data_dir / 'rhythms')
report('loading solo scale patterns')
scales = generator.scales.library.load_library(data_dir / 'scales')
report('loading solo interval sequences')
sequences = generator.solo.sequences.load_library(data_dir / 'sequences')
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.arrange.generate.generate_performance_scores(data_dir / 'songs', staged / 'songs', rhythms=rhythms, scales=scales, sequences=sequences,
composition_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))
if getattr(args, 'export_solo_xml', False):
from generator.solo.export import export_tree
export_tree(staged)
generator.score.validate_generated_tree(staged)
if args.check:
generator.summary.validate_expected_performance_summary(data_dir / 'expected' / 'performance_summary.json', staged / 'songs')
report('compiling playback')
generator.playback.compile_tree(staged)
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('rendering Pico titles')
generator.pico_artwork.generate_titles(staged, static_assets_dir / 'fonts' / 'petaluma')
report('validating score, page, and audio assets')
generator.score.validate_generated_tree(staged, validated_pages)
report('building catalog database')
generator.catalog.build_catalog(staged)
with modulus.filesystem.publish_directories([(staged, output_dir)]):
pass
report('publishing complete')
profile_path=getattr(args,'profile_output',None)
if profile_path:
import platform,os
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())