# 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 // предел длины одного -документа (сверх — режется по абзацам) } } ``` Модели описаны в массиве `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-билдер: /

//, 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 (/

///), разбиение по 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() → /

/// → синтез через 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: подготовленные сегменты собираются в ``-документы (app/ssml.py) и подаются в модель параметром `ssml_text`. Структура: предложение → ``, абзац (пустая строка во входе) → `

`, всё содержимое при заданном `rate`/`pitch` оборачивается в ``. | Метка во входе | Эффект | |---------------------------|------------------------------------------------------------------------| | `rate` / `pitch` (API/UI) | Темп и тон речи (``); невалидное значение → `422` | | `|||` | Явная пауза по умолчанию (150 мс) → `` | | `|||1200` | Явная пауза 1200 мс (обрезается до 5000 мс) → `` | | `+` перед гласной | Ручное ударение, сохраняется до модели (рядом с автоударением) | | Скобки/кавычки/markdown | Удаляются как символы; на их границе вставляется пауза 150 мс | Паузные границы кодируются внутренним sentinel-символом из PUA, который не виден в модели и в отображаемом тексте (`normalized_text` в `/webui/speech` показывается уже без него). Длинные тексты режутся по границам абзацев на документы не длиннее `runtime.ssml_max_chars` (по умолчанию 500 символов — экспериментальный предел модели). **Откат.** Если SSML-путь нужно полностью отключить, поставьте `"use_ssml": false` в `config.json` — синтез вернётся на легаси-ветку `text=` по одному сегменту за вызов (без ``/``, ручные `+` и явные паузы в этом режиме не действуют). Поле `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** — только для некоммерческого использования. Убедитесь, что ваше использование соответствует её условиям.