"""Validation-only decision calibration, then a frozen matched-pool test. The reference language is synthetic and closed-world: these scores do not stand in for human judgments. Both classifiers receive identical generated candidates. """ import argparse import csv import hashlib import json import random import time from pathlib import Path import torch from .corrections import read, editing_cases from .ranker import Reranker from .train_ranker import encode_groups, validation from diffuthink.v2.editor import rewrite_span from diffuthink.v2.inference import load def normalized(text):return text.strip().casefold().strip(' .!?') def accepted(case,word):return normalized(word) in {normalized(x) for x in case['accepted']} def decision(row,threshold,floor): best=max(row['candidates'],key=lambda c:c['ranker_score'],default=None) if best and best['ranker_score']-row['original_score']>threshold and best['ranker_score']>=floor: return best['replacement'] return row['original_fragment'] def metrics(rows,chooser): error=[r for r in rows if not r['case']['clean']];clean=[r for r in rows if r['case']['clean']] corrected=sum(accepted(r['case'],chooser(r)) for r in error) unchanged=sum(chooser(r)==r['original_fragment'] for r in clean) bad=sum(chooser(r)!=r['original_fragment'] and not accepted(r['case'],chooser(r)) for r in error) return {'error_cases':len(error),'clean_cases':len(clean),'error_corrected':corrected/len(error), 'clean_preserved':unchanged/len(clean),'balanced_success':.5*(corrected/len(error)+unchanged/len(clean)), 'wrong_replacement_on_errors':bad/len(error),'clean_modified':1-unchanged/len(clean)} def calibrate(rows): # Predeclared grid and harm gates; correctness is only synthetic reference matching. margins=[0,.25,.5,1,1.5,2,3,4,6,8,12,1000000.] floors=[-1000000.,-4,-2,0,1,2,3,4,6,8,1000000.] best=None for margin in margins: for floor in floors: m=metrics(rows,lambda r:decision(r,margin,floor)) if m['clean_modified']>.05+1e-9 or m['wrong_replacement_on_errors']>.10+1e-9:continue key=(m['balanced_success'],-m['clean_modified'],-m['wrong_replacement_on_errors'],margin,floor) if best is None or key>best[0]:best=(key,margin,floor,m) return {'margin_threshold':best[1],'score_floor':best[2],'validation':best[3], 'method':'Grid search on validation only: maximize balanced success subject to <=5% clean edits and <=10% wrong edits on errors.', 'margin_grid':margins,'score_grid':floors,'contexts':len(rows)//2, 'limitations':'Finite synthetic validation; thresholds are not guarantees on user text.'} def generate_rows(model,tokenizer,ranker,contexts,device,path): path=Path(path);rows=[] # A completed cache is tied to both model hashes by the caller's manifest. if path.exists():return read(path) temporary=path.with_suffix('.jsonl.tmp') with temporary.open('w',encoding='utf-8') as file: for index,case in enumerate(editing_cases(contexts)): result=rewrite_span(model,tokenizer,case['text'],case['start'],case['end'],seed=301+index//2, precision='bf16' if device=='cuda' else 'fp32') a,b=result['selection']['start'],result['selection']['end'] scores=ranker.scores([(case['text'],a,b)]+[(c['text'],a,a+len(c['replacement'])) for c in result['candidates']]) candidates=[{k:c[k] for k in ('replacement','text','local_nll','seed','masked_tokens')} for c in result['candidates']] for c,s in zip(candidates,scores[1:]):c['ranker_score']=s row={'case':case,'original_fragment':result['selection']['text'],'original_nll':result['original_local_nll'], 'original_score':scores[0],'candidates':candidates,'latency_ms':result['latency_ms'], 'context_preserved':all(c['text']==result['prefix']+c['replacement']+result['suffix'] for c in candidates)} file.write(json.dumps(row,ensure_ascii=False)+'\n');file.flush();rows.append(row) if (index+1)%40==0:print(json.dumps({'file':str(path),'cases':index+1}),flush=True) temporary.replace(path);return rows def nll_choice(row): best=min(row['candidates'],key=lambda c:c['local_nll'],default=None) return best['replacement'] if best and best['local_nll']