Solo
ci / checks (push) Failing after 23m43s

This commit is contained in:
khodowany
2026-10-02 06:33:01 -05:00
parent a0bd19b3ba
commit 5fff5a165e
13 changed files with 786 additions and 211 deletions
+52 -3
View File
@@ -22,6 +22,7 @@ def describe(solo: generator.solo.types.Solo, scales, measure_offset=0, time_off
if solo.diagnostics:
lines.append('Composition diagnostics: ' + json.dumps({k: v for k, v in solo.diagnostics.items() if not k.endswith('_seconds')}, sort_keys=True))
detail = structure(solo)
lines.append("Sustained phrasing: " + json.dumps(detail["phrasing"], sort_keys=True))
for passage in detail['passages']:
lines.append(f"Melodic passage {passage['id']}: {passage['start']}-{passage['end']}; phrases {', '.join(passage['phrases'])}")
lines.append(f"Harmonic exposure: {detail['harmonic_exposure']:.4f}; unresolved treatments: {len(detail['unresolved_treatments'])}")
@@ -108,13 +109,61 @@ def structure(solo):
entry['lead_stages'] = {name: {'id': draft.id, 'attacks': len(draft.notes), 'notes': rational([dataclasses.asdict(n) for n in draft.notes])} for name, draft in (('initial', phrase.initial), ('refined', phrase.refined)) if draft}
entry['complete_routes'] = rational([dataclasses.asdict(r) for r in phrase.routes])
if phrase.initial and phrase.refined:
entry['head_comparison_plain']=generator.solo.similarity.compare(phrase.refined.notes,phrase.initial.notes)
entry['head_comparison_elaborated']=generator.solo.similarity.compare(tuple(n for d in phrase.decisions for n in d.notes),phrase.initial.notes)
# A source hold may already be sounding at this generated phrase boundary.
# Detach its clipped entrance for the local comparison only; the full
# source comparison below still merges its original tied continuations.
local_source=generator.solo.drafts.local_sounding(phrase.initial.notes)
entry['head_comparison_plain']=generator.solo.similarity.compare(phrase.refined.notes,local_source)
entry['head_comparison_elaborated']=generator.solo.similarity.compare(tuple(n for d in phrase.decisions for n in d.notes),local_source)
if phrase.state and phrase.state.home:
entry['theme_relationship']=generator.solo.references.relationship(phrase.refined.notes,phrase.state.home)
entry['score'] = phrase.score
phrases.append(entry)
return {'source_comparison': {stage:generator.solo.similarity.compare(tuple(n for p in solo.phrases for n in (p.refined.notes if stage=='plain' and p.refined else tuple(n for d in p.decisions for n in d.notes))),tuple(n for p in solo.phrases if p.initial for n in p.initial.notes)) for stage in ('plain','elaborated')}, 'fretboard': fretboard_report(solo), 'passages': [{'id': p.id, 'start': str(p.start), 'end': str(p.end), 'phrases': list(p.phrase_ids), 'seed': p.seed} for p in solo.passages], 'phrases': phrases, 'harmonic_roles': harmony.describe(solo.notes(), contracts), 'harmonic_exposure': checked.exposure, 'harmonic_valid': checked.valid, 'harmonic_error': checked.error, 'unresolved_treatments': rational([dataclasses.asdict(t) for t in checked.pending]), 'treatments': rational([dataclasses.asdict(t) for t in contracts])}
return {'phrasing': phrasing_report(solo), 'source_comparison': {stage:generator.solo.similarity.compare(tuple(n for p in solo.phrases for n in (p.refined.notes if stage=='plain' and p.refined else tuple(n for d in p.decisions for n in d.notes))),tuple(n for p in solo.phrases if p.initial for n in p.initial.notes)) for stage in ('plain','elaborated')}, 'fretboard': fretboard_report(solo), 'passages': [{'id': p.id, 'start': str(p.start), 'end': str(p.end), 'phrases': list(p.phrase_ids), 'seed': p.seed} for p in solo.passages], 'phrases': phrases, 'harmonic_roles': harmony.describe(solo.notes(), contracts), 'harmonic_exposure': checked.exposure, 'harmonic_valid': checked.valid, 'harmonic_error': checked.error, 'unresolved_treatments': rational([dataclasses.asdict(t) for t in checked.pending]), 'treatments': rational([dataclasses.asdict(t) for t in contracts])}
def phrasing_report(solo):
import collections
from generator.solo import phrasing
notes=solo.notes()
figures=[phrasing.heard(notes,'chorus')] if notes else []
exact=collections.Counter();relative=collections.Counter()
for figure in figures:
# Count occurrences, not just unique vocabulary within each phrase.
for i in range(len(figure.events)-3):
group=figure.events[i:i+4];start,_,pitch=group[0]
exact[tuple((a-start,d,p) for a,d,p in group)]+=1
relative[tuple((a-start,d,p-pitch) for a,d,p in group)]+=1
endings=[];regions=[];appearances=[]
for phrase in solo.phrases:
local=tuple(n for d in phrase.decisions for n in d.notes)
for anchor in phrase.anchors:
if anchor.role!='terminal' or phrase.plan.closure!='closed':continue
note=generator.solo.harmonic.anchor_note(anchor,phrase.state.notes)
endings.append({'phrase':phrase.plan.id,'arrival':str(note.start) if note else None,'pitch':anchor.pitch.label(),
'required_beats':str(anchor.duration),'sounding_beats':str(note.end-note.start) if note else None,
'release':str(phrase.plan.release),'completed':note is not None})
if local:
regions.append({'start':str(phrase.plan.start),'end':str(phrase.plan.end),
'low':min(n.pitch.number() for n in local),'high':max(n.pitch.number() for n in local),
'duration_weighted_mean':sum(n.pitch.number()*float(n.end-n.start) for n in local)/sum(float(n.end-n.start) for n in local)})
if phrase.draft:
for transform,feature in zip(phrase.draft.transformations,phrase.draft.features):
appearances.append({'start':str(transform.start),'operation':transform.operation,'home':transform.home,
'preserves':list(feature.preserves),'feature_source':feature.source})
activity={}
for intent in ('active','sustained_arrival','space'):
spans=[s for p in solo.phrases for s in p.plan.passages if s.intent==intent]
duration=sum(float(s.end-s.start) for s in spans)
attacks=sum(any(s.start<=n.start<s.end for s in spans) for n in notes)
occupancy=sum(float(max(0,min(n.end,s.end)-max(n.start,s.start))) for n in notes for s in spans)
activity[intent]={'beats':duration,'attacks':attacks,'attacks_per_four_beats':4*attacks/duration if duration else 0,
'occupancy':occupancy/duration if duration else 0}
return {'completed_sustained_endings':sum(e['completed'] and fractions.Fraction(e['sounding_beats'])>=2 for e in endings),
'endings':endings,'appearances':appearances,'clear_returns':sum(a['operation']=='return' for a in appearances),
'exact_four_note_recurrences':sum(max(0,n-1) for n in exact.values()),
'interval_rhythm_four_note_recurrences':sum(max(0,n-1) for n in relative.values()),
'activity':activity,'register_regions':regions}
def fretboard_report(solo):