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