Comp
This commit is contained in:
@@ -524,6 +524,10 @@ def checked_phrase(plan, draft, notes, contracts, incoming, harmony, tempo, edit
|
||||
return None
|
||||
features = evaluation.features(score_notes, previous=incoming.last if not shared else None, movement=travel + (incoming.travel if shared else 0))
|
||||
features.update(generator.solo.drafts.relationship_features(draft, notes))
|
||||
if draft.lead_identity:
|
||||
supported = {e for feature in draft.features for e in feature.events}
|
||||
motions = [(a, b) for a, b in zip(notes, notes[1:]) if not (a.event_id in supported and b.event_id in supported)]
|
||||
features['contour'] = sum(min(1, max(0, abs(b.pitch.number() - a.pitch.number()) - 5) / 12) for a, b in motions) / max(1, len(motions))
|
||||
# Fidelity is checked against explicit transformed identities, not against
|
||||
# the source head as one undifferentiated phrase-long quotation.
|
||||
features.pop('fidelity', None)
|
||||
@@ -719,7 +723,14 @@ def elaborate(phrase, incoming, harmony, sequences, tempo, context):
|
||||
left = next((i for i in range(right - 1, -1, -1) if notes[right].start - notes[i].start >= 4), None)
|
||||
if left is not None and notes[right].start - notes[left].start <= 4:
|
||||
windows.append((notes[left].start, notes[right].start))
|
||||
for low, high in windows[-2:]:
|
||||
limit = max(1, min(2, int((phrase.plan.end - phrase.plan.start) / 16))) if phrase.draft.lead_identity else 2
|
||||
if phrase.draft.lead_identity:
|
||||
intended = {phrase.plan.start + a for reference in context.references for a, _ in reference.runs}
|
||||
windows.sort(key=lambda window: (sum(n.event_id in protected for n in notes if window[0] <= n.start < window[1]), window[0] not in intended, window))
|
||||
selected_windows = windows[:limit]
|
||||
else:
|
||||
selected_windows = windows[-limit:]
|
||||
for low, high in selected_windows:
|
||||
if any(low < r.destination.end and high > r.notes[0].start for r in phrase.routes):
|
||||
continue
|
||||
notes = emitted_notes(phrase)
|
||||
@@ -754,7 +765,21 @@ def elaborate(phrase, incoming, harmony, sequences, tempo, context):
|
||||
if any(not any((n.event_id, n.start, n.end) == (p.event_id, p.start, p.end) for p in pattern) for n in notes[left:right + 1] if n.event_id in protected):
|
||||
continue
|
||||
for route in generator.solo.routes.candidates(pattern, protected, harmony, sequences, phrase.plan.register, 'approach the theme destination'):
|
||||
if phrase.draft.lead_identity:
|
||||
# A run must actually contain connected eighth-note movement;
|
||||
# relabeling the unchanged quarter-note answer is not elaboration.
|
||||
run = peak = 0
|
||||
previous_end = None
|
||||
for note in route.notes:
|
||||
eighth = note.end - note.start == fractions.Fraction(1, 2)
|
||||
run = run + 1 if eighth and previous_end == note.start else int(eighth)
|
||||
peak = max(peak, run)
|
||||
previous_end = note.end
|
||||
if peak < 4:
|
||||
continue
|
||||
actual = (*notes[:left], *route.notes, *notes[right + 1:])
|
||||
if phrase.draft.lead_identity and len(actual) > generator.solo.policy.DEFAULT.lead_activity_range[1] * float(phrase.plan.end - phrase.plan.start):
|
||||
continue
|
||||
context.diagnostics['complete_routes_tried'] = context.diagnostics.get('complete_routes_tried', 0) + 1
|
||||
if not generator.solo.drafts.contract_valid(phrase.draft, actual)[0]:
|
||||
continue
|
||||
@@ -767,7 +792,11 @@ def elaborate(phrase, incoming, harmony, sequences, tempo, context):
|
||||
viable.append(dataclasses.replace(revised, state=state, initial=phrase.initial, refined=phrase.refined, routes=(*phrase.routes, route)))
|
||||
else:
|
||||
context.diagnostics['complete_routes_rejected'] = context.diagnostics.get('complete_routes_rejected', 0) + 1
|
||||
chosen = min(viable, key=lambda p: (p.score, len(p.edits), tuple(n.pitch for n in emitted_notes(p))))
|
||||
# A selected approach is an explicit musical operation. Its complete valid
|
||||
# routes compete with one another; a zero-edit movement advantage must not
|
||||
# silently cancel every requested run. Failure retains the plain tune.
|
||||
eligible = viable[1:] if phrase.draft.lead_identity and len(viable) > 1 else viable
|
||||
chosen = min(eligible, key=lambda p: (p.score, len(p.edits), tuple(n.pitch for n in emitted_notes(p))))
|
||||
if chosen is not phrase:
|
||||
context.diagnostics['complete_routes_selected'] = context.diagnostics.get('complete_routes_selected', 0) + 1
|
||||
phrase = chosen
|
||||
@@ -831,6 +860,9 @@ def revise_pair(first, second, incoming, choices, sequences, tempo, context):
|
||||
|
||||
def alternatives(plan, measures, choices, sequences, rhythms, tempo, previous=None, context=None):
|
||||
context = context if context is not None else SearchContext()
|
||||
if plan.treatment == 'lead':
|
||||
from generator.solo import leads
|
||||
return leads.alternatives(plan, measures, choices, sequences, tempo, previous, context)
|
||||
# Keep resources alive while identity-keyed cache entries exist. A later
|
||||
# library cannot accidentally reuse the identity of a collected object.
|
||||
context.resources[id(sequences)] = sequences
|
||||
|
||||
Reference in New Issue
Block a user