Files
product-vamp/generator/solo/report.py
T
khodowany 3f63b255ac
ci / checks (push) Failing after 23m26s
Fix Playability
2026-10-08 19:04:06 -05:00

75 lines
4.2 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 collections
import dataclasses, fractions, json
import generator.solo.harmonic
import generator.solo.placement
import generator.vocabulary
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, *, composition=None):
composition = composition or generator.vocabulary.current()
harmony = generator.solo.harmonic.Harmony(solo.harmony, composition=composition)
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), 'guitar_placement': generator.solo.placement.describe(solo.notes(), solo.tempo),
'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, composition=None):
composition = composition or generator.vocabulary.current()
detail = detail or structure(solo, composition=composition)
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)}