"""Провайдер TTS на Piper — полностью офлайн-фолбэк. Используется, если edge-tts недоступен (сервис Microsoft ломался в прошлом, а интернет у отца может пропасть). Piper работает локально на CPU. Установка и голос (однократно): pip install piper-tts python -m piper.download_voices ru_RU-dmitri-medium --download-dir models/piper Пайпер медленнее edge-tts и голоса проще, но работает без сети. API: PiperVoice.load(path); voice.synthesize(text) -> чанки AudioChunk (audio_int16_bytes, sample_rate, sample_width, sample_channels). """ from __future__ import annotations import time from pathlib import Path from typing import List, Optional import numpy as np from .base import SynthResult, TtsProvider from .text_split import split_sentences class PiperTtsProvider(TtsProvider): name = "piper" def __init__( self, model_path: Union[str, Path], length_scale: float = 1.15, # 1.0 = обычная скорость; 1.15 ≈ −15% noise_scale: float = 0.667, noise_w_scale: float = 0.8, sentence_pause_sec: float = 0.35, ) -> None: from piper import PiperVoice, SynthesisConfig # ленивый импорт path = Path(model_path) if not path.is_file(): raise FileNotFoundError( f"Модель Piper не найдена: {path}\n" f"Скачай голос: python -m piper.download_voices ru_RU-dmitri-medium " f"--download-dir {path.parent}" ) self._voice = PiperVoice.load(str(path)) self._config = SynthesisConfig( length_scale=length_scale, noise_scale=noise_scale, noise_w_scale=noise_w_scale, ) self._pause = float(sentence_pause_sec) def synthesize(self, text: str) -> SynthResult: t0 = time.perf_counter() sentences = split_sentences(text) or [text] pieces: List[np.ndarray] = [] samplerate: Optional[int] = None for i, sentence in enumerate(sentences): chunks = list(self._voice.synthesize(sentence, syn_config=self._config)) for ch in chunks: if samplerate is None: samplerate = ch.sample_rate pcm = np.frombuffer(ch.audio_int16_bytes, dtype=np.int16) pieces.append(pcm.astype(np.float32) / 32768.0) if i < len(sentences) - 1 and self._pause > 0 and samplerate: pieces.append(np.zeros(int(self._pause * samplerate), dtype=np.float32)) audio = np.concatenate(pieces) if pieces else np.zeros(0, dtype=np.float32) return SynthResult( text=text, audio=audio, samplerate=samplerate or 22050, generation_sec=time.perf_counter() - t0, provider=self.name, sentence_count=len(sentences), )