@@ -20,7 +20,6 @@ import generator.pico_artwork
|
||||
import generator.performance
|
||||
import generator.score
|
||||
import generator.summary
|
||||
import generator.solo.export
|
||||
import platform
|
||||
import os
|
||||
|
||||
@@ -44,7 +43,6 @@ def parse_args() -> argparse.Namespace:
|
||||
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")
|
||||
@@ -96,8 +94,6 @@ def generate_assets(args: argparse.Namespace) -> None:
|
||||
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):
|
||||
generator.solo.export.export_tree(staged)
|
||||
generator.score.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')
|
||||
|
||||
@@ -6,7 +6,6 @@ import pathlib
|
||||
import generator.musicxml
|
||||
import generator.parts
|
||||
import generator.production
|
||||
import generator.solo.export
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
@@ -15,19 +14,13 @@ def main():
|
||||
parser.add_argument('--data-dir', type=pathlib.Path, default=pathlib.Path('data'))
|
||||
parser.add_argument('--output', type=pathlib.Path, required=True)
|
||||
parser.add_argument('--cache-dir', type=pathlib.Path)
|
||||
parser.add_argument('--solo-xml', type=pathlib.Path, help='Also export a completed solo role as MusicXML.')
|
||||
parser.add_argument('--settings', type=pathlib.Path)
|
||||
args = parser.parse_args()
|
||||
if args.solo_xml and not args.role.endswith('_solo'):
|
||||
parser.error('--solo-xml requires a solo role')
|
||||
source = generator.production.SourceAnalysis.read(generator.parts.prepare_document(generator.musicxml.load_musicxml(args.song)))
|
||||
start = time.monotonic()
|
||||
result = generator.production.generate_role(source, args.role, args.data_dir, json.loads(args.settings.read_text()) if args.settings else {}, args.cache_dir)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(result, indent=2))
|
||||
if args.solo_xml:
|
||||
manifest = generator.solo.export.write_role(result, args.solo_xml)
|
||||
args.solo_xml.with_suffix('.manifest.solo').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||
print(f'{args.role}: {time.monotonic() - start:.2f}s wall; cached={result["cache_hit"]}; role generation/cache lookup {result["seconds"]:.2f}s')
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
"""Optional MusicXML views of finished production roles; never composes."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import pathlib
|
||||
import fractions
|
||||
|
||||
import generator.model
|
||||
import generator.musicxml_writer
|
||||
import generator.score
|
||||
|
||||
def region(document, part):
|
||||
measures = document['staves'][0]['measures']
|
||||
first = next(i for i, m in enumerate(measures) if m.get('section_label') == part.capitalize() + ' Solo')
|
||||
end = next((i for i in range(first + 1, len(measures)) if measures[i].get('section_label')), len(measures))
|
||||
return first, end - first
|
||||
|
||||
def excerpt(document, first, count):
|
||||
result = copy.deepcopy(document)
|
||||
staff = result['staves'][0]
|
||||
original = staff['measures']
|
||||
staff['measures'] = original[first:first + count]
|
||||
if not staff['measures']:
|
||||
raise ValueError('Empty solo export region')
|
||||
notes = [n for m in staff['measures'] for n in generator.score.measure_notes(m)]
|
||||
identities = {p['id'] for n in notes for p in n['pitches']}
|
||||
events = {n['id'] for n in notes}
|
||||
for note in notes:
|
||||
for pitch in note['pitches']:
|
||||
for key in ('tie_from', 'tie_to'):
|
||||
if pitch.get(key) not in identities:
|
||||
pitch.pop(key, None)
|
||||
staff['tuplets'] = [t for t in staff.get('tuplets', []) if t['start'] in events and t['end'] in events]
|
||||
tuplets = {t['id'] for t in staff['tuplets']}
|
||||
for note in notes:
|
||||
if 'tuplet_ids' in note:
|
||||
note['tuplet_ids'] = [i for i in note['tuplet_ids'] if i in tuplets]
|
||||
offset = sum((generator.model.fraction(m['duration']) for m in original[:first]), fractions.Fraction())
|
||||
end = offset + sum((generator.model.fraction(m['duration']) for m in staff['measures']), fractions.Fraction())
|
||||
for key in ('scale_spans', 'octave_shifts'):
|
||||
staff[key] = [{**s, 'start': generator.model.rational(max(offset, generator.model.fraction(s['start'])) - offset),
|
||||
'end': generator.model.rational(min(end, generator.model.fraction(s['end'])) - offset)} for s in staff.get(key, [])
|
||||
if generator.model.fraction(s['start']) < end and generator.model.fraction(s['end']) > offset]
|
||||
generator.model.validate_document(result)
|
||||
return result
|
||||
|
||||
def write_role(result, path):
|
||||
role = result['role']
|
||||
if role not in ('acoustic_solo', 'electric_solo'):
|
||||
raise ValueError('Solo XML export requires an acoustic_solo or electric_solo role')
|
||||
part = role.split('_')[0]
|
||||
document = result['document']
|
||||
first, count = region(document, part)
|
||||
leadin = int(result.get('details', {}).get('allocation', {}).get('lead_in_measures', 0))
|
||||
first -= leadin
|
||||
count += leadin
|
||||
selected = excerpt(document, first, count)
|
||||
numbers = [m['number'] for m in selected['staves'][0]['measures']]
|
||||
for i, measure in enumerate(selected['staves'][0]['measures'], 1):
|
||||
measure['number'] = str(i)
|
||||
path = pathlib.Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
generator.musicxml_writer.write_musicxml(path, selected, tablature=True)
|
||||
return {'role': role, 'file': path.name, 'form_start_measure': 1 + leadin,
|
||||
'measure_count': count, 'arrangement_measures': numbers,
|
||||
'arrangement_start_beat': str(sum((generator.model.fraction(m['duration']) for m in document['staves'][0]['measures'][:first]), fractions.Fraction())),
|
||||
'pitch_convention': 'guitar-written; explicit octave transposition -1',
|
||||
'trace': result['trace'], 'phrase_report': result.get('details', {})}
|
||||
|
||||
def export_tree(assets, output=None):
|
||||
assets = pathlib.Path(assets)
|
||||
output = pathlib.Path(output) if output else assets / 'solo_xml'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
entries = {}
|
||||
for path in sorted((assets / 'analysis').glob('*.roles')):
|
||||
roles = json.loads(path.read_text())
|
||||
for i, part in enumerate(('acoustic', 'electric'), 1):
|
||||
name = path.stem.replace('_', '-') + f'-solo{i}.xml'
|
||||
entries[path.stem + '/' + part] = write_role(roles[part + '_solo'], output / name)
|
||||
manifest = {'version': 1, 'composition_inputs': False, 'solos': entries}
|
||||
# Runtime score discovery treats *.json under assets as score documents.
|
||||
# Diagnostic manifests deliberately use a non-score extension, like .roles.
|
||||
(output / 'manifest.solo').write_text(json.dumps(manifest, sort_keys=True, indent=2) + '\n')
|
||||
return manifest
|
||||
Reference in New Issue
Block a user