This commit is contained in:
khodowany
2026-10-04 15:51:18 -05:00
parent 405066eb43
commit f234dd6d51
54 changed files with 2104 additions and 330 deletions
+73
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import fractions
import dataclasses
import generator.solo.policy
import generator.solo.types
@@ -116,3 +117,75 @@ def value(features):
def feasible(plan, notes, choices, until):
return Evaluation(plan, choices, until).feasible(notes)
@dataclasses.dataclass(frozen=True)
class MusicalEvaluation:
version: str
scope: tuple
valid: bool
pending: tuple
raw: dict
denominators: dict
weights: dict
contributions: dict
total: float
error: str = ''
MUSICAL_WEIGHTS = {'rhythm':3., 'destination':2., 'harmony':2.,
'register':1., 'repetition':1., 'activity':1., 'difficulty':.5}
def musical(plan, draft, notes, incoming, harmony, *, travel=0, weights=None, bar=None):
"""The sole final evaluator of the active thematic composer.
Scores are normalized on complete sounding events, never notation fragments.
Contracts are validity requirements, not additional similarity rewards.
Retrieval estimates are deliberately not added to this result.
"""
from generator.solo import drafts, groups, harmonic, phrasing, types
notes=types.sounding_notes(notes)
bar=getattr(harmony,'bar',fractions.Fraction(4)) if bar is None else bar
from generator import vocabulary
weights=dict(vocabulary.data('grammar').get('evaluation',{}).get('weights',MUSICAL_WEIGHTS) if weights is None else weights)
if set(weights)!=set(MUSICAL_WEIGHTS):raise ValueError('unknown or missing evaluation contribution')
checked=harmony.evaluate(notes, incoming.treatments, until=plan.end, complete=True,
enclosures=draft.enclosures, expressions=draft.expressions)
identity,identity_error=drafts.contract_valid(draft,notes)
anchors,anchor_error=harmonic.anchors_valid(draft.anchors,notes,plan.end,complete=True,closure=plan.closure)
feasible=Evaluation(plan,harmony.choices).feasible(notes)
valid=checked.valid and identity and anchors and feasible
error=checked.error or identity_error or anchor_error or ('' if feasible else 'activity intent not fulfilled')
span=float(plan.end-plan.start); sounded=sum(float(n.end-n.start) for n in notes)
islands=sum(a.end<b.start and b.end<c.start and b.end-b.start<=fractions.Fraction(1,2)
for a,b,c in zip(notes,notes[1:],notes[2:]))
# Rests at the edges can be entrance/release. Long internal gaps need a
# completed gesture, rather than receiving a universal occupancy penalty.
settled={a.hold_until for a in draft.anchors}
broken=sum(max(0,float(b.start-a.end)-1) for a,b in zip(notes,notes[1:])
if a.end not in settled and a.end-a.start<2)
entry=max(0,abs(notes[0].pitch.number()-incoming.last.pitch.number())-7)/12 if notes and incoming.last else 0.
rhythm=min(1.,islands/max(1,len(notes)-2)+broken/max(1,span)+entry/2)
# Explicit source groups retain their imported timing contract. Generated
# groups are checked before construction, not retroactively quantized here.
destinations=[]
for anchor in draft.anchors:
approach=[n for n in notes if n.start<anchor.earliest][-3:]
if approach:
distances=[abs(n.pitch.number()-anchor.pitch.number()) for n in approach]
# A broad approach may turn; large last-second jumps are a preference,
# not a nearest-note mandate for all intermediate events.
destinations.append(min(1,max(0,distances[-1]-5)/12))
goal=harmony.profile.reference
register=sum(float(n.end-n.start)*min(1,max(0,abs(n.pitch.number()-goal)-9)/12)
for n in notes)/max(sounded,.001) if goal is not None else 0.
history=phrasing.repetition(phrasing.heard(notes,draft.relationship),incoming.history,bar) if notes else 0.
activity=Evaluation(plan,harmony.choices).features(notes)['activity']
raw={'rhythm':rhythm,'destination':sum(destinations)/max(1,len(destinations)),
'harmony':checked.exposure,'register':register,'repetition':min(1,history),
'activity':activity,'difficulty':min(1,travel/max(1,len(notes))/2000)}
contributions={name:weights[name]*value for name,value in raw.items()}
return MusicalEvaluation('joint-musical-1',(plan.start,plan.end),valid,checked.pending,raw,
{'span':span,'sounding':sounded,'attacks':len(notes),'destinations':len(destinations)},weights,
contributions,sum(contributions.values()) if valid else float('inf'),error)