Files
product-vamp/audition/phrase_candidates.py
T
khodowany 654299decd Cleanup
2026-10-05 08:51:03 -05:00

75 lines
4.0 KiB
Python

"""Export source-only complete candidates; references are not accepted here."""
import argparse
import copy
import dataclasses
from fractions import Fraction as F
import json
from pathlib import Path
import time
import cProfile
from audition.fit_selection import build
from generator import model, musicxml_writer, score, vocabulary
from generator.solo import notation, placement
def export(builder,document,output):
output.mkdir(parents=True,exist_ok=True)
source=score.source_staff(document)['measures']
start_index=score.body_start_measure_index(source)
if builder.start<0:start_index-=1
length=F();selected=[]
for m in source[start_index:]:
if length>=builder.end-builder.start:break
selected.append(m);length+=model.fraction(m['duration'])
if length!=builder.end-builder.start:raise ValueError('Diagnostic export requires complete source measures')
records=[]
for index,state in enumerate(builder.completed,1):
played=placement.place(state.notes,builder.analysis.tempo,profile=builder.harmony.profile)
if played is None:raise ValueError('Completed candidate is unplayable')
shifted=tuple(dataclasses.replace(n,start=n.start-builder.start,end=n.end-builder.start) for n in played[1])
part=copy.deepcopy(document);staff=part['staves'][0]
part['staves']=[staff];staff['measures']=notation.measures(selected,shifted,'candidate')
staff['tuplets']=notation.tuplets(staff['measures']);staff['tablature']=True
for key in ('scale_spans','octave_shifts','slurs','wedges'):staff.pop(key,None)
path=output/f'candidate-{index}.xml'
musicxml_writer.write_musicxml(path,part,tablature=True)
records.append({'file':path.name,'automatic_winner':index==1,'evaluation':dataclasses.asdict(state.evaluation),
'home':[(str(n.start),str(n.end),n.pitch.number()) for n in state.home],
'events':[(str(n.start),str(n.end),n.pitch.number()) for n in state.notes],
'actions':[{'kind':a.kind,'start':str(a.start),'end':str(a.end),'contracts':[c.rule for c in a.contracts]} for a in state.actions]})
value={'source_only':True,'automatic_winner':'candidate-1.xml','start':str(builder.start),'end':str(builder.end),
'candidates':records,'frontiers':builder.frontiers,'timings':dict(builder.timings),
'note':'Diagnostic candidates, not publication or evidence of exact reproduction.'}
(output/'candidates.json').write_text(json.dumps(value,indent=2,default=str)+'\n')
return value
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--source',type=Path,required=True);p.add_argument('--role',choices=('acoustic_solo','electric_solo'),required=True)
p.add_argument('--data-dir',type=Path,default=Path('data'));p.add_argument('--output',type=Path,required=True)
p.add_argument('--cache-dir',type=Path);p.add_argument('--bars',type=int,default=4)
p.add_argument('--profile',action='store_true')
args=p.parse_args()
if args.bars<1:p.error('--bars must be positive')
with vocabulary.using(vocabulary.load(args.data_dir/'composition'),args.role):
b,document=build(args.source,args.role,args.data_dir,args.cache_dir)
ends=[x-b.analysis.body_offset for x in b.analysis.boundaries if x-b.analysis.body_offset>0]
b.end=min(b.end,ends[min(args.bars-1,len(ends)-1)]);b.operators.end=b.end
started=time.monotonic();seen=set()
def progress(stage,at,state,proposed,retained):
if stage=='frontier' and at.denominator==1 and at not in seen:
seen.add(at);print(f'frontier {at}: {len(proposed)} histories, {len(retained)} retained; {time.monotonic()-started:.1f}s',flush=True)
b.observe=progress
args.output.mkdir(parents=True,exist_ok=True)
profiler=cProfile.Profile() if args.profile else None
if profiler:profiler.enable()
try:b.compose()
finally:
if profiler:profiler.disable();profiler.dump_stats(args.output/'search.prof')
export(b,document,args.output)
print(json.dumps({'completed':len(b.completed),'seconds':b.timings['action_search_seconds'],'winner':str(b.winner_evaluation.total)}))
if __name__=='__main__':main()