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:
mARTin-B78 2026-06-21 14:36:06 +02:00
parent 602cf9e5df
commit 70fd8140e0

144
config/find_best_seed.py Normal file
View 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()