Add pinned local Qwen GPU voice generation and first puzzle drafts
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user