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

145 lines
8.4 KiB
Python

"""Fit shared ranking from completed ordinary generation, never prefix roll-ins.
This development command reads explicit targets and saved complete alternatives.
It writes a *proposal*, not a released model. A staged data copy may be passed to
the ordinary generator for a separate target-free cold reproduction test.
"""
import argparse
import copy
from fractions import Fraction as F
import hashlib
import json
from pathlib import Path
import shutil
import sys
from audition.fit_selection import build,derive
from audition.reference_review import read
from audition.structured_learning import vector,constraint,solve,sounded,CoverageTrace
from generator import production,score,vocabulary
from generator.solo import construction,selection,policy
def evaluation_vector(value):
if value['version']!=selection.VERSION:raise ValueError('Completed output uses another evaluator version')
span=F(value['denominators']['span'])
features={k:F(v)*span for k,v in value['raw'].items() if not k.startswith('prior/') and F(v)}
selection.validate_weights(dict.fromkeys(features,0))
prior=sum(F(v)*span*selection.DEFAULT[k.removeprefix('prior/')] for k,v in value['raw'].items() if k.startswith('prior/'))
return features,prior
def training_context(data):
"""Changing learned weights is allowed between rounds; musical rules are not."""
musical=copy.deepcopy(data)
musical['roles'].pop('provenance',None)
for role in musical['roles']['roles'].values():role.pop('selection_weights',None)
return {'evaluator':selection.VERSION,'algorithm':policy.DEFAULT.composition_version,
'fixed_priors':selection.DEFAULT,'musical_data':vocabulary.fingerprint(musical)}
def previous_constraints(evidence,cases,context):
if evidence.get('schema')!='complete-candidate-training-1' or evidence.get('context')!=context:
raise ValueError('Previous round has incompatible evaluation or musical data')
earlier=evidence['training_dependency']
if earlier.keys()!=cases.keys():raise ValueError('Previous round has different training cases')
for name in cases:
for key in ('source_sha256','target_sha256','role'):
if cases[name][key]!=earlier[name][key]:raise ValueError('Previous round changed '+name+'/'+key)
rows=[]
for row in evidence['constraints']:
if row['case'] not in cases or row['role']!=cases[row['case']]['role']:
raise ValueError('Previous constraint has a mismatched case/role')
selection.validate_weights(dict.fromkeys(row['delta'],0))
rows.append({**row,'delta':{k:F(v) for k,v in row['delta'].items()},'prior':F(row['prior'])})
return rows
def main():
p=argparse.ArgumentParser(description=__doc__)
for name in ('source','reference','role'):p.add_argument('--'+name,action='append',required=True,metavar='CASE=VALUE')
p.add_argument('--form-start',action='append',default=[])
p.add_argument('--data-dir',type=Path,default=Path('data'))
p.add_argument('--completed-dir',type=Path,required=True,help='Completed ordinary assets directory containing analysis/*.roles')
p.add_argument('--output',type=Path,required=True);p.add_argument('--cache-dir',type=Path)
p.add_argument('--solver-path',type=Path,required=True)
p.add_argument('--stage-data',action='store_true',help='Write an isolated data copy for ordinary inference; does not publish it')
p.add_argument('--previous-round',type=Path,action='append',default=[],help='Accumulate complete-output constraints from a compatible training.json; never a production input')
args=p.parse_args();sys.path.insert(0,str(args.solver_path.resolve()))
pairs=lambda values:dict(v.split('=',1) for v in values)
sources,refs,roles=map(pairs,(args.source,args.reference,args.role));starts=pairs(args.form_start)
if sources.keys()!=refs.keys() or sources.keys()!=roles.keys():p.error('Cases must agree')
args.output.mkdir(parents=True,exist_ok=True);rows=[];cases={};contexts=[];data=vocabulary.load(args.data_dir/'composition')
for name in sorted(sources):
with vocabulary.using(data,roles[name]):
b,document=build(sources[name],roles[name],args.data_dir,args.cache_dir)
target=read(refs[name],name,int(starts[name]) if name in starts else None,'guitar-written')
gold,coverage=derive(b,target)
if not coverage['complete']:raise ValueError(name+': '+coverage['status'])
s=construction.State()
for a in gold:s=b.advance(s,None,a)
positive=vector(b,gold,s.home)
contexts.append((name,roles[name],b,gold))
path=args.completed_dir/'analysis'/(score.slugify(Path(sources[name]).stem)+'.roles')
generated=json.loads(path.read_text())[roles[name]]
if generated['trace']['source']!=production.SourceAnalysis.read(document).musical_key:
raise ValueError(name+': completed candidates belong to different canonical source music')
if generated['trace']['role']!=roles[name]:raise ValueError(name+': wrong completed role')
if generated['trace']['vocabulary']!=vocabulary.fingerprint(production.dependencies(roles[name],data)):
raise ValueError(name+': completed candidates use different production data/settings')
candidates=generated['details']['selection_trace']['completed_candidates']
negatives=0;exact=False
for c in candidates:
stream=tuple((F(a),F(z),int(n)) for a,z,n in c['events'])
same=stream==sounded(gold)
if c['rank']==1:exact=same
if not same:rows.append(constraint(roles[name],positive,evaluation_vector(c['evaluation']),name));negatives+=1
cases[name]={'source':sources[name],'reference':refs[name],'role':roles[name],
'source_sha256':hashlib.sha256(Path(sources[name]).read_bytes()).hexdigest(),
'target_sha256':target['sha256'],'completed_artifact_sha256':hashlib.sha256(path.read_bytes()).hexdigest(),
'coverage':coverage,'ordinary_winner_exact':exact,'complete_negative_candidates':negatives}
print(name,exact,negatives,flush=True)
context=training_context(data);parents=[]
for path in args.previous_round:
raw=path.read_bytes();rows.extend(previous_constraints(json.loads(raw),cases,context))
parents.append({'path':str(path),'sha256':hashlib.sha256(raw).hexdigest()})
unique={}
for row in rows:
key=(row['role'],row['case'],tuple(sorted(row['delta'].items())),row['prior'])
unique[key]=row
rows=[unique[key] for key in sorted(unique)]
result=solve(rows)
evidence={'schema':'complete-candidate-training-1','training_dependency':cases,'solver':result,
'context':context,'previous_rounds':parents,
'constraints':[{**r,'delta':{k:str(v) for k,v in r['delta'].items()},'prior':str(r['prior'])} for r in rows],
'released':False,'acceptance':'Exact four full outputs from ordinary cold target-free generation, plus transfer; not satisfied by this training solve.'}
if result['status']=='solved':
# A zero here proves this proposed model cannot reproduce the full output.
# A nonzero result proves neither full retention nor exact generation.
# This is read-only diagnosis, not prefix fitting or candidate injection.
cuts={}
for name,role,b,gold in contexts:
with vocabulary.using(data,role):
b.selector.weights=result['weights'].get(role,{})
trace=CoverageTrace(gold);b.observe=trace
retained=b.candidates(b.start,construction.State());b.observe=None
count=sum(trace.matches(a.notes,a.end) for _,a in retained)
cuts[name]={'matching_first_successors':count,'observed_failures':trace.records,
'full_reproduction_already_excluded':count==0}
evidence['post_fit_cutoff_diagnosis']=cuts
(args.output/'training.json').write_text(json.dumps(evidence,indent=2)+'\n')
if result['status']=='solved':
(args.output/'proposed-weights.json').write_text(json.dumps(result['weights'],indent=2)+'\n')
if args.stage_data:
staged=args.output/'data'
if staged.exists():raise ValueError('Refusing to overwrite a previous staged data proposal')
shutil.copytree(args.data_dir,staged)
path=staged/'composition'/'roles.json';value=json.loads(path.read_text())
for role,weights in result['weights'].items():value['roles'][role]['selection_weights']=weights
value['provenance']='Shared bounded coefficients learned from explicit authored training examples; see adjacent development training.json. No runtime target reads or stored phrases.'
path.write_text(json.dumps(value,indent=2)+'\n')
print(json.dumps(result,indent=2))
return 0 if result['status']=='solved' else 2
if __name__=='__main__':raise SystemExit(main())