Files
Shaman 82c306ff69 init
2026-10-01 07:38:01 +03:00

370 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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** — только для некоммерческого
использования. Убедитесь, что ваше использование соответствует её условиям.