Cleanup
This commit is contained in:
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import argparse,concurrent.futures,contextlib,copy,json,multiprocessing,tempfile
|
||||
from pathlib import Path
|
||||
from generator import production,vocabulary
|
||||
from tests.test_solo import song
|
||||
from tests.solo_fixtures import song
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--output',type=Path,required=True);a=p.parse_args();a.output.mkdir(parents=True,exist_ok=True)
|
||||
|
||||
@@ -138,7 +138,7 @@ def audition_bounds(*reviews):
|
||||
return min(a for a,b in points),max(a+b for a,b in points)
|
||||
|
||||
|
||||
def audition_pair(actual,expected,role,assets,output,cache):
|
||||
def audition_pair(actual,expected,role,assets,output,cache,baseline=None):
|
||||
from generator import model, playback, score
|
||||
from generator.solo.export import excerpt,region
|
||||
song,part=role.split('/')
|
||||
@@ -153,14 +153,15 @@ def audition_pair(actual,expected,role,assets,output,cache):
|
||||
duration=end-origin
|
||||
timeline=playback.compile_timeline(backing)
|
||||
back=render_events([(model.fraction(e['onset'])-origin,model.fraction(e['onset'])+model.fraction(e['duration'])-origin,model.fraction(e['pitch'])) for e in timeline['events']],duration,tempo,output,cache)
|
||||
(output/'audio').mkdir(parents=True,exist_ok=True)
|
||||
clips=[]
|
||||
for label,r in (('generated',actual),('reference',expected)):
|
||||
for label,r in (('generated',actual),('reference',expected),*((('baseline',baseline),) if baseline else ())):
|
||||
audio=render_events([(F(n['start'])-origin,F(n['end'])-origin,F(n['pitch'])) for n in events(r)],duration,tempo,output,cache)
|
||||
combined=array.array('f',(a+.55*b for a,b in zip(audio,back)))
|
||||
for context,values in (('solo',audio),('fixed_backing',combined)):
|
||||
peak=.65*max(map(abs,values),default=0)
|
||||
if peak>=1:raise ValueError('Comparison would clip at the shared rendering gain')
|
||||
name=role.replace('/','_')+'_'+label+'_'+context+'.wav'
|
||||
name='audio/'+role.replace('/','_')+'_'+label+'_'+context+'.wav'
|
||||
playback.write_pcm(output/name,(.65*v for v in values))
|
||||
clips.append({'file':name,'version':label,'context':context,'peak':peak})
|
||||
if context=='fixed_backing':
|
||||
@@ -176,18 +177,24 @@ def audition_pair(actual,expected,role,assets,output,cache):
|
||||
'listening_status':'Rendered and signal-checked, not subjectively auditioned.'}
|
||||
|
||||
|
||||
def run(generated_dir,reference_paths,starts,pitch_convention,output,assets=None):
|
||||
def run(generated_dir,reference_paths,starts,pitch_convention,output,assets=None,baseline_dir=None):
|
||||
generated_dir,output=Path(generated_dir),Path(output)
|
||||
manifest=json.loads((generated_dir/'manifest.solo').read_text())
|
||||
output.mkdir(parents=True,exist_ok=True)
|
||||
tone_cache={}
|
||||
baseline_manifest=json.loads((Path(baseline_dir)/'manifest.solo').read_text()) if baseline_dir else None
|
||||
result={'comparison_only':True,'feeds_generation':False,'solos':{}}
|
||||
for role,path in sorted(reference_paths.items()):
|
||||
item=manifest['solos'][role]
|
||||
actual=read(generated_dir/item['file'],role,item['form_start_measure'])
|
||||
expected=read(path,role,starts.get(role),pitch_convention)
|
||||
review=compare(actual,expected)
|
||||
if assets:review['audition']=audition_pair(actual,expected,role,assets,output,tone_cache)
|
||||
baseline=None
|
||||
if baseline_manifest:
|
||||
old=baseline_manifest['solos'][role]
|
||||
baseline=read(Path(baseline_dir)/old['file'],role,old['form_start_measure'])
|
||||
review['baseline']=musical_summary(baseline)
|
||||
if assets:review['audition']=audition_pair(actual,expected,role,assets,output,tone_cache,baseline)
|
||||
review['production_trace']=item['trace']
|
||||
review['production_phrases']=item.get('phrase_report',{})
|
||||
# Preserve the real role cutoff; reference material beyond it is not trimmed.
|
||||
@@ -208,15 +215,19 @@ def run(generated_dir,reference_paths,starts,pitch_convention,output,assets=None
|
||||
reference_path=Path(reference_paths[role])
|
||||
rows += ['<h2>'+html.escape(role)+'</h2>',f'<p>Exact event match: <strong>{"yes" if r["exact_musical_match"] else "no"}</strong>. {r["exact_matching_attacks"]} exactly matching attacks; {len(r["differences"])} differing onset positions. This is not a musical-quality score.</p>',
|
||||
'<p><a href="'+html.escape(os.path.relpath(actual_path,output))+'">Generated MusicXML</a> · <a href="'+html.escape(os.path.relpath(reference_path,output))+'">Untouched reference MusicXML</a></p>']
|
||||
if baseline_manifest:
|
||||
baseline_path=Path(baseline_dir)/baseline_manifest['solos'][role]['file']
|
||||
rows.append('<p><a href="'+html.escape(os.path.relpath(baseline_path,output))+'">Previous generated MusicXML</a></p>')
|
||||
selected=collections.Counter(c['kind'] for c in r['production_phrases'].get('expressions',[]))
|
||||
rows.append('<p>Selected, validated production treatments: '+html.escape(str(dict(selected)))+'. Ordinary mapped material remains separate.</p>')
|
||||
if r.get('reference_events_after_production_end'):
|
||||
rows.append('<p>Reference material extends beyond the production vocal-protection window: '+str(len(r['reference_events_after_production_end']))+' events. The reference and vocal timing remain unchanged.</p>')
|
||||
funnel=gesture_coverage(r['production_phrases'].get('selection_trace',{}).get('joint_gestures',{}))
|
||||
funnel=r['production_phrases'].get('selection_trace',{}).get('operation_coverage',{})
|
||||
if funnel:
|
||||
rows.append('<details><summary>Construction coverage (not similarity scoring)</summary><table><tr><th>Purpose</th><th>Proposed structures</th><th>Retained jobs</th><th>Valid routes</th><th>Retained units</th></tr>')
|
||||
stages=('eligible_contexts','proposed','rhythm_valid','complete_routes','valid','finalists','placed','lookahead_valid','selected')
|
||||
rows.append('<details><summary>Construction coverage (not similarity scoring)</summary><table><tr><th>Purpose</th>'+''.join('<th>'+k.replace('_',' ')+'</th>' for k in stages)+'</tr>')
|
||||
for purpose,counts in sorted(funnel.items()):
|
||||
rows.append('<tr><td>'+html.escape(purpose)+'</td>'+''.join('<td>'+str(counts[k])+'</td>' for k in ('proposed','retained_jobs','valid_routes','retained_units'))+'</tr>')
|
||||
rows.append('<tr><td>'+html.escape(purpose)+'</td>'+''.join('<td>'+str(counts.get(k,0))+'</td>' for k in stages)+'</tr>')
|
||||
rows.append('</table></details>')
|
||||
for clip in r.get('audition',{}).get('clips',[]):
|
||||
rows.append('<p>'+html.escape(clip['version']+' / '+clip['context'])+'<br><audio controls preload="none" src="'+html.escape(clip['file'])+'"></audio></p>')
|
||||
@@ -236,10 +247,11 @@ def main():
|
||||
parser.add_argument('--form-start',action='append',default=[],metavar='SONG/ROLE=MEASURE')
|
||||
parser.add_argument('--reference-pitch',choices=('encoded','guitar-written'),default='encoded')
|
||||
parser.add_argument('--output',type=Path,required=True)
|
||||
parser.add_argument('--baseline-dir',type=Path,help='Optional previous completed solo exports; never generation inputs.')
|
||||
parser.add_argument('--assets',type=Path,help='Optional completed assets for neutral reference/generated audio with identical backing.')
|
||||
args=parser.parse_args()
|
||||
run(args.generated_dir,dict(x.split('=',1) for x in args.reference),
|
||||
{k:int(v) for k,v in (x.split('=',1) for x in args.form_start)},args.reference_pitch,args.output,args.assets)
|
||||
{k:int(v) for k,v in (x.split('=',1) for x in args.form_start)},args.reference_pitch,args.output,args.assets,args.baseline_dir)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
|
||||
Reference in New Issue
Block a user