24 KiB
Silero TTS Server
Локальный сервер синтеза речи на базе Silero TTS с OpenAI-совместимым API.
Принимает текст на русском и английском, определяет язык запроса по наличию
кириллицы, нормализует числа/аббревиатуры (rutextnorm), транслитерирует
латинские слова в кириллицу (RU) и возвращает готовый аудиопоток.
Работает полностью офлайн — модели грузятся из локальных файлов .pt, сеть не требуется.
Возможности
- 🔌 OpenAI-совместимые эндпоинты:
/v1/audio/speech,/v1/models,/v1/audio/voices - 🌍 Один язык и один голос на запрос — если в тексте есть хоть одна кириллическая буква, весь текст читает русская модель; иначе — английская. Голос не меняется внутри ответа.
- 🔢 Нормализация текста — цифры, даты, единицы измерения читаются словами (только RU)
- 🎙️ 14 голосов (имена из набора OpenAI) с отдельными спикерами для RU и EN
- 🎵 6 форматов вывода:
mp3,wav,pcm,opus,aac,flac - 📡 Потоковая передача (
stream: true) - 🖥️ Веб-интерфейс для тестирования на
/webui(включается вconfig.json) - ⚙️ Полностью CPU-синтез, без обязательной GPU
Быстрый старт
Windows
run.bat
Linux / macOS
./run.sh
Скрипты автоматически создают .venv, устанавливают зависимости и запускают
сервер на http://0.0.0.0:5215.
Обновление из репозитория
update.bat
./update.sh
Останавливают сервис, подтягивают изменения из origin (ветка main),
переустанавливают зависимости и перезапускают сервер.
Зависимости от системы
- Python 3.12
- ffmpeg — нужен для
mp3/opus/aac/flac(дляwav/pcmне требуется). Ставится автоматически вместе сimageio-ffmpeg(входит в зависимости), отдельных действий не нужно. Свой бинарь можно указать вconfig.json→runtime.ffmpeg_path.
Ручной запуск
python -m venv .venv
.venv/Scripts/pip install -e ".[dev]" # Windows
.venv/bin/pip install -e ".[dev]" # Linux/macOS
.venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 5215
Использование
Базовый запрос
curl http://localhost:5215/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{"voice": "alloy", "input": "Привет, мир! This is a test.", "response_format": "mp3"}' \
--output speech.mp3
Параметры запроса
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
model |
string |
UniosTTS |
Игнорируется (совместимость с OpenAI) |
voice |
string |
— | Имя голоса (см. ниже), при неизвестном — default |
input |
string |
— | Текст для синтеза (не пустой) |
response_format |
string |
mp3 |
mp3 / wav / pcm / opus / aac / flac |
speed |
float |
1.0 |
Пока поддерживается только 1.0 |
stream |
bool |
false |
Потоковая отдача аудио |
stream_format |
string |
— | audio включает стрим; sse не поддерживается |
rate |
string |
— | Темп просодии: x-slow/slow/medium/fast/x-fast (иное значение → 422) |
pitch |
string |
— | Тон просодии: x-low/low/medium/high/x-high (иное значение → 422) |
Голоса
Имена голосов соответствуют встроенному набору OpenAI. RU-сторона ограничена
5 спикерами модели v5_5_ru, EN-сторона — спикерами v3_en.
| Имя | RU-спикер | EN-спикер |
|---|---|---|
alloy |
eugene |
en_2 |
ash |
aidar |
en_1 |
ballad |
baya |
en_3 |
coral |
kseniya |
en_4 |
echo |
aidar |
en_8 |
fable |
baya |
en_5 |
nova |
xenia |
en_6 |
onyx |
eugene |
en_8 |
sage |
xenia |
en_7 |
shimmer |
kseniya |
en_9 |
verse |
eugene |
en_10 |
marin |
aidar |
en_11 |
cedar |
kseniya |
en_12 |
default |
baya |
en_0 |
Проверка работоспособности
curl http://localhost:5215/health
# {"status":"ok"}
Эндпоинты
| Метод | Путь | Назначение |
|---|---|---|
POST |
/v1/audio/speech |
Синтез речи (OpenAI-совместимый) |
GET |
/v1/models |
Список моделей (бренд-модель UniosTTS) |
GET |
/v1/audio/voices |
Список голосов из config.json |
GET |
/health |
Проверка работоспособности |
GET |
/webui |
Веб-интерфейс (если webui: true) |
GET |
/webui/models |
Список моделей для UI (UniosTTS) |
GET |
/webui/voices |
Список голосов для UI |
POST |
/webui/speech |
Синтез + метаданные (JSON: base64, язык, нормализация) |
Веб-интерфейс
Для интерактивного тестирования. Включается флагом "webui": true в
config.json; по умолчанию выключен (эндпоинты /webui/* отдают 404).
Откройте http://localhost:5215/webui: вводите текст, выбирайте голос и
формат, слушайте результат. Поведение идентично API — модель выбирается
автоматически по языку текста (model в UI — это UniosTTS, фактически
игнорируется), голос берётся из стандартного справочника.
Отличие от API — /webui/speech возвращает JSON с base64-аудио и
метаданными вместо бинарного потока, чтобы показать диагностику:
| Поле | Описание |
|---|---|
audio |
Аудио в base64 |
language |
Определённый язык (ru / en) |
model / voice |
Модель и имя голоса из справочника |
speaker |
Реальный спикер модели |
normalized_text |
Текст после нормализации |
segments |
Число сегментов |
synthesis_time_ms |
Время синтеза, мс |
Интерфейс также хранит историю запросов текущей сессии и даёт скачать файл.
Конфигурация
Настройки в config.json:
{
"webui": true, // включить веб-интерфейс на /webui
"use_ssml": true, // SSML-просодия/паузы; false = откат на легаси text= путь
"models": [ // список моделей, порядок = приоритет при выборе по языку
{ "file": "v5_5_ru", "languages": ["ru"] }, // models/v5_5_ru.pt
{ "file": "v3_en", "languages": ["en"] } // models/v3_en.pt
],
"model_voices": { // сопоставление стандартного имени голоса со спикером каждой модели
"v5_5_ru": { "default": "baya", "alloy": "eugene" /* ... */ },
"v3_en": { "default": "en_0", "alloy": "en_2" /* ... */ }
},
"runtime": {
"sample_rate": 24000,
"ffmpeg_path": "ffmpeg",
"translit_dict": "translit_dict.json",
"ssml_max_chars": 500 // предел длины одного <speak>-документа (сверх — режется по абзацам)
}
}
Модели описаны в массиве models и грузятся через torch.package при старте
(см. app/synthesis.py). Стандартные имена голосов (alloy, ash, …)
одинаковы для всех моделей, а во model_voices задано, какой реальный спикер
соответствует имени в каждой конкретной модели. Язык текста определяет, какая
модель используется, — из её languages.
Структура проекта
.
├── app/ # FastAPI-приложение
│ ├── main.py # инициализация приложения, /health, /v1/models
│ ├── speech.py # эндпоинты /v1/audio/speech и /v1/audio/voices
│ ├── webui.py # веб-интерфейс /webui и /webui/* (опционально)
│ ├── webui.html # одностраничный UI (inline CSS/JS)
│ ├── synthesis.py # движок синтеза (набор моделей Silero)
│ ├── segmentation.py # определение языка запроса и подготовка сегментов
│ ├── normalize.py # нормализация RU-текста (rutextnorm)
│ ├── ssml.py # SSML-билдер: <s>/<p>/<break>/<prosody>, sentinel-паузы
│ ├── translit.py # транслитерация латинских слов в кириллицу (RU)
│ ├── audio.py # кодирование PCM → mp3/wav/opus/… (ffmpeg)
│ ├── config.py # загрузка config.json
│ └── errors.py # OpenAI-формат ошибок
├── silero_tts/ # локальный шим-обёртка над моделями Silero
├── models/ # файлы моделей .pt (грузятся офлайн)
├── config.json # модели, голоса и рантайм-настройки
├── translit_dict.json # словари транслитерации (exact + abbreviations)
├── tests/ # pytest-тесты (транслит, сегментация, синтез)
├── pyproject.toml # зависимости и сборка
├── run.bat / run.sh # скрипты запуска
├── update.bat / update.sh # скрипты обновления из git
Как это работает
текст → определение языка запроса (есть кириллица → RU, иначе EN)
→ извлечение явных пауз `|||` / `|||мс` (→ sentinel)
→ допостановка точек на концах строк
→ разметка паузных границ (разделители удаляются, вставляется sentinel)
→ разбиение на абзацы и предложения (razdel)
→ транслитерация латинских слов в кириллицу (только RU)
→ нормализация RU (rutextnorm) — см. «Нормализация текста»
→ сборка SSML (<speak>/<p>/<s>/<break>/<prosody>), разбиение по ssml_max_chars
→ синтез одним голосом через ssml_text (автоударения + ручные «+»)
→ кодирование в нужный формат (ffmpeg)
Нормализация текста
Перед синтезом текст нормализуется в два слоя:
- Общий слой — языконезависимые правила, применяются ко всему тексту целиком, независимо от языка.
- Региональный слой — правила конкретного языка, применяются только к сегментам распознанного языка (сейчас реализован только для русского).
сырой текст
→ detect_language() # один язык на весь запрос
→ extract_explicit_pauses() # `|||`/`|||мс` → sentinel-пауза (без цифр в тексте)
→ normalize_common() # общий слой (+ разметка паузных границ sentinel'ом)
→ разбиение на абзацы (пустые строки) и предложения (razdel.sentenize)
→ transliterate_latin() # только для RU: латынь → кириллица
→ normalize_language(sent, lang) # региональный слой по языку
→ build_ssml() → <speak>/<p>/<s>/<break>/<prosody>
→ синтез через ssml_text (put_accent=True, put_yo)
Логика сосредоточена в app/normalize.py, app/translit.py и app/segmentation.py.
Общий слой — normalize_common() (любой язык)
| Вид | Метод | Что делает |
|---|---|---|
| Точка на переносе строки | ensure_line_punctuation |
Если строка заканчивается на букву/цифру без знака препинания, перед переводом строки добавляется .. Нужно для заголовков и списков: иначе razdel склеивает строку со следующей, и синтез читает их слитно без паузы. Строки, уже заканчивающиеся на пунктуацию (. ! ? , : …) или на закрывающие » ) ", не изменяются. Каждый перевод строки трактуется как граница — пауза ставится намеренно. |
| Пробелы | (в normalize_common) |
Схлопывание повторяющихся пробелов и табуляций. |
Региональный слой — normalize_language() (по языку)
Региональные нормализаторы перечислены в словаре
_REGIONAL_NORMALIZERS = {"ru": normalize_russian} (app/normalize.py).
Функция вызывается для каждого сегмента с его языком; языки без собственных
правил (например, английский) проходят без изменений — числа и даты в них
произносит сама модель.
Как добавить язык. Положите функцию-нормализатор в
_REGIONAL_NORMALIZERSпод нужным кодом языка — общий слой останется прежним.
Русский — normalize_russian() (rutextnorm)
Применяется к сегментам языка ru. Методы идут в таком порядке:
| Группа | Методы |
|---|---|
| Типографика и пробелы | normalize_typography, схлопывание повторных пробелов |
| Веб-текст | normalize_web (URL, e-mail, домены, символы @ / . #) |
| Сокращения и разделы | normalize_abbreviations, expand_abbreviations, normalize_sections (§, ст., п.), normalize_case_context |
| Числа | normalize_number_groups (разряды), normalize_ranges (диапазоны), normalize_ordinals + normalize_ordinal_triggers (порядковые), normalize_decimals (десятичные), normalize_negatives (отрицания), normalize_text_with_numbers (цифры → слова) |
| Даты и время | normalize_dates, normalize_time, normalize_scores (счёт N:M) |
| Дроби и проценты | normalize_fractions, normalize_percent, normalize_multipliers (×, кратное) |
| Деньги и величины | normalize_symbol_currency, currency_normalization, normalize_measurements (кг, м, …), normalize_symbols |
| Прочее | normalize_math (+ − = %), normalize_versions, normalize_text_with_phone_numbers |
| Латиница в RU-тексте | обрабатывается до вызова normalize_russian — транслитерацией из app/translit.py (см. «Смешанный RU/EN текст»), поэтому внутренние latin-правила rutextnorm (cyrrilize) фактически получают уже кириллический текст |
| Буква Ё | restore_yo |
Смешанный RU/EN текст
Язык выбирается на весь запрос: хотя бы одна кириллическая буква (U+0400–U+04FF)
— русская модель и RU-спикер, иначе — английская модель и EN-спикер. Голос не
меняется внутри ответа. Чтобы английские слова не выпадали из аудиодорожки
(русская модель Silero их не произносит), перед нормализацией они транслитерируются
в кириллицу (app/translit.py):
- Словарь аббревиатур (
abbreviations) — для токенов только в верхнем регистре:IT→ «айти»,DVD→ «ди-вэ-ди». - Словарь точных соответствий (
exact) — бренды и термины:Ozon→ «Озон»,Python→ «Пайтон» (регистр выхода повторяет вход). - Фонетические правила — для слов вне словаря:
shipping→ «шиппинг».
Словарь лежит в translit_dict.json (путь настраивается в
config.json → runtime.translit_dict) и пополняется без изменения кода —
достаточно добавить строку "spotify": "спотифай" в раздел exact.
Транслитерация применяется только к RU-запросам; чисто английский текст идёт в EN-модель без изменений.
Автоударения (на стороне модели)
При синтезе включены put_accent=True и put_yo=True (app/synthesis.py) —
расстановка ударений и разрешение омографов средствами модели v5_5_ru
(это функция Silero, а не нормализатора).
SSML-разметка (просодия, паузы, ручные ударения)
По умолчанию ("use_ssml": true) синтез идёт через SSML: подготовленные
сегменты собираются в <speak>-документы (app/ssml.py) и подаются в модель
параметром ssml_text. Структура: предложение → <s>, абзац (пустая строка
во входе) → <p>, всё содержимое при заданном rate/pitch оборачивается в
<prosody>.
| Метка во входе | Эффект |
|---|---|
rate / pitch (API/UI) |
Темп и тон речи (<prosody>); невалидное значение → 422 |
| ` | |
| ` | |
+ перед гласной |
Ручное ударение, сохраняется до модели (рядом с автоударением) |
| Скобки/кавычки/markdown | Удаляются как символы; на их границе вставляется пауза 150 мс |
Паузные границы кодируются внутренним sentinel-символом из PUA, который не
виден в модели и в отображаемом тексте (normalized_text в /webui/speech
показывается уже без него). Длинные тексты режутся по границам абзацев на
документы не длиннее runtime.ssml_max_chars (по умолчанию 500 символов —
экспериментальный предел модели).
Откат. Если SSML-путь нужно полностью отключить, поставьте
"use_ssml": false в config.json — синтез вернётся на легаси-ветку text=
по одному сегменту за вызов (без <break>/<prosody>, ручные + и явные
паузы в этом режиме не действуют). Поле rate/pitch при этом игнорируется.
Ограничения
- Скорость воспроизведения (
speed) отличная от1.0не поддерживается. stream_format: "sse"возвращает ошибку.- Синтез выполняется на CPU (
num_threads=4); для реального времени при больших нагрузках рекомендуется GPU.
Лицензия
Модели Silero TTS (включая используемые здесь v5_5_ru и v3_en)
распространяются под лицензией CC BY-NC 4.0 — только для некоммерческого
использования. Убедитесь, что ваше использование соответствует её условиям.