feat: add find_best_seed.py script for voice seed comparison
Generates WAV samples with different seeds for a given voice so the user can listen and pick the most natural-sounding one. Uses the server's hot-reload to switch seeds without restarting. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
602cf9e5df
commit
70fd8140e0
144
config/find_best_seed.py
Normal file
144
config/find_best_seed.py
Normal file
@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Find the best RNG seed for a voice by generating audio samples with different
|
||||
seeds and saving them as numbered WAV files for comparison.
|
||||
|
||||
How it works:
|
||||
1. For each seed, temporarily writes that seed into voices.json
|
||||
2. Calls the running server (hot-reload picks it up automatically)
|
||||
3. Saves the audio as seed_0001.wav, seed_0042.wav, etc.
|
||||
4. Restores voices.json to its original state when done
|
||||
|
||||
Then just listen to the WAV files and pick the seed number you prefer.
|
||||
Add it to your voice in voices.json: "seed": <number>
|
||||
|
||||
Usage:
|
||||
python find_best_seed.py --voice EN_F_NatashaNeural --seeds 1 2 3 42 100
|
||||
python find_best_seed.py --voice EN_F_NatashaNeural --range 1 20
|
||||
python find_best_seed.py --voice EN_F_NatashaNeural --range 1 50 --text "Hello!"
|
||||
|
||||
# Different server port:
|
||||
python find_best_seed.py --voice DE_5_28 --range 1 10 --port 8020
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
VOICES_JSON = "/config/voices.json"
|
||||
DEFAULT_TEXT = (
|
||||
"The morning light filtered softly through the curtains, casting a warm glow "
|
||||
"across the room. Outside, birds were already singing."
|
||||
)
|
||||
|
||||
|
||||
def load_voices() -> dict:
|
||||
with open(VOICES_JSON, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_voices(voices: dict) -> None:
|
||||
with open(VOICES_JSON, "w", encoding="utf-8") as f:
|
||||
json.dump(voices, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def generate(voice: str, text: str, host: str, port: int, timeout: int = 60) -> bytes:
|
||||
url = f"http://{host}:{port}/v1/audio/speech"
|
||||
resp = requests.post(
|
||||
url,
|
||||
json={"model": "tts-1", "input": text, "voice": voice, "response_format": "wav"},
|
||||
timeout=timeout,
|
||||
stream=False,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.content
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Compare seeds for a voice")
|
||||
p.add_argument("--voice", required=True, help="Voice name (must exist in voices.json)")
|
||||
p.add_argument("--seeds", type=int, nargs="+", help="Explicit list of seeds to try")
|
||||
p.add_argument("--range", type=int, nargs=2, metavar=("START", "END"),
|
||||
help="Try seeds START through END (inclusive)")
|
||||
p.add_argument("--text", default=DEFAULT_TEXT, help="Text to synthesize")
|
||||
p.add_argument("--host", default="localhost")
|
||||
p.add_argument("--port", type=int, default=8020)
|
||||
p.add_argument("--out-dir", default="./seed_samples", help="Directory to save WAV files")
|
||||
args = p.parse_args()
|
||||
|
||||
if not args.seeds and not args.range:
|
||||
print("ERROR: provide --seeds or --range", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
seeds = list(args.seeds or [])
|
||||
if args.range:
|
||||
seeds += list(range(args.range[0], args.range[1] + 1))
|
||||
seeds = sorted(set(seeds))
|
||||
|
||||
os.makedirs(args.out_dir, exist_ok=True)
|
||||
|
||||
# Back up voices.json
|
||||
backup = VOICES_JSON + ".seed_backup"
|
||||
shutil.copy2(VOICES_JSON, backup)
|
||||
print(f"Backed up voices.json → {backup}")
|
||||
|
||||
voices = load_voices()
|
||||
if args.voice not in voices:
|
||||
print(f"ERROR: voice {args.voice!r} not found in voices.json", file=sys.stderr)
|
||||
print(f"Available: {', '.join(voices.keys())}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
original_seed = voices[args.voice].get("seed", "<none>")
|
||||
print(f"\nVoice: {args.voice}")
|
||||
print(f"Text: {args.text[:80]}{'...' if len(args.text) > 80 else ''}")
|
||||
print(f"Seeds: {seeds}")
|
||||
print(f"Output: {os.path.abspath(args.out_dir)}/")
|
||||
print(f"Original seed: {original_seed}\n")
|
||||
|
||||
results = []
|
||||
|
||||
try:
|
||||
for seed in seeds:
|
||||
voices[args.voice]["seed"] = seed
|
||||
save_voices(voices)
|
||||
# Brief pause so the server's hot-reload detects the mtime change
|
||||
time.sleep(0.3)
|
||||
|
||||
out_path = os.path.join(args.out_dir, f"seed_{seed:05d}.wav")
|
||||
print(f" seed {seed:5d} → ", end="", flush=True)
|
||||
try:
|
||||
t0 = time.time()
|
||||
wav = generate(args.voice, args.text, args.host, args.port)
|
||||
elapsed = time.time() - t0
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(wav)
|
||||
print(f"{out_path} ({elapsed:.1f}s)")
|
||||
results.append((seed, out_path, None))
|
||||
except Exception as e:
|
||||
print(f"FAILED: {e}")
|
||||
results.append((seed, None, str(e)))
|
||||
|
||||
finally:
|
||||
# Always restore the original voices.json
|
||||
shutil.copy2(backup, VOICES_JSON)
|
||||
os.remove(backup)
|
||||
print(f"\nRestored voices.json (seed reset to {original_seed!r})")
|
||||
|
||||
print(f"\n{'─'*60}")
|
||||
print("Done! Listen to the files and find the seed you prefer.")
|
||||
print(f"Then add it to voices.json under {args.voice!r}:")
|
||||
print(f' "seed": <your_chosen_number>')
|
||||
print(f"{'─'*60}")
|
||||
ok = [(s, p) for s, p, e in results if p]
|
||||
if ok:
|
||||
print(f"\nGenerated {len(ok)} samples:")
|
||||
for seed, path in ok:
|
||||
print(f" seed {seed:5d} → {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user