This commit is contained in:
Shaman
2026-10-01 07:38:01 +03:00
commit 82c306ff69
33 changed files with 2629 additions and 0 deletions
+369
View File
@@ -0,0 +1,369 @@
# 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** — только для некоммерческого
использования. Убедитесь, что ваше использование соответствует её условиям.