"""One authoritative evaluation of completed phrases, never guitar travel.""" from dataclasses import dataclass from fractions import Fraction as F from generator import vocabulary from generator.solo import types @dataclass(frozen=True) class Evaluation: version: str valid: bool raw: dict weights: dict contributions: dict total: float denominators: dict def signature(notes): ns=types.sounding_notes(notes) if not ns:return () origin=ns[0].start;pitch=ns[0].pitch.number() return tuple((n.start-origin,n.end-n.start,n.pitch.number()-pitch) for n in ns) def evaluate(phrase,incoming,history,center,weights=None): ns=types.sounding_notes(phrase.notes); span=float(phrase.end-phrase.start) weights=dict(vocabulary.data('solo')['weights'] if weights is None else weights) if set(weights)!={'purpose','continuity','repetition','register'}:raise ValueError('Unknown musical contribution') if not ns: valid=phrase.purpose=='space';raw=dict.fromkeys(weights,0.) else: valid=True movement=[b.pitch.number()-a.pitch.number() for a,b in zip(ns,ns[1:])] # Declared expansion permits arpeggio leaps; avoid unprepared octave flailing. purpose=sum(max(0,abs(v)-7)/12 for v in movement)/max(1,len(movement)) oscillation=sum(a==-b==c and a!=0 for a,b,c in zip(movement,movement[1:],movement[2:])) purpose+=max(0,oscillation-1)/max(1,len(movement)) if phrase.destination: prior=next((n for n in reversed(ns) if n.end<=phrase.destination.earliest),None) if prior:purpose+=max(0,abs(prior.pitch.number()-phrase.destination.pitch.number())-3)/12 continuity=max(0,abs(ns[0].pitch.number()-incoming.pitch.number())-5)/12 if incoming else 0. current=signature(ns[:6]); repetition=0. for age,old in enumerate(reversed(history[-4:])): if len(current)>=3 and current==signature(old.notes[:6]):repetition+=1/(age+1) if phrase.purpose=='return':repetition=max(0,repetition-.5) duration=sum(float(n.end-n.start) for n in ns) register=sum(float(n.end-n.start)*max(0,abs(n.pitch.number()-center)-10)/12 for n in ns)/max(duration,.001) raw={'purpose':min(1,purpose),'continuity':min(1,continuity),'repetition':min(1,repetition),'register':min(1,register)} contributions={k:raw[k]*weights[k] for k in weights} return Evaluation('phrase-musical-1',valid,raw,weights,contributions,sum(contributions.values()) if valid else float('inf'),{'span':span,'attacks':len(ns),'sounding':sum(float(n.end-n.start) for n in ns)})