"""The sole production solo composer: frozen learned model plus music validity.""" import dataclasses import fractions import pathlib import time import generator.harmonic_time import generator.solo.decoder import generator.solo.groups import generator.solo.harmonic import generator.solo.learned import generator.solo.placement import generator.solo.profiles import generator.solo.types import generator.vocabulary def compose(analysis, measures, part, end, *, learned_model=None, initial=None, expressions_enabled=True, start=fractions.Fraction(), composition=None): composition = composition or generator.vocabulary.current() 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, composition=composition) model = learned_model or generator.solo.learned.Model.load(pathlib.Path(__file__).resolve().parents[2] / 'data/models/solo.json') settings = composition.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', ''), composition=composition) 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(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, composition=composition) return result def validate(solo, *, composition=None): composition = composition or generator.vocabulary.current() 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 generator.solo.placement.positions(n.pitch.number(), solo.profile): raise ValueError('invalid guitar placement') previous = n checked = generator.solo.harmonic.Harmony(solo.harmony, composition=composition).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 if cursor != solo.end: raise ValueError('incomplete solo duration')