This commit is contained in:
khodowany
2026-10-04 22:29:06 -05:00
parent 0e68f5364b
commit ef62af892b
40 changed files with 1754 additions and 789 deletions
+26 -17
View File
@@ -17,7 +17,7 @@ import tempfile
import time
from fractions import Fraction as F
from generator import accompaniment, comping, fretboard, model, musicxml, parts, rhythm, score, vocabulary
from generator import accompaniment, comping, fretboard, model, musicxml, parts, rhythm, score, vocabulary, resources as catalogs
from generator.arrange import form, generate, pickup
from generator.solo import analysis, composer, notation, report, sequences
from generator.scales import library as scales
@@ -66,13 +66,13 @@ def windows(document, part):
a, _, old = result[-1]; result[-1] = (a, start, old)
result.append((start, cursor, part+'_'+role))
end = cursor+duration
result.append((cursor, end, part+'_'+role))
if part == 'electric' and section.role == 'slash' and score.body_start_measure_index(source):
attacks = [model.fraction(n['onset']) for n in score.measure_notes(source[0]) if n['pitches']]
if attacks:
cutoff = end-model.fraction(source[0]['duration'])+min(attacks)
result[-1] = (cursor, cutoff, part+'_solo')
result.append((cutoff, end, part+'_rest'))
start=cursor
if section.role == 'slash' and score.body_start_measure_index(source) and result:
# Allocate the source's complete pickup bar before composition. This
# does not inspect either generated solo; a role may leave it silent.
start=cursor-model.fraction(source[0]['duration'])
a,_,old=result[-1];result[-1]=(a,start,old)
result.append((start, end, part+'_'+role))
cursor = end
return tuple(result)
@@ -89,7 +89,7 @@ def _code_key(role):
paths += list((root/'arrange').glob('*.py'))
if role.endswith('_solo'):
paths += [p for p in (root/'solo').glob('*.py') if 'audition' not in p.name and p.name != 'references.py']
paths += list((root/'scales').glob('*.py')) + [root/'rhythm.py']
paths += list((root/'scales').glob('*.py')) + [root/'rhythm.py', root/'resources.py', root/'musicxml.py']
elif role.endswith('_accompaniment'):
paths += [root/n for n in ('comping.py', 'accompaniment.py', 'guitar.py')]
elif role == 'electric_melody':
@@ -102,8 +102,8 @@ def _resource_key(directory, role):
return {name: vocabulary.fingerprint([(p.relative_to(directory/name).as_posix(), hashlib.sha256(p.read_bytes()).hexdigest()) for p in sorted((directory/name).rglob('*.xml'))]) for name in names}
@functools.lru_cache(maxsize=4)
def _solo_libraries(directory, fingerprints):
return scales.load_library(directory/'scales'),sequences.load_library(directory/'sequences'),rhythm.load_library(directory/'rhythms')
def _solo_libraries(directory, fingerprints, cache_dir=None):
return catalogs.load(directory,dict(fingerprints),cache_dir)
def _mask(document, allowed):
@@ -153,7 +153,7 @@ def generate_role(source, role, data_dir, settings=None, cache_dir=None):
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['trace']={**result['trace'],'input_boundary':'data, generator code, Python runtime, own role cache, content-addressed immutable catalogs'}
result['observed_reads']=sorted(reads)
result['seconds']=time.monotonic()-started
return result
@@ -194,15 +194,24 @@ def _generate_role(source, role, data_dir, settings=None, cache_dir=None):
effective=generate.section_source(authored,section,form.full_arrangement_sections(authored,part)[si+1])
template=copy.deepcopy(effective[section.source_measure_index:section.source_measure_index+count])
initial=accompaniment.carried_chords(effective)[section.source_measure_index]
end=next(b for a,b,r in windows(document,part) if r==role)-offset
scale_library,sequence_library,rhythm_library=_solo_libraries(data_dir,tuple(sorted(inputs['catalogs'].items())))
start,end=next((a-offset,b-offset) for a,b,r in windows(document,part) if r==role)
notation_first=first
if start<0:
# The preceding arrangement harmony comes from the source form. Do
# not invent harmony from a comparison score's unlabelled lead-in.
template.insert(0,copy.deepcopy(result['staves'][0]['measures'][first-1]))
notation_first-=1
initial=accompaniment.carried_chords(result['staves'][0]['measures'])[first-1]
scale_library,sequence_library,rhythm_library=_solo_libraries(data_dir,tuple(sorted(inputs['catalogs'].items())),cache_dir)
solo=composer.compose(analysis.analyze(document),template,part,end,
initial=initial,scales=scale_library,sequences=sequence_library,rhythms=rhythm_library,
expressions_enabled=vocabulary.role().get('expressions',True))
expressions_enabled=vocabulary.role().get('expressions',True),start=start)
composition_timings={k:v for k,v in solo.diagnostics.items() if k.endswith('_seconds')}
output=notation.measures(result['staves'][0]['measures'][first:first+count],solo.notes(),'solo_'+part)
notation.replace_region(result,first,output);annotate(result,solo,offset)
relative=tuple(dataclasses.replace(n,start=n.start-start,end=n.end-start) for n in solo.notes())
output=notation.measures(result['staves'][0]['measures'][notation_first:first+count],relative,'solo_'+part)
notation.replace_region(result,notation_first,output);annotate(result,solo,offset)
details=report.structure(solo)
details['allocation']={'start':str(start),'end':str(end),'lead_in_measures':first-notation_first}
solo_report=report.describe(solo,scale_library,first,offset,detail=details)
elif role.endswith('_accompaniment'):
canonical=comping.Canonical(document,part,seed)