A3_text_to_speach/scripts/warm_voice.py
2026-09-03 00:10:18 +04:00

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()