This commit is contained in:
khodowany
2026-10-04 15:51:18 -05:00
parent 405066eb43
commit f234dd6d51
54 changed files with 2104 additions and 330 deletions
+17 -4
View File
@@ -43,7 +43,7 @@ class SourceAnalysis:
staff.update(id='source', name='Source')
melodic = analysis.analyze(prepared)
content = {'melody': [(str(n.start), str(n.end), dataclasses.asdict(n.pitch)) for n in melodic.melody],
'measures': [{k: m.get(k) for k in ('duration', 'meter', 'key', 'harmonies', 'tempos', 'section_label', 'implicit')} for m in staff['measures']]}
'measures': [{k: m.get(k) for k in ('duration', 'time_signature', 'key', 'harmonies', 'tempos', 'section_label', 'implicit')} for m in staff['measures']]}
# Fraction/string spelling is canonical and never includes filesystem IDs.
key = hashlib.sha256(json.dumps(content, sort_keys=True, default=str).encode()).hexdigest()
return cls(json.dumps(prepared, sort_keys=True), key)
@@ -148,6 +148,18 @@ def _fill(notes, length, prefix, voice='voice_arranged'):
def generate_role(source, role, data_dir, settings=None, cache_dir=None):
from generator.input_audit import composition
started=time.monotonic()
with composition(data_dir,role,cache_dir) as reads:
result=_generate_role(source,role,data_dir,settings,cache_dir)
# Observed reads are diagnostics, never cache identity or musical seeds.
result['trace']={**result['trace'],'input_boundary':'data, generator code, Python runtime, own role cache'}
result['observed_reads']=sorted(reads)
result['seconds']=time.monotonic()-started
return result
def _generate_role(source, role, data_dir, settings=None, cache_dir=None):
if role not in ROLES: raise ValueError('Unknown generation role: '+role)
data_dir=Path(data_dir); resources=vocabulary.load(data_dir/'composition')
settings=settings or {}
@@ -190,7 +202,7 @@ def generate_role(source, role, data_dir, settings=None, cache_dir=None):
output=notation.measures(result['staves'][0]['measures'][first:first+count],solo.notes(),'solo_'+part)
notation.replace_region(result,first,output);annotate(result,solo,offset)
details=report.structure(solo)
solo_report=report.describe(solo,scale_library,first,offset)
solo_report=report.describe(solo,scale_library,first,offset,detail=details)
elif role.endswith('_accompaniment'):
canonical=comping.Canonical(document,part,seed)
result=generate._arrange_document(document,part,{},owned={'accompaniment'},accompaniment=canonical)
@@ -251,7 +263,7 @@ def _job(request):
return generate_role(source,role,directory,settings,cache)
def generate_tree(source_dir, output_dir, *, jobs=2, cache_dir=None, progress=False):
def generate_tree(source_dir, output_dir, *, jobs=2, cache_dir=None, progress=False, timings=None):
if type(jobs) is not int or not 1<=jobs<=16:
raise ValueError('composition workers must be between 1 and 16')
source_dir,output_dir=Path(source_dir),Path(output_dir)
@@ -263,6 +275,7 @@ def generate_tree(source_dir, output_dir, *, jobs=2, cache_dir=None, progress=Fa
results={p:{} for p in sources}
def accept(path,role,value):
results[path][role]=value
if timings is not None:timings.append({'source':str(path),'role':role,'seconds':value['seconds'],'cache_hit':value['cache_hit'],'observed_reads':value.get('observed_reads',[])})
if progress:print(f'Composition: {path.stem}/{role}: '+('cached' if value['cache_hit'] else f'{value["seconds"]:.1f}s'),flush=True)
if jobs==1:
for path,role,args in requests:
@@ -283,6 +296,6 @@ def generate_tree(source_dir, output_dir, *, jobs=2, cache_dir=None, progress=Fa
analysis_dir=output_dir.parent/'analysis';analysis_dir.mkdir(parents=True,exist_ok=True)
(analysis_dir/(score.slugify(path.stem)+'.txt')).write_text('\n'.join(results[path][r]['report'] for r in ROLES))
# Not a score file: diagnostics are deliberately outside the songs schema.
stable={role:{k:v for k,v in value.items() if k not in ('seconds','cache_hit')} for role,value in results[path].items()}
stable={role:{k:v for k,v in value.items() if k not in ('seconds','cache_hit','observed_reads')} for role,value in results[path].items()}
(analysis_dir/(score.slugify(path.stem)+'.roles')).write_text(json.dumps(stable,sort_keys=True,indent=2))
return len(sources)*3