Sepearate Generation

This commit is contained in:
khodowany
2026-10-04 09:05:25 -05:00
parent e8ac7b5edd
commit 8fdd736e13
56 changed files with 1739 additions and 341 deletions
+33 -12
View File
@@ -10,7 +10,8 @@ from fractions import Fraction as F
import generator.model
import generator.harmony
from generator.solo import types, policy, sequences, routes, references, similarity, phrasing, profiles
import generator.vocabulary
from generator.solo import types, policy, sequences, routes, features as references, similarity, phrasing, profiles
def source_notes(analysis):
@@ -60,20 +61,23 @@ def seeds(analysis, bar, seed):
event_id=n.sources[0]) for n in analysis.melody)
groups=[]
for i in range(len(melody)):
for count in ((len(melody),) if len(melody)<3 else (3,4,5)):
for count in ((len(melody),) if len(melody)<3 else (3,4,5,6,7,8)):
group=melody[i:i+count]
if len(group)!=count or group[-1].end-group[0].start>max(bar,F(3)) or group[0].start>=8*bar:
if len(group)!=count or group[-1].end-group[0].start>max(2*bar,F(3)):
continue
local=tuple(dataclasses.replace(n,start=n.start-group[0].start,end=n.end-group[0].start) for n in group)
parent=next((s.id for s in analysis.source_phrases if s.start<=max(0,group[0].start)<s.end),'pickup')
for treatment in ('rhythmic_seed','contour_seed','opening_fragment','characteristic_interval'):
for treatment in generator.vocabulary.role().get('preferred_features',('rhythmic_seed','contour_seed','opening_fragment','characteristic_interval')):
identity=policy.stable_rank(seed,(group,treatment))[:12]
groups.append(types.SourceReference(identity,parent,group[0].start,group[-1].end,local,treatment))
def rank(ref):
rhythm={(n.start,n.end-n.start) for n in ref.notes}
intervals={b.pitch.number()-a.pitch.number() for a,b in zip(ref.notes,ref.notes[1:])}
return (-len(intervals),-len(rhythm),ref.start,ref.id)
return sequences.diverse(groups,rank,lambda r:r.treatment,policy.DEFAULT.theme_seed_limit)
preferred=generator.vocabulary.role().get('preferred_features',[])
preference=preferred.index(ref.treatment) if ref.treatment in preferred else 0
turn=any(a*b<0 for a,b in zip(ref.notes and tuple(b.pitch.number()-a.pitch.number() for a,b in zip(ref.notes,ref.notes[1:])),tuple(b.pitch.number()-a.pitch.number() for a,b in zip(ref.notes,ref.notes[1:]))[1:]))
return (-min(4,len(intervals))-.3*turn+.15*preference,-min(3,len(rhythm)),ref.id)
return sequences.diverse(groups,rank,lambda r:(r.treatment,int(max(0,r.start)//(4*bar))),policy.DEFAULT.theme_seed_limit)
def feature(reference, actual, *, treatment=None):
@@ -85,6 +89,8 @@ def feature(reference, actual, *, treatment=None):
'return':('rhythm','intervals'), 'develop_rhythm':('rhythm',),
'develop_contour':('intervals',), 'continue':('contour',)}[treatment]
original=reference.notes
authored=next((r for r in generator.vocabulary.data('grammar')['transformations'] if r['id']==treatment),None)
if authored:required=tuple(authored['preserve'])
origin=original[0].start
octave=12*round((actual[0].pitch.number()-original[0].pitch.number())/12)
return types.IdentifyingFeature(reference.id+':'+treatment,tuple(n.event_id for n in actual),
@@ -95,7 +101,8 @@ def feature(reference, actual, *, treatment=None):
def rhythm_windows(library,meter,length,context):
from generator.solo import drafts
key=('theme-rhythm',id(library),meter,length)
tempo=getattr(context,'tempo',120)
key=('theme-rhythm',id(library),meter,length,tempo)
if key not in context.pools:
patterns={}
bar=F(meter.split('/')[0])*4/int(meter.split('/')[1])
@@ -115,9 +122,18 @@ def rhythm_windows(library,meter,length,context):
if offset+length>total or any(a<offset<b or a<offset+length<b for a,b in pattern):continue
selected=tuple((a-offset,b-offset) for a,b in pattern if offset<=a and b<=offset+length)
if selected and len(selected)<=16:patterns.setdefault(selected,identifier)
# Authored cells can start late and continue over computational barlines.
for identifier,pattern,total in generator.vocabulary.rhythm_cells(tempo):
if total>length:continue
for offset in (F(),F(1,2),max(F(),length-total)):
if offset+total>length:continue
head=tuple((a+offset,b+offset) for a,b in pattern)
tails=tuple((a,b) for a,b in next(iter(patterns),()) if a>=offset+total)
combined=(*head,*tails)
if 2<=len(combined)<=16:patterns.setdefault(combined,'authored:'+identifier)
context.pools[key]=sequences.diverse(tuple(patterns.items()),
lambda x:(not .7*float(length)<=len(x[0])<=1.8*float(length),policy.stable_rank('rhythm-window',x[0])),
lambda x:(len(x[0]),x[0][0][0],any(a[1]!=b[0] for a,b in zip(x[0],x[0][1:]))),8)
lambda x:(len(x[0])>16,policy.stable_rank('rhythm-window',x[0])),
lambda x:(x[1].startswith('authored:'),len(x[0]),x[0][0][0],any(a[1]!=b[0] for a,b in zip(x[0],x[0][1:]))),8)
return context.pools[key]
@@ -366,6 +382,11 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
cursor=source.start
while cursor<source.end and states:
finish=min(cursor+unit_length,source.end)
if cursor==source.start and source.end-source.start>=3*bar:
# Selected home rhythms finish as objects, not at the nearest barline.
signature=incoming.home.signature if incoming.home else ()
if signature and signature[-1].end>unit_length:
finish=min(source.end,cursor+signature[-1].end)
if source.end-finish<6 or any(ending.earliest<finish for *_,ending in states):finish=source.end
next_states=[]
for cost,units,home,state,ending in states:
@@ -409,7 +430,7 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
if introducing and similarity.near_copy(unit.notes,head,cursor,finish)['near_copy']:
context.diagnostics['near_copy_rejections']=context.diagnostics.get('near_copy_rejections',0)+1;continue
actual_home=home or make_home(unit,harmony,cursor,finish)
figure=phrasing.heard(unit.notes,operation,unit.anchor)
figure=phrasing.heard(unit.notes,operation,unit.anchor,bar)
repetition=phrasing.repetition(figure,state.history,bar)
appearance=phrasing.appearance_cost(operation,state,cursor,solo_end) if home else 0.
history=phrasing.remember(state.history,figure,bar)
@@ -423,7 +444,7 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
remaining=max(F(),ending.earliest-finish)
possible=attacks+2*float(remaining)+(0 if finish==source.end else 1)
expected=float(source.end-source.start)
activity_bound=.35*max(0,expected-possible,attacks-1.5*expected)/max(1,float(source.end-source.start)/4)
activity_bound=0.0 # Activity is a gesture/difficulty property, not a chorus quota.
return cost/max(1,len(units))+activity_bound,policy.stable_rank(seed,tuple(u.notes for u in units))
states=sequences.diverse(next_states,rank,
lambda v:(v[2].id,v[4].pitch.number(),v[4].duration,len(v[1][-1].notes),v[1][-1].operation),policy.DEFAULT.theme_continuations)
@@ -474,7 +495,7 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
history=incoming.history
for transform in transforms:
group=tuple(n for n in notes if n.event_id in transform.events)
if group:history=phrasing.remember(history,phrasing.heard(group,transform.operation,ending if group[-1].end==ending.hold_until else None),bar)
if group:history=phrasing.remember(history,phrasing.heard(group,transform.operation,ending if group[-1].end==ending.hold_until else None,bar),bar)
current=dataclasses.replace(phrase.state,next_destination=phrasing.next_destination(ending,harmony),home=home,source_reference=source_ref,callbacks=incoming.callbacks+sum(u.operation=='callback' for u in units),
history=history,returns=state.returns,closures=incoming.closures+1,register_peak=max(state.register_peak,*(n.pitch.number() for n in notes)))
if not incoming.home: