147 lines
7.2 KiB
Python
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('evaluations',None);trace.pop('source_candidate_pools',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())
|