126 lines
4.5 KiB
Python
126 lines
4.5 KiB
Python
"""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()
|