Так вышло, что у меня образовалось некоторое количество свободного времени, которое мне захотелось потратить на небольшой хобби-проект, который, как надеюсь, даже имеет шанс превратиться в стартап. Идея была в том, чтобы создать очень доступный сервис транскрипции потока речи в текст. Я решил назвать его Blobfish, т.к. мне очень нравится внешний вид рыбы-капли. Только давайте без намёков на всем известный мем!
По моему мнению, конкурентоспособность подобного сервиса может определяться или исключительным качеством транскрибирования или же значительно более низкой, чем у конкурентов, ценою. К сожалению, ни мои компетенции, ни имеющиеся в распоряжении ресурсы не позволяли пойти по первому пути. Действительно, я планировал использовать компоненты, находящиеся в публичном доступе, потому на сногсшибательное качество надеяться не приходилось. Оставалось рассчитывать на привлекательность низкой цены. Было решено остановиться на стоимости $0.002 за минуту.
Архитектура и реализация
Основная идея реализации подобного сервиса достаточно проста. Речевой поток разбивается на фрагменты, которые поступают в offline-модель, транскрибирующую их в текст. Открытых моделей, поддерживающих поточную транскрипцию, я не нашёл. Потому ориентировался на Whisper, который обеспечивает достаточно высокое качество, но, к сожалению, не поддерживает потоковую обработку. Отсюда и возникла потребность в вышеупомянутом сегментировании речи. Для решения этой задачи я вначале рассматривал библиотеки VAD, но эксперименты показали, что те не способны сегментировать речь на законченные фрагменты. В итоге остановился на pyannote-audio, доступных альтернатив которому просто не нашлось. Эта библиотека плохо справляется с диаризацией (насколько мне известно, это задача вообще толком не решается в наши дни), но сегментировать речь умеет очень недурно.
Не имея стороннего финансирования мне было важно выбрать архитектуру, обеспечивающую максимальное масштабирование. Т.е. вместо аренды выделенного сервера, оборудованного мощными видеокартами, я решил начинать с копеечных VPS, которые можно добавлять по мере необходимости. В случае успеха можно подключать тяжёлую CUDA-артиллерию, опять же, при наличии финансового обоснования. Исходя из данных требований архитектура системы предусматривала один управляющий сервер с публичным IPv4 адресом и множество рабочих узлов, на которых и будет происходить транскрипция. Управляющий сервер принимает и декодирует пользовательский аудио-поток через веб-сокет, перенаправляя его PCM-версию одному из рабочих узлов для сегментации, а далее в реальном времени назначает полученные сегменты рабочим узлам для транскрипции. По мере получения текстовых фрагментов те возвращаются обратно пользователю через веб-сокет. Управляющий сервер ведёт учёт потребляемых ресурсов, а также балансов пользователей.
Важно, чтобы управляющий сервер был максимально эффективным, поскольку выбранная архитектура не предусматривает его репликацию/шардирование. Я решил его реализовать на Rust, т.к. последние годы пишу в основном на этом языке и успел его полюбить (хотя думаю, что Go также неплохо подошёл бы на эту роль). Итак, сервер базируется на экосистеме tokio
: принимает запросы с помощью axum
, взаимодействует с PostgreSQL через tokio-postgres
, HTTP вызовы совершает при помощи reqwest
, а к веб-сокетам соединяется через tokio-tungstenite
.
Изначально я планировал на Rust писать и софт для рабочего узла, но очень быстро понял, что почти все нужные мне AI модели доступны почти исключительно из Python, так что сервер рабочего узла было решено писать на этом языке. Для меня это был не самый приятный выбор, поскольку никогда профессионально не писал на Python. Набор пакетов также довольно стандартный: uvicorn
в качестве сервера, pyannote-audio
и faster-whisper
соответственно для сегментирования и транскрипции.
Поскольку над данным проектом работал в гордом одиночестве, то, не будучи frontend разработчиком, я совершенно проигнорировал web-оформление, предоставляя пользователю голый API. Если система окажется востребованной, то, наверное, добавлю простенький сайт с онлайн-демонстрацией.
Проблемы
Поскольку Whisper это offline-модель, обученная на временном окне фиксированного размера (обычно 30 секунд), то ей приходится скармливать достаточно длинные аудио-сегменты. На коротких сегментах вычислительные ресурсы прожигаются впустую, а точность транскрипции сильно падает. Поэтому пришлось разбивать аудио-поток на сегменты от 15-ти до 30-ти секунд. А это означает некоторую задержку во время транскрипции. Увы, о подборе слов в реальном времени, как это делает Google, остаётся только мечтать.
Так как проект, можно сказать, написан на коленке без оглядки на бизнес-ангелов и инвесторов, то количество рабочих узлов весьма мало, а значит и ресурсов для транскрипции. Так что при наплыве пользователей неизбежны вылеты. Увы, транскрипция требует много вычислительных ресурсов (как уже писал, я пока не использую видеокарты в целях экономии). Но это дело поправимое: если будет спрос, то я добавлю новые узлы.
Whisper, как и множество других моделей, страдает галлюцинациями. Поведение модели вполне пристойно, пока есть отчётливая речь. Но когда настаёт тишина или же речь становится неразборчивой в транскрипте то и дело всплывают однотипные повторяющиеся фразы. Пока не очень понимаю, как с этим бороться.
Пробуем
-
Создаём email-токен для регистрации:
curl -X POST https://api.blobfish.no/token -H "Content-Type: application/json" -d '{"email":"vasya.pupkin@gmail.com"}'
{}
На почту должно прийти письмо с токеном, которое будем использовать для регистрации пользователя. Например, такой:
OGcpfTLcSo2ifZQhYjkJZINuMfu/VwtfK92PsjbdjcxI4S7NkfRKEVqAVh3P6PND
. -
Регистрируем пользователя с токеном из письма:
curl -X POST https://api.blobfish.no/user -H "Content-Type: application/json" -H "Authorization: Bearer OGcpfTLcSo2ifZQhYjkJZINuMfu/VwtfK92PsjbdjcxI4S7NkfRKEVqAVh3P6PND" -d '{}'
{"id":"6319b452-b568-423c-b7d3-ff6d336f58f2","token":"6M/vC84qQnSpooQP5gVoIZiN4mWTpPeRlFVP3ugfKwLYfIh7P74FdUpFNubZzkGG","tokenId":"454d0a5f-9f8b-432e-a567-af84368d736f"}
Безопасно сохраняем вернувшийся токен, так как он является ключом в новому аккаунту. Старый токен для подтверждения почты можно забыть, он больше недействителен.
-
Теперь посмотрим на созданный аккаунт:
curl https://api.blobfish.no/user -H "Authorization: Bearer 6M/vC84qQnSpooQP5gVoIZiN4mWTpPeRlFVP3ugfKwLYfIh7P74FdUpFNubZzkGG"
{"user":{"balance":"1.0","campaign":"05a1e610-3483-4142-bc98-3954c9eae00e","createdAt":"2024-06-16T10:13:33.165454Z","email":"vasya.pupkin@gmail.com","id":"6319b452-b568-423c-b7d3-ff6d336f58f2"}}
У нас на счету $1, что эквивалентно 500 минутам транскрипции.
-
Теперь можно транскрибировать. Для этого будем использовать команды
sox
иwebsocat
:sox -d -t vorbis -q - | websocat -bE "wss://api.blobfish.no/transcribe?tariff=basic" -H "Content-Type: audio/ogg; codecs=vorbis" -H "Authorization: Bearer 6M/vC84qQnSpooQP5gVoIZiN4mWTpPeRlFVP3ugfKwLYfIh7P74FdUpFNubZzkGG"
{"begin":3.9290938,"end":19.82347,"text":"Именно курс, который по итогам торгов формировался на Морсбирже, используется для определения официального курса к центральным банкам. И теперь этой точки отсчёта нет. В среду днём 12 июня Минфин, американский Минфин, вёл санкции. В шесть часов вечера по Москве."}
{"begin":20.0,"end":45.925343,"text":" В Мосбирже объявилось, что останавливает торги евро и доллар. Пропаганда отреагировала только вечером. Сухая сводка. Спокойствие, только спокойствие. Центральный банк разберётся. Под санкциями Московская биржа с завтрашнего дня она не будет проводить торги по долларам и евро. Как пояснил Центробанк, сделки продолжатся на внебиржевом рынке. А для определения курса инструментов УЦБ есть. Среди них банковская отчётка."}
Таким образом клиент шлёт в веб-сокет двоичный поток OGG Vorbis (пока доступен только этот формат, в будущем, может, добавлю ещё и Opus), а в ответ получает JSON-строки с транскрибированными сегментами.
ababo Автор
К сожалению, мне вам нечего дать. На данный момент есть только REST+Websocket API для сторонних приложений. Т.е. приложения или демки для конечного пользователя пока нету.