import hashlib,html,json,os,shutil,time from pathlib import Path import numpy as np ROOT=Path(__file__).resolve().parent OLD=Path.home()/'.cache/robo-v3' WORK=Path.home()/'.cache/robo-v4' FEATURES=ROOT.parent/'.cache/speaker-v4' SEEDS=(2021,2022,2023) _previous=json.loads((ROOT/'progress.json').read_text()) if (ROOT/'progress.json').exists() else {} STATE={'phase':'Preparing','started':_previous.get('started',time.time())} def atomic(path,value): path=Path(path);path.parent.mkdir(parents=True,exist_ok=True);tmp=path.with_suffix(path.suffix+'.tmp') tmp.write_text(json.dumps(value,indent=2,allow_nan=False));tmp.replace(path) def guard(): if (ROOT/'STOP').exists():raise RuntimeError('STOP requested; completed artifacts retained') if FEATURES.is_symlink() and not FEATURES.exists(): target=FEATURES.readlink() if target.parent==Path('/dev/shm') and target.name.startswith('robo-v4-layers-'): FEATURES.unlink() # only our dangling temporary-cache pointer else:raise RuntimeError('Unexpected dangling cache symlink; refusing to change it') for path in (WORK,FEATURES): path.mkdir(parents=True,exist_ok=True) if shutil.disk_usage(path).free<3*1024**3:raise RuntimeError('Storage safety stop: less than 3 GiB free on '+str(path)) def update(**fields): STATE.update(fields,updated=time.time());atomic(ROOT/'progress.json',STATE) WORK.mkdir(parents=True,exist_ok=True) with (WORK/'events.jsonl').open('a') as out:out.write(json.dumps({'time':time.time(),**fields},allow_nan=False)+'\n') def digest(path): h=hashlib.sha256() with Path(path).open('rb') as stream: for part in iter(lambda:stream.read(2**20),b''):h.update(part) return h.hexdigest() def cleanup_unused(): """Only unreferenced surplus MP3s from our completed selective downloader.""" sources=OLD/'sources';referenced=set() for row in json.loads((OLD/'sources_manifest.json').read_text()): referenced.update(Path(c['path']).resolve() for c in row['clips']) for path in sources.glob('*/clips.json'): referenced.update(Path(c['path']).resolve() for c in json.loads(path.read_text())) candidates=[] for path in sources.glob('*/*.mp3'): if path.is_symlink() or not path.is_file() or path.resolve() in referenced:continue if path.resolve().parent.parent!=sources.resolve():raise RuntimeError('Cleanup scope mismatch') candidates.append({'path':str(path),'bytes':path.stat().st_size,'sha256':digest(path)}) atomic(WORK/'cleanup_manifest.json',{'recovery':'Regenerate surplus downloads from pinned V3 dataset ranges; no local trash copy', 'retained_referenced_clips':len(referenced),'files':candidates}) for item in candidates: path=Path(item['path']) if path.resolve() in referenced or digest(path)!=item['sha256']:raise RuntimeError('Cleanup verification failed') path.unlink() update(cleaned_surplus_files=len(candidates),cleaned_bytes=sum(c['bytes'] for c in candidates)) def report(results=None,finished=False): if results is not None:atomic(WORK/'results.json',results) data=results if results is not None else json.loads((WORK/'results.json').read_text()) if (WORK/'results.json').exists() else {} rows=[];summary=[] for stage in ('stage1','factorial'): for arm,group in data.get(stage+'_aggregates',{}).items(): v=group.get('validation',{});t=group.get('test',{}) if not v:continue def value(values,key,percent=False): if key not in values:return '-' multiplier=100 if percent else 1 return f"{values[key]['mean']*multiplier:.2f} ± {values[key]['std_across_seeds']*multiplier:.2f}"+('%' if percent else '') summary.append(''+''.join(''+html.escape(str(x))+'' for x in [stage,arm,group['seeds'], value(v,'macro_speaker_accuracy',True),value(v,'eer',True),value(t,'macro_speaker_accuracy',True), value(t,'source_utterance_teacher_cosine')])+'') for stage in ('stage1','factorial'): for row in data.get(stage,[]): v=row['validation'];test=row.get('test',{}) rows.append(''+''.join(''+html.escape(str(x))+'' for x in [stage,row['arm'],row['seed'], f"{100*v['macro_speaker_accuracy']:.1f}%",f"{v['source_utterance_teacher_cosine']:.3f}",f"{100*v['eer']:.1f}%", '-' if not test else f"{100*test['macro_speaker_accuracy']:.1f}%",'-' if not test else f"{test['source_utterance_teacher_cosine']:.3f}"] )+'') body='''320-ms speaker ablations ← Original study

