Files
product-vamp/generator/solo/report.py
T
khodowany 82349cf897
ci / checks (push) Failing after 23m37s
Playback
2026-10-05 23:41:11 -05:00

72 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Compact reports of actual selected phrases, not duplicated draft ancestry."""
import dataclasses, fractions, json
import generator.solo.placement
import generator.solo.harmonic
import collections
import generator.fretboard
def plain(value):
if dataclasses.is_dataclass(value):
return plain(dataclasses.asdict(value))
if isinstance(value, dict):
return {k: plain(v) for k, v in value.items()}
if isinstance(value, (tuple, list, set, frozenset)):
return [plain(v) for v in value]
if isinstance(value, fractions.Fraction):
return str(value)
return value
def structure(solo):
harmony = generator.solo.harmonic.Harmony(solo.harmony)
contracts = tuple(c for p in solo.phrases for c in p.expressions)
checked = harmony.evaluate(solo.notes(), until=solo.end, complete=True, expressions=contracts)
phrases = []
for p in solo.phrases:
entry = {'id': p.id, 'start': str(p.start), 'end': str(p.end), 'purpose': p.purpose, 'direction': p.direction, 'ending': p.ending,
'theme': p.theme, 'notes': plain(p.notes), 'rhythm_groups': plain(p.groups), 'sources': p.sources,
'identifying_events': plain(p.identity),
'evaluation': plain(p.evaluation), 'score': p.score, 'expressions': plain(p.expressions)}
phrases.append(entry)
ns = solo.notes()
duration = solo.end - solo.start
return {'schema': 'composer-report-3', 'selection_trace': plain({k: v for k, v in solo.diagnostics.items() if not k.endswith('_seconds')}),
'instrument_profile': plain(solo.profile), 'phrases': phrases, 'expressions': plain(contracts), 'fretboard': fretboard_report(solo),
'phrasing': {'attacks': len(ns), 'allocation_beats': str(duration),
'attacks_per_four_beats': 4 * len(ns) / float(duration) if duration else 0,
'occupancy': sum(float(n.end - n.start) for n in ns) / float(duration) if duration else 0},
'harmonic_roles': harmony.describe(ns, expressions=contracts), 'harmonic_policy': 'contextual (shared by authored and predicted music)', 'harmonic_valid': checked.valid, 'harmonic_error': checked.error,
'harmonic_exposure': checked.exposure, 'unresolved_treatments': plain(checked.pending)}
def describe(solo, measure_offset=0, time_offset=0, *, detail=None):
detail = detail or structure(solo)
lines = [f'{solo.diagnostics.get("construction", "solo")}: {solo.part}', 'Times are exact quarter beats from the chorus start; negative times are allocated lead-ins. Pitches sound one octave below guitar notation.',
'Approximate fingering costs are not a biomechanical guarantee.', json.dumps(detail['phrasing'])]
for p in solo.phrases:
lines.append(f'{p.id}: {p.start}–{p.end}; {p.purpose}, {p.direction}, {p.ending}; theme {p.theme}; score {p.score:.4f}')
for n in p.notes:
lines.append(f' {n.start}–{n.end}: {n.pitch.label()}, {n.role}; string {n.position.string_number}, fret {n.position.fret}')
for c in p.expressions:
lines.append(f' {c.kind}/{c.rule} → {c.target.pitch.label()} at {c.target.start}, held to {c.target.end}')
return '\n'.join(lines) + '\n'
def fretboard_report(solo):
notes = solo.notes()
attacks = collections.Counter()
durations = collections.Counter()
forced = optional = 0
for note in notes:
string = note.position.string_number
attacks[string] += 1
durations[string] += float(note.end - note.start)
if string == 1:
if len(generator.solo.placement.positions(note.pitch.number(), solo.profile)) == 1:
forced += 1
else:
optional += 1
total = sum(durations.values())
return {'attacks_by_string': dict(sorted(attacks.items())),
'duration_fraction_by_string': {s: d / max(.001, total) for s, d in sorted(durations.items())},
'first_string_forced': forced, 'first_string_optional': optional,
'range_midi': [min(n.pitch.number() for n in notes), max(n.pitch.number() for n in notes)] if notes else [],
'cross_bar_sounding_events': sum(any(n.start < t < n.end for t in {c.start for c in solo.harmony if c.beat == 1}) for n in notes)}