Training Model

This commit is contained in:
khodowany
2026-10-05 19:44:33 -05:00
parent db7966a430
commit e7180e557b
6603 changed files with 8919 additions and 523063 deletions
+45 -38
View File
@@ -1,50 +1,57 @@
"""The single production entry point for independent phrase composition."""
from fractions import Fraction as F
import time
"""The sole production solo composer: frozen learned model plus music validity."""
import dataclasses
from generator import harmony as chords, vocabulary
from generator.scales import timeline, selection
from generator.solo import types, profiles, harmonic, construction, placement, policy
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
def compose(analysis,measures,part,end,*,scales,sequences,rhythms,initial=None,expressions_enabled=True,start=F()):
def compose(analysis,measures,part,end,*,learned_model=None,initial=None,expressions_enabled=True,start=F()):
began=time.monotonic()
choices=selection.select(timeline.read({'measures':measures},initial=initial),timeline.Solo(F(),end-start,part.capitalize()+' Solo'),scales)
choices=tuple(dataclasses.replace(c,span=dataclasses.replace(c.span,start=c.span.start+start,end=c.span.end+start)) for c in choices)
for c in choices:
if c.span.chord and c.scale is None:raise ValueError(f'{part} solo, source measure {c.span.source_measure}, beat {c.span.beat}, {chords.symbol(c.span.chord)}: No exact match within the permitted catalog')
profile=profiles.for_part(part,analysis);harmony=harmonic.Harmony(choices,profile)
source=tuple((n.start,n.end,n.pitch) for n in analysis.melody)
seed=vocabulary.role().get('choice_seed') or policy.stable_rank(part,(source,tuple((c.span.start,c.span.end,c.span.chord) for c in choices),vocabulary.role(part+'_solo'),policy.DEFAULT.composition_version))
builder=construction.Builder(analysis,measures,harmony,sequences,rhythms,end,seed,expressions_enabled,start=start)
phrases=builder.compose()
# Resolve hand-position transitions over the selected whole melody. This
# cannot reselect pitches, rhythm or another independently generated part.
raw=tuple(n for p in phrases for n in p.notes)
placed=placement.place(raw,analysis.tempo,profile=profile) if raw else (0,())
if placed is None:raise construction.SearchExhausted('selected melody has no complete legal guitar path')
by_id={n.event_id:n for n in placed[1]}
phrases=tuple(dataclasses.replace(p,notes=tuple(by_id[n.event_id] for n in p.notes)) for p in phrases)
result=types.Solo(part,choices,phrases,analysis.tempo,end,{'construction':'source_melodic_phrases','version':policy.DEFAULT.composition_version,
'construction_seconds':time.monotonic()-began,**builder.timings,'operation_coverage':{k:dict(v) for k,v in builder.coverage.items()},
'whole_stream_diagnostic':dataclasses.asdict(builder.winner_evaluation),
'opening_candidates':builder.opening_candidates,
'frontiers':builder.frontiers,'completed_candidates':[{'rank':i+1,'evaluation':{'selection_total': str(s.total / (end-start)), 'phrases': [dataclasses.asdict(p.evaluation) for p in s.phrases]},'events':[(str(n.start),str(n.end),n.pitch.number()) for n in s.notes]} for i,s in enumerate(builder.completed)],'rejections':dict(builder.failures),'decisions':builder.decisions,'expressions_enabled':expressions_enabled},profile,start)
validate(result)
return result
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')
# 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
def validate(solo):
previous=None
for note in solo.notes():
if note.start<solo.start or note.end>solo.end or note.end<=note.start or previous and previous.end>note.start:raise ValueError('overlapping/out-of-bounds sounding event')
if note.position not in placement.positions(note.pitch.number(),solo.profile):raise ValueError('invalid guitar placement')
previous=note
expressions=tuple(c for p in solo.phrases for c in p.expressions)
checked=harmonic.Harmony(solo.choices).evaluate(solo.notes(),until=solo.end,complete=True,expressions=expressions)
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,
expressions=tuple(c for p in solo.phrases for c in p.expressions))
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
valid,error=harmonic.anchors_valid(p.arrivals or ((p.destination,) if p.destination else ()),p.notes,p.end,complete=True,closure='closed' if p.ending in ('complete','handoff') else 'open')
if not valid:raise ValueError(error)
if cursor!=solo.end:raise ValueError('incomplete solo duration')