Improving 320-ms speaker predictions

'''+html.escape(STATE.get('phase','Preparing'))+'''

Stage 1: specialize the existing Query-GRU for four newest 80-ms states, and compare utterance targets against mixtures with robust training-speaker centroids (initial 50% or 80% centroid objective). If both initial mixtures lose to specialization alone on validation, a documented validation-only extension tests a gentler 20% mixture before test evaluation. Stage 2: matched 2×2 factorial on a fixed subset: baseline (1+2), layer mixing (+3), hard-example learning (+4), and both (+3+4). Three fixed seeds per arm. Layer IDs are one-based 4, 8, 12 and 15; all are frozen, from the same causal eight-second input and four newest states.

320 ms means the state-pooling span, not a 320-ms-only cold-start audio limit. Encoder history is unchanged. The original 480/10/10 identity split is retained. No private microphone recordings or enrolled user voiceprints. Original V3 artifacts are not overwritten.

All checkpoint, centroid weight, and experiment selection use validation identities only. Test results are deferred until all arms/seeds are frozen. The existing ten test speakers were used in the preceding study; these are repeated benchmark comparisons, not a new independent test population. Unknown-speaker operating thresholds are calibrated on validation.

A higher genuine-speaker cosine alone is not sufficient: a changed target can raise impostor similarities too. Identification, EER and genuine-versus-impostor cosine must be considered together. Utterance-target cosine and enrollment-reference cosine are different metrics and are both retained in the detailed results.

Summary across seeds

Mean ± standard deviation across available seeds; this is not a confidence interval over independent people. Partial groups are provisional. New test metrics remain blank until all arms are frozen.

'''+''.join(summary)+'''
StageArmSeedsVal macro accuracyVal EERTest macro accuracyTest utterance cosine

Individual runs

'''+''.join(rows)+'''
StageArmSeedVal macro accuracyVal utterance cosineVal EERTest macro accuracyTest utterance cosine

Resources and interpretation

Stage 1 reuses the 500,000-window V3 cache. Factorial training uses the same fixed 200 windows per training anchor voice (96,000 training windows) plus all 10,000 validation and 10,000 test windows; do not attribute differences between full-data and subset stages solely to architecture. Only three additional layers' four-frame states are cached, about 1.43 GB int8 plus float16 scales. No complete audio or hidden-state sequences are duplicated. The derived cache may be migrated, after hash verification, to bounded RAM-backed temporary storage when disk space is tight. It is regenerable from preserved public sources/seeds; a reboot can lose this cache, not the persistent models, original dataset or results. One GPU is used; live TTS on the other GPU is untouched. Browser audio sessions have priority between encoder batches. A three-GiB floor on working filesystems stops further work without deleting results.

Stable centroids use only the training voices' prediction clips, not held-out probe audio. The enrollment gallery remains the original disjoint reference clips. Hard-example arms add cross-emotion positive pairs and nearest-speaker negative pairs, preferring matching full-utterance transcript signatures when available. Matching a full transcript does not guarantee matching phonemes in a 320-ms crop. Additional pair forwards mean higher training compute; measured costs and pair-match rates are recorded. A random-pairs control uses the same extra pair losses/forwards without targeted mining. No guaranteed accuracy gain or real-human authentication claim.

Interpretation

'''+''.join('

'+html.escape(line)+'

' for line in data.get('conclusions',[]))+'''

The factorial retains (1+2) as its matched baseline even if stable targets do not beat specialization alone. The full-data specialization-only control is reported separately; selecting a centroid weight does not establish that centroids help.

Progress / protocol / complete results

'''+html.escape(json.dumps(data,indent=2,allow_nan=False))+'''
''' WORK.mkdir(parents=True,exist_ok=True);tmp=WORK/'report.tmp.html';tmp.write_text(body);tmp.replace(WORK/'report.html')