Назад к блогу

Whistle: как устроена модель распознавания речи на 16,9 МБ

Whistle: как устроена модель распознавания речи на 16,9 МБ

Разработчики представили Whistle — компактную модель распознавания речи, которая умещается в один файл на 16,9 МБ и обходится без GPU и сторонних библиотек. Разбор показывает, как устроены энкодер и декодер, где проходит граница между заявленными возможностями и реальными замерами, и почему такой формат весов меняет подход к запуску модели на носимых устройствах и микроконтроллерах.

Whistle — это модель распознавания речи, которая целиком помещается в один файл размером 16.9 МБ и работает на CPU без GPU и без зависимостей. Заявленный класс устройств — мобильные и носимые устройства, роботы, умный дом, автомобили и микроконтроллеры. Такой размер и такая среда исполнения диктуют всё остальное: архитектуру, способ хранения весов, набор платформ и то, как выглядит API. Ниже — механика: из чего модель состоит, что происходит при вызове транскрипции шаг за шагом, как устроен инструментальный вызов поверх аудио и что показывают замеры.

Что заявлено и в каких рамках

Вся модель — один файл 16.9 МБ, который работает на том же CPU-движке, что и Needle, из того же контейнера и с той же квантизацией, без зависимостей и без GPU. Движок поставляется предсобранным для семнадцати целей, включая macOS, Linux, Android, iOS, watchOS, Windows on ARM, RISC-V, MIPS, браузер и WASI-компонент.

Аудио ограничено 16 кГц моно, до 30 секунд за один проход. Транскрипция поддерживает английский, немецкий, французский, испанский, итальянский, нидерландский и польский языки.

Из чего состоит модель

Whistle состоит из энкодера и декодера. Декодер — восемь блоков Laddered Simple Attention шириной 512, с 8 query-голов на 2 KV-головы, 48-мерными query и key, 64-мерными value, 3-tap причинной свёрткой на Q, K и V и engram-поисками на слоях 3 и 7 по 18 432 слота.

Каждый слой декодера дополнительно читает энкодер через gated cross attention по формуле

x ← x + σ(g) · softmax(q̂ K̂ᵀ/√d) V

где gate обучается для каждого слоя, а K и V берутся из клипа. Эти проекции выполняются один раз при поступлении клипа — 375 кадров на 8 слоёв — и затем удерживаются на всё декодирование.

Декодирование ведётся пятью лучами, оцениваемыми нормализованной по длине логарифмической вероятностью. Транскрипт ограничен 320 токенами, словарь — 8192 текстовых фрагмента плюс семь языковых токенов, по одному на язык, поэтому определённый язык выдаётся как токен, а не возвращается отдельно. Смещение по ключевым словам прогоняет автомат Ахо-Корасик по переданным фразам параллельно с лучами и повышает их логарифмическую вероятность по мере продвижения автомата, чтобы нужные имена и термины чаще попадали в транскрипт.

Глубина декодера выбирается при загрузке через --audio-depth. Каждый уровень глубины начиная с 2 слоёв обучался как отдельная модель, но энкодер никогда не срезается: все восемь блоков работают на любой глубине.

Как аудио превращается в признаки

Аудио ожидается как 16 кГц моно, до 30 секунд за один проход. Фронтенд нарезает его окном 25 мс с шагом 10 мс в 80 log-mel бинов, ограниченных полосой 250–3500 Гц и нормализованных по каналу. Тридцать секунд — это 3 000 кадров.

Свёрточный стем из 128 каналов с ядром 9 трижды уменьшает число кадров вдвое, оставляя 375 кадров по одному на 80 мс. Все последующие стадии работают на этой частоте. Энкодер — восемь Simple Attention блоков с четырьмя mHC residual lanes и Monarch Hadamard MLP вместо feed-forward; внимание не причинное. Выход энкодера — одна строка на 80-мс кадр, и её можно использовать как speech embedding.

Как 16,9 МБ распределяются и в каком формате лежат веса

Вся модель — это один файл размером 16,9 МБ, который работает на CPU без зависимостей и без GPU. Формат весов — контейнер .cact, переиспользующий контейнер Needle, Cactus Quants, SIMD-ядра и KV-кэш. Файл модели называется whistle.cact и запускается командой needle --model whistle.cact --audio clip.wav. Точность весов описана как «Whistle 2 to 4 bit», а квантизация — та же, что у Needle, из того же контейнера.

Что происходит при вызове needle_transcribe

needle_transcribe — одна из трёх функций, составляющих весь речевой C API.

Перед запуском декодера движок измеряет диапазон громкости клипа. Если он ниже порога, распознавание не выполняется: транскрипт и язык остаются пустыми, и поиск по лучам не запускается вовсе.

Если порог пройден, энкодер прогоняется целиком: все восемь блоков работают на любой глубине, энкодер никогда не срезается. Проекции K и V из энкодера вычисляются один раз при поступлении клипа — 375 кадров на 8 слоёв — и удерживаются на всё декодирование. Поэтому пять лучей стоят пять коротких кэшей транскрипта, а не пять проходов по аудио.

Декодирование ведётся пятью лучами с ограничением транскрипта в 320 токенов. Каждый вызов возвращает текст, язык, миллисекунды до первого токена и токены в секунду декодера после него.

Как определяется язык

Язык можно задать извне явным флагом: параметр language="de" принудительно устанавливает язык вместо его автоопределения. По умолчанию, если флаг не передан, язык определяется автоматически. Результат каждого вызова возвращает распознанный текст и язык наряду с прочими полями; распознанный текст читается по ключу ["text"].

Что возвращает вызов и как управлять его поведением

Каждый вызов возвращает текст, язык, миллисекунды до первого токена и токены в секунду декодера после него. Флаг word_timestamps=True добавляет каждое слово с его началом, концом и вероятностью, keywords=[...] отдаёт предпочтение именам, которые произносят пользователи, а language="de" принудительно задаёт язык вместо его определения. Движок и веса загружаются один раз и кэшируются. needle.Whistle() — та же модель в виде объекта, для embed(audio) или чтобы держать один настроенный .cact.

Три вызова C API и почему их ровно три

Весь речевой C API состоит из трёх вызовов: needle_load, needle_transcribe и needle_embed. needle_load читает ту модель, которую несёт файл .cact, поэтому один и тот же бинарник может сам транскрибировать, сам отвечать текстом или делать и то и другое сразу. needle_transcribe выполняет транскрипцию, а needle_embed возвращает вложение аудио.

API сведён к трём вызовам, потому что движок не читает переменные окружения: каждое поведение — это скомпилированное значение по умолчанию или явный флаг, поэтому каждый запуск одного и того же блоба даёт один и тот же результат. Явно передавать нужно то, что не задано вкомпилированным значением: модель и аудио через флаги командной строки, а для других частот дискретизации и захвата с микрофона — дополнительный набор [mic], который добавляет soxr и sounddevice. В Python-вызовах явными параметрами являются word_timestamps=True, keywords=[...] и language="de".

Аудио на входе — вызовы инструментов на выходе

При передаче аудиоклипа напрямую движок сам его транскрибирует, сопоставляет полученный транскрипт с инструментами и возвращает один JSON-объект с вызовами и полями речи, причём поля речи имеют префикс audio_. Вызывающая сторона транскрипт не обрабатывает. Тот же движок обслуживает обе модели, поэтому «аудио на входе — вызовы инструментов на выходе» — это один вызов.

В возвращаемом объекте есть function_calls — список вызовов, каждый с полями name и arguments, confidence — числовая уверенность, а также audio_text и audio_language с распознанным текстом и языком:

{"function_calls":[{"name":"set_lights","arguments":{"room":"kitchen","on":false}}],
 "confidence":0.94, "audio_text":"turn off the kitchen lights", "audio_language":"en"}

Развёртывание и платформы

Платформенные папки движка лежат в репозитории Cactus-Compute/needle3, и каждая такая папка содержит бинарник needle, статическую библиотеку libneedle.a и заголовок needle.h, а также загружает любой переданный ей .cact.

Установка Python-пакета — pip install cactus-needle. Для CLI сначала скачивают бинарник под платформу и модель:

needle download macos-arm64
needle download whistle

Затем запускают скачанный бинарник, указав модель и аудио: ./macos-arm64/needle --model whistle.cact --audio clip.wav --audio-word-timestamps. Возможен и запуск через уже установленный needle --model whistle.cact --audio clip.wav.

Аудиовход 16 кГц в формате WAV или в виде raw samples работает сразу после базовой установки. Для других частот дискретизации и для захвата с микрофона требуется набор [mic], добавляющий soxr и sounddevice. Захват с микрофона в терминале выполняет needle whistle playground.

Как сравнивали и что получилось

Стенд и методология

Замеры проводились на CPU Apple M4 Pro на десяти секундах аудио. Каждая модель запускалась на своём официальном рантайме со значениями по умолчанию: C++ движок Whistle с 5 лучами, openai-whisper и moonshine-voice в нестриминговом режиме по всему аудио.

Измерялись три метрики. Время до первого токена — от подачи аудио до первого токена. Декодирование — число токенов, делённое на время после первого токена, чтобы энкодер не считался дважды. Частота ошибок в словах (WER) оценивалась нормализаторами Whisper. Столбцы диаграмм масштабируются отдельно внутри каждой панели.

WER для Whistle измерялся на 86 174 высказываниях, а для Whisper и Moonshine взяты опубликованные авторами цифры с многоязычных чекпоинтов, а не с англоязычных. Для сравнения использовались таблицы 9, 10 и 13 из статьи Whisper и таблица 3 из статьи Moonshine. Строка Whisper соответствует многоязычному чекпоинту, а не base.en. Для FLEURS и MLS результаты усреднены по семи языкам Whistle (для MLS — по шести, без английского), одинаковый набор для всех моделей. Отсутствие тестового аудио в обучающих и валидационных данных Whistle проверено сравнением контрольных сумм аудио и идентификаторов дикторов по каждому тестовому набору.

Повторить замер можно, скачав бинарник и модель командами needle download macos-arm64 и needle download whistle, а затем запустив движок на своём аудио: ./macos-arm64/needle --model whistle.cact --audio clip.wav --audio-word-timestamps.

Результаты

На десяти секундах аудио:

МодельВремя до первого токенаДекодированиеРазмер
Whistle11.1 мс1 319 токенов/с16.9 МБ
Whisper base73.2 мс266 токенов/с145.3 МБ
Moonshine tiny v2———

Время до первого токена у Whistle зависит от длины клипа: 5.9 мс на 5 секундах, 11.1 мс на 10 и 36.3 мс на 30. У Whisper оно постоянно, потому что вход дополняется до 30 секунд.

По WER Whistle опережает на LibriSpeech test-clean и test-other, SPGISpeech, Earnings-22 и в среднем по FLEURS. Whisper base опережает на TED-LIUM, AMI и в среднем по MLS.

Оговорки

Цифры не всегда сопоставимы. Пропущенная полоса означает, что авторы модели этот бенчмарк не публиковали: Moonshine доступен только для английского языка, а Whisper не сообщает результаты по SPGISpeech, Earnings-22 и AMI cleaned. Для AMI у Whisper указан поднабор AMI-IHM, отличный от AMI у двух других моделей. WER для Whistle измерены на 86 174 высказываниях, а для Whisper и Moonshine взяты опубликованные авторами цифры с многоязычных чекпоинтов, а не с англоязычных. Каждая модель запускалась на своём официальном рантайме со своими настройками по умолчанию, поэтому условия различаются.

Что из этого следует на практике

  • Размер и среда определяют применимость. 16.9 МБ в одном файле без зависимостей и без GPU — это профиль, при котором модель влезает туда, где 145.3 МБ Whisper base уже проблема. Предсобранные сборки под семнадцать целей, включая RISC-V, MIPS, браузер и WASI, означают, что переносить нужно только .cact-файл.
  • Пять лучей не умножают работу по аудио. Проекции K и V считаются один раз при поступлении клипа и удерживаются на всё декодирование, поэтому дополнительные лучи стоят коротких кэшей транскрипта, а не повторных проходов по аудио.
  • Глубину декодера можно менять без потери энкодера. --audio-depth выбирает один из обученных уровней, но все восемь блоков энкодера работают всегда — урезание глубины не трогает извлечение признаков.
  • Поведение детерминировано по построению. Движок не читает переменные окружения: всё либо вкомпилировано, либо передано явным флагом. Один и тот же блоб при каждом запуске даёт один и тот же результат — это упрощает воспроизведение и отладку.
  • Тишина отсекается до поиска. Если диапазон громкости клипа ниже порога, поиск по лучам не запускается — на заведомо пустом аудио не тратится работа декодера.
  • Аудио можно сразу превращать в вызовы инструментов. needle_complete принимает клип, сам его транскрибирует и сопоставляет с инструментами, возвращая один JSON с function_calls, confidence и полями audio_. Вызывающей стороне не нужно обрабатывать транскрипт.
  • Язык — это токен словаря. Семь языковых токенов, по одному на язык, позволяют выдавать определённый язык как токен, а не возвращать его отдельно; при этом язык можно и принудительно задать флагом.
  • Замеры latency нужно читать с длиной клипа. У Whistle время до первого токена растёт с длиной аудио (5.9 мс на 5 с, 11.1 мс на 10, 36.3 мс на 30), тогда как у Whisper оно постоянно из-за дополнения входа до 30 секунд — сравнивать эти цифры на разных длинах напрямую нельзя.

Источники

Похожее