"""Explain complete optional solos. No composer calls or production-data writes.""" from __future__ import annotations import argparse import collections import json from fractions import Fraction as F from pathlib import Path from audition import reference_review, solo_compare from generator import harmony, model def chord(h): if not h or not h['root']:return None root=h['root']+('#'*int(F(h['alter'])) if F(h['alter'])>0 else 'b'*-int(F(h['alter']))) if h['kind']=='other': # The explicit displayed quality is evidence; never substitute a target # chord for a missing source chord. Preserve any parse error in the report. return harmony.parse_symbol(root+(h['display'] or '')) return {'root':{'letter':h['root'].lower(),'alter':model.rational(F(h['alter']))},'kind':h['kind'], 'degrees':[{'value':int(d['value']),'alter':model.rational(F(d['alter'])),'type':d['type']} for d in h['degrees']]} def members(h): c=chord(h) if not c:return {} root=model.NATURAL[c['root']['letter']]+model.alteration(c['root']['alter']) return {int(root+harmony.degree_semitones(d,a))%12:str(d) for d,a in harmony.degrees(c).items()} def explain(review, source=None): """Observed relationships and hypotheses, not retrospective permissions.""" notes=solo_compare.events(review);offset=F(review['alignment']['encoded_offset'] or 0) hs=[{**h,'time':str(F(h['time'])-offset)} for h in review['harmonies']] output=[] for i,n in enumerate(notes): start,end=F(n['start']),F(n['end']);p=int(F(n['pitch'])) active=next((h for h in reversed(hs) if F(h['time'])<=start),None) future=next((h for h in hs if F(h['time'])>start),None) relation=[];uncertainty=[] try: cm=members(active);fm=members(future) except (ValueError,KeyError) as e:cm=fm={};uncertainty.append(str(e)) if p%12 in cm:relation.append('chord member '+cm[p%12]) else:relation.append('non-basic pitch: needs collection/color/motion analysis') if future and start0 else 'turn') for target in notes[i+1:i+4]: t=F(target['start']);q=int(F(target['pitch'])) if t-start>F(3,2):break if abs(q-p)==1 and end<=t and t-end<=F(1,2): relation.append('semitone approach to '+target['pitch']+' at '+target['start']);break if end-start>=2:relation.append('sustained event; '+('breath follows' if i+1end else 'continuation follows')) label=[a['text'] for a in review['annotations'] if start<=F(a['time'])-offset1], 'source_four_event_relationships':source_matches,'source_relationship_limit':'Exact relative four-event evidence only; absence does not disprove thematic influence.', 'interpretation':'Descriptive hypotheses; no label waives production validation. No output from this command is loaded by production.'} def rule_review(review, choices, offset): """Read-only recognition under production policies; never run composition. Recognition deliberately need not cover every reference note. Failed or ambiguous relationships remain failures, rather than changing a policy. """ import dataclasses from generator import vocabulary from generator.solo import types, harmonic, expressions choices=tuple(dataclasses.replace(c,span=dataclasses.replace(c.span,start=c.span.start-offset,end=c.span.end-offset)) for c in choices) h=harmonic.Harmony(choices);ns=[] for i,n in enumerate(solo_compare.events(review)): p=n['spelling'];ns.append(types.Note(F(n['start']),F(n['end']),types.Pitch(p['step'].lower(),F(p['alter']),p['octave']-1),event_id=str(i))) contracts=[];rejections=collections.defaultdict(list) def accept(c): if expressions.validate(tuple(ns),(*contracts,c),h)[1]:return False contracts.append(c);return True for n in ns: for boundary in (s.start for s in h.spans if n.start0 else 'b'*-int(F(raw['alter'])))+str(raw['octave']-1) lines.append(f"| {n['measure']} | {n['start']} | {pitch} / {F(n['end'])-F(n['start'])} | {'; '.join(n['relationships'])} | {'; '.join(n['annotation']).replace(chr(10),' / ')} |") lines.extend(['',f"Relative four-event returns: {analysis['relative_four_event_returns']}",'',f"Exact relative source relationships: {analysis['source_four_event_relationships']}",'', 'General rule recognition against ORIGINAL source harmony: '+str(recognized['statuses']), 'Recognized policies: '+str(recognized['rules']),'',recognized['limit'],'', '| Onset | Pitch | Collection-valid throughout | Recognition | Policy / source harmony |','|---|---|---|---|---|']) for n in recognized['events']: failures='; '.join(r['rule']+': '+r['rejection'].split(': ',1)[-1]+' (expected '+r['expected_spelling']+')' for r in n['candidate_rejections']) lines.append(f"| {n['start']} | {n['pitch']} | {n['mapped_throughout']} | {n['status']} | {n['rule'] or ''} / {', '.join(n['harmonies'])} {failures} |") (args.output/'theory.json').write_text(json.dumps(results,indent=2)) (args.output/'theory.md').write_text('\n'.join(lines)+'\n') print('Reviewed every sounding event in',len(results),'complete reference solos; no composer invoked.') if __name__=='__main__':main()