370 lines
24 KiB
Markdown
370 lines
24 KiB
Markdown
# Silero TTS Server
|
||
|
||
Локальный сервер синтеза речи на базе [Silero TTS](https://github.com/snakers4/silero-models) с **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
|
||
```bat
|
||
run.bat
|
||
```
|
||
|
||
### Linux / macOS
|
||
```bash
|
||
./run.sh
|
||
```
|
||
|
||
Скрипты автоматически создают `.venv`, устанавливают зависимости и запускают
|
||
сервер на `http://0.0.0.0:5215`.
|
||
|
||
### Обновление из репозитория
|
||
|
||
```bat
|
||
update.bat
|
||
```
|
||
```bash
|
||
./update.sh
|
||
```
|
||
|
||
Останавливают сервис, подтягивают изменения из `origin` (ветка `main`),
|
||
переустанавливают зависимости и перезапускают сервер.
|
||
|
||
> **Зависимости от системы**
|
||
> - **Python 3.12**
|
||
> - **ffmpeg** — нужен для `mp3`/`opus`/`aac`/`flac` (для `wav`/`pcm` не требуется).
|
||
> Ставится автоматически вместе с `imageio-ffmpeg` (входит в зависимости),
|
||
> отдельных действий не нужно. Свой бинарь можно указать в
|
||
> `config.json` → `runtime.ffmpeg_path`.
|
||
|
||
### Ручной запуск
|
||
```bash
|
||
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
|
||
```
|
||
|
||
---
|
||
|
||
## Использование
|
||
|
||
### Базовый запрос
|
||
```bash
|
||
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` |
|
||
|
||
### Проверка работоспособности
|
||
```bash
|
||
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`:
|
||
|
||
```jsonc
|
||
{
|
||
"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)
|
||
```
|
||
|
||
---
|
||
|
||
## Нормализация текста
|
||
|
||
Перед синтезом текст нормализуется в **два слоя**:
|
||
|
||
1. **Общий слой** — языконезависимые правила, применяются ко **всему тексту**
|
||
целиком, независимо от языка.
|
||
2. **Региональный слой** — правила конкретного языка, применяются только к
|
||
сегментам распознанного языка (сейчас реализован только для русского).
|
||
|
||
```
|
||
сырой текст
|
||
→ 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`):
|
||
|
||
1. **Словарь аббревиатур** (`abbreviations`) — для токенов только в верхнем
|
||
регистре: `IT` → «айти», `DVD` → «ди-вэ-ди».
|
||
2. **Словарь точных соответствий** (`exact`) — бренды и термины: `Ozon` → «Озон»,
|
||
`Python` → «Пайтон» (регистр выхода повторяет вход).
|
||
3. **Фонетические правила** — для слов вне словаря: `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` |
|
||
| `|||` | Явная пауза по умолчанию (150 мс) → `<break time="150ms"/>` |
|
||
| `|||1200` | Явная пауза 1200 мс (обрезается до 5000 мс) → `<break time="1200ms"/>` |
|
||
| `+` перед гласной | Ручное ударение, сохраняется до модели (рядом с автоударением) |
|
||
| Скобки/кавычки/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** — только для некоммерческого
|
||
использования. Убедитесь, что ваше использование соответствует её условиям.
|