Зачем вообще это нужно? (И почему Wispr Flow иногда бесит)
Wispr Flow – отличная штука. Но он облачный. Это значит, что каждый ваш разговор летит куда-то на сервер, жрет токены, а если интернет упал – вы в ауте. На Mac с Neural Engine это вообще кощунство. Зачем платить и ждать, когда можно все делать локально, быстрее и без слежки?
Я собрал свой меню-бар апп за выходные. Работает офлайн, вставляет текст куда угодно по хоткею, и не просит ни копейки. Вот как.
Что будем использовать в 2026 году
Все уже придумано до нас. Главное – выбрать актуальные инструменты.
- WhisperKit 5.3 – последняя стабильная версия на январь 2026. Поддерживает Whisper v4 Large, оптимизирована под CoreML и Neural Engine. Берет аудио с микрофона в реальном времени.
- Xcode 18 – потому что без него на Mac никуда. Swift 6.2 уже умеет в concurrency без боли.
- CoreML 7 – фреймворк для запуска моделей на Apple Silicon. WhisperKit конвертирует модель в .mlpackage, который летает на Neural Engine.
- SwiftUI для меню-бара – да, теперь можно делать меню-бар приложения на SwiftUI без костылей. Спасибо Apple.
1Ставим WhisperKit и качаем модель
Открываем терминал. Не пугайтесь.
git clone https://github.com/argmaxinc/WhisperKit.git
cd WhisperKit
swift package updateМодели Whisper лежат на Hugging Face. Но WhisperKit умеет качать их сам. Выбираем версию – для баланса скорости и точности я взял whisper-large-v4. Но если у вас MacBook Air с базовым M3, возможно, стоит взять whisper-medium.
# Внутри проекта Swift, но проще через Python-скрипт, который идет в WhisperKit
python scripts/download_models.py --model large-v4 --quantization int8Инт8 квантование – чтобы модель заняла меньше памяти и работала быстрее. Точность почти не страдает.
Внимание: большая модель займет около 3 ГБ. Убедитесь, что есть место. Если нет, используйте medium или small.
2Создаем проект меню-бара в Xcode
Запускаем Xcode. Новый проект → macOS → App. Выбираем SwiftUI. Называем, например, LocalWispr.
Добавляем пакет WhisperKit как зависимость: File → Add Packages → вставляем URL репозитория.
Теперь самое важное: меню-бар. В SwiftUI это делается через MenuBarExtra. Открываем LocalWisprApp.swift и пишем:
import SwiftUI
@main
struct LocalWisprApp: App {
var body: some Scene {
MenuBarExtra("LocalWispr", systemImage: "mic.fill") {
ContentView()
}
.menuBarExtraStyle(.window)
}
}Да, вот так просто. ContentView – ваше меню, которое выпадает при клике на иконку в меню-баре.
3Подключаем Whisper и записываем аудио
В ContentView нужно добавить кнопку для старта записи и отображения текста. Но сначала импортируем WhisperKit и AVFoundation для аудио.
Создаем класс-менеджер, который будет управлять записью и транскрипцией. Вот упрощенная версия:
import WhisperKit
import AVFoundation
class TranscriptionManager: ObservableObject {
@Published var transcribedText = ""
private var whisper: WhisperKit?
private var audioEngine = AVAudioEngine()
init() {
setupWhisper()
}
func setupWhisper() {
Task {
do {
// Указываем путь к модели, которую скачали
let modelPath = "/path/to/whisper-large-v4-int8.mlpackage"
whisper = try await WhisperKit(
modelPath: modelPath,
computeUnits: .cpuAndNeuralEngine
)
} catch {
print("Ошибка инициализации Whisper: \(error)")
}
}
}
func startRecording() {
// Настраиваем аудиосессию и запись
let inputNode = audioEngine.inputNode
let recordingFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, time in
// Здесь буфер аудио отправляем в Whisper
self.processAudioBuffer(buffer)
}
audioEngine.prepare()
try? audioEngine.start()
}
func stopRecording() {
audioEngine.stop()
audioEngine.inputNode.removeTap(onBus: 0)
// Финализируем транскрипцию
finalizeTranscription()
}
func processAudioBuffer(_ buffer: AVAudioPCMBuffer) {
// Конвертируем аудио в формат, понятный Whisper
// И отправляем в модель
Task {
if let whisper = self.whisper {
let segments = try? await whisper.transcribe(audioArray: buffer.floatChannelData![0], sampleCount: Int(buffer.frameLength))
if let text = segments?.first?.text {
DispatchQueue.main.async {
self.transcribedText += text + " "
}
}
}
}
}
func finalizeTranscription() {
// Можно добавить постобработку текста
}
}Это базовая реализация. В реальности нужно обрабатывать ошибки, управлять памятью, возможно, использовать буфер для накопления аудио.
4Добавляем глобальный хоткей и авто-вставку
Без хоткея приложение бесполезно. Нужно, чтобы можно было нажать, например, Cmd+Shift+Space и начать диктовать.
Используем Carbon или EventTap. Но в 2026 году есть более простой способ – через KeyboardShortcuts от Sindre Sorhus. Добавляем пакет.
import KeyboardShortcuts
KeyboardShortcuts.onKeyDown(for: .startRecording) {
// Начать запись
transcriptionManager.startRecording()
}Определяем шорткат в настройках. Пользователь сможет сам назначить комбинацию.
Авто-вставка: после транскрипции нужно вставить текст в активное приложение. Используем Accessibility API. Добавляем в Info.plist разрешения.
import ApplicationServices
func pasteText(_ text: String) {
let source = CGEventSource(stateID: .combinedSessionState)
let pasteCommand = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: true) // Cmd+V
pasteCommand?.flags = .maskCommand
pasteCommand?.post(tap: .cgAnnotatedSessionEventTap)
}Но это хак. Лучше использовать AXUIElement для доступности. Однако для простоты можно скопировать текст в буфер обмена и вставить с помощью событий клавиатуры.
Предупреждение: Accessibility API требует, чтобы пользователь вручную дал разрешение приложению в настройках безопасности. Иначе не сработает.
5Собираем, тестируем, фиксим баги
Запускаем приложение. Иконка появляется в меню-баре. Нажимаем хоткей – говорим – отпускаем – текст появляется в буфере и вставляется.
Если что-то не работает, проверьте:
- Разрешения на микрофон и доступность.
- Модель загружена и находится по правильному пути.
- Не забыли добавить фоновый режим в Capabilities.
Теперь сравним с альтернативами.
Чем это лучше или хуже Wispr Flow и других
| Инструмент | Локальный | Скорость на M3 Max | Точность | Цена |
|---|---|---|---|---|
| Наше приложение | Да | ~0.7x реального времени | Как у Whisper Large v4 | Бесплатно |
| Wispr Flow | Нет | Зависит от интернета | Высокая (но модель неизвестна) | Подписка |
| Whisper.cpp | Да | ~1.2x реального времени | Точность оригинала | Бесплатно |
| Scriberr | Да | ~0.9x реального времени | Хорошая | Бесплатно |
Whisper.cpp – отличная вещь, но для меню-бара на Mac менее удобен, чем нативный Swift пакет. Scriberr – тоже хорош, но в 2026 году WhisperKit обгоняет по интеграции с CoreML.
Если вы не хотите собирать приложение сами, посмотрите обзор лучших AI-приложений для диктовки. Но там в основном облачные решения.
Кому это подойдет? (А кому нет)
Беритесь за этот проект, если:
- У вас Mac на Apple Silicon (M1 или новее). Neural Engine ускорит транскрипцию в разы.
- Вам надоело платить за токены или беспокоиться о конфиденциальности.
- Хотите кастомные фичи: например, транскрипцию сразу в Markdown или интеграцию с локальной LLM.
- Вы разработчик и готовы потратить пару дней на сборку.
Не стоит, если:
- Вы не готовы к тому, что что-то сломается после обновления macOS. Локальные решения требуют поддержки.
- Вам нужна транскрипция на лету с субтитрами – тогда смотрите Whisper.cpp в продакшене.
- Вы хотите готовое решение без программирования – тогда EasyWhisperUI или TranscriptionSuite.
Для Windows-пользователей есть свои варианты, например, локальный диктофон для Windows.
А теперь – неочевидный совет. Если вы сделали это приложение, добавьте возможность выбора модели на лету. И сохраняйте историю транскрипций в SQLite. Потому что через месяц вы забудете, что диктовали, а это может быть важно.
Удачи. И помните: лучший инструмент – тот, который вы сделали сами под свои нужды.