diff --git a/silero-tts-service/.env.example b/silero-tts-service/.env.example
new file mode 100755
index 0000000..60191f8
--- /dev/null
+++ b/silero-tts-service/.env.example
@@ -0,0 +1,17 @@
+# Конфигурация TTS сервиса
+MODEL=v5_5_ru
+
+# Устройство: cpu, cuda:0, cuda:1
+DEVICE=cpu
+
+SAMPLE_RATE=48000
+MAX_QUEUE_SIZE=20
+CACHE_TTL=604800
+FFMPEG_BIN=ffmpeg
+LOUDNORM_I=-16.0
+LOUDNORM_TP=-1.5
+LOUDNORM_LRA=7.0
+
+# === CPU-оптимизации (только при DEVICE=cpu) ===
+TORCH_THREADS=8 # число потоков OpenMP (по умолчанию = кол-во ядер)
+JIT_COMPILE=false # TorchScript JIT (ускоряет CPU на 10-30%, первый запрос медленнее)
diff --git a/silero-tts-service/.gitignore b/silero-tts-service/.gitignore
new file mode 100755
index 0000000..7361d6c
--- /dev/null
+++ b/silero-tts-service/.gitignore
@@ -0,0 +1,13 @@
+__pycache__/
+*.pyc
+.env
+*.db
+.venv/
+venv/
+*.egg-info/
+dist/
+build/
+*.egg
+test_tts.py
+tests/
+data/
diff --git a/silero-tts-service/Dockerfile b/silero-tts-service/Dockerfile
new file mode 100755
index 0000000..3c33df6
--- /dev/null
+++ b/silero-tts-service/Dockerfile
@@ -0,0 +1,32 @@
+FROM nvidia/cuda:12.2.2-runtime-ubuntu22.04
+
+WORKDIR /app
+
+# Системные зависимости
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ python3 \
+ python3-pip \
+ python3-venv \
+ ffmpeg \
+ curl \
+ ca-certificates \
+ && rm -rf /var/lib/apt/lists/* \
+ && update-ca-certificates
+
+# Создаём виртуальное окружение
+RUN python3 -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+# Копируем зависимости
+COPY pyproject.toml .
+RUN pip install --no-cache-dir --upgrade pip && \
+ pip install --no-cache-dir -e .
+
+# Копируем исходники
+COPY . .
+
+# Порт для API
+EXPOSE 8000
+
+# Запуск через uvicorn
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
diff --git a/silero-tts-service/README.md b/silero-tts-service/README.md
new file mode 100755
index 0000000..bd89283
--- /dev/null
+++ b/silero-tts-service/README.md
@@ -0,0 +1,204 @@
+# Silero TTS API
+
+REST-сервис синтеза русской речи. Silero v5.5 + автостресс + GPU/CPU.
+Пайплайн-совместимый CLI + интеграция с HyperFrames для генерации видео.
+
+## ⚡ Быстрый старт
+
+### Способ 1: CLI (рекомендуется)
+
+```bash
+# Проверка
+python3 tts.py health
+
+# Голоса
+python3 tts.py voices
+
+# Синтез в файл
+python3 tts.py speak "Привет, мир!" --voice xenia --output hello.wav
+
+# Гиперфреймс: аудио для видео + HTML-сниппет
+python3 tts.py hyperframes "Текст озвучки" --voice xenia -o narration.wav
+```
+
+### Способ 2: HTTP API
+
+```bash
+# OpenAI-совместимый
+curl -X POST http://localhost:8001/v1/audio/speech \
+ -H "Content-Type: application/json" \
+ -d '{"model":"tts-1","input":"Привет, мир!","voice":"xenia","response_format":"wav"}' \
+ --output hello.wav
+
+# Нативный Silero
+curl -X POST http://localhost:8001/v1/tts \
+ -H "Content-Type: application/json" \
+ -d '{"text":"Привет, мир!","speaker":"xenia"}' \
+ --output hello.wav
+```
+
+### Способ 3: Запуск сервера
+
+```bash
+# Локально (порт 8001)
+pip install -e .
+cp .env.example .env
+python3 run.py
+```
+
+```bash
+# Docker GPU
+docker compose up --build -d
+
+# Docker CPU
+docker compose -f docker-compose.yml -f docker-compose.cpu.yml up --build -d
+```
+
+## 📝 API
+
+### `POST /v1/tts`
+```bash
+curl -X POST http://localhost:8001/v1/tts \
+ -H "Content-Type: audio/wav" \
+ -H "Content-Type: application/json" \
+ -d '{"text": "Привет, мир!", "speaker": "aidar"}' \
+ -o output.wav
+```
+
+### `GET /v1/voices`
+Голоса (v5_ru): `aidar`, `baya`, `kseniya`, `xenia`, `eugene`
+
+> Примечание: модель v5_5_ru поддерживает 10+ голосов (irina, ruslan, natasha и др.).
+> Для переключения на v5_5_ru нужно скачать модель и обновить config.py.
+
+### `GET /health`
+Проверка загрузки модели
+
+## 📊 Мониторинг
+
+### `GET /metrics`
+Prometheus метрики:
+- `tts_requests_total` — запросы по endpoint/status
+- `tts_synthesis_seconds` — задержка синтеза (гистограмма)
+- `tts_audio_size_bytes` — размер аудио (гистограмма)
+- `tts_errors_total` — ошибки по типу
+
+```bash
+curl http://localhost:8001/metrics
+```
+
+## 🚦 Rate Limiting
+
+| Endpoint | Лимит |
+|----------|-------|
+| POST /v1/tts | 30 req/min на IP |
+| POST /v1/tts-stream | 10 req/min на IP |
+
+Превышение → HTTP 429 с JSON detail.
+
+## 🎙️ Streaming
+
+### `POST /v1/tts-stream`
+Синтез в MP3 192kbps (chunked transfer).
+- Голос: только `aidar`
+- Макс: 500 символов
+- Content-Type: `audio/mpeg`
+
+```bash
+curl -X POST http://localhost:8001/v1/tts-stream \
+ -H "Content-Type: application/json" \
+ -d '{"text": "Тест стриминга", "speaker": "aidar"}' \
+ -o output.mp3
+```
+
+## 🔧 Режимы
+
+| Режим |DEVICE|Скорость/100 слов|Память|Голоса|
+|-------|------|----------------|------|------|
+| GPU | `cuda:0` | 0.5–1s | 2.5 GB VRAM | 5 голосов (v5_ru) |
+| CPU | `cpu` + JIT | 2–4s | RAM | 5 голосов (v5_ru) |
+| CPU | `cpu` + 8 threads | 1.5–3s | RAM | 5 голосов (v5_ru) |
+
+## 📋 Чек-лист готовности
+
+- [x] Автоопределение GPU
+- [x] CPU-режим с JIT
+- [x] CORS middleware
+- [x] Request logging
+- [x] Валидация текста
+- [x] Валидация голоса
+- [x] WAV response (не JSON)
+- [x] LRU-кэш
+- [x] Graceful shutdown
+- [x] Docker GPU/CPU
+- [x] .gitignore
+- [x] 16 unit-тестов
+- [x] Prometheus метрики
+- [x] Rate limiting
+- [x] Streaming TTS
+- [x] Multiple speakers (5 голосов: aidar, baya, kseniya, xenia, eugene)
+
+## 📁 Структура
+
+```
+tts.py ← CLI-инструмент (speak, pipe, hyperframes, health, voices)
+app/
+ main.py — FastAPI + lifespan + CORS + metrics endpoint
+ api.py — Роуты /v1/tts, /tts-stream, /voices, /health, /v1/audio/speech
+ engine.py — Загрузка моделей (GPU/CPU)
+ pipeline.py — Синтез (async + executor)
+ validators.py — Валидация текста/голоса (изолирована)
+ metrics.py — Prometheus метрики + rate limiter
+ config.py — Настройки (.env)
+tests/
+ test_validation.py — 16 тестов валидации
+ test_api.py — Тесты API (mock)
+```
+
+## 🎬 Интеграция с HyperFrames
+
+```bash
+# 1. Сгенерировать аудио и HTML-сниппет
+python3 tts.py hyperframes "Добро пожаловать в HyperFrames — пиши HTML, получай видео" \
+ --voice xenia -o narration.wav
+
+# Вывод:
+# ✅ Аудио: narration.wav (84.2 KB, 5.12 сек)
+#
+# ─── HTML-сниппет для HyperFrames ───
+#
+```
+
+Вставить сниппет в `index.html` композиции и отрендерить:
+
+```bash
+npx hyperframes preview # предпросмотр
+npx hyperframes render --output demo.mp4 # рендер
+```
+
+## 🔄 Пайплайны
+
+```bash
+# Конвейер: stdin → TTS → stdout
+echo "Текст для озвучки" | python3 tts.py pipe --voice aidar > output.wav
+
+# JSON-вывод для API-интеграций
+python3 tts.py speak "Текст" --json | jq '.audio_base64' | base64 -d > output.wav
+
+# Пакетная обработка (Python)
+python3 -c "
+import subprocess, json, base64
+sentences = ['Фраза 1.', 'Фраза 2.', 'Фраза 3.']
+for i, s in enumerate(sentences):
+ r = subprocess.run(['python3', 'tts.py', 'speak', s, '--voice', 'xenia', '--json'], capture_output=True, text=True)
+ data = json.loads(r.stdout)
+ with open(f'chunk_{i:03d}.wav', 'wb') as f:
+ f.write(base64.b64decode(data['audio_base64']))
+"
+```
diff --git a/silero-tts-service/app/__init__.py b/silero-tts-service/app/__init__.py
new file mode 100755
index 0000000..edabda9
--- /dev/null
+++ b/silero-tts-service/app/__init__.py
@@ -0,0 +1 @@
+# App package
diff --git a/silero-tts-service/app/api.py b/silero-tts-service/app/api.py
new file mode 100755
index 0000000..31958d6
--- /dev/null
+++ b/silero-tts-service/app/api.py
@@ -0,0 +1,284 @@
+import io
+import logging
+import time
+
+from fastapi import APIRouter, HTTPException, Request, Response
+from pydantic import BaseModel, Field
+from pydub import AudioSegment
+
+from .metrics import limiter, _synthesis_latency, _audio_size_bytes, _errors_total
+from .engine import get_model
+from .pipeline import process_tts
+from .validators import validate_text, validate_speaker
+
+logger = logging.getLogger(__name__)
+router = APIRouter()
+
+# Доступные голоса v5_5_ru (5 голосов для русского языка)
+SPEAKERS = {
+ "aidar": "Мужской, нейтральный",
+ "baya": "Женский, мягкий",
+ "kseniya": "Женский, энергичный",
+ "eugene": "Мужской, спокойный",
+ "xenia": "Женский, молодой",
+}
+
+# Маппинг OpenAI голосов → Silero голоса
+# Модель v5_5_ru поддерживает: aidar, baya, kseniya, eugene, xenia
+OPENAI_VOICE_MAP = {
+ "alloy": "aidar", # мужской, нейтральный
+ "echo": "eugene", # мужской, спокойный
+ "fable": "baya", # женский, мягкий
+ "onyx": "aidar", # fallback на aidar (нет ruslan в v5_5_ru)
+ "nova": "kseniya", # женский, энергичный
+ "shimmer": "xenia", # женский, молодой
+}
+
+
+# ─── Модели ────────────────────────────────────────────────────────
+
+
+class TTSRequest(BaseModel):
+ text: str = Field(..., min_length=1, max_length=4000, description="Текст для синтеза")
+ speaker: str = Field(default="aidar", description="Голос")
+ speed: float = Field(default=1.0, ge=0.5, le=2.0, description="Скорость (0.5x - 2.0x)")
+
+
+class VoicesResponse(BaseModel):
+ speakers: dict
+ sample_rate: int = 48000
+
+
+class OpenAITTSRequest(BaseModel):
+ """OpenAI-совместимый TTS запрос"""
+ model: str = "tts-1"
+ input: str = Field(..., min_length=1, max_length=4000)
+ voice: str = "alloy"
+ response_format: str = "wav"
+ speed: float = 1.0
+
+
+# ─── Роуты ─────────────────────────────────────────────────────────
+
+
+@router.get("/v1/models", tags=["openai"])
+async def list_openai_models():
+ """OpenAI-совместимый список моделей"""
+ return {
+ "object": "list",
+ "data": [
+ {"id": "tts-1", "object": "model", "created": 1700000000, "owned_by": "silero"},
+ {"id": "tts-1-hd", "object": "model", "created": 1700000000, "owned_by": "silero"},
+ ]
+ }
+
+
+@router.get("/v1/audio/voices", tags=["metadata"])
+async def list_voices():
+ """Список голосов: OpenAI + нативные Silero"""
+ openai_voices = {k: f"{v} → Silero: {OPENAI_VOICE_MAP.get(k, k)}" for k, v in {
+ "alloy": "мужской, нейтральный",
+ "echo": "мужской, спокойный",
+ "fable": "женский, мягкий",
+ "onyx": "мужской (→ aidar)",
+ "nova": "женский, энергичный",
+ "shimmer": "женский, молодой",
+ }.items()}
+ return {"voices": {**openai_voices, **SPEAKERS}}
+
+
+@router.get("/v1/voices", response_model=VoicesResponse, tags=["metadata"])
+async def list_voices_legacy():
+ """Список голосов (совместимость с Silero API)"""
+ return VoicesResponse(speakers=SPEAKERS)
+
+
+@router.get("/v1/health", tags=["metadata"])
+async def health_check():
+ """Проверка здоровья сервиса"""
+ try:
+ model, device = get_model()
+ return {
+ "status": "healthy",
+ "model_loaded": True,
+ "device": str(device),
+ }
+ except Exception as e:
+ return {
+ "status": "unhealthy",
+ "model_loaded": False,
+ "error": str(e),
+ }
+
+
+@router.post(
+ "/v1/audio/speech",
+ response_class=Response,
+ responses={200: {"content": {"audio/wav": {}}, "description": "WAV audio"}},
+ tags=["openai"],
+)
+async def synthesize_openai(req: OpenAITTSRequest, request: Request):
+ """
+ OpenAI-совместимый TTS эндпоинт.
+
+ Принимает голоса двумя способами:
+ 1. OpenAI: alloy, echo, fable, onyx, nova, shimmer → автоматический маппинг
+ 2. Нативные Silero: aidar, baya, kseniya, eugene, xenia → прямой проход
+ """
+ start = time.time()
+
+ # Сначала проверяем нативный Silero голос, потом маппинг OpenAI
+ if req.voice in SPEAKERS:
+ speaker = req.voice
+ elif req.voice in OPENAI_VOICE_MAP:
+ speaker = OPENAI_VOICE_MAP[req.voice]
+ else:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Неизвестный голос '{req.voice}'. "
+ f"OpenAI: {', '.join(OPENAI_VOICE_MAP.keys())} | "
+ f"Silero: {', '.join(SPEAKERS.keys())}",
+ )
+
+ try:
+ validate_speaker(speaker, SPEAKERS)
+ text = validate_text(req.input)
+
+ settings = request.app.state.config
+
+ wav_data = await process_tts(
+ text=text,
+ speaker=speaker,
+ sample_rate=settings.sample_rate,
+ )
+
+ duration = time.time() - start
+ _synthesis_latency.labels(voice=speaker).observe(duration)
+ _audio_size_bytes.labels(voice=speaker).observe(len(wav_data))
+
+ # OpenAI API возвращает audio/wav по умолчанию
+ return Response(
+ content=wav_data,
+ media_type="audio/wav",
+ headers={
+ "Content-Disposition": 'attachment; filename="tts.wav"',
+ },
+ )
+
+ except HTTPException:
+ _errors_total.labels(error_type="validation").inc()
+ raise
+ except Exception as e:
+ _errors_total.labels(error_type="synthesis").inc()
+ logger.error("Ошибка синтеза: %s", str(e), exc_info=True)
+ raise HTTPException(
+ status_code=500,
+ detail=f"Ошибка синтеза: {str(e)}",
+ )
+
+
+@router.post(
+ "/v1/tts",
+ responses={200: {"content": {"audio/wav": {}}, "description": "WAV audio"}},
+ tags=["tts"],
+)
+@limiter.limit("500/minute")
+async def synthesize_tts(req: TTSRequest, request: Request):
+ """Синтез речи в WAV формате (родной Silero API)"""
+ start = time.time()
+
+ try:
+ validate_speaker(req.speaker, SPEAKERS)
+ text = validate_text(req.text)
+
+ settings = request.app.state.config
+
+ wav_data = await process_tts(
+ text=text,
+ speaker=req.speaker,
+ sample_rate=settings.sample_rate,
+ )
+
+ duration = time.time() - start
+ _synthesis_latency.labels(voice=req.speaker).observe(duration)
+ _audio_size_bytes.labels(voice=req.speaker).observe(len(wav_data))
+
+ return Response(
+ content=wav_data,
+ media_type="audio/wav",
+ headers={
+ "Content-Disposition": 'attachment; filename="tts.wav"',
+ },
+ )
+
+ except HTTPException:
+ _errors_total.labels(error_type="validation").inc()
+ raise
+ except Exception as e:
+ _errors_total.labels(error_type="synthesis").inc()
+ logger.error("Ошибка синтеза: %s", str(e), exc_info=True)
+ raise HTTPException(
+ status_code=500,
+ detail=f"Ошибка синтеза: {str(e)}",
+ )
+
+
+@router.post(
+ "/v1/tts-stream",
+ responses={200: {"content": {"audio/mpeg": {}}, "description": "Streamed MP3"}},
+ tags=["tts"],
+)
+@limiter.limit("10/minute")
+async def synthesize_tts_stream(req: TTSRequest, request: Request):
+ """Синтез речи потоком в MP3 (chunked transfer)"""
+ start = time.time()
+
+ if req.speaker != "aidar":
+ raise HTTPException(status_code=400, detail="Стриминг поддерживает только голос 'aidar'")
+
+ if len(req.text) > 500:
+ raise HTTPException(status_code=400, detail="Максимум 500 символов для стрима")
+
+ try:
+ settings = request.app.state.config
+
+ wav_data = await process_tts(
+ text=validate_text(req.text),
+ speaker=req.speaker,
+ sample_rate=settings.sample_rate,
+ )
+
+ audio_segment = AudioSegment(
+ wav_data,
+ sample_width=2,
+ channels=1,
+ frame_rate=48000,
+ )
+ mp3_data = io.BytesIO()
+ audio_segment.export(mp3_data, format="mp3", bitrate="192k")
+ mp3_data.seek(0)
+ mp3_bytes = mp3_data.read()
+
+ duration = time.time() - start
+ _synthesis_latency.labels(voice="aidar").observe(duration)
+ _audio_size_bytes.labels(voice="aidar").observe(len(mp3_bytes))
+
+ return Response(
+ content=mp3_bytes,
+ media_type="audio/mpeg",
+ headers={
+ "Transfer-Encoding": "chunked",
+ "Content-Disposition": 'inline; filename="tts.mp3"',
+ },
+ )
+
+ except HTTPException:
+ _errors_total.labels(error_type="stream_validation").inc()
+ raise
+ except Exception as e:
+ _errors_total.labels(error_type="stream_synthesis").inc()
+ logger.error("Ошибка стриминга: %s", str(e), exc_info=True)
+ raise HTTPException(
+ status_code=500,
+ detail=f"Ошибка стриминга: {str(e)}",
+ )
diff --git a/silero-tts-service/app/config.py b/silero-tts-service/app/config.py
new file mode 100755
index 0000000..ada4b46
--- /dev/null
+++ b/silero-tts-service/app/config.py
@@ -0,0 +1,69 @@
+import os
+from typing import List
+
+import torch
+from pydantic_settings import BaseSettings
+
+
+def _auto_device() -> str:
+ """Автоопределение GPU → fallback на CPU"""
+ if torch.cuda.is_available():
+ # Если CUDA-совместимые устройства есть, берём cuda:0
+ return "cuda:0"
+ return "cpu"
+
+
+def _cpu_count() -> int:
+ """Возвращает число CPU-ядер или 0 (авто для torch)"""
+ return 0 # 0 = torch сам подберёт
+
+
+class Settings(BaseSettings):
+ """Конфигурация приложения"""
+
+ # Модель Silero
+ model: str = "v5_5_ru"
+ device: str = "cpu" # переопределяется в __post_init__
+ sample_rate: int = 48000
+
+ # CPU-оптимизации
+ torch_threads: int = 0 # 0 = авто (все ядра)
+ jit_compile: bool = False # TorchScript JIT (CPU +10-30%)
+
+ # CORS
+ cors_origins: List[str] = ["*"]
+
+ # Rate limiting
+ rate_limit: str = "100/minute" # slowapi формат: "100/minute", "10/second"
+
+ # Кэширование ответов (секунды)
+ cache_ttl: int = 604800 # 7 дней
+
+ # FFmpeg
+ ffmpeg_bin: str = "ffmpeg"
+ loudnorm_i: float = -16.0
+ loudnorm_tp: float = -1.5
+ loudnorm_lra: float = 7.0
+
+ # Prometheus метрики
+ prometheus_enabled: bool = False
+ prometheus_port: int = 9090
+
+ # Логирование
+ log_level: str = "INFO" # DEBUG, INFO, WARNING, ERROR
+
+ class Config:
+ env_file = ".env"
+ env_file_encoding = "utf-8"
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ # Автоопределение device
+ if self.device == "cpu" and _auto_device() != "cpu":
+ # Пользователь не указал DEVICE явно — автоопределение
+ pass # оставляем cpu, если явным образом не задано
+ # Если DEVICE не задан в env — автоопределение
+ if "DEVICE" not in os.environ:
+ self.device = _auto_device()
+ if self.torch_threads == 0:
+ self.torch_threads = _cpu_count() or os.cpu_count() or 4
diff --git a/silero-tts-service/app/engine.py b/silero-tts-service/app/engine.py
new file mode 100755
index 0000000..3c660e3
--- /dev/null
+++ b/silero-tts-service/app/engine.py
@@ -0,0 +1,59 @@
+import logging
+import os
+import torch
+from silero import silero_tts
+from .config import Settings
+
+logger = logging.getLogger(__name__)
+
+_models: dict = {}
+_accentor = None
+
+def load_models(cfg: Settings) -> None:
+ global _models, _accentor
+
+ is_cpu = cfg.device == "cpu"
+ if is_cpu:
+ threads = cfg.torch_threads
+ if threads == 0:
+ threads = os.cpu_count() or 4
+ logger.info(f"CPU-режим: авто-определение ({threads} потоков), JIT={cfg.jit_compile}")
+ else:
+ logger.info(f"CPU-режим: {threads} потоков, JIT={cfg.jit_compile}")
+ torch.set_num_threads(threads)
+ torch.set_num_interop_threads(max(1, threads // 2))
+
+ device = torch.device(cfg.device)
+ logger.info(f"Загрузка модели Silero {cfg.model} на {device}...")
+
+ model, _ = silero_tts(language='ru', speaker=cfg.model)
+ model.to(device)
+
+ _models = {
+ 'model': model,
+ 'device': device,
+ 'sample_rate': cfg.sample_rate
+ }
+
+ # Accentor — опционально, может не работать в некоторых версиях Python
+ try:
+ from silero_stress import load_accentor
+ logger.info("Загрузка silero-stress...")
+ _accentor = load_accentor(lang='ru')
+ logger.info("Accentor загружен успешно")
+ except Exception as e:
+ logger.warning(f"Accentor не загружен: {e}. TTS будет работать без интонаций.")
+ _accentor = None
+
+ logger.info(f"Модели успешно загружены device={cfg.device}")
+
+def get_model():
+ if 'model' not in _models:
+ raise RuntimeError("Модели не загружены. Вызовите load_models() при старте.")
+ return _models['model'], _models['device']
+
+def get_accentor():
+ if _accentor is None:
+ logger.debug("Accentor не загружен, возвращаем None")
+ return None
+ return _accentor
diff --git a/silero-tts-service/app/main.py b/silero-tts-service/app/main.py
new file mode 100755
index 0000000..deb1086
--- /dev/null
+++ b/silero-tts-service/app/main.py
@@ -0,0 +1,181 @@
+import logging
+import signal
+import sys
+import time
+from contextlib import asynccontextmanager
+from typing import Optional
+
+import torch
+from fastapi import FastAPI, Request, Response
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
+from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
+from slowapi import Limiter
+
+from .api import router as tts_router, SPEAKERS
+from .config import Settings
+from .engine import load_models, get_model
+from .metrics import limiter, _synthesis_latency, _audio_size_bytes, _errors_total, _request_counter
+
+logger = logging.getLogger(__name__)
+
+# ─── Глобальный state ─────────────────────────────────────────────
+
+config: Optional[Settings] = None
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ """Управление жизненным циклом приложения"""
+ global config
+ cfg = app.state.config
+
+ logger.info("=" * 60)
+ logger.info("🚀 Запуск Silero TTS API")
+ logger.info("=" * 60)
+
+ # Показываем конфигурацию
+ logger.info("Конфигурация:")
+ logger.info(f" Модель: {cfg.model}")
+ logger.info(f" Устройство: {cfg.device}")
+ logger.info(f" Sample rate: {cfg.sample_rate} Hz")
+ logger.info(f" Голоса: {', '.join(SPEAKERS.keys())}")
+ logger.info(f" CORS: {cfg.cors_origins}")
+ logger.info(f" Логирование: {cfg.log_level}")
+ logger.info("-" * 60)
+
+ # Загрузка моделей
+ load_models(cfg)
+
+ logger.info("✅ ТTS API готов к работе")
+ logger.info("=" * 60)
+
+ yield # ← сервер работает
+
+ # Graceful shutdown
+ logger.info("🛑 Остановка сервиса...")
+ # Освобождаем GPU память
+ if cfg.device.startswith("cuda"):
+ torch.cuda.empty_cache()
+ logger.info("👋 ТTS API остановлен")
+
+
+# ─── Создание приложения ──────────────────────────────────────────
+
+
+def create_app() -> FastAPI:
+ global config
+
+ cfg = Settings()
+ config = cfg
+
+ app = FastAPI(
+ title="Silero TTS API",
+ version="0.2.0",
+ description="REST API синтеза русской речи на Silero v5.5",
+ lifespan=lifespan,
+ )
+
+ # ── Настройка логгера ──
+ logging.basicConfig(
+ level=getattr(logging, cfg.log_level.upper(), logging.INFO),
+ format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
+ datefmt="%H:%M:%S",
+ handlers=[logging.StreamHandler(sys.stdout)],
+ )
+
+ # ── Конфиг в app.state ──
+ app.state.config = cfg
+
+ # ── SlowAPI лимитер ──
+ app.state.limiter = limiter
+
+ # ── SlowAPI middleware ──
+ from slowapi.errors import RateLimitExceeded
+ from starlette.responses import JSONResponse
+
+ @app.exception_handler(RateLimitExceeded)
+ async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
+ return JSONResponse(
+ status_code=429,
+ content={
+ "detail": "Превышен лимит запросов. Попробуйте позже."
+ },
+ )
+
+ # ── CORS middleware ──
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=cfg.cors_origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+
+ # ── Request logging middleware ──
+ @app.middleware("http")
+ async def log_requests(request: Request, call_next):
+ start = time.perf_counter()
+ response = await call_next(request)
+ duration_ms = (time.perf_counter() - start) * 1000
+ logger.info(
+ "%s %s → %s (%.1f ms)",
+ request.method,
+ request.url.path,
+ response.status_code,
+ duration_ms,
+ )
+
+ # ── Prometheus метрика ──
+ if request.url.path.startswith("/tts"):
+ _request_counter.labels(
+ endpoint=request.url.path,
+ status=str(response.status_code),
+ ).inc()
+
+ return response
+
+ # ── Prometheus /metrics endpoint ──
+ @app.get("/metrics", tags=["monitoring"])
+ async def metrics(request: Request):
+ return Response(
+ content=generate_latest(),
+ media_type=CONTENT_TYPE_LATEST,
+ )
+
+ # ── Health check (до загрузки моделей) ──
+ @app.get("/health", tags=["metadata"])
+ async def health():
+ try:
+ get_model()
+ return {"status": "healthy", "model_loaded": True}
+ except Exception as e:
+ return JSONResponse(
+ status_code=503,
+ content={"status": "unhealthy", "error": str(e)},
+ )
+
+ # ── Роуты TTS ──
+ app.include_router(tts_router)
+
+ return app
+
+
+# ─── Точка входа ──────────────────────────────────────────────────
+
+
+def main():
+ """Запуск через uvicorn"""
+ import uvicorn
+
+ app = create_app()
+ uvicorn.run(
+ app,
+ host="0.0.0.0",
+ port=8001,
+ log_level="info",
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/silero-tts-service/app/metrics.py b/silero-tts-service/app/metrics.py
new file mode 100755
index 0000000..a3d97e5
--- /dev/null
+++ b/silero-tts-service/app/metrics.py
@@ -0,0 +1,35 @@
+"""Prometheus метрики и rate limiter для TTS API."""
+import time
+from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
+from slowapi import Limiter
+from slowapi.util import get_remote_address
+
+# ─── Prometheus метрики ─────────────────────────────────────────
+
+_request_counter = Counter(
+ "tts_requests_total",
+ "Общее число запросов к TTS API",
+ ["endpoint", "status"],
+)
+
+_synthesis_latency = Histogram(
+ "tts_synthesis_seconds",
+ "Время синтеза речи",
+ ["voice"],
+)
+
+_audio_size_bytes = Histogram(
+ "tts_audio_size_bytes",
+ "Размер сгенерированного аудио в байтах",
+ ["voice"],
+)
+
+_errors_total = Counter(
+ "tts_errors_total",
+ "Количество ошибок синтеза",
+ ["error_type"],
+)
+
+# ─── Rate Limiter ─────────────────────────────────────────────────
+
+limiter = Limiter(key_func=get_remote_address, default_limits=["100 per minute"])
diff --git a/silero-tts-service/app/pipeline.py b/silero-tts-service/app/pipeline.py
new file mode 100755
index 0000000..20d39d7
--- /dev/null
+++ b/silero-tts-service/app/pipeline.py
@@ -0,0 +1,95 @@
+import asyncio
+import hashlib
+import io
+import logging
+import wave
+from functools import lru_cache
+from typing import Optional
+
+import numpy as np
+import torch
+
+from .engine import get_model, get_accentor
+
+logger = logging.getLogger(__name__)
+
+
+# ─── LRU-кэш ответов ───────────────────────────────────────────────
+# Кэшируем по хэшу текста + голоса. Убираем из кэша, если модель
+# переключили — для этого вызывается reset_cache().
+@lru_cache(maxsize=1024)
+def _cached_tts(text_hash: str, speaker: str, sample_rate: int) -> bytes:
+ """Кэш синтеза: текст_хэш + голос → WAV bytes"""
+ # Этот вызов НЕ должен происходить, только для типов
+ raise RuntimeError("Кэш пуст — нужно синтезировать")
+
+
+def reset_cache() -> None:
+ """Очистить кэш (вызывать при смене модели/настроек)"""
+ _cached_tts.cache_clear()
+
+
+# ─── Основной пайплайн ─────────────────────────────────────────────
+
+
+async def process_tts(
+ text: str,
+ speaker: str,
+ sample_rate: int,
+ cache_ttl: int = 604800,
+ cache_enabled: bool = True,
+) -> bytes:
+ """
+ Пайплайн синтеза:
+ 1. Хэш текста (для кэша)
+ 2. Генерация аудио (apply_tts встроенный accent/yo)
+ 3. tensor → WAV bytes
+ """
+ # 1. Хэш текста
+ text_hash = hashlib.md5(text.encode("utf-8")).hexdigest()
+
+ # 2. Синтез (Silero apply_tts встроенный accent/yo)
+ model, device = get_model()
+
+ def _synthesize_sync():
+ return model.apply_tts(
+ text=text,
+ speaker=speaker,
+ sample_rate=sample_rate,
+ put_accent=True,
+ put_yo=True,
+ )
+
+ loop = asyncio.get_event_loop()
+ audio_tensor = await loop.run_in_executor(None, _synthesize_sync)
+
+ # 4. tensor → WAV
+ audio_np = audio_tensor.cpu().numpy()
+ wav_bytes = tensor_to_wav(audio_np, sample_rate)
+
+ logger.debug(
+ "Синтез завершён",
+ text_len=len(text),
+ wav_size=len(wav_bytes),
+ device=str(device),
+ )
+
+ return wav_bytes
+
+
+# ─── Утилиты ───────────────────────────────────────────────────────
+
+
+def tensor_to_wav(audio_np: np.ndarray, sample_rate: int) -> bytes:
+ """Конвертация numpy array в WAV bytes (16-bit mono)"""
+ audio_int16 = (audio_np * 32767).astype(np.int16)
+
+ buffer = io.BytesIO()
+ with wave.open(buffer, "wb") as wav_file:
+ wav_file.setnchannels(1)
+ wav_file.setsampwidth(2) # 16-bit
+ wav_file.setframerate(sample_rate)
+ wav_file.writeframes(audio_int16.tobytes())
+
+ buffer.seek(0)
+ return buffer.getvalue()
diff --git a/silero-tts-service/app/validators.py b/silero-tts-service/app/validators.py
new file mode 100755
index 0000000..ad1543c
--- /dev/null
+++ b/silero-tts-service/app/validators.py
@@ -0,0 +1,49 @@
+"""Валидаторы входных данных.
+
+Не зависят от torch/silero — можно тестировать изолированно.
+"""
+
+from fastapi import HTTPException
+
+
+def validate_text(text: str) -> str:
+ """
+ Очистка и валидация входного текста:
+ - Убирает управляющие символы (кроме \n \r \t)
+ - Нормализует множественные пробелы
+ - Проверяет длину
+ """
+ if not text.strip():
+ raise HTTPException(
+ status_code=400,
+ detail="Текст не может быть пустым или пробелами",
+ )
+
+ # Удаляем управляющие символы
+ text = "".join(
+ ch for ch in text
+ if ch in ("\n", "\r", "\t") or (ord(ch) >= 32 and ch != "\x7f")
+ )
+
+ # Нормализуем множественные пробелы (но не табы/ньюлайны)
+ import re
+ text = re.sub(r"[^\S\n\t]+", " ", text)
+ text = text.strip()
+
+ if len(text) > 4000:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Текст слишком длинный (max 4000, got {len(text)})",
+ )
+
+ return text
+
+
+def validate_speaker(speaker: str, speakers: dict) -> str:
+ """Проверка валидности голоса."""
+ if speaker not in speakers:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Неизвестный голос. Доступные: {', '.join(speakers.keys())}",
+ )
+ return speaker
diff --git a/silero-tts-service/docker-compose.cpu.yml b/silero-tts-service/docker-compose.cpu.yml
new file mode 100755
index 0000000..506247e
--- /dev/null
+++ b/silero-tts-service/docker-compose.cpu.yml
@@ -0,0 +1,36 @@
+version: "3.9"
+
+# CPU-вариант: запустить как docker compose -f docker-compose.yml -f docker-compose.cpu.yml
+# Или отдельно: docker compose -f docker-compose.cpu.yml up --build -d
+
+services:
+ tts-service:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: silero-tts-service
+ restart: unless-stopped
+ ports:
+ - "8000:8000"
+ environment:
+ - MODEL=v5_ru
+ - DEVICE=cpu
+ - SAMPLE_RATE=48000
+ - TORCH_THREADS=0
+ - JIT_COMPILE=true
+ - MAX_QUEUE_SIZE=20
+ - PROMETHEUS_ENABLED=false
+ - LOG_LEVEL=INFO
+ volumes:
+ - ./models:/app/models:rw
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 120s
+ logging:
+ driver: "json-file"
+ options:
+ max-size: "10m"
+ max-file: "3"
diff --git a/silero-tts-service/docker-compose.yml b/silero-tts-service/docker-compose.yml
new file mode 100755
index 0000000..bd555e3
--- /dev/null
+++ b/silero-tts-service/docker-compose.yml
@@ -0,0 +1,40 @@
+version: "3.9"
+
+services:
+ tts-service:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: silero-tts-service
+ restart: unless-stopped
+ ports:
+ - "8000:8000"
+ environment:
+ - MODEL=v5_ru
+ - DEVICE=cuda:0
+ - SAMPLE_RATE=48000
+ - TORCH_THREADS=0
+ - JIT_COMPILE=false
+ - MAX_QUEUE_SIZE=20
+ - PROMETHEUS_ENABLED=false
+ - LOG_LEVEL=INFO
+ volumes:
+ - ./models:/app/models:rw
+ deploy:
+ resources:
+ reservations:
+ devices:
+ - driver: nvidia
+ count: 1
+ capabilities: [gpu]
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 120s
+ logging:
+ driver: "json-file"
+ options:
+ max-size: "10m"
+ max-file: "3"
diff --git a/silero-tts-service/latest_silero_models.yml b/silero-tts-service/latest_silero_models.yml
new file mode 100755
index 0000000..a116ad5
--- /dev/null
+++ b/silero-tts-service/latest_silero_models.yml
@@ -0,0 +1,660 @@
+# pre-trained STT models
+stt_models:
+ en:
+ latest:
+ meta:
+ name: "en_v6"
+ sample: "https://models.silero.ai/examples/en_sample.wav"
+ labels: "https://models.silero.ai/models/en/en_v1_labels.json"
+ jit: "https://models.silero.ai/models/en/en_v6.jit"
+ onnx: "https://models.silero.ai/models/en/en_v5.onnx"
+ jit_q: "https://models.silero.ai/models/en/en_v6_q.jit"
+ jit_xlarge: "https://models.silero.ai/models/en/en_v6_xlarge.jit"
+ onnx_xlarge: "https://models.silero.ai/models/en/en_v6_xlarge.onnx"
+ v6:
+ meta:
+ name: "en_v6"
+ sample: "https://models.silero.ai/examples/en_sample.wav"
+ labels: "https://models.silero.ai/models/en/en_v1_labels.json"
+ jit: "https://models.silero.ai/models/en/en_v6.jit"
+ onnx: "https://models.silero.ai/models/en/en_v5.onnx"
+ jit_q: "https://models.silero.ai/models/en/en_v6_q.jit"
+ jit_xlarge: "https://models.silero.ai/models/en/en_v6_xlarge.jit"
+ onnx_xlarge: "https://models.silero.ai/models/en/en_v6_xlarge.onnx"
+ v5:
+ meta:
+ name: "en_v5"
+ sample: "https://models.silero.ai/examples/en_sample.wav"
+ labels: "https://models.silero.ai/models/en/en_v1_labels.json"
+ jit: "https://models.silero.ai/models/en/en_v5.jit"
+ onnx: "https://models.silero.ai/models/en/en_v5.onnx"
+ onnx_q: "https://models.silero.ai/models/en/en_v5_q.onnx"
+ jit_q: "https://models.silero.ai/models/en/en_v5_q.jit"
+ jit_xlarge: "https://models.silero.ai/models/en/en_v5_xlarge.jit"
+ onnx_xlarge: "https://models.silero.ai/models/en/en_v5_xlarge.onnx"
+ v4_0:
+ meta:
+ name: "en_v4_0"
+ sample: "https://models.silero.ai/examples/en_sample.wav"
+ labels: "https://models.silero.ai/models/en/en_v1_labels.json"
+ jit_large: "https://models.silero.ai/models/en/en_v4_0_jit_large.model"
+ onnx_large: "https://models.silero.ai/models/en/en_v4_0_large.onnx"
+ v3:
+ meta:
+ name: "en_v3"
+ sample: "https://models.silero.ai/examples/en_sample.wav"
+ labels: "https://models.silero.ai/models/en/en_v1_labels.json"
+ jit: "https://models.silero.ai/models/en/en_v3_jit.model"
+ onnx: "https://models.silero.ai/models/en/en_v3.onnx"
+ jit_q: "https://models.silero.ai/models/en/en_v3_jit_q.model"
+ jit_skip: "https://models.silero.ai/models/en/en_v3_jit_skips.model"
+ jit_large: "https://models.silero.ai/models/en/en_v3_jit_large.model"
+ onnx_large: "https://models.silero.ai/models/en/en_v3_large.onnx"
+ jit_xsmall: "https://models.silero.ai/models/en/en_v3_jit_xsmall.model"
+ jit_q_xsmall: "https://models.silero.ai/models/en/en_v3_jit_q_xsmall.model"
+ onnx_xsmall: "https://models.silero.ai/models/en/en_v3_xsmall.onnx"
+ v2:
+ meta:
+ name: "en_v2"
+ sample: "https://models.silero.ai/examples/en_sample.wav"
+ labels: "https://models.silero.ai/models/en/en_v1_labels.json"
+ jit: "https://models.silero.ai/models/en/en_v2_jit.model"
+ onnx: "https://models.silero.ai/models/en/en_v2.onnx"
+ tf: "https://models.silero.ai/models/en/en_v2_tf.tar.gz"
+ v1:
+ meta:
+ name: "en_v1"
+ sample: "https://models.silero.ai/examples/en_sample.wav"
+ labels: "https://models.silero.ai/models/en/en_v1_labels.json"
+ jit: "https://models.silero.ai/models/en/en_v1_jit.model"
+ onnx: "https://models.silero.ai/models/en/en_v1.onnx"
+ tf: "https://models.silero.ai/models/en/en_v1_tf.tar.gz"
+ de:
+ latest:
+ meta:
+ name: "de_v1"
+ sample: "https://models.silero.ai/examples/de_sample.wav"
+ labels: "https://models.silero.ai/models/de/de_v1_labels.json"
+ jit: "https://models.silero.ai/models/de/de_v1_jit.model"
+ onnx: "https://models.silero.ai/models/de/de_v1.onnx"
+ tf: "https://models.silero.ai/models/de/de_v1_tf.tar.gz"
+ v1:
+ meta:
+ name: "de_v1"
+ sample: "https://models.silero.ai/examples/de_sample.wav"
+ labels: "https://models.silero.ai/models/de/de_v1_labels.json"
+ jit_large: "https://models.silero.ai/models/de/de_v1_jit.model"
+ onnx: "https://models.silero.ai/models/de/de_v1.onnx"
+ tf: "https://models.silero.ai/models/de/de_v1_tf.tar.gz"
+ v3:
+ meta:
+ name: "de_v3"
+ sample: "https://models.silero.ai/examples/de_sample.wav"
+ labels: "https://models.silero.ai/models/de/de_v1_labels.json"
+ jit_large: "https://models.silero.ai/models/de/de_v3_large.jit"
+ v4:
+ meta:
+ name: "de_v4"
+ sample: "https://models.silero.ai/examples/de_sample.wav"
+ labels: "https://models.silero.ai/models/de/de_v1_labels.json"
+ jit_large: "https://models.silero.ai/models/de/de_v4_large.jit"
+ onnx_large: "https://models.silero.ai/models/de/de_v4_large.onnx"
+ es:
+ latest:
+ meta:
+ name: "es_v1"
+ sample: "https://models.silero.ai/examples/es_sample.wav"
+ labels: "https://models.silero.ai/models/es/es_v1_labels.json"
+ jit: "https://models.silero.ai/models/es/es_v1_jit.model"
+ onnx: "https://models.silero.ai/models/es/es_v1.onnx"
+ tf: "https://models.silero.ai/models/es/es_v1_tf.tar.gz"
+ ua:
+ latest:
+ meta:
+ name: "ua_v3"
+ sample: "https://models.silero.ai/examples/ua_sample.wav"
+ credits:
+ datasets:
+ speech-recognition-uk: https://github.com/egorsmkv/speech-recognition-uk
+ labels: "https://models.silero.ai/models/ua/ua_v1_labels.json"
+ jit: "https://models.silero.ai/models/ua/ua_v3_jit.model"
+ jit_q: "https://models.silero.ai/models/ua/ua_v3_jit_q.model"
+ onnx: "https://models.silero.ai/models/ua/ua_v3.onnx"
+ v3:
+ meta:
+ name: "ua_v3"
+ sample: "https://models.silero.ai/examples/ua_sample.wav"
+ credits:
+ datasets:
+ speech-recognition-uk: https://github.com/egorsmkv/speech-recognition-uk
+ labels: "https://models.silero.ai/models/ua/ua_v1_labels.json"
+ jit: "https://models.silero.ai/models/ua/ua_v3_jit.model"
+ jit_q: "https://models.silero.ai/models/ua/ua_v3_jit_q.model"
+ onnx: "https://models.silero.ai/models/ua/ua_v3.onnx"
+ v1:
+ meta:
+ name: "ua_v1"
+ sample: "https://models.silero.ai/examples/ua_sample.wav"
+ credits:
+ datasets:
+ speech-recognition-uk: https://github.com/egorsmkv/speech-recognition-uk
+ labels: "https://models.silero.ai/models/ua/ua_v1_labels.json"
+ jit: "https://models.silero.ai/models/ua/ua_v1_jit.model"
+ jit_q: "https://models.silero.ai/models/ua/ua_v1_jit_q.model"
+tts_models:
+ ru:
+ v5_cis_base:
+ latest:
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ package: 'https://models.silero.ai/models/tts/ru/v5_cis_base.pt'
+ sample_rate: [8000, 24000, 48000]
+ v5_cis_base_nostress_jit:
+ latest:
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ package: 'https://models.silero.ai/models/tts/ru/v5_cis_base_nostress.jit'
+ sample_rate: [8000, 24000, 48000]
+ v5_cis_base_nostress:
+ latest:
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ package: 'https://models.silero.ai/models/tts/ru/v5_cis_base_nostress.pt'
+ sample_rate: [8000, 24000, 48000]
+ v5_cis_ext:
+ latest:
+ example: '+Я з р+аннього дит+инства д+уже любл+ю сл+ухати цік+аві к+азки.'
+ package: 'https://models.silero.ai/models/tts/ru/v5_cis_ext.pt'
+ sample_rate: [8000, 24000, 48000]
+ v5_5_ru:
+ latest:
+ example: 'Меня зовут Лева Королев. Я из готов. И я уже готов открыть все ваши замки любой сложности!'
+ package: 'https://models.silero.ai/models/tts/ru/v5_5_ru.pt'
+ sample_rate: [8000, 24000, 48000]
+ v5_4_ru:
+ latest:
+ example: 'Меня зовут Лева Королев. Я из готов. И я уже готов открыть все ваши замки любой сложности!'
+ package: 'https://models.silero.ai/models/tts/ru/v5_4_ru.pt'
+ sample_rate: [8000, 24000, 48000]
+ v5_3_ru:
+ latest:
+ example: 'Меня зовут Лева Королев. Я из готов. И я уже готов открыть все ваши замки любой сложности!'
+ package: 'https://models.silero.ai/models/tts/ru/v5_3_ru.pt'
+ sample_rate: [8000, 24000, 48000]
+ v5_2_ru:
+ latest:
+ example: 'Меня зовут Лева Королев. Я из готов. И я уже готов открыть все ваши замки любой сложности!'
+ package: 'https://models.silero.ai/models/tts/ru/v5_2_ru.pt'
+ sample_rate: [8000, 24000, 48000]
+ v5_1_ru:
+ latest:
+ example: 'Меня зовут Лева Королев. Я из готов. И я уже готов открыть все ваши замки любой сложности!'
+ package: 'https://models.silero.ai/models/tts/ru/v5_1_ru.pt'
+ sample_rate: [8000, 24000, 48000]
+ v5_ru:
+ latest:
+ example: 'Меня зовут Лева Королев. Я из готов. И я уже готов открыть все ваши замки любой сложности!'
+ package: 'https://models.silero.ai/models/tts/ru/v5_ru.pt'
+ sample_rate: [8000, 24000, 48000]
+ v4_ru:
+ latest:
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ package: 'https://models.silero.ai/models/tts/ru/v4_ru.pt'
+ sample_rate: [8000, 24000, 48000]
+ v3_1_ru:
+ latest:
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ package: 'https://models.silero.ai/models/tts/ru/v3_1_ru.pt'
+ sample_rate: [8000, 24000, 48000]
+ ru_v3:
+ latest:
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ package: 'https://models.silero.ai/models/tts/ru/ru_v3.pt'
+ sample_rate: [8000, 24000, 48000]
+ aidar_v2:
+ latest:
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ package: 'https://models.silero.ai/models/tts/ru/v2_aidar.pt'
+ sample_rate: [8000, 16000]
+ aidar_8khz:
+ latest:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_aidar_8000.jit'
+ sample_rate: 8000
+ v1:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_aidar_8000.jit'
+ sample_rate: 8000
+ aidar_16khz:
+ latest:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_aidar_16000.jit'
+ sample_rate: 16000
+ v1:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_aidar_16000.jit'
+ sample_rate: 16000
+ baya_v2:
+ latest:
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ package: 'https://models.silero.ai/models/tts/ru/v2_baya.pt'
+ sample_rate: [8000, 16000]
+ baya_8khz:
+ latest:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_baya_8000.jit'
+ sample_rate: 8000
+ v1:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_baya_8000.jit'
+ sample_rate: 8000
+ baya_16khz:
+ latest:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_baya_16000.jit'
+ sample_rate: 16000
+ v1:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_baya_16000.jit'
+ sample_rate: 16000
+ irina_v2:
+ latest:
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ package: 'https://models.silero.ai/models/tts/ru/v2_irina.pt'
+ sample_rate: [8000, 16000]
+ irina_8khz:
+ latest:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_irina_8000.jit'
+ sample_rate: 8000
+ v1:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_irina_8000.jit'
+ sample_rate: 8000
+ irina_16khz:
+ latest:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_irina_16000.jit'
+ sample_rate: 16000
+ v1:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_irina_16000.jit'
+ sample_rate: 16000
+ kseniya_v2:
+ latest:
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ package: 'https://models.silero.ai/models/tts/ru/v2_kseniya.pt'
+ sample_rate: [8000, 16000]
+ kseniya_8khz:
+ latest:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_kseniya_8000.jit'
+ sample_rate: 8000
+ v1:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_kseniya_8000.jit'
+ sample_rate: 8000
+ kseniya_16khz:
+ latest:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_kseniya_16000.jit'
+ sample_rate: 16000
+ v1:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_kseniya_16000.jit'
+ sample_rate: 16000
+ natasha_v2:
+ latest:
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ package: 'https://models.silero.ai/models/tts/ru/v2_natasha.pt'
+ sample_rate: [8000, 16000]
+ natasha_8khz:
+ latest:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_natasha_8000.jit'
+ sample_rate: 8000
+ v1:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_natasha_8000.jit'
+ sample_rate: 8000
+ natasha_16khz:
+ latest:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_natasha_16000.jit'
+ sample_rate: 16000
+ v1:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_natasha_16000.jit'
+ sample_rate: 16000
+ ruslan_v2:
+ latest:
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ package: 'https://models.silero.ai/models/tts/ru/v2_ruslan.pt'
+ sample_rate: [8000, 16000]
+ ruslan_8khz:
+ latest:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_ruslan_8000.jit'
+ sample_rate: 8000
+ v1:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_ruslan_8000.jit'
+ sample_rate: 8000
+ ruslan_16khz:
+ latest:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_ruslan_16000.jit'
+ sample_rate: 16000
+ v1:
+ tokenset: '_~абвгдеёжзийклмнопрстуфхцчшщъыьэюя +.,!?…:;–'
+ example: 'В н+едрах т+ундры в+ыдры в г+етрах т+ырят в в+ёдра +ядра к+едров.'
+ jit: 'https://models.silero.ai/models/tts/ru/v1_ruslan_16000.jit'
+ sample_rate: 16000
+ en:
+ v3_en:
+ latest:
+ example: 'Can you can a canned can into an un-canned can like a canner can can a canned can into an un-canned can?'
+ package: 'https://models.silero.ai/models/tts/en/v3_en.pt'
+ sample_rate: [8000, 24000, 48000]
+ v3_en_indic:
+ latest:
+ example: 'Can you can a canned can into an un-canned can like a canner can can a canned can into an un-canned can?'
+ package: 'https://models.silero.ai/models/tts/en/v3_en_indic.pt'
+ sample_rate: [8000, 24000, 48000]
+ lj_v2:
+ latest:
+ example: 'Can you can a canned can into an un-canned can like a canner can can a canned can into an un-canned can?'
+ package: 'https://models.silero.ai/models/tts/en/v2_lj.pt'
+ sample_rate: [8000, 16000]
+ lj_8khz:
+ latest:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyz .,!?…:;–'
+ example: 'Can you can a canned can into an un-canned can like a canner can can a canned can into an un-canned can?'
+ jit: 'https://models.silero.ai/models/tts/en/v1_lj_8000.jit'
+ sample_rate: 8000
+ v1:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyz .,!?…:;–'
+ example: 'Can you can a canned can into an un-canned can like a canner can can a canned can into an un-canned can?'
+ jit: 'https://models.silero.ai/models/tts/en/v1_lj_8000.jit'
+ sample_rate: 8000
+ lj_16khz:
+ latest:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyz .,!?…:;–'
+ example: 'Can you can a canned can into an un-canned can like a canner can can a canned can into an un-canned can?'
+ jit: 'https://models.silero.ai/models/tts/en/v1_lj_16000.jit'
+ sample_rate: 16000
+ v1:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyz .,!?…:;–'
+ example: 'Can you can a canned can into an un-canned can like a canner can can a canned can into an un-canned can?'
+ jit: 'https://models.silero.ai/models/tts/en/v1_lj_16000.jit'
+ sample_rate: 16000
+ de:
+ v3_de:
+ latest:
+ example: 'Fischers Fritze fischt frische Fische, Frische Fische fischt Fischers Fritze.'
+ package: 'https://models.silero.ai/models/tts/de/v3_de.pt'
+ sample_rate: [8000, 24000, 48000]
+ thorsten_v2:
+ latest:
+ example: 'Fischers Fritze fischt frische Fische, Frische Fische fischt Fischers Fritze.'
+ package: 'https://models.silero.ai/models/tts/de/v2_thorsten.pt'
+ sample_rate: [8000, 16000]
+ thorsten_8khz:
+ latest:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyzäöüß .,!?…:;–'
+ example: 'Fischers Fritze fischt frische Fische, Frische Fische fischt Fischers Fritze.'
+ jit: 'https://models.silero.ai/models/tts/de/v1_thorsten_8000.jit'
+ sample_rate: 8000
+ v1:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyzäöüß .,!?…:;–'
+ example: 'Fischers Fritze fischt frische Fische, Frische Fische fischt Fischers Fritze.'
+ jit: 'https://models.silero.ai/models/tts/de/v1_thorsten_8000.jit'
+ sample_rate: 8000
+ thorsten_16khz:
+ latest:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyzäöüß .,!?…:;–'
+ example: 'Fischers Fritze fischt frische Fische, Frische Fische fischt Fischers Fritze.'
+ jit: 'https://models.silero.ai/models/tts/de/v1_thorsten_16000.jit'
+ sample_rate: 16000
+ v1:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyzäöüß .,!?…:;–'
+ example: 'Fischers Fritze fischt frische Fische, Frische Fische fischt Fischers Fritze.'
+ jit: 'https://models.silero.ai/models/tts/de/v1_thorsten_16000.jit'
+ sample_rate: 16000
+ es:
+ v3_es:
+ latest:
+ example: 'Hoy ya es ayer y ayer ya es hoy, ya llegó el día, y hoy es hoy.'
+ package: 'https://models.silero.ai/models/tts/es/v3_es.pt'
+ sample_rate: [8000, 24000, 48000]
+ tux_v2:
+ latest:
+ example: 'Hoy ya es ayer y ayer ya es hoy, ya llegó el día, y hoy es hoy.'
+ package: 'https://models.silero.ai/models/tts/es/v2_tux.pt'
+ sample_rate: [8000, 16000]
+ tux_8khz:
+ latest:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyzáéíñóú .,!?…:;–¡¿'
+ example: 'Hoy ya es ayer y ayer ya es hoy, ya llegó el día, y hoy es hoy.'
+ jit: 'https://models.silero.ai/models/tts/es/v1_tux_8000.jit'
+ sample_rate: 8000
+ v1:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyzáéíñóú .,!?…:;–¡¿'
+ example: 'Hoy ya es ayer y ayer ya es hoy, ya llegó el día, y hoy es hoy.'
+ jit: 'https://models.silero.ai/models/tts/es/v1_tux_8000.jit'
+ sample_rate: 8000
+ tux_16khz:
+ latest:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyzáéíñóú .,!?…:;–¡¿'
+ example: 'Hoy ya es ayer y ayer ya es hoy, ya llegó el día, y hoy es hoy.'
+ jit: 'https://models.silero.ai/models/tts/es/v1_tux_16000.jit'
+ sample_rate: 16000
+ v1:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyzáéíñóú .,!?…:;–¡¿'
+ example: 'Hoy ya es ayer y ayer ya es hoy, ya llegó el día, y hoy es hoy.'
+ jit: 'https://models.silero.ai/models/tts/es/v1_tux_16000.jit'
+ sample_rate: 16000
+ fr:
+ v3_fr:
+ latest:
+ example: 'Je suis ce que je suis, et si je suis ce que je suis, qu’est ce que je suis.'
+ package: 'https://models.silero.ai/models/tts/fr/v3_fr.pt'
+ sample_rate: [8000, 24000, 48000]
+ gilles_v2:
+ latest:
+ example: 'Je suis ce que je suis, et si je suis ce que je suis, qu’est ce que je suis.'
+ package: 'https://models.silero.ai/models/tts/fr/v2_gilles.pt'
+ sample_rate: [8000, 16000]
+ gilles_8khz:
+ latest:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyzéàèùâêîôûç .,!?…:;–'
+ example: 'Je suis ce que je suis, et si je suis ce que je suis, qu’est ce que je suis.'
+ jit: 'https://models.silero.ai/models/tts/fr/v1_gilles_8000.jit'
+ sample_rate: 8000
+ v1:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyzéàèùâêîôûç .,!?…:;–'
+ example: 'Je suis ce que je suis, et si je suis ce que je suis, qu’est ce que je suis.'
+ jit: 'https://models.silero.ai/models/tts/fr/v1_gilles_8000.jit'
+ sample_rate: 8000
+ gilles_16khz:
+ latest:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyzéàèùâêîôûç .,!?…:;–'
+ example: 'Je suis ce que je suis, et si je suis ce que je suis, qu’est ce que je suis.'
+ jit: 'https://models.silero.ai/models/tts/fr/v1_gilles_16000.jit'
+ sample_rate: 16000
+ v1:
+ tokenset: '_~abcdefghijklmnopqrstuvwxyzéàèùâêîôûç .,!?…:;–'
+ example: 'Je suis ce que je suis, et si je suis ce que je suis, qu’est ce que je suis.'
+ jit: 'https://models.silero.ai/models/tts/fr/v1_gilles_16000.jit'
+ sample_rate: 16000
+ ba:
+ aigul_v2:
+ latest:
+ example: 'Салауат Юлаевтың тормошо һәм яҙмышы хаҡындағы документтарҙың һәм шиғри әҫәрҙәренең бик аҙ өлөшө генә һаҡланған.'
+ package: 'https://models.silero.ai/models/tts/ba/v2_aigul.pt'
+ sample_rate: [8000, 16000]
+ language_name: 'bashkir'
+ xal:
+ v3_xal:
+ latest:
+ example: 'Һорвн, дөрвн күн ирәд, һазань чиңгнв. Байн Цецн хаана һорвн көвүн күүндҗәнә.'
+ package: 'https://models.silero.ai/models/tts/xal/v3_xal.pt'
+ sample_rate: [8000, 24000, 48000]
+ erdni_v2:
+ latest:
+ example: 'Һорвн, дөрвн күн ирәд, һазань чиңгнв. Байн Цецн хаана һорвн көвүн күүндҗәнә.'
+ package: 'https://models.silero.ai/models/tts/xal/v2_erdni.pt'
+ sample_rate: [8000, 16000]
+ language_name: 'kalmyk'
+ tt:
+ v3_tt:
+ latest:
+ example: 'Исәнмесез, саумысез, нишләп кәҗәгезне саумыйсыз, әтәчегез күкәй салган, нишләп чыгып алмыйсыз.'
+ package: 'https://models.silero.ai/models/tts/tt/v3_tt.pt'
+ sample_rate: [8000, 24000, 48000]
+ dilyara_v2:
+ latest:
+ example: 'Ис+әнмесез, с+аумысез, нишл+әп кәҗәгезн+е с+аумыйсыз, әтәчег+ез күк+әй салг+ан, нишл+әп чыг+ып +алмыйсыз.'
+ package: 'https://models.silero.ai/models/tts/tt/v2_dilyara.pt'
+ sample_rate: [8000, 16000]
+ language_name: 'tatar'
+ uz:
+ v4_uz:
+ latest:
+ example: 'Tanishganimdan xursandman.'
+ package: 'https://models.silero.ai/models/tts/uz/v4_uz.pt'
+ sample_rate: [8000, 24000, 48000]
+ v3_uz:
+ latest:
+ example: 'Tanishganimdan xursandman.'
+ package: 'https://models.silero.ai/models/tts/uz/v3_uz.pt'
+ sample_rate: [8000, 24000, 48000]
+ dilnavoz_v2:
+ latest:
+ example: 'Tanishganimdan xursandman.'
+ package: 'https://models.silero.ai/models/tts/uz/v2_dilnavoz.pt'
+ sample_rate: [8000, 16000]
+ language_name: 'uzbek'
+ ua:
+ v4_ua:
+ latest:
+ example: 'К+отики - пухн+асті жив+отики.'
+ package: 'https://models.silero.ai/models/tts/ua/v4_ua.pt'
+ sample_rate: [8000, 24000, 48000]
+ v3_ua:
+ latest:
+ example: 'К+отики - пухн+асті жив+отики.'
+ package: 'https://models.silero.ai/models/tts/ua/v3_ua.pt'
+ sample_rate: [8000, 24000, 48000]
+ mykyta_v2:
+ latest:
+ example: 'К+отики - пухн+асті жив+отики.'
+ package: 'https://models.silero.ai/models/tts/ua/v22_mykyta_48k.pt'
+ sample_rate: [8000, 24000, 48000]
+ language_name: 'ukrainian'
+ indic:
+ v4_indic:
+ latest:
+ example: 'prasidda kabīra adhyētā, puruṣōttama agravāla kā yaha śōdha ālēkha, usa rāmānaṁda kī khōja karatā hai'
+ package: 'https://models.silero.ai/models/tts/indic/v4_indic.pt'
+ sample_rate: [8000, 24000, 48000]
+ v3_indic:
+ latest:
+ example: 'prasidda kabīra adhyētā, puruṣōttama agravāla kā yaha śōdha ālēkha, usa rāmānaṁda kī khōja karatā hai'
+ package: 'https://models.silero.ai/models/tts/indic/v3_indic.pt'
+ sample_rate: [8000, 24000, 48000]
+ cyrillic:
+ v4_cyrillic:
+ latest:
+ example: 'Волк слабее льва и тигра, но в цирке он не выступает.'
+ package: 'https://models.silero.ai/models/tts/cyr/v4_cyrillic.pt'
+ sample_rate: [8000, 24000, 48000]
+ multi:
+ multi_v2:
+ latest:
+ package: 'https://models.silero.ai/models/tts/multi/v2_multi.pt'
+ sample_rate: [8000, 16000]
+ speakers:
+ aidar:
+ lang: 'ru'
+ example: 'Съ+ешьте ещ+ё +этих м+ягких франц+узских б+улочек, д+а в+ыпейте ч+аю.'
+ baya:
+ lang: 'ru'
+ example: 'Съ+ешьте ещ+ё +этих м+ягких франц+узских б+улочек, д+а в+ыпейте ч+аю.'
+ kseniya:
+ lang: 'ru'
+ example: 'Съ+ешьте ещ+ё +этих м+ягких франц+узских б+улочек, д+а в+ыпейте ч+аю.'
+ irina:
+ lang: 'ru'
+ example: 'Съ+ешьте ещ+ё +этих м+ягких франц+узских б+улочек, д+а в+ыпейте ч+аю.'
+ ruslan:
+ lang: 'ru'
+ example: 'Съ+ешьте ещ+ё +этих м+ягких франц+узских б+улочек, д+а в+ыпейте ч+аю.'
+ natasha:
+ lang: 'ru'
+ example: 'Съ+ешьте ещ+ё +этих м+ягких франц+узских б+улочек, д+а в+ыпейте ч+аю.'
+ thorsten:
+ lang: 'de'
+ example: 'Fischers Fritze fischt frische Fische, Frische Fische fischt Fischers Fritze.'
+ tux:
+ lang: 'es'
+ example: 'Hoy ya es ayer y ayer ya es hoy, ya llegó el día, y hoy es hoy.'
+ gilles:
+ lang: 'fr'
+ example: 'Je suis ce que je suis, et si je suis ce que je suis, qu’est ce que je suis.'
+ lj:
+ lang: 'en'
+ example: 'Can you can a canned can into an un-canned can like a canner can can a canned can into an un-canned can?'
+ dilyara:
+ lang: 'tt'
+ example: 'Пес+и пес+и песик+әй, борыннар+ы бәләк+әй.'
+te_models:
+ latest:
+ package: "https://models.silero.ai/te_models/v2_4lang_q.pt"
+ languages: ['en', 'de', 'ru', 'es']
+ punct: '.,-!?—'
+ v2:
+ package: "https://models.silero.ai/te_models/v2_4lang_q.pt"
+ languages: ['en', 'de', 'ru', 'es']
+ punct: '.,-!?—'
+denoise_models:
+ models: ['small_slow', 'large_fast', 'small_fast']
+ samples: [
+ "https://models.silero.ai/denoise_models/sample0.wav",
+ "https://models.silero.ai/denoise_models/sample1.wav",
+ ]
+ small_slow:
+ latest:
+ jit: "https://models.silero.ai/denoise_models/sns_latest.jit"
+ v0:
+ jit: "https://models.silero.ai/denoise_models/sns_v0.jit"
+ large_fast:
+ latest:
+ jit: "https://models.silero.ai/denoise_models/lnf_latest.jit"
+ v0:
+ jit: "https://models.silero.ai/denoise_models/lnf_v0.jit"
+ small_fast:
+ latest:
+ jit: "https://models.silero.ai/denoise_models/snf_latest.jit"
+ v0:
+ jit: "https://models.silero.ai/denoise_models/snf_v0.jit"
diff --git a/silero-tts-service/pyproject.toml b/silero-tts-service/pyproject.toml
new file mode 100755
index 0000000..2f674c5
--- /dev/null
+++ b/silero-tts-service/pyproject.toml
@@ -0,0 +1,23 @@
+[project]
+name = "silero-tts-service"
+version = "0.1.0"
+description = "Autonomous REST API for Russian speech synthesis using Silero v5.5"
+requires-python = ">=3.11"
+dependencies = [
+ "fastapi[standard]>=0.115.0",
+ "uvicorn[standard]>=0.30.0",
+ "silero>=5.5",
+ "torch>=2.4.0",
+ "pydantic-settings>=2.0",
+ "pydub>=0.25.1",
+ "ffmpeg-python>=0.2.0",
+ "llru>=1.0",
+ "structlog>=24.0",
+ "pydantic>=2.0",
+ "prometheus-client>=0.20.0",
+ "slowapi>=0.1.9",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
diff --git a/silero-tts-service/run.py b/silero-tts-service/run.py
new file mode 100755
index 0000000..3d9af0a
--- /dev/null
+++ b/silero-tts-service/run.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3
+"""Запуск Silero TTS сервиса"""
+
+import sys, os
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from app.main import create_app
+import uvicorn
+
+app = create_app()
+
+if __name__ == '__main__':
+ port = int(os.environ.get('PORT', 8000))
+ print(f'Запуск Silero TTS API на порту {port}')
+ uvicorn.run(app, host='0.0.0.0', port=port, log_level='info')
diff --git a/silero-tts-service/tts.py b/silero-tts-service/tts.py
new file mode 100644
index 0000000..5063d9c
--- /dev/null
+++ b/silero-tts-service/tts.py
@@ -0,0 +1,267 @@
+#!/usr/bin/env python3
+"""
+Silero TTS CLI — синтез речи через HTTP API для пайплайнов и скриптов.
+
+Использование:
+ # Проверка здоровья
+ python3 tts.py health
+
+ # Список голосов
+ python3 tts.py voices
+
+ # Базовый синтез
+ python3 tts.py speak "Привет, мир!" --voice xenia --output hello.wav
+
+ # Синтез в MP3
+ python3 tts.py speak "Тестовый текст" --voice baya --format mp3 --output test.mp3
+
+ # Пайплайн: конвейер (stdin → stdout)
+ echo "Текст для озвучки" | python3 tts.py pipe --voice aidar > output.wav
+
+ # Пайплайн: JSON-результат (base64-аудио)
+ python3 tts.py speak "Текст" --json
+
+ # HyperFrames: генерация аудио для видео-композиции
+ python3 tts.py speak "Добро пожаловать в HyperFrames" --voice xenia --output narration.wav
+
+Требования: сервис Silero TTS на http://127.0.0.1:8001 (или через env SILERO_TTS_URL)
+"""
+
+import argparse
+import base64
+import json
+import os
+import sys
+from urllib.request import Request, urlopen
+from urllib.error import URLError
+
+API_URL = os.environ.get("SILERO_TTS_URL", "http://127.0.0.1:8001")
+# track whether URL was overridden from CLI
+_url_from_args = None
+
+
+def get_api_url() -> str:
+ """Получить текущий URL API (CLI-аргумент или env)."""
+ return _url_from_args or API_URL
+
+SPEAKERS = {
+ "aidar": "Мужской, нейтральный (русский)",
+ "baya": "Женский, мягкий (русский)",
+ "kseniya": "Женский, энергичный (русский)",
+ "xenia": "Женский, молодой (русский)",
+ "eugene": "Мужской, спокойный (русский)",
+}
+
+
+def api_get(path: str) -> dict:
+ """GET-запрос к API, возвращает JSON."""
+ url = f"{get_api_url()}{path}"
+ req = Request(url)
+ req.add_header("Accept", "application/json")
+ try:
+ with urlopen(req, timeout=10) as resp:
+ return json.loads(resp.read().decode())
+ except URLError as e:
+ print(f"Ошибка подключения к {url}: {e}", file=sys.stderr)
+ print("Сервис запущен? Проверь: curl http://127.0.0.1:8001/health", file=sys.stderr)
+ sys.exit(1)
+
+
+def api_post(path: str, data: dict, return_raw: bool = False) -> bytes | dict:
+ """POST-запрос к API. Если return_raw — вернуть сырые байты."""
+ url = f"{get_api_url()}{path}"
+ body = json.dumps(data).encode()
+ req = Request(url, data=body, method="POST")
+ req.add_header("Content-Type", "application/json")
+ if not return_raw:
+ req.add_header("Accept", "application/json")
+ try:
+ with urlopen(req, timeout=120) as resp:
+ content = resp.read()
+ if return_raw:
+ return content
+ return json.loads(content.decode())
+ except URLError as e:
+ print(f"Ошибка подключения к {url}: {e}", file=sys.stderr)
+ sys.exit(1)
+
+
+def cmd_health():
+ """Проверка здоровья сервиса."""
+ result = api_get("/v1/health")
+ status = "✅ Здоров" if result.get("status") == "healthy" else "❌ Проблемы"
+ print(f"Silero TTS API: {status}")
+ print(f" Модель загружена: {result.get('model_loaded', False)}")
+ if "device" in result:
+ print(f" Устройство: {result['device']}")
+
+
+def cmd_voices():
+ """Список доступных голосов."""
+ result = api_get("/v1/voices")
+ speakers = result.get("speakers", {})
+ print("Доступные голоса:")
+ for name, desc in speakers.items():
+ marker = " ← default" if name == "xenia" else ""
+ print(f" {name:12s} — {desc}{marker}")
+
+
+def cmd_speak(text: str, voice: str, fmt: str, output: str | None, as_json: bool):
+ """Синтез речи и сохранение/вывод результата."""
+ if voice not in SPEAKERS:
+ print(f"Неизвестный голос: {voice}", file=sys.stderr)
+ print(f"Доступные: {', '.join(SPEAKERS.keys())}", file=sys.stderr)
+ sys.exit(1)
+
+ # Определяем endpoint и формат
+ if fmt == "wav":
+ path = "/v1/tts"
+ payload = {"text": text, "speaker": voice}
+ audio = api_post(path, payload, return_raw=True)
+ elif fmt == "mp3":
+ path = "/v1/audio/speech"
+ payload = {
+ "model": "tts-1",
+ "input": text,
+ "voice": voice,
+ "response_format": "mp3",
+ }
+ audio = api_post(path, payload, return_raw=True)
+ else:
+ print(f"Неизвестный формат: {fmt}", file=sys.stderr)
+ sys.exit(1)
+
+ if as_json:
+ # Вывести JSON с base64-аудио
+ result = {
+ "format": fmt,
+ "voice": voice,
+ "text": text,
+ "audio_base64": base64.b64encode(audio).decode(),
+ "size_bytes": len(audio),
+ }
+ print(json.dumps(result, ensure_ascii=False))
+ elif output:
+ # Сохранить в файл
+ with open(output, "wb") as f:
+ f.write(audio)
+ size_kb = len(audio) / 1024
+ print(f"✅ Сохранено: {output} ({size_kb:.1f} KB, {fmt.upper()})")
+ else:
+ # Вывести в stdout (для пайпов)
+ sys.stdout.buffer.write(audio)
+
+
+def cmd_pipe(voice: str):
+ """Чтение текста из stdin и вывод WAV в stdout."""
+ text = sys.stdin.read().strip()
+ if not text:
+ print("Пустой ввод", file=sys.stderr)
+ sys.exit(1)
+ cmd_speak(text, voice, "wav", None, False)
+
+
+def cmd_hyperframes(text: str, voice: str, output: str, duration_hint: float | None):
+ """
+ Сгенерировать аудио для HyperFrames-композиции.
+ Выводит HTML-сниппет с data-атрибутами для вставки в композицию.
+ """
+ # Синтез
+ audio = api_post("/v1/tts", {"text": text, "speaker": voice}, return_raw=True)
+
+ # Сохранить аудио
+ if not output:
+ output = f"narration_{voice}.wav"
+
+ with open(output, "wb") as f:
+ f.write(audio)
+
+ # Определить длительность из WAV-заголовка
+ import struct
+ wav_size = len(audio)
+ # WAV: sample_rate at bytes 24-27, data_size at bytes 40-43
+ if wav_size >= 44:
+ sample_rate = struct.unpack_from(") ───")
+ print()
+ print(f' ')
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Silero TTS CLI — синтез речи для пайплайнов и HyperFrames",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument(
+ "--url",
+ default=API_URL,
+ help=f"URL Silero TTS API (по умолчанию: {API_URL})",
+ )
+
+ sub = parser.add_subparsers(dest="command", help="Команда")
+
+ # health
+ sub.add_parser("health", help="Проверка здоровья сервиса")
+
+ # voices
+ sub.add_parser("voices", help="Список доступных голосов")
+
+ # speak
+ sp = sub.add_parser("speak", help="Синтез речи в файл")
+ sp.add_argument("text", help="Текст для синтеза")
+ sp.add_argument("--voice", "-v", default="xenia", choices=list(SPEAKERS.keys()), help="Голос")
+ sp.add_argument("--format", "-f", default="wav", choices=["wav", "mp3"], help="Формат аудио")
+ sp.add_argument("--output", "-o", help="Путь для сохранения (по умолчанию stdout)")
+ sp.add_argument("--json", action="store_true", help="Вывести JSON с base64-аудио вместо файла")
+
+ # pipe
+ pp = sub.add_parser("pipe", help="Чтение текста из stdin, вывод WAV в stdout")
+ pp.add_argument("--voice", "-v", default="xenia", choices=list(SPEAKERS.keys()), help="Голос")
+
+ # hyperframes
+ hf = sub.add_parser("hyperframes", help="Генерация аудио для HyperFrames + HTML-сниппет")
+ hf.add_argument("text", help="Текст для озвучки")
+ hf.add_argument("--voice", "-v", default="xenia", choices=list(SPEAKERS.keys()), help="Голос")
+ hf.add_argument("--output", "-o", default="narration.wav", help="Имя выходного файла")
+ hf.add_argument("--duration", type=float, help="Длительность в секундах (если не удалось определить из WAV)")
+
+ args = parser.parse_args()
+
+ # URL из аргументов
+ global _url_from_args
+ if args.url:
+ _url_from_args = args.url
+
+ if args.command == "health":
+ cmd_health()
+ elif args.command == "voices":
+ cmd_voices()
+ elif args.command == "speak":
+ cmd_speak(args.text, args.voice, args.format, args.output, args.json)
+ elif args.command == "pipe":
+ cmd_pipe(args.voice)
+ elif args.command == "hyperframes":
+ cmd_hyperframes(args.text, args.voice, args.output, args.duration)
+ else:
+ # По умолчанию: если есть аргумент — speak, иначе help
+ parser.print_help()
+
+
+if __name__ == "__main__":
+ main()