+61
-46
@@ -1,57 +1,72 @@
|
||||
"""The sole production solo composer: frozen learned model plus music validity."""
|
||||
import dataclasses
|
||||
import time
|
||||
from fractions import Fraction as F
|
||||
from pathlib import Path
|
||||
from generator import harmonic_time, vocabulary
|
||||
from generator.solo import types, profiles, harmonic, placement, learned, decoder, groups
|
||||
import fractions
|
||||
import pathlib
|
||||
import generator.harmonic_time
|
||||
import generator.vocabulary
|
||||
import generator.solo.types
|
||||
import generator.solo.profiles
|
||||
import generator.solo.harmonic
|
||||
import generator.solo.placement
|
||||
import generator.solo.learned
|
||||
import generator.solo.decoder
|
||||
import generator.solo.groups
|
||||
|
||||
|
||||
def compose(analysis,measures,part,end,*,learned_model=None,initial=None,expressions_enabled=True,start=F()):
|
||||
began=time.monotonic()
|
||||
spans=tuple(dataclasses.replace(s,start=s.start+start,end=s.end+start) for s in harmonic_time.read({'measures':measures},initial=initial))
|
||||
profile=profiles.for_part(part,analysis);harmony=harmonic.Harmony(spans,profile)
|
||||
model=learned_model or learned.Model.load(Path(__file__).resolve().parents[2]/'data/models/solo.json')
|
||||
settings=vocabulary.role(part+'_solo')
|
||||
builder=decoder.Decoder(analysis,harmony,model,part,start,end,expressions_enabled=expressions_enabled,
|
||||
attempts=settings.get('attempts',4),seed=settings.get('choice_seed',''))
|
||||
selected=builder.compose();decoded=time.monotonic()
|
||||
placed=placement.place(selected.notes,analysis.tempo,profile=profile)
|
||||
if placed is None:raise decoder.SearchExhausted('learned solo has no complete legal guitar path')
|
||||
def compose(analysis, measures, part, end, *, learned_model=None, initial=None, expressions_enabled=True, start=fractions.Fraction()):
|
||||
began = time.monotonic()
|
||||
spans = tuple(dataclasses.replace(s, start=s.start + start, end=s.end + start) for s in generator.harmonic_time.read({'measures': measures}, initial=initial))
|
||||
profile = generator.solo.profiles.for_part(part, analysis)
|
||||
harmony = generator.solo.harmonic.Harmony(spans, profile)
|
||||
model = learned_model or generator.solo.learned.Model.load(pathlib.Path(__file__).resolve().parents[2] / 'data/models/solo.json')
|
||||
settings = generator.vocabulary.role(part + '_solo')
|
||||
builder = generator.solo.decoder.Decoder(analysis, harmony, model, part, start, end, expressions_enabled=expressions_enabled, attempts=settings.get('attempts', 4), seed=settings.get('choice_seed', ''))
|
||||
selected = builder.compose()
|
||||
decoded = time.monotonic()
|
||||
placed = generator.solo.placement.place(selected.notes, analysis.tempo, profile=profile)
|
||||
if placed is None:
|
||||
raise generator.solo.decoder.SearchExhausted('learned solo has no complete legal guitar path')
|
||||
# Phrase labels describe heard rests/arrivals. They do not impose a new
|
||||
# endpoint or reset the learned history at barlines or arbitrary windows.
|
||||
notes=placed[1];ends=[n.end for i,n in enumerate(notes[:-1]) if notes[i+1].start-n.end>=1]
|
||||
boundaries=[start,*[t for t in ends if start<t<end],end];phrases=[]
|
||||
home=tuple(n for n in notes if n.start<(ends[0] if ends else min(end,start+8)))
|
||||
for i,(a,b) in enumerate(zip(boundaries,boundaries[1:])):
|
||||
ns=tuple(n for n in notes if a<=n.start<b)
|
||||
cs=tuple(c for c in selected.contracts if a<=c.target.start<b)
|
||||
ending='handoff' if b==end else 'settle'
|
||||
phrases.append(types.Phrase(f'learned_{i+1}',a,b,ns,'learned_sequence','learned',ending,
|
||||
expressions=cs,groups=groups.describe(tuple((n.start,n.end) for n in ns)),
|
||||
theme='introduced' if i==0 else 'conditional_history',identity=home if i==0 else ()))
|
||||
result=types.Solo(part,spans,tuple(phrases),analysis.tempo,end,
|
||||
{'construction':'learned_joint_events','version':learned.VERSION,'decode_seconds':decoded-began,
|
||||
'placement_seconds':time.monotonic()-decoded,'selection_total':selected.cost,
|
||||
'objective':'seeded sampling from validated learned joint-event likelihoods; score is diagnostic, not a complete-solo ranking',
|
||||
'budget':{'attempts':builder.attempts},
|
||||
'counts':dict(builder.counts),'frontiers':builder.trace,'completed_candidates':[
|
||||
{'rank':i+1,'cost':s.cost,'events':[(str(a),str(b),p) for a,b,p in s.events]} for i,s in enumerate(builder.completed)],
|
||||
'expressions_enabled':expressions_enabled},profile,start)
|
||||
validate(result);return result
|
||||
|
||||
notes = placed[1]
|
||||
ends = [n.end for i, n in enumerate(notes[:-1]) if notes[i + 1].start - n.end >= 1]
|
||||
boundaries = [start, *[t for t in ends if start < t < end], end]
|
||||
phrases = []
|
||||
home = tuple(n for n in notes if n.start < (ends[0] if ends else min(end, start + 8)))
|
||||
for i, (a, b) in enumerate(zip(boundaries, boundaries[1:])):
|
||||
ns = tuple(n for n in notes if a <= n.start < b)
|
||||
cs = tuple(c for c in selected.contracts if a <= c.target.start < b)
|
||||
ending = 'handoff' if b == end else 'settle'
|
||||
phrases.append(generator.solo.types.Phrase(f'learned_{i + 1}', a, b, ns, 'learned_sequence', 'learned', ending,
|
||||
expressions=cs, groups=generator.solo.groups.describe(tuple((n.start, n.end) for n in ns)),
|
||||
theme='introduced' if i == 0 else 'conditional_history', identity=home if i == 0 else ()))
|
||||
result = generator.solo.types.Solo(part, spans, tuple(phrases), analysis.tempo, end,
|
||||
{'construction': 'learned_joint_events', 'version': generator.solo.learned.VERSION, 'decode_seconds': decoded - began,
|
||||
'placement_seconds': time.monotonic() - decoded, 'selection_total': selected.cost,
|
||||
'objective': 'seeded sampling from validated learned joint-event likelihoods; score is diagnostic, not a complete-solo ranking',
|
||||
'budget': {'attempts': builder.attempts},
|
||||
'counts': dict(builder.counts), 'frontiers': builder.trace, 'completed_candidates': [
|
||||
{'rank': i + 1, 'cost': s.cost, 'events': [(str(a), str(b), p) for a, b, p in s.events]} for i, s in enumerate(builder.completed)],
|
||||
'expressions_enabled': expressions_enabled}, profile, start)
|
||||
validate(result)
|
||||
return result
|
||||
|
||||
def validate(solo):
|
||||
previous=None
|
||||
previous = None
|
||||
for n in solo.notes():
|
||||
if n.start<solo.start or n.end>solo.end or n.end<=n.start or previous and previous.end>n.start:raise ValueError('overlapping/out-of-bounds sounding event')
|
||||
if n.position not in placement.positions(n.pitch.number(),solo.profile):raise ValueError('invalid guitar placement')
|
||||
previous=n
|
||||
checked=harmonic.Harmony(solo.harmony).evaluate(solo.notes(),until=solo.end,complete=True,
|
||||
if n.start < solo.start or n.end > solo.end or n.end <= n.start or previous and previous.end > n.start:
|
||||
raise ValueError('overlapping/out-of-bounds sounding event')
|
||||
if n.position not in generator.solo.placement.positions(n.pitch.number(), solo.profile):
|
||||
raise ValueError('invalid guitar placement')
|
||||
previous = n
|
||||
checked = generator.solo.harmonic.Harmony(solo.harmony).evaluate(solo.notes(), until=solo.end, complete=True,
|
||||
expressions=tuple(c for p in solo.phrases for c in p.expressions))
|
||||
if not checked.valid:raise ValueError(checked.error)
|
||||
cursor=solo.start
|
||||
if not checked.valid:
|
||||
raise ValueError(checked.error)
|
||||
cursor = solo.start
|
||||
for p in solo.phrases:
|
||||
if p.start!=cursor or p.end<=p.start:raise ValueError('phrase coverage gap or overlap')
|
||||
cursor=p.end
|
||||
if cursor!=solo.end:raise ValueError('incomplete solo duration')
|
||||
if p.start != cursor or p.end <= p.start:
|
||||
raise ValueError('phrase coverage gap or overlap')
|
||||
cursor = p.end
|
||||
if cursor != solo.end:
|
||||
raise ValueError('incomplete solo duration')
|
||||
|
||||
Reference in New Issue
Block a user