Все статьи

· Casimiro Ferreira· 8 мин чтения

Семейство речевых библиотек на чистом ONNX

  • ONNX
  • TTS
  • voice cloning
  • VAD
  • self-hosted
  • phoonnx

ONNX — это формат файла для обученной нейросети: веса и вычислительный граф, замороженные, без зависимости от фреймворка, который её обучил. Модель, экспортированная в ONNX, может выполняться через ONNX Runtime — небольшой движок вывода, который делает только одно: исполняет этот граф. Он не знает, как модель обучалась, не поддерживает обучение и не требует установленных PyTorch или TensorFlow.

Несколько наших библиотек придерживаются одного правила: во время выполнения единственные зависимости — это onnxruntime и numpy. Не «в основном» — сам импорт пакета никогда не подтягивает фреймворк обучения. audiosronnx (расширение полосы пропускания и шумоподавление), voiceclonnx (клонирование голоса), speakeronnx (эмбеддинги дикторов), speechonnxmetrics (оценка), stressonnx (словесное ударение), vadonnx (обнаружение речевой активности) и phoonnx (фонемизация и синтез речи) — все следуют этому правилу, каждая в собственном пакете PyPI. Ещё две, phoonnx.js и precise-onnx-js, применяют ту же идею в браузере, используя вместо этого onnxruntime-web.

Зачем это нужно

Очевидный способ поставлять речевую модель — держать фреймворк обучения и для вывода тоже. Это удобно во время разработки. Это обуза в продакшене.

  • Размер установки. Установка PyTorch + CUDA переваливает за гигабайты ещё до загрузки хотя бы одной модели. onnxruntime и numpy вместе — это несколько десятков мегабайт.
  • Нет CUDA, за которой нужно следить. Согласование драйвера GPU, версии CUDA toolkit и сборки фреймворка — это постоянный источник поломок. ONNX Runtime только для CPU полностью это обходит и всё равно выполняет тот же граф на GPU, где он доступен.
  • Работает на скромном оборудовании. Raspberry Pi или десятилетний ноутбук может комфортно запускать onnxruntime. Обычно он не может запустить полный стек PyTorch с приемлемой скоростью или вообще установить его на 32-битной или ограниченной по памяти плате.
  • Один артефакт, любая платформа. Один и тот же файл .onnx работает без изменений на Linux, macOS, Windows — и, через onnxruntime-web, внутри вкладки браузера. Нет отдельного шага экспорта под каждую цель.
  • Никаких конфликтов версий обучение/обслуживание. Стек обучения фиксирует конкретные версии фреймворка и CUDA. Стек обслуживания хочет как можно меньший и стабильный набор зависимостей. Разделение их означает, что можно обновлять один, не ломая другой.

Чего это стоит

Ограничение реально, и оно не бесплатно.

Нельзя дообучать в процессе. У ONNX-графа нет оптимизатора, нет обратного прохода. Каждая из этих библиотек трактует модели как фиксированные артефакты: вы их загружаете и запускаете. Обучение или дообучение происходит отдельно, в исходном фреймворке, а результат впоследствии экспортируется в ONNX. stressonnx и speechonnxmetrics оба держат опциональный экстра export, который подтягивает torch исключительно для этого офлайн-шага конвертации — никогда для вывода.

Не каждая архитектура экспортируется чисто. Динамический поток управления, пользовательские CUDA-ядра или операции без эквивалента в ONNX могут заблокировать прямой экспорт. README audiosronnx прямо документирует это: он ведёт список неотправленных моделей, которые были оценены и отклонены, с указанием причин, вместо того чтобы делать вид, что каждая исследовательская модель переносится без проблем.

Предобработку приходится реализовывать вручную заново. Такой фреймворк, как PyTorch или Kaldi, поставляет быстрые, протестированные реализации STFT (превращение волновой формы в спектрограмму), мел-фильтрбанковых признаков и передискретизации. Как только сама модель перестаёт зависеть от этого фреймворка, её предобработка тоже не может — speakeronnx заново реализует 80-полосный лог-мел фильтрбанк на чистом NumPy именно по этой причине, а audiosronnx делает то же самое для STFT и передискретизации. Это больше кода, который нужно сделать правильно, и он требует собственных тестов на паритет с оригиналом.

Одна задача, несколько движков, один API

Обученные речевые модели сильно различаются по языку, условиям записи и целевому домену. Модель верификации диктора, обученная на чистой прочитанной речи, может дать сбой на телефонном аудио. Модель клонирования голоса, настроенная на перенос тембра для английского, может потерять разборчивость на тональных языках. Нет единой модели, которая выигрывает везде, так что выбор одной заранее — это догадка.

Каждая библиотека этого семейства выбирает одну задачу и оборачивает несколько независимо опубликованных моделей за одним интерфейсом, так что смена движка — это однострочное изменение, а не переписывание.

audiosronnx разделяет свои две задачи — шумоподавление и расширение полосы пропускания (превращение узкополосной записи, например 8 кГц телефонного аудио, в более полнозвучный сигнал с более высокой частотой дискретизации) — за двумя загрузчиками, каждый на нескольких движках:

from audiosronnx import load_denoise, load_sr

clean, rate = load_denoise("dpdfnet").denoise("noisy_call.wav")   # remove noise
wide, _ = load_sr("lavasr").upscale(clean, rate)                  # extend to 48 kHz

load_denoise в настоящее время регистрирует десять шумоподавителей (dpdfnet, mossformer2, frcrn, mpsenet, gtcrn, cmgan, metadenoiser, mossformergan, voicefixer, deepfilternet), от модели 0.54 МБ до модели 415 МБ, под разными лицензиями. load_sr регистрирует семь расширителей полосы пропускания (lavasr, novasr, flowhigh, hifiganbwe, apbwe, sidon, callenhancer). Доминируемые модели — те, которых другой движок превосходит по каждому измеренному показателю — всё равно остаются в реестре, так что опубликованный результат бенчмарка остаётся воспроизводимым по запросу.

voiceclonnx применяет тот же подход к клонированию голоса — преобразованию голоса в существующей записи так, чтобы он звучал как другой референсный диктор, минуя текст:

from voiceclonnx import VoiceCloner

cloner = VoiceCloner(engine="facodec")
out = cloner.clone_voice("source.wav", "reference.wav", "out.wav")

Зарегистрировано десять движков (facodec, openvoice, chatterbox, triaan, cosyvoice, bicodec, knnvc, focalcodec, lscodec, rvc), охватывающих шесть различных семейств моделей — kNN-замена признаков, факторизованный кодек, flow-matching, перенос тонального окраса, AR codec-LM и кодек с разделением диктора. За кулисами каждый поставляется с опубликованными цифрами разборчивости и сходства дикторов, так что выбор движка — это сравнение, а не подбрасывание монеты.

vadonnx применяет этот паттерн к обнаружению речевой активности — определению, какие части аудиопотока вообще содержат речь:

from vadonnx import load_vad

vad = load_vad("silero")
segments = vad.get_speech_segments(audio, sample_rate=16000)
# -> [SpeechSegment(start=0.32, end=2.27), SpeechSegment(start=3.27, end=4.45), ...]

Зарегистрировано шесть семейств моделей (silero, marblenet, pyannote, fsmn, speechbrain, ten), а декларативная IOSignature позволяет одному общему движку приводить в действие большинство из них или указывать на любой пользовательский файл .onnx VAD.

speakeronnx извлекает эмбеддинг диктора — вектор фиксированной длины, суммирующий, кто говорит, независимо от того, что было сказано — и сравнивает два эмбеддинга по косинусному сходству, чтобы проверить, один ли это диктор в двух клипах:

from speakeronnx import SpeakerEmbedder, cosine

embedder = SpeakerEmbedder(model="wespeaker-resnet34")
alice1 = embedder.embed("alice_clip1.wav")
alice2 = embedder.embed("alice_clip2.wav")
print(cosine(alice1, alice2))   # e.g. 0.82 - same speaker

Он регистрирует девять моделей из четырёх семейств архитектур (WeSpeaker, CAM++, ERes2Net, ReDimNet), с опубликованными размерностями эмбеддингов и лицензиями.

stressonnx расставляет словесное ударение для фронтендов синтеза речи — какой слог слова несёт акцент, информация, которую многие языки не прописывают на письме (русское за́мок против замо́к делят все буквы). Он регистрирует нейросетевой конвейер для русского, второй для украинского и белорусского и бэкенд на правилах и словаре, покрывающий 26 языков вообще без нейросетевого вывода:

from stressonnx import stress

stress("старинный замок стоит на горе", "ru")
# 'стари́нный за́мок сто́ит на горе́'

phoonnx фонемизирует текст (превращает написанные слова в звуковые единицы, которые потребляет модель TTS) и выполняет синтез речи на 17 зарегистрированных движках синтеза и голосах, экспортированных из нескольких экосистем (нативный phoonnx, Piper, Mimic3, Coqui, MMS, Transformers):

import wave
from phoonnx.voice import TTSVoice

voice = TTSVoice.load("model.onnx", "model.json")
with wave.open("hello.wav", "wb") as wav_file:
    voice.synthesize_wav("Hello world!", wav_file)

phoonnx.js переносит те же пути токенизатора в браузер с onnxruntime-web, а precise-onnx-js портирует обнаружение слова активации (извлечение MFCC-признаков плюс ONNX-классификатор, совместимый с моделями Mycroft Precise) на JavaScript, оба без сервера:

import { loadVoice, synthesizeWav } from "phoonnx";
import { getVoice } from "phoonnx/voices";

const voice = await loadVoice(getVoice("phoonnx_eu-ES_dii_unicode")!);
const blob = await synthesizeWav(voice, "Kaixo mundua!");

Веса для audiosronnx (18 опубликованных моделей) и voiceclonnx (10 опубликованных моделей) живут как отдельные загрузки в организации TigreGótico на Hugging Face, скачиваются при первом использовании и кэшируются локально, так что выбор другого движка — это изменение конфигурации, а не передеплой.

Замыкание цикла: оценка движков вместо угадывания

Регистрация множества движков за одним API окупается только тогда, когда можно определить, какой из них на самом деле лучше для вашего входа. Именно для этого существует speechonnxmetrics: библиотека метрик, построенная на том же ограничении numpy + onnxruntime, так что оценка модели не требует ничего лишнего для установки.

Она группирует метрики в три вида. Безреференсные оценки MOS — UTMOS, DNSMOS, NISQA, SIGMOS — предсказывают Mean Opinion Score, оценку естественности от 1 до 5, которую поставила бы группа человеческих слушателей, без необходимости в чистом референсе для сравнения. Интрузивные метрики — STOI (кратковременная объективная разборчивость), SI-SDR (масштабно-инвариантное отношение сигнал/искажение), MCD (мел-кепстральное искажение) — требуют совпадающего чистого референса и измеряют, насколько к нему близок результат. Метрики на основе ASR — WER (частота ошибок по словам) и CER (частота ошибок по символам) — прогоняют распознаватель речи по результату и сравнивают транскрипт с ожидаемым текстом, отлавливая случаи, когда модель производит аудио, звучащее нормально, но говорящее не те слова.

import speechonnxmetrics as s

print(s.score("degraded.wav", ["utmos"]))
# -> {'utmos': 4.41...}

print(s.score("clone_output.wav", ["stoi", "mcd", "si_sdr"], ref="source.wav"))
# -> {'stoi': 0.662..., 'mcd': 10.459..., 'si_sdr': -26.937...}

Это превращает выбор движка из прослушивания в таблицу. voiceclonnx публикует именно такое сравнение для своих десяти движков клонирования — WER относительно исходного транскрипта плюс отдельная оценка сходства дикторов для каждого, так что «facodec даёт 0% WER» или «lscodec жертвует WER ради более сильного переноса тембра» — это измеренные утверждения, а не впечатления. Умножьте это на языки и условия записи, и ручное сравнение перестаёт быть реалистичным; объективная метрика — вот что делает реестр из десяти движков пригодным для использования, а не непосильным.

Где это полезно

Если вам нужна офлайн-обработка речи — очистка записи, клонирование голоса, определение, кто говорит, или синтез — на оборудовании, которое никогда не увидит GPU, вот форма, которую стоит искать: небольшая зависимость времени выполнения, выбор из опубликованных моделей вместо одной фиксированной по умолчанию и способ измерить, какая на самом деле работает в вашем случае. Каждая библиотека выше — это pip install, лицензирована на уровне кода как MIT или Apache (отдельные веса моделей несут собственные апстрим-лицензии, задокументированные для каждого движка), и работает одинаково на ноутбуке, сервере или Raspberry Pi.

Свяжитесь с нами через /contact или посмотрите, что ещё мы строим на /services.