131 lines
4.9 KiB
Python
131 lines
4.9 KiB
Python
"""List and audition the PC voices available to the simulator.
|
|
|
|
python scripts/voices.py # list what is installed
|
|
python scripts/voices.py --demo # speak a sample line in each voice
|
|
python scripts/voices.py --try Zira # hear one voice, with .env's rate/volume
|
|
|
|
Whichever you prefer goes in .env:
|
|
|
|
MOCK_LOCAL_AUDIO=true
|
|
MOCK_VOICE=Zira
|
|
|
|
IMPORTANT - this is the *simulator's* voice, not the robot's. The real AGIBOT A3
|
|
synthesises speech on-board; AgiBot does not publish which engine or timbre it
|
|
uses, so no PC voice can be claimed to match it. This is a stand-in for
|
|
rehearsing a demo. See docs/AGIBOT_A3_INTEGRATION.md.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import platform
|
|
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.local_audio import create_local_voice, list_local_voices # noqa: E402
|
|
|
|
SAMPLE = "Good afternoon, and welcome to our showroom. I am the AGIBOT A3 humanoid robot."
|
|
|
|
|
|
def speak_with(hint, text, rate, volume, pitch=0) -> None:
|
|
voice = create_local_voice(voice_hint=hint, rate=rate, volume=volume, pitch=pitch)
|
|
if voice is None:
|
|
print(" No speech engine available on this PC.")
|
|
return
|
|
try:
|
|
voice.start(text)
|
|
while voice.is_speaking():
|
|
time.sleep(0.05)
|
|
finally:
|
|
voice.close()
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="List and audition simulator voices.")
|
|
parser.add_argument("--demo", action="store_true", help="Speak a sample in every voice.")
|
|
parser.add_argument("--try", dest="which", metavar="NAME", help="Speak a sample in one voice.")
|
|
parser.add_argument("--text", default=SAMPLE, help="Say this instead of the default sample.")
|
|
args = parser.parse_args()
|
|
|
|
settings = get_settings()
|
|
rate, volume = settings.mock.speech_rate, settings.mock.speech_volume
|
|
pitch = settings.mock.speech_pitch
|
|
|
|
if args.which:
|
|
print('\n Voice "{0}" (rate {1}, volume {2}, pitch {3})'.format(
|
|
args.which, rate, volume, pitch))
|
|
print(" " + args.text + "\n")
|
|
speak_with(args.which, args.text, rate, volume, pitch)
|
|
return
|
|
|
|
if settings.mock.voice_engine == "gemini":
|
|
from backend.robot.gemini_voice import VOICES as GEMINI_VOICES
|
|
|
|
current = (settings.mock.gemini_voice or "").lower()
|
|
print("\n Gemini neural voices (active: MOCK_VOICE_ENGINE=gemini)")
|
|
print(" " + "-" * 58)
|
|
for name, tone in sorted(GEMINI_VOICES.items()):
|
|
mark = "*" if name.lower() == current else " "
|
|
note = " <- young + male, closest to the robot's Yunxiao" \
|
|
if name in ("Puck", "Fenrir") else ""
|
|
print(" {0} {1:<16} {2}{3}".format(mark, name, tone, note))
|
|
print("")
|
|
print(" * = current GEMINI_VOICE. Change it in .env, then warm your lines:")
|
|
print(' python scripts/warm_voice.py "Welcome to our showroom."')
|
|
|
|
voices = list_local_voices()
|
|
print("\n System voices on this PC (used when MOCK_VOICE_ENGINE=system,")
|
|
print(" and as the fallback if Gemini fails)")
|
|
print(" " + "-" * 58)
|
|
if not voices:
|
|
if platform.system().lower().startswith("win"):
|
|
print(" None found.")
|
|
else:
|
|
print(" Listing is Windows-only; on macOS the simulator uses `say`,")
|
|
print(" on Linux `espeak-ng`.")
|
|
return
|
|
|
|
for description, source in voices:
|
|
marker = "*" if settings.mock.voice and settings.mock.voice.lower() in description.lower() else " "
|
|
print(" {0} {1:<48} [{2}]".format(marker, description, source))
|
|
|
|
print("")
|
|
print(" * = currently selected by MOCK_VOICE={0}".format(settings.mock.voice or "(unset)"))
|
|
print(" 'onecore' voices are Windows' newer, better-sounding set.")
|
|
print("")
|
|
print(" Hear one: python scripts/voices.py --try Zira")
|
|
print(" Hear all: python scripts/voices.py --demo")
|
|
print("")
|
|
print(" Want more voices (including Mandarin, to match the A3's demo language)?")
|
|
print(" Windows Settings > Time & language > Speech > Manage voices > Add voices.")
|
|
print("")
|
|
|
|
if args.demo:
|
|
for description, _ in voices:
|
|
short = description.replace("Microsoft ", "").split(" - ")[0]
|
|
print(" ->", description)
|
|
speak_with(description, "{0} speaking. {1}".format(short, args.text),
|
|
rate, volume, pitch)
|
|
time.sleep(0.4)
|
|
print("")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|