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.
Результаты
На десяти секундах аудио:
| Модель | Время до первого токена | Декодирование | Размер |
|---|---|---|---|
| Whistle | 11.1 мс | 1 319 токенов/с | 16.9 МБ |
| Whisper base | 73.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 секунд — сравнивать эти цифры на разных длинах напрямую нельзя.