This commit is contained in:
khodowany
2026-10-03 10:12:12 -05:00
parent 5fff5a165e
commit 59afd78205
14 changed files with 666 additions and 41 deletions
+26 -8
View File
@@ -78,13 +78,13 @@ def fingerprint():
def resources():
return (generator.rhythm.load_library(pathlib.Path('data/rhythms')), generator.scales.library.load_library(pathlib.Path('data/scales')), generator.solo.sequences.load_library(pathlib.Path('data/sequences')))
def capture(root, stage, libraries=None, contexts=None):
def capture(root, stage, libraries=None, contexts=None, *, enclosures_enabled=True):
destination = root / stage
if destination.exists():
raise ValueError(f'{destination}: snapshot already exists; choose a new output root')
rhythms, scales, sequences = libraries or resources()
reference_library = generator.solo.references.load_library(pathlib.Path('data/lead_phrases'))
result = {'stage': stage, 'implementation': fingerprint(), 'reference_versions': [r.version for r in reference_library], 'songs': {}}
result = {'enclosures_enabled':enclosures_enabled,'stage': stage, 'implementation': fingerprint(), 'reference_versions': [r.version for r in reference_library], 'songs': {}}
try:
result['policy'] = importlib.import_module('generator.solo.policy').describe()
except ModuleNotFoundError:
@@ -94,7 +94,7 @@ def capture(root, stage, libraries=None, contexts=None):
checkpoint = root / f'.{stage}_{path.stem}_checkpoint.json'
if checkpoint.exists():
saved = json.loads(checkpoint.read_text())
if saved['implementation'] == result['implementation'] and saved.get('reference_versions') == result['reference_versions'] and saved['song']['source_sha256'] == source_hash:
if saved.get('enclosures_enabled',True)==enclosures_enabled and saved['implementation'] == result['implementation'] and saved.get('reference_versions') == result['reference_versions'] and saved['song']['source_sha256'] == source_hash:
result['songs'][path.stem] = saved['song']
result.setdefault('resumed_songs', []).append(path.stem)
continue
@@ -107,7 +107,7 @@ def capture(root, stage, libraries=None, contexts=None):
if shared:
for context in shared.values():
context.diagnostics = generator.solo.realization.SearchContext().diagnostics
generator.solo.arrangement.apply(document, parts, rhythms=rhythms, scales=scales, sequences=sequences, solos=solos, revise=stage != 'phrase', contexts=shared, references=reference_library)
generator.solo.arrangement.apply(document, parts, rhythms=rhythms, scales=scales, sequences=sequences, solos=solos, revise=stage != 'phrase', contexts=shared, references=reference_library,enclosures_enabled=enclosures_enabled)
song = {'source_sha256': hashlib.sha256(path.read_bytes()).hexdigest(), 'parts': parts, 'solos': {}, 'elapsed_seconds': time.monotonic() - started}
regions = generator.solo.arrangement.regions(document)
for solo, index in zip(solos, (3, 4)):
@@ -135,7 +135,7 @@ def capture(root, stage, libraries=None, contexts=None):
drafts.append({'id': phrase.plan.id, 'draft_id': phrase.draft.id, 'first': begin, 'count': stop - begin, 'document': view})
song['solos'][solo.part]['draft_documents'] = drafts
stages = {}
for stage_name in ('initial', 'refined'):
for stage_name in ('initial', 'refined', 'before_enclosure'):
if not all(getattr(p, stage_name) for p in solo.phrases):
continue
notes = tuple(n for p in solo.phrases for n in getattr(p, stage_name).notes)
@@ -157,7 +157,7 @@ def capture(root, stage, libraries=None, contexts=None):
print(path.stem, solo.part, json.dumps({k: v for k, v in song['solos'][solo.part]['metrics'].items() if k != 'search'}), flush=True)
result['songs'][path.stem] = song
root.mkdir(parents=True, exist_ok=True)
checkpoint.write_text(json.dumps({'implementation': result['implementation'], 'reference_versions': result['reference_versions'], 'song': song}) + '\n')
checkpoint.write_text(json.dumps({'enclosures_enabled':enclosures_enabled,'implementation': result['implementation'], 'reference_versions': result['reference_versions'], 'song': song}) + '\n')
destination.mkdir(parents=True)
(destination / 'snapshot.json').write_text(json.dumps(result, indent=2) + '\n')
@@ -240,6 +240,19 @@ def render(root):
samples = mix((foreground, backing, vocal), fractions.Fraction(solo['tempo']), root, cache)
clips.append((path, samples))
manifest['clips'].append({'path': path.relative_to(root).as_posix(), 'window': label, 'first': offset, 'count': count, 'tempo': solo['tempo'], 'responsive_backing': responsive, 'source_sha256': song['source_sha256'], 'metrics': solo['metrics'], 'elapsed_seconds': song['elapsed_seconds']})
if stage == 'after' and 'before_enclosure' in solo.get('lead_stages', {}):
strict = excerpt(solo['lead_stages']['before_enclosure']['document'], offset, count)
strict_prefix=prefix.with_name(prefix.name+'_strict')
generator.musicxml_writer.write_musicxml(strict_prefix.with_suffix('.musicxml'),strict)
backing=excerpt(baseline['parts'][other],first,count)
vocal=excerpt(baseline['parts']['vocal'],first,count)
path=strict_prefix.with_name(strict_prefix.name+'_fixed').with_suffix('.wav')
clips.append((path,mix((strict,backing,vocal),fractions.Fraction(solo['tempo']),root,cache)))
manifest['clips'].append({'path':path.relative_to(root).as_posix(),'kind':'same_selected_theme_before_enclosures','window':label})
if label=='full':
path=strict_prefix.with_name(strict_prefix.name+'_solo').with_suffix('.wav')
clips.append((path,mix((strict,),fractions.Fraction(solo['tempo']),root,cache)))
manifest['clips'].append({'path':path.relative_to(root).as_posix(),'kind':'strict_unaccompanied','window':label})
if stage == 'after' and 'refined' in solo.get('lead_stages', {}):
plain = excerpt(solo['lead_stages']['refined']['document'], offset, count)
plain_prefix = prefix.with_name(prefix.name + '_plain')
@@ -328,13 +341,17 @@ def write_passage_index(root, snapshots):
for stage in ('before', 'after'):
metric = snapshots[stage]['songs'][name]['solos'][part]['metrics']
body.append(f"<p>{stage.title()}: {metric['attacks_per_four_beats']:.2f} attacks / four beats; {metric['sounding_occupancy']:.1%} occupancy; longest unplanned gap {metric['longest_unplanned_gap_beats']:g} beats.</p>")
strict_available='before_enclosure' in song['solos'][part].get('lead_stages',{})
plain_available='refined' in song['solos'][part].get('lead_stages',{})
body.append('<table><tr><th>Passage</th><th>Before, fixed backing</th><th>After, fixed backing</th><th>After, responsive backing</th>' + ('<th>Plain theme, fixed backing</th>' if plain_available else '') + '</tr>')
body.append('<table><tr><th>Passage</th><th>Before, fixed backing</th><th>After, fixed backing</th><th>After, responsive backing</th>' + ('<th>Same selected solo, without enclosures</th>' if strict_available else '') + ('<th>Plain theme, fixed backing</th>' if plain_available else '') + '</tr>')
for window in ('full', 'opening8', 'closing4'):
body.append(f'<tr><th>{window}</th>')
for stage, mix_name in (('before', 'fixed'), ('after', 'fixed'), ('after', 'responsive')):
prefix = stage + '/' + name + '_' + part + '_' + window
body.append(f'<td><audio controls preload="none" src="{prefix}_{mix_name}.wav"></audio><small><a href="{prefix}.musicxml">MusicXML</a> · <a href="{prefix}.txt">Report</a>' + (f' · <a href="{prefix}.json">Passage data</a>' if stage == 'after' else '') + '</small></td>')
if strict_available:
prefix=f'after/{name}_{part}_{window}_strict'
body.append(f'<td><audio controls preload="none" src="{prefix}_fixed.wav"></audio><small><a href="{prefix}.musicxml">Strict notation</a></small></td>')
if plain_available:
prefix=f'after/{name}_{part}_{window}_plain'
body.append(f'<td><audio controls preload="none" src="{prefix}_fixed.wav"></audio><small><a href="{prefix}.musicxml">Plain theme notation</a></small></td>')
@@ -352,13 +369,14 @@ def main():
action.add_argument('--capture', choices=(*STAGES, 'before', 'after', 'phrase', 'revised'))
action.add_argument('--compare-phrases', action='store_true', help='capture both new stages using shared concrete draft pools')
action.add_argument('--render', action='store_true')
parser.add_argument('--no-enclosures',action='store_true',help='retain the identical selected strict melody before ornamentation')
args = parser.parse_args()
if args.compare_phrases:
libraries, contexts = resources(), {}
capture(args.output, 'phrase', libraries, contexts)
capture(args.output, 'revised', libraries, contexts)
elif args.capture:
capture(args.output, args.capture)
capture(args.output, args.capture,enclosures_enabled=not args.no_enclosures)
else:
render(args.output)