"""Pre-synthesise demo lines so the Gemini voice speaks instantly. Cloud TTS costs a network round trip - roughly 4 s for a sentence, 8 s for a paragraph. That is fine while rehearsing and painful in front of an audience. This warms the on-disk cache ahead of time, so a warmed line plays with **no delay at all**. python scripts/warm_voice.py "Good afternoon, welcome to our showroom." python scripts/warm_voice.py --file demo_lines.txt python scripts/warm_voice.py --stats python scripts/warm_voice.py --clear Audio is saved in audio_library/ as ordinary .wav files with readable names, so it survives restarts, plays in any media player, and needs no internet once saved. The server reads the same folder - warm before you present. Only relevant when MOCK_VOICE_ENGINE=gemini. """ from __future__ import annotations import argparse import sys import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) def _use_utf8_console() -> None: for stream in (sys.stdout, sys.stderr): try: if stream is not None and hasattr(stream, "reconfigure"): stream.reconfigure(encoding="utf-8", errors="replace") except Exception: pass _use_utf8_console() from backend.config.settings import get_settings # noqa: E402 from backend.robot.gemini_voice import GeminiVoice # noqa: E402 def build(settings) -> GeminiVoice: return GeminiVoice( api_key=settings.mock.gemini_api_key or "", model=settings.mock.gemini_model, voice=settings.mock.gemini_voice, style=settings.mock.gemini_style, chunk_chars=settings.mock.gemini_chunk_chars, fallback=None, ) def main() -> None: parser = argparse.ArgumentParser(description="Pre-synthesise lines for the Gemini voice.") parser.add_argument("lines", nargs="*", help="Lines to warm.") parser.add_argument("--file", help="Text file, one line per utterance.") parser.add_argument("--stats", action="store_true", help="Show library size and exit.") parser.add_argument("--clear", action="store_true", help="Delete all saved audio and exit.") args = parser.parse_args() settings = get_settings() if not settings.mock.gemini_api_key: print("\n GEMINI_API_KEY is not set in .env - nothing to warm.\n") raise SystemExit(1) voice = build(settings) if args.clear: voice.clear_cache() print("\n Cache cleared.\n") return if args.stats or not (args.lines or args.file): stats = voice.cache_stats() print("\n Saved audio") print(" " + "-" * 52) print(" Entries : {0}".format(stats["entries"])) print(" Size : {0:.1f} MB".format(stats["bytes"] / 1_048_576)) print(" Location: {0}".format(stats["dir"])) print(" Voice : {0} ({1})".format(settings.mock.gemini_voice, settings.mock.gemini_model)) if not (args.lines or args.file): print("") print(' Warm a line: python scripts/warm_voice.py "Welcome to our showroom."') print(" Warm a file: python scripts/warm_voice.py --file demo_lines.txt") print("") return lines = list(args.lines) if args.file: path = Path(args.file) if not path.exists(): print("\n No such file: {0}\n".format(path)) raise SystemExit(1) lines += [ln.strip() for ln in path.read_text(encoding="utf-8").splitlines() if ln.strip()] print("\n Warming {0} line(s) with voice '{1}'...".format(len(lines), settings.mock.gemini_voice)) print(" " + "-" * 60) total_new = 0 started = time.time() for index, line in enumerate(lines, 1): t0 = time.time() try: fetched = voice.warm(line) except Exception as exc: print(" {0:>3}. FAILED {1}".format(index, exc)) continue total_new += fetched state = "cached already" if fetched == 0 else "fetched {0} chunk(s)".format(fetched) preview = line if len(line) <= 48 else line[:45] + "..." print(" {0:>3}. {1:<50} {2:>16} {3:5.1f}s".format( index, preview, state, time.time() - t0)) stats = voice.cache_stats() print(" " + "-" * 60) print(" {0} new clip(s) in {1:.1f}s. Library now holds {2} clips ({3:.1f} MB).".format( total_new, time.time() - started, stats["entries"], stats["bytes"] / 1_048_576)) print(" These lines will now speak instantly.\n") voice.close() if __name__ == "__main__": main()