Files
product-vamp/generator/solo/scoring.py
T
khodowany 0e68f5364b Cleanup
2026-10-04 18:48:02 -05:00

49 lines
2.4 KiB
Python

"""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)})