Preserve immutable per-run voice inputs and validate source history

This commit is contained in:
NINI
2026-10-05 02:24:06 +09:00
parent fb71c444f6
commit 8afff106da
15 changed files with 2616 additions and 2 deletions
+47 -1
View File
@@ -92,6 +92,31 @@ def waveform_metadata(path):
(['clipping candidate'] if peak >= .999 else [])}
def load_run_inputs(manifest, root):
run_id = manifest.get('runId', '')
if not isinstance(run_id, str) or not re.fullmatch(r'[A-Za-z0-9_]+', run_id):
raise ValueError('invalid input snapshot runId')
references = manifest.get('inputSnapshots', {'catalog': 'catalog.json', 'profile': 'profile.json'})
if references != {'catalog': 'catalog.json', 'profile': 'profile.json'}:
raise ValueError('input snapshots must reside in their immutable run directory')
run_dir = Path(root) / 'generated' / run_id
catalog = load_json(run_dir / references['catalog'])
profile = load_json(run_dir / references['profile'])
if not isinstance(catalog, dict) or not isinstance(profile, dict):
raise ValueError('input snapshots must contain JSON objects')
if digest(catalog) != manifest.get('catalogHash') or digest(profile) != manifest.get('profileHash'):
raise ValueError('input snapshot hash differs from manifest')
validate_inputs(catalog, profile)
if profile['modelRevision'] != manifest.get('modelRevision'):
raise ValueError('input snapshot model revision differs from manifest')
clips = {clip['id']: clip for clip in catalog['clips']}
for entry in manifest.get('clips', []):
clip = clips.get(entry.get('id'))
if not clip or entry.get('textHash') != digest(clip['text']) or entry.get('settingsHash') != digest(profile):
raise ValueError(f"{entry.get('id')}: input snapshot differs from clip text/settings hashes")
return catalog, profile
def clip_errors(entry, clip, profile, root):
errors = []
if entry.get('result') not in ('generated', 'reused'):
@@ -108,8 +133,21 @@ def clip_errors(entry, clip, profile, root):
errors.append(f"{clip['id']}: {key} mismatch")
if actual['channels'] != 1:
errors.append(f"{clip['id']}: expected mono")
run_id = entry['originalRunId']
if not isinstance(run_id, str) or not re.fullmatch(r'[A-Za-z0-9_]+', run_id):
raise ValueError('invalid original generation runId')
origin = load_json(root / 'generated' / run_id / 'manifest.json')
if origin.get('runId') != run_id:
raise ValueError('original manifest runId mismatch')
load_run_inputs(origin, root)
original_entries = [c for c in origin['clips'] if c.get('id') == clip['id']]
if len(original_entries) != 1 or original_entries[0].get('result') != 'generated':
raise ValueError('missing successful original generation entry')
for key in ('file', 'sha256', 'textHash', 'settingsHash'):
if original_entries[0].get(key) != entry.get(key):
raise ValueError(f'original generation {key} differs')
except (KeyError, OSError, ValueError, sf.LibsndfileError) as error:
errors.append(f"{clip['id']}: invalid WAV: {error}")
errors.append(f"{clip['id']}: invalid source artifact: {error}")
return errors
@@ -117,6 +155,10 @@ def validate_manifest(manifest, catalog, profile, root, ids=None):
validate_inputs(catalog, profile)
requested = select_clips(catalog, ids, required_only=ids is None)
errors = []
try:
load_run_inputs(manifest, root)
except (KeyError, OSError, ValueError, TypeError) as error:
errors.append(f'input snapshots invalid: {error}')
if manifest.get('schemaVersion') != 1 or manifest.get('modelRevision') != profile['modelRevision']:
errors.append('manifest schema/revision differs')
if manifest.get('catalogHash') != digest(catalog) or manifest.get('profileHash') != digest(profile):
@@ -139,6 +181,7 @@ def reusable_entries(root, catalog, profile):
for path in sorted((root / 'generated').glob('*/manifest.json'), reverse=True):
try:
manifest = load_json(path)
load_run_inputs(manifest, root)
for entry in manifest.get('clips', []):
clip = by_id.get(entry.get('id'))
if clip and clip['id'] not in result and not clip_errors(entry, clip, profile, root):
@@ -177,10 +220,13 @@ def generate(catalog, profile, root, ids=None, synthesize=None, environment=None
run_id = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S%fZ_') + uuid.uuid4().hex[:8]
run_dir = root / 'generated' / run_id
run_dir.mkdir(parents=True)
write_json(run_dir / 'catalog.json', catalog)
write_json(run_dir / 'profile.json', profile)
manifest = {'schemaVersion': 1, 'runId': run_id, 'createdUtc': datetime.now(timezone.utc).isoformat(),
'modelRevision': profile['modelRevision'], 'catalogHash': digest(catalog),
'profileHash': digest(profile), 'scope': 'selected' if ids else 'all',
'regenerateRequested': regenerate,
'inputSnapshots': {'catalog': 'catalog.json', 'profile': 'profile.json'},
'requestedIds': [c['id'] for c in selected], 'environment': environment or {}, 'clips': []}
try:
for clip in selected: