64 lines
4.1 KiB
Python
64 lines
4.1 KiB
Python
"""Explicit registered repertoire. Authored playback is not model prediction.
|
||
|
||
Matching is by canonical source music and role, never title or filename. This
|
||
path intentionally reads the registered solo, following the user's exact-reuse
|
||
policy. Unknown song/role pairs go to the learned composer.
|
||
"""
|
||
from __future__ import annotations
|
||
import dataclasses
|
||
import hashlib
|
||
import json
|
||
from pathlib import Path
|
||
from fractions import Fraction as F
|
||
from generator import musicxml,parts,training_io,harmonic_time
|
||
from generator.solo import analysis,types,placement,profiles
|
||
|
||
|
||
def lookup(source,role,data_dir):
|
||
from generator.production import SourceAnalysis
|
||
root=Path(data_dir);manifest=root/'training/manifest.json'
|
||
if not manifest.exists():return None
|
||
value=json.loads(manifest.read_text())
|
||
if value.get('version')!=1:raise ValueError('Unsupported training manifest')
|
||
matches=[]
|
||
for entry in value['examples']:
|
||
if entry['slot']!=(1 if role=='acoustic' else 2):continue
|
||
path=(manifest.parent/entry['source']).resolve()
|
||
if not path.is_relative_to((root/'songs').resolve()):raise ValueError('Registered sources must be in data/songs')
|
||
doc=parts.prepare_document(musicxml.load_musicxml(path))
|
||
if SourceAnalysis.read(doc).musical_key==source.musical_key:
|
||
target=(manifest.parent/entry['solo']).resolve()
|
||
if not target.is_relative_to(manifest.parent.resolve()):raise ValueError('Registered solo must be inside data/training')
|
||
evidence=training_io.read(target,role,pitch_convention=entry['pitch_convention'])
|
||
training_io.validate_harmony(doc,evidence,entry['id'])
|
||
matches.append({**entry,'path':target,'sha256':hashlib.sha256(target.read_bytes()).hexdigest()})
|
||
if len(matches)>1:raise ValueError('Multiple authored solos registered for the same source music and role')
|
||
return matches[0] if matches else None
|
||
|
||
|
||
def realize(entry,source_analysis,measures,part,start,end,initial=None):
|
||
evidence=training_io.read(entry['path'],part,pitch_convention=entry['pitch_convention'])
|
||
if evidence['discrepancies']:raise ValueError(entry['id']+': invalid authored timing/ties: '+str(evidence['discrepancies']))
|
||
expected=[(a,b-a) for a,b in zip(source_analysis.boundaries,source_analysis.boundaries[1:])]
|
||
if [(F(m['start']),F(m['duration'])) for m in evidence['measures']]!=expected:raise ValueError(entry['id']+': source/solo forms differ; register and retrain the correct pair')
|
||
notes=[]
|
||
for i,event in enumerate(evidence['sounding_events']):
|
||
a,b=F(event['start'])-source_analysis.body_offset,F(event['end'])-source_analysis.body_offset
|
||
if a<start or b>end:raise ValueError(f'{entry["id"]}: authored event {a}–{b} conflicts with arrangement window {start}–{end}; no clipping applied')
|
||
p=event['pitch'];number=F(event['sounding_midi'])
|
||
if number.denominator!=1:raise ValueError('Microtonal guitar solo is unsupported')
|
||
pitch=types.Pitch(p['step'].lower(),F(p['alter']),p['octave']+(int(number)-int(F(event['encoded_midi'])))//12)
|
||
notes.append(types.Note(a,b,pitch,('authored:'+entry['id'],),'authored',event_id=f'authored_{i}'))
|
||
profile=profiles.for_part(part,source_analysis)
|
||
illegal=next((n for n in notes if not placement.positions(n.pitch.number(),profile)),None)
|
||
if illegal:raise ValueError(f'{entry["id"]}: {illegal.pitch.label()} at {illegal.start} is outside strings 1–6/frets 1–12; authored solo left unchanged')
|
||
placed=placement.place(tuple(notes),source_analysis.tempo,profile=profile)
|
||
if placed is None:raise ValueError(entry['id']+': no legal complete guitar placement')
|
||
spans=tuple(dataclasses.replace(s,start=s.start+start,end=s.end+start) for s in harmonic_time.read({'measures':measures},initial=initial))
|
||
phrase=types.Phrase('authored',start,end,placed[1],'authored','authored','handoff',theme=entry['id'])
|
||
return types.Solo(part,spans,(phrase,),source_analysis.tempo,end,
|
||
{'construction':'authored_repertoire','authored_id':entry['id'],'authored_sha256':entry['sha256'],
|
||
'pitch_convention':entry['pitch_convention'],'exact_sounding_events':True,
|
||
'note':'Authored music is preserved, not reclassified as generated harmonic exceptions.',
|
||
'annotations':evidence['annotations']},profile,start)
|