Локальная транскрипция речи в iOS: WhisperKit против SFSpeechRecognizer и SpeechAnalyzer#
Когда я проектировал слой распознавания речи для Lanternly — дневника, который я сейчас разрабатываю, — требование было простым и одновременно жёстким: голосовая заметка превращается в текст прямо на телефоне, и ни один байт аудио не имеет права уйти на сервер. Дневник — это одна из самых личных категорий данных, которые вообще существуют, и «мы всё шифруем на сервере» здесь не звучит убедительно ни для меня, ни для пользователя. Единственный честный ответ — не иметь сервера для этой операции вовсе.
За последние пару лет на iOS появилось три реалистичных способа сделать это: старый добрый SFSpeechRecognizer с флагом on-device, стороннее ядро WhisperKit от Argmax поверх моделей Whisper, и совсем свежий SpeechAnalyzer из iOS 26. В этой статье — без маркетинга, с цифрами и кодом — разбираю, чем они отличаются и как я выбирал между ними.
Зачем вообще нужна локальная транскрипция#
Облачная транскрипция — это самый простой путь: закинул аудио в API, получил текст, следующая строка кода. Но у него есть цена, которую разработчики любят замалчивать. Во-первых, приватность: голос попадает на чужой сервер, и что бы ни было написано в политике конфиденциальности провайдера, юридически и технически это уже не «только на вашем устройстве». Во-вторых, зависимость от сети: диктофонная заметка в метро без связи просто не транскрибируется. В-третьих, стоимость — каждая минута аудио через облачный API это счёт, который растёт вместе с базой пользователей.
Для приложения вроде дневника первый пункт не обсуждается вообще — это не техническое предпочтение, а условие существования продукта. Поэтому вопрос был не «локально или в облако», а «какой именно локальный движок».
Три движка на столе#
SFSpeechRecognizer — часть фреймворка Speech, доступная с iOS 10. У него есть флаг requiresOnDeviceRecognition, который заставляет распознавание работать полностью на устройстве, без обращения к серверам Apple. Модель уже встроена в систему для поддерживаемых языков — разработчику не нужно ничего скачивать.
WhisperKit — открытый Swift-пакет от Argmax, который переносит модели OpenAI Whisper на Core ML и Apple Neural Engine. В 2026 году Argmax объединили WhisperKit с SpeakerKit (диаризация) и TTSKit в единый open-source SDK, но ядро транскрипции осталось тем же: вы сами выбираете и скачиваете модель нужного размера, а WhisperKit исполняет её локально с оптимизацией под ANE.
SpeechAnalyzer / SpeechTranscriber — новый модульный API из Speech framework, появившийся в iOS 26. Это не косметическое обновление SFSpeechRecognizer, а другая архитектура: асинхронный API на AsyncSequence, отдельные модули SpeechTranscriber (длинная речь), DictationTranscriber (короткие фразы, аналог старого recognizer) и SpeechDetector (детекция голосовой активности).
WhisperKit: что под капотом#
WhisperKit не тренирует свою модель — он берёт готовые веса Whisper (tiny, base, small, medium, large-v3 и компактный large-v3-turbo) и конвертирует их в Core ML, распределяя вычисления между CPU, GPU и Neural Engine. Разница в весе моделей огромна: tiny занимает около 40 МБ и подходит скорее для отладки, large-v3 — это уже около 1.5 ГБ. Практический компромисс, который обычно используют в проде на iPhone 13 и новее, — large-v3-turbo: почти точность полного large-v3 при примерно пятикратном выигрыше в скорости и размере около 600 МБ на диске.
Ключевое инженерное преимущество WhisperKit — не сама модель Whisper (она открыта и её мог бы подключить кто угодно), а именно ANE-оптимизация: по независимым бенчмаркам она даёт дополнительное ускорение в 1.3–1.8 раза поверх обычного запуска через Metal, особенно заметное на чипах линейки M3/M4. На iPhone 13 и новее это позволяет стримить транскрипцию в реальном времени, а не ждать окончания записи.
Главный козырь Whisper как модели — мультиязычность из коробки: она обучена на 99 языках одновременно, включая русский, и умеет естественно обрабатывать переключение языка внутри одной фразы («встреча была на русском, но полтора термина по-английски» — для Whisper это норма, а не край возможностей).
SpeechAnalyzer: новый козырь Apple#
SpeechAnalyzer — не просто ещё один API, а ответ Apple на то, что универсальные модели вроде Whisper обгоняли системный recognizer по качеству. И ответ получился сильным: на чистом срезе датасета LibriSpeech SpeechAnalyzer показывает WER (word error rate, долю ошибочно распознанных слов) около 2.12%, на зашумлённом — около 4.56%. Для сравнения, Whisper Small на том же чистом срезе показывает около 3.74% — то есть на английском языке новый системный движок Apple объективно обходит открытую модель сопоставимого размера.
Но у этой победы есть важное ограничение, которое стоит проговорить честно: на момент выхода iOS 26 набор поддерживаемых языков SpeechTranscriber покрывает в основном языки, в которых Apple уже давно инвестировала в Siri, — английский, испанский, французский, немецкий, итальянский, португальский, китайский, японский, корейский. Русского языка в этом списке нет. Для приложения, где транскрипция должна работать на русском (как в Lanternly), это снимает SpeechAnalyzer с рассмотрения сразу — какой бы впечатляющей ни была точность на английском.
Ещё одна архитектурная деталь: языковые модели SpeechAnalyzer не зашиваются в бинарь вашего приложения, а управляются системой через AssetInventory и подтягиваются по запросу — это снижает размер вашего app bundle, но добавляет зависимость от того, что нужный языковой ассет вообще существует и доступен на устройстве пользователя.
Честное сравнение#
Таблица ниже — не «какой движок лучше», а какой движок лучше для какой задачи. Все три имеют право на жизнь в разных сценариях.
| Критерий | SFSpeechRecognizer | WhisperKit | SpeechAnalyzer (iOS 26+) |
|---|---|---|---|
| Точность | Средняя, зависит от языка | Высокая, устойчива к шуму и акценту | Максимальная на поддерживаемых языках (WER ~2.1% на чистой речи) |
| Языки | Ограниченный список locale, но включает ru-RU | 99 языков, включая русский, свободный code-switching | ~9 языков, русского нет на старте iOS 26 |
| Офлайн | Да, при requiresOnDeviceRecognition и наличии локальной модели | Да, полностью, модель качается один раз | Да, ассеты системные, подтягиваются AssetInventory |
| Вес для приложения | 0 МБ — модель уже в системе | От ~40 МБ (tiny) до ~1.5 ГБ (large-v3), практика — ~600 МБ (turbo) | 0 МБ в бинаре, но нужен ассет ОС для языка |
| Минимальная iOS | iOS 10+ (on-device — iOS 13+) | iOS 16+ (зависит от модели) | iOS 26+ |
Практика: как это выглядит в коде#
Ниже — базовый каркас интеграции WhisperKit: инициализация, подбор модели под память устройства, транскрипция уже записанного файла и стриминг во время записи. Это иллюстративный, но рабочий по духу код — именно та точка, куда я закладывал возможность переключения движка в Lanternly, не трогая вызывающий код выше.
import WhisperKit
final class LocalTranscriber {
private var pipeline: WhisperKit?
/// Initializes WhisperKit, downloading and warming up the model
func setUp() async throws {
let config = WhisperKitConfig(
model: "large-v3-turbo", // compact yet accurate option
downloadBase: nil, // model is pulled from the Hugging Face Hub
verbose: false,
logLevel: .none,
prewarm: true, // warm up the Neural Engine ahead of time
load: true
)
pipeline = try await WhisperKit(config)
}
}extension LocalTranscriber {
/// Picks a model based on the device's available memory
static func recommendedModel() -> String {
let physicalMemory = ProcessInfo.processInfo.physicalMemory
let memoryGB = Double(physicalMemory) / 1_073_741_824
switch memoryGB {
case ..<4:
return "tiny" // ~40 MB, for older devices
case 4..<6:
return "base" // ~150 MB, speed/accuracy balance
case 6..<8:
return "small" // ~500 MB
default:
return "large-v3-turbo" // ~600 MB, best balance on the Neural Engine
}
}
}extension LocalTranscriber {
/// Transcribes an already-recorded audio file (m4a/wav) entirely on-device
func transcribe(fileAt url: URL) async throws -> String {
guard let pipeline else {
throw TranscriptionError.notReady
}
let options = DecodingOptions(
language: "ru", // a hint to the model, not a hard constraint
temperature: 0.0,
withoutTimestamps: true
)
let results = try await pipeline.transcribe(
audioPath: url.path,
decodeOptions: options
)
return results?.text ?? ""
}
}
enum TranscriptionError: Error {
case notReady
}extension LocalTranscriber {
/// Streaming transcription while a voice note is being recorded
func startStreaming(onPartialResult: @escaping (String) -> Void) async throws {
guard let pipeline else {
throw TranscriptionError.notReady
}
let streamer = AudioStreamTranscriber(
audioEncoder: pipeline.audioEncoder,
featureExtractor: pipeline.featureExtractor,
segmentSeeker: pipeline.segmentSeeker,
textDecoder: pipeline.textDecoder,
tokenizer: pipeline.tokenizer!,
audioProcessor: pipeline.audioProcessor,
decodingOptions: DecodingOptions(language: "ru")
) { _, result in
onPartialResult(result.text)
}
try await streamer.startStreamTranscription()
}
}Что выбрал бы я#
Если бы русский язык не был обязателен, а минимальной версией был iOS 26, я бы без колебаний взял SpeechAnalyzer — точность выше, модель не раздувает бинарь, а API спроектирован современно, под структурированную конкурентность Swift. Но реальность большинства продуктов — это поддержка русского языка и минимальной версии iOS ниже 26, а значит, выбор идёт между SFSpeechRecognizer и WhisperKit.
SFSpeechRecognizer с requiresOnDeviceRecognition — это честный и почти бесплатный по весу вариант: он уже в системе, не требует загрузки моделей и прекрасно работает как базовый уровень. Именно на нём сегодня стоит транскрипция в Lanternly — я сознательно спроектировал интерфейс так, чтобы движок можно было подменить одной точкой в коде, без изменений выше по стеку. WhisperKit — следующий шаг, когда нужна более высокая устойчивость к шуму, акценту и смешанной речи, а несколько сотен мегабайт на модель — приемлемая цена за это качество.
Чек-лист перед выбором движка#
- Нужен ли обязательно офлайн-режим без какого-либо запроса к серверу — если да, любые «облачные с кэшем» варианты сразу отпадают
- Поддерживается ли ваш основной язык в
SpeechAnalyzerна целевой минимальной версии iOS - Готовы ли вы к дополнительным 150–600 МБ в приложении ради модели WhisperKit
- Нужен ли code-switching (смешение языков в одной фразе) — тогда однозначно WhisperKit
- Достаточно ли встроенной точности
SFSpeechRecognizerдля вашего сценария, или клиенты жалуются на распознавание
Заключение#
Универсального ответа «какой движок лучше» не существует — есть три честные точки на кривой компромисса между точностью, весом и языковым покрытием. SpeechAnalyzer выигрывает по чистой точности там, где хватает поддерживаемых языков. WhisperKit остаётся самым гибким выбором для мультиязычных и акцентированных сценариев ценой веса модели. А SFSpeechRecognizer — рабочая лошадка, которая почти ничего не стоит и часто оказывается достаточной. Проектируя такой слой, самое важное решение — не выбор конкретного движка, а архитектура, которая позволяет заменить его позже без переписывания приложения.



