initial: silero-tts-service + README
This commit is contained in:
1 parent
8c60c25ad2
commit
984b1fec6d
18 files changed
+2080
No files matched your search
Executable
+17
@@ -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%, первый запрос медленнее)
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
*.db
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg
|
||||||
|
test_tts.py
|
||||||
|
tests/
|
||||||
|
data/
|
||||||
Executable
+32
@@ -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"]
|
||||||
Executable
+204
@@ -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 ───
|
||||||
|
# <audio id="narration"
|
||||||
|
# data-start="0"
|
||||||
|
# data-duration="5.1"
|
||||||
|
# data-track-index="0"
|
||||||
|
# data-volume="1.0"
|
||||||
|
# src="narration.wav">
|
||||||
|
# </audio>
|
||||||
|
```
|
||||||
|
|
||||||
|
Вставить сниппет в `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']))
|
||||||
|
"
|
||||||
|
```
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
# App package
|
||||||
Executable
+284
@@ -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)}",
|
||||||
|
)
|
||||||
Executable
+69
@@ -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
|
||||||
Executable
+59
@@ -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
|
||||||
Executable
+181
@@ -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()
|
||||||
Executable
+35
@@ -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"])
|
||||||
Executable
+95
@@ -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()
|
||||||
Executable
+49
@@ -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
|
||||||
Executable
+36
@@ -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"
|
||||||
Executable
+40
@@ -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"
|
||||||
Executable
+660
@@ -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"
|
||||||
Executable
+23
@@ -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"
|
||||||
Executable
+15
@@ -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')
|
||||||
@@ -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("<I", audio, 24)[0]
|
||||||
|
data_size = struct.unpack_from("<I", audio, 40)[0]
|
||||||
|
duration = data_size / (sample_rate * 2) # 16-bit mono = 2 bytes/sample
|
||||||
|
else:
|
||||||
|
duration = duration_hint or 3.0
|
||||||
|
|
||||||
|
size_kb = wav_size / 1024
|
||||||
|
print(f"✅ Аудио: {output} ({size_kb:.1f} KB, {duration:.2f} сек)")
|
||||||
|
|
||||||
|
# Генерировать HTML-сниппет для HyperFrames композиции
|
||||||
|
print()
|
||||||
|
print("─── HTML-сниппет для HyperFrames (вставить в <div data-composition-id=\"...\">) ───")
|
||||||
|
print()
|
||||||
|
print(f' <audio id="narration"')
|
||||||
|
print(f' data-start="0"')
|
||||||
|
print(f' data-duration="{duration:.1f}"')
|
||||||
|
print(f' data-track-index="0"')
|
||||||
|
print(f' data-volume="1.0"')
|
||||||
|
print(f' src="{output}">')
|
||||||
|
print(f' </audio>')
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
Reference in new issue
Block a user