first commit
This commit is contained in:
@@ -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),
|
||||
)
|
||||
Reference in New Issue
Block a user