@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user