Files
verstack/scripts/validate_sound_catalog.py
Verstack Local f73167de85 Add downloadable VMs and improve catalog processing
Package Apple Silicon VM exports and cached runtime switching with local cabinet controls and conagent provisioning. Preserve experimental SPIKE 2 emulation.

Improve preparation concurrency, Ghidra checkpoints, signature importing, sound indexing and client-side spectrograms. Include regression tests and validation notes.
2026-09-14 20:03:59 -05:00

74 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Run sound-catalog recovery against retained extracted ROMs via the local API.
Stores scripts and hash-bound evidence, without restarting running analysis or
rewriting archived audio. Scratch inputs are removed after each release.
"""
import argparse
import json
import pathlib
import shutil
import sys
import tempfile
import urllib.parse
import urllib.request
sys.path.insert(0,str(pathlib.Path(__file__).resolve().parents[1]/'plugins'))
import godot_scripts
from ram_workspace import ram_workspace
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--config',type=pathlib.Path,default=pathlib.Path('config.json'))
parser.add_argument('--api',default='http://127.0.0.1:8080')
parser.add_argument('--output',type=pathlib.Path,default=pathlib.Path('data/validation/sound-catalog'))
args=parser.parse_args();config=json.loads(args.config.read_text());scratch,budget=ram_workspace(config)
args.output.mkdir(parents=True,exist_ok=True)
summaries=[]
def get(path):return json.load(urllib.request.urlopen(args.api+path,timeout=120))
snapshots=get('/api/snapshots')
for snapshot in snapshots:
if snapshot['layer']!='extracted':continue
manifest=get('/api/snapshots/'+snapshot['id'])
entries={e['path']:e for e in manifest['entries'] if e['kind']=='file'}
scripts=[p for p in entries if p.endswith(('.pck','.gd','.gdc'))]
row={'snapshot':snapshot['id'],'release':snapshot['release'],'script_inputs':len(scripts)}
print('BEGIN',snapshot['release'],flush=True)
if not scripts:
row.update(status='no_script_catalog',cues=0,named_sounds=0);summaries.append(row);continue
selected=set(scripts)
for script in scripts:
for parent in pathlib.PurePosixPath(script).parents:
image,game=str(parent/'image.bin'),str(parent/'game')
if image in entries and game in entries:
selected.update((image,game));break
if sum(entries[p]['size'] for p in selected)>budget//2:raise ValueError('Sound validation inputs exceed scratch budget')
with tempfile.TemporaryDirectory(prefix='sound-catalog-',dir=scratch) as temp:
root=pathlib.Path(temp);source=root/'input';source.mkdir();output=root/'output';output.mkdir()
for path in sorted(selected):
dest=source/path;dest.parent.mkdir(parents=True,exist_ok=True)
url=args.api+'/api/file/'+snapshot['id']+'?'+urllib.parse.urlencode({'path':path})
with urllib.request.urlopen(url,timeout=180) as response,dest.open('wb') as stream:shutil.copyfileobj(response,stream)
if dest.stat().st_size!=entries[path]['size']:raise ValueError('Incomplete archived input read')
result=godot_scripts.run({'protocol':1,'input_dir':str(source),'output_dir':str(output),'workspace_bytes':budget,'settings':config['plugins']['godot-scripts']['settings']})
evidence=json.loads((output/'godot-scripts-evidence.json').read_text())
catalogs=[r['sound_catalog'] for r in evidence['scripts'] if 'sound_catalog' in r]
row.update(status=result['coverage'],cues=sum(len(c['cues']) for c in catalogs),
linked_cues=sum(len({r['name'] for r in c['links']}) for c in catalogs),
named_sounds=sum(len({r['index'] for r in c['links']}) for c in catalogs),
recovered_scripts=evidence['summary']['recovered'],failed_scripts=evidence['summary']['failed'])
destination=args.output/snapshot['id'];destination.mkdir(exist_ok=True)
shutil.copyfile(output/'godot-scripts-evidence.json',destination/'godot-scripts-evidence.json')
# The cue source and its evidence are sufficient to reproduce naming;
# omit unrelated recovered scripts from the persisted validation set.
for record in evidence['scripts']:
if 'sound_catalog' in record:
target=destination/record['output'];target.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(output/record['output'],target)
row['evidence']=str(destination/'godot-scripts-evidence.json')
summaries.append(row)
(args.output/'summary.json').write_text(json.dumps(summaries,indent=2)+'\n')
print('DONE',json.dumps(row),flush=True)
(args.output/'summary.json').write_text(json.dumps(summaries,indent=2)+'\n')
if __name__=='__main__':main()