first commit

This commit is contained in:
workD12
2026-09-11 19:47:15 +03:00
commit 32754a1fe2
56 changed files with 3836 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
"""Провайдер 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),
)