tts-dgx-spark-faster-qwen3-tts/config/find_best_seed.py
mARTin-B78 377b9f16e5 feat: add /voice-seed API endpoint and update find_best_seed.py (v6.7.1)
POST /voice-seed — writes or clears the seed for a named voice in
voices.json directly from the server, enabling the voice creator GUI
to save the chosen seed with one click.

Also updates find_best_seed.py to support --all-voices, per-voice
subdirectories, and the mixed DE/EN test sentence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 14:53:31 +02:00

199 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Find the best RNG seed for one or all voices by generating audio samples with
different seeds and saving them as numbered WAV files for comparison.
How it works:
1. For each voice × seed, temporarily sets that seed in voices.json
2. Calls the running server (hot-reload picks it up automatically)
3. Saves audio as <out-dir>/<voice-name>/seed_<N>.wav
4. Restores voices.json to its original state when done
Then listen to the files and pick the seed you prefer.
Add it to voices.json: "seed": <number>
Usage:
# All voices, seeds 115:
python find_best_seed.py --all-voices --range 1 15 --port 8020
# Single voice:
python find_best_seed.py --voice EN_F_NatashaNeural --range 1 20
python find_best_seed.py --voice DE_5_28 --seeds 1 7 42 100
"""
import argparse
import json
import os
import shutil
import sys
import time
import requests
VOICES_JSON = "/config/voices.json"
DE_TEXT = (
"Die 3.500 neuen High-End Geräte für das Server-Update benötigen eine "
"außergewöhnlich starke Kühlung und regelmäßige Maßnahmen, um die Performance "
"bei großer Last zu gewährleisten."
)
EN_TEXT = (
"The system administrator successfully configured the customized Docker stacks "
"and benchmarked the inference engines at exactly 8:45 AM."
)
MIXED_TEXT = DE_TEXT + " - " + EN_TEXT
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 = 90) -> 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 pick_text(voice_name: str, override: str | None) -> str:
if override:
return override
name_lower = voice_name.lower()
if name_lower.startswith("de_"):
return MIXED_TEXT
if name_lower.startswith("en_") or name_lower.startswith("gb_"):
return EN_TEXT
return MIXED_TEXT
def run_voice(voice_name: str, voices: dict, seeds: list[int],
text: str, host: str, port: int, out_dir: str) -> list[tuple]:
voice_dir = os.path.join(out_dir, voice_name)
os.makedirs(voice_dir, exist_ok=True)
results = []
for seed in seeds:
voices[voice_name]["seed"] = seed
save_voices(voices)
time.sleep(0.3) # let hot-reload detect mtime change
out_path = os.path.join(voice_dir, f"seed_{seed:05d}.wav")
print(f" seed {seed:5d}", end="", flush=True)
try:
t0 = time.time()
wav = generate(voice_name, text, host, port)
elapsed = time.time() - t0
with open(out_path, "wb") as f:
f.write(wav)
print(f"OK ({elapsed:.1f}s)")
results.append((seed, out_path, None))
except Exception as e:
print(f"FAILED: {e}")
results.append((seed, None, str(e)))
# Remove the temporary seed so the voice returns to its auto-seed
voices[voice_name].pop("seed", None)
save_voices(voices)
return results
def main():
p = argparse.ArgumentParser(description="Compare seeds for one or all voices")
target = p.add_mutually_exclusive_group(required=True)
target.add_argument("--voice", help="Single voice name")
target.add_argument("--all-voices", action="store_true", help="Run for every voice 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). Default: 115")
p.add_argument("--text", default=None,
help="Override text (default: auto-picks DE/EN/mixed based on voice name)")
p.add_argument("--host", default="localhost")
p.add_argument("--port", type=int, default=8020)
p.add_argument("--out-dir", default="./seed_samples",
help="Root output directory (voice subdirs created inside)")
args = p.parse_args()
seeds = list(args.seeds or [])
if args.range:
seeds += list(range(args.range[0], args.range[1] + 1))
if not seeds:
seeds = list(range(1, 16)) # default 115
seeds = sorted(set(seeds))
voices = load_voices()
if args.voice:
voice_names = [args.voice]
if args.voice not in voices:
print(f"ERROR: voice {args.voice!r} not in voices.json", file=sys.stderr)
sys.exit(1)
else:
voice_names = list(voices.keys())
os.makedirs(args.out_dir, exist_ok=True)
# Single backup at the start; we restore on every error/exit
backup = VOICES_JSON + ".seed_backup"
shutil.copy2(VOICES_JSON, backup)
print(f"voices : {len(voice_names)}")
print(f"seeds : {seeds}")
print(f"output : {os.path.abspath(args.out_dir)}/")
print(f"total : ~{len(voice_names) * len(seeds)} requests\n")
summary: dict[str, list] = {}
try:
for i, voice_name in enumerate(voice_names, 1):
text = pick_text(voice_name, args.text)
print(f"[{i}/{len(voice_names)}] {voice_name}")
print(f" text: {text[:90]}{'...' if len(text) > 90 else ''}")
results = run_voice(voice_name, voices, seeds, text, args.host, args.port, args.out_dir)
summary[voice_name] = results
ok = sum(1 for _, p, _ in results if p)
print(f"{ok}/{len(seeds)} OK\n")
except KeyboardInterrupt:
print("\n\nInterrupted — restoring voices.json...")
finally:
shutil.copy2(backup, VOICES_JSON)
os.remove(backup)
print("voices.json restored.")
# Write summary
summary_path = os.path.join(args.out_dir, "summary.txt")
with open(summary_path, "w") as f:
f.write(f"Seed samples — {len(voice_names)} voices, seeds {seeds}\n")
f.write("=" * 60 + "\n\n")
for voice_name, results in summary.items():
ok = [(s, path) for s, path, err in results if path]
failed = [(s, err) for s, path, err in results if not path]
f.write(f"{voice_name}:\n")
for s, path in ok:
f.write(f" seed {s:5d} {path}\n")
for s, err in failed:
f.write(f" seed {s:5d} FAILED: {err}\n")
f.write("\n")
total_ok = sum(1 for r in summary.values() for _, p, _ in r if p)
total = sum(len(r) for r in summary.values())
print(f"\nDone: {total_ok}/{total} samples generated.")
print(f"Summary written to {summary_path}")
print(f"\nListen to the WAV files, then add \"seed\": <number> to your chosen voices in voices.json")
if __name__ == "__main__":
main()