"""Тест TTS-модуля (Модуль 3). Запуск: python modules/tts/test_tts.py # синтез фразы по умолчанию, сохранить wav python modules/tts/test_tts.py --play # то же + воспроизвести python modules/tts/test_tts.py --text "Своё предложение" python modules/tts/test_tts.py --voice ru-RU-DmitryNeural --play python modules/tts/test_tts.py --rate -10 # другая скорость python modules/tts/test_tts.py --provider piper --model models/piper/ru_RU-dmitri-medium.onnx python modules/tts/test_tts.py --listen # интерактивно: вводишь текст — слышишь Критерии приёмки Модуля 3: 1. Русская фраза звучит естественно, скорость на ~15% медленнее обычной. 2. Несколько предложений — с заметными паузами между ними. 3. Синтез короткой фразы (до 100 символов) — <= 3 c. """ from __future__ import annotations import argparse import sys import time from pathlib import Path try: from modules.audio_io import Player from modules.audio_io.codec import decode_audio_bytes # noqa: F401 (использование ниже) from modules.tts import get_provider except ImportError: sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from modules.audio_io import Player from modules.audio_io.codec import decode_audio_bytes # noqa: F401 from modules.tts import get_provider SAMPLES_DIR = Path(__file__).resolve().parent / "samples" DEFAULT_TEXT = ( "Здравствуйте! Это проверка голоса ассистента. " "Сейчас проверим, насколько понятна речь. Погода сегодня хорошая." ) def parse_args() -> argparse.Namespace: p = argparse.ArgumentParser(description="Тест TTS-модуля") p.add_argument("--text", default=DEFAULT_TEXT, help="текст для синтеза") p.add_argument("--provider", default="edge", choices=["edge", "piper"]) p.add_argument("--voice", default="ru-RU-SvetlanaNeural", help="голос edge-tts (ru-RU-DmitryNeural — мужской)") p.add_argument("--rate", default=-15, help="скорость речи, %% (−15 по ТЗ)") p.add_argument("--model", default="models/piper/ru_RU-dmitri-medium.onnx", help="путь к .onnx модели Piper") p.add_argument("--pause", type=float, default=0.35, help="пауза между предложениями, с") p.add_argument("--per-sentence", action="store_true", help="режим «предложение = запрос» (сравнение латентности)") p.add_argument("--play", action="store_true", help="воспроизвести результат") p.add_argument("--listen", action="store_true", help="интерактивно: вводишь текст — слышишь звук") p.add_argument("--save", default=str(SAMPLES_DIR / "tts_out.wav")) return p.parse_args() def speak_once(tts, text: str, play: bool, save: Path) -> None: result = tts.synthesize(text) print(result) save = Path(save) save.parent.mkdir(parents=True, exist_ok=True) import soundfile as sf sf.write(str(save), result.audio, result.samplerate) print(f"Сохранено: {save} ({len(result.audio) / result.samplerate:.1f} c)") if play: player = Player() print("Воспроизвожу...") player.play(result.audio, result.samplerate, blocking=True) def listen_loop(tts) -> None: player = Player() print("Режим «слушать»: вводи текст, Enter — озвучить, пустая строка — выход.") while True: text = input("текст> ").strip() if not text: return result = tts.synthesize(text) print(f" синтез {result.generation_sec:.2f} c, аудио {result.duration_sec:.1f} c") player.play(result.audio, result.samplerate, blocking=True) def main() -> int: args = parse_args() kwargs = {"sentence_pause_sec": args.pause} if args.provider == "edge": kwargs.update(voice=args.voice, rate=int(args.rate), per_sentence=args.per_sentence) else: kwargs.update(model_path=args.model) tts = get_provider(args.provider, **kwargs) if args.listen: listen_loop(tts) else: speak_once(tts, args.text, args.play, args.save) return 0 if __name__ == "__main__": sys.exit(main())