"""Resample validated source WAVs into draft Unity clips without claiming listening approval.""" import argparse from pathlib import Path import math import sys import numpy as np import soundfile as sf from scipy.signal import resample_poly from generate_voice import load_json, validate_manifest, waveform_metadata, write_json def prepare(manifest, catalog, profile, root, target, ids=None): errors = validate_manifest(manifest, catalog, profile, root, ids) if errors: raise ValueError('; '.join(errors)) selected = set(ids or [c['id'] for c in catalog['clips'] if c['required']]) for entry in manifest['clips']: if entry['id'] in selected and entry['id'].startswith('count_') and entry['duration'] > 2: raise ValueError(f"{entry['id']}: count utterance exceeds 2 seconds; review before runtime export") target.mkdir(parents=True, exist_ok=True) edited_dir = root / 'edited' / manifest['runId'] edited_dir.mkdir(parents=True, exist_ok=True) selections = [] for entry in manifest['clips']: if entry['id'] not in selected: continue wave, rate = sf.read(root / entry['file'], dtype='float32') divisor = math.gcd(rate, 44100) edited = resample_poly(wave, 44100 // divisor, rate // divisor) if np.max(np.abs(edited)) >= 1: raise ValueError(f"{entry['id']}: editing would clip; review level before export") edited_path = edited_dir / (entry['id'] + '.wav') sf.write(edited_path, edited, 44100, subtype='PCM_16') runtime_path = target / edited_path.name if runtime_path.exists() and runtime_path.read_bytes() != edited_path.read_bytes(): raise ValueError(f"{entry['id']}: existing runtime clip preserved; review new edited candidate before replacing it") runtime_path.write_bytes(edited_path.read_bytes()) selections.append({'id': entry['id'], 'sourceRunId': entry['originalRunId'], 'sourceFile': entry['file'], 'sourceSha256': entry['sha256'], 'editedFile': edited_path.relative_to(root).as_posix(), 'runtimeFile': 'Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice/' + runtime_path.name, 'editedMetadata': waveform_metadata(edited_path), 'review': 'not_listened', 'status': 'draft', 'processing': 'polyphase resample to mono PCM16 44100 Hz; no speech trimming'}) result = {'schemaVersion': 1, 'runId': manifest['runId'], 'profileHash': manifest['profileHash'], 'catalogHash': manifest['catalogHash'], 'selections': selections} write_json(edited_dir / 'selection.json', result) return result def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--catalog', type=Path, required=True) parser.add_argument('--profile', type=Path, required=True) parser.add_argument('--manifest', type=Path, required=True) parser.add_argument('--ids') parser.add_argument('--target', type=Path, default=Path('Assets/MagnetismHeroes/UnityRuntime/Presentation/Audio/Voice')) args = parser.parse_args() try: selection = prepare(load_json(args.manifest), load_json(args.catalog), load_json(args.profile), args.catalog.parent, args.target, args.ids.split(',') if args.ids else None) for clip in selection['selections']: print(f"{clip['id']}: draft {clip['editedMetadata']['duration']:.3f}s {clip['editedMetadata']['sha256']}") return 0 except Exception as error: print(str(error), file=sys.stderr) return 1 if __name__ == '__main__': sys.exit(main())