LirafyБлогWendo ↗
Українська

Блог

Whisper на телефоні без інтернету: як диктує Wendo

Так, Whisper працює на телефоні без інтернету. З whisper.cpp і моделлю ggml, яку завантажують один раз, телефон сам перетворює голос на текст, і аудіо не залишає пристрою. Так диктує Wendo, наш SSH-клієнт для iOS і Android, з якого стежать за агентами на кшталт Claude Code. Тут розповідаємо, як це зроблено: які моделі ми пропонуємо, як вони завантажуються, у якому форматі ми записуємо, що робимо з тим, що повертає Whisper, і навіщо є ще варіант з API OpenAI або Groq і вашим власним ключем.

Whisper без інтернету на телефоні: навіщо він нам

Писати агентові коду з клавіатури телефона повільно. Інструкції для Claude Code зазвичай довгі («подивись, чому падає тест логіну, і не чіпай міграцію»), і саме їх найменше хочеться набирати великим пальцем. Говорити швидше.

Проблема в тому, що ви кажете. Диктуючи агентові, ви називаєте файли, шляхи, гілки, а часом і речі клієнта. Відправляти такий запис на чужий сервер, щоб отримати текст, хочеться не завжди, а з поганим зв’язком це й не працює. Тому типовий рушій у Wendo — локальний Whisper. У lib/core/settings.dart початкове значення speechEngine = 'whisper' з моделлю base, а екран налаштувань пояснює це одним рядком: «Whisper розпізнає мовлення на телефоні через whisper.cpp: аудіо не надсилається на жоден сервер».

whisper.cpp у Flutter: whisper_ggml і record

whisper.cpp є реалізацією Whisper, моделі розпізнавання мовлення від OpenAI, на C/C++, і використовує формат ваг ggml. Він працює на CPU без важких залежностей, а в його репозиторії є офіційні приклади для iOS і Android. У Flutter не треба писати біндинги вручну: на pub.dev є кілька пакетів-обгорток.

Wendo використовує два, згідно з pubspec.yaml:

record: ^7.1.2
whisper_ggml: ^2.8.1

record записує мікрофон, а whisper_ggml викликає whisper.cpp.

Є деталь, яку не видно одразу. whisper_ggml перетворює вхідне аудіо через ffmpeg, а тягнути ffmpeg у мобільний застосунок лише заради цього означає носити велику залежність. Wendo вже записує у форматі, якого чекає whisper.cpp, тому pubspec.yaml підміняє цей пакет локальною заглушкою:

dependency_overrides:
  # whisper_ggml перетворює аудіо через ffmpeg; Wendo вже записує WAV 16 кГц моно (див. пакет).
  ffmpeg_kit_flutter_new_min:
    path: packages/ffmpeg_kit_stub

Заглушка (packages/ffmpeg_kit_stub/lib/ffmpeg_kit.dart) реалізує лише execute, який завжди «падає». Отримавши цю помилку, whisper_ggml розпізнає файл як є. Якщо ви збираєте офлайн-Whisper у Flutter на цьому пакеті й самі записуєте WAV 16 кГц, так само можна позбутися ffmpeg.

Диктування з Whisper на телефоні: запис іде у WAV 16 кГц, whisper.cpp розпізнає його завантаженою моделлю ggml, а текст очищається; API OpenAI або Groq — за бажанням

Які моделі Whisper є і скільки вони займають

Wendo не вшиває жодної моделі в застосунок. У розділі «Налаштування › Мовлення» їх сім, вони описані в WHISPER_MODELS у lib/core/dictation.dart і поділені на дві групи:

  • Багатомовні: Tiny (75 MB), Base (142 MB), Small (466 MB) і Turbo, тобто large-v3-turbo (застосунок показує 1.6 GB).
  • Лише англійська: tiny.en, base.en і small.en, з тими самими розмірами, що й багатомовні версії.

Ці розміри збігаються з офіційною таблицею моделей whisper.cpp (models/README.md у репозиторії ggml-org), де вказано 75 MiB, 142 MiB, 466 MiB і 1,5 GiB для large-v3-turbo. Різниця в Turbo тільки в одиницях: 1,5 GiB це приблизно 1,6 GB.

Головний README whisper.cpp також наводить приблизний обсяг пам’яті для кожного розміру: близько 273 MB для tiny, 388 MB для base і 852 MB для small. На комп’ютері це байдуже. На телефоні, де водночас відкриті сесія SSH, термінал і, може, браузер, це відчутно.

Яку модель Whisper вибрати для телефона

Описи в самому застосунку все підсумовують: Tiny «Найшвидша, нижча точність», Base «Швидка, добра точність», Small «Повільніше, чудова точність», Turbo «Найвища точність». Для диктування агентові base є добрим стартом, тому вона й стоїть за замовчуванням.

Моделі .en розуміють тільки англійську. Wendo примусово задає це в коді, тож налаштування мови на них не впливає:

// Моделі «.en» розуміють тільки англійську.
final language = whisperModel.endsWith('.en') ? 'en' : settings.whisperLanguage;

Якщо ви завжди диктуєте англійською, вони того варті: у репозиторії Whisper від OpenAI сказано, що версії .en дають кращий результат англійською, особливо в розмірах tiny і base. Якщо ви змішуєте мови або говорите українською з назвами функцій англійською, беріть багатомовну модель із зафіксованою мовою. Автовизначення (auto) стоїть за замовчуванням, але в диктовці з трьох слів у Whisper замало аудіо, щоб вгадати мову, і фіксована мова знімає це питання.

Поміряйте затримку на своєму телефоні

Ми не публікуємо час розпізнавання, бо він залежить від телефона, моделі й того, скільки ви говорите. Натомість Wendo дає поміряти його самим. У «Налаштування › Мовлення» є кнопка мікрофона для перевірки рушія, а під розпізнаним текстом з’являється рядок із рушієм, моделлю, мовою, форматом, тривалістю аудіо та затримкою в мілісекундах. Дані беруться з TranscriptionResult, який зберігає durationMs і latencyMs для кожного розпізнавання. Спробуйте base і small з тією самою фразою і вирішуйте за своїми цифрами.

Як завантажується модель

Моделі беруться з репозиторію whisper.cpp на Hugging Face зі стандартною назвою файла:

const _hf = 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main';
String _fileName(String id) => 'ggml-$id.bin';

Коли ви торкаєтеся моделі, якої ще немає, downloadModel спершу завантажує її у файл .part у тимчасовій теці, а після завершення переносить у теку документів застосунку. Якщо теки на різних томах і перейменувати не вдається, файл копіюється, а тимчасовий видаляється. Тож обірване завантаження ніколи не видаватиметься за робочу модель.

Модель однаково може пошкодитися. Коли whisper.cpp відповідає «failed to load the model», Wendo не показує сиру помилку: він повідомляє, що модель пошкоджена, і просить видалити її в «Налаштування › Мовлення» та завантажити знову.

Запис у форматі, якого чекає whisper.cpp

whisper.cpp працює з WAV PCM 16 біт, 16 кГц, моно; його README навіть дає команду ffmpeg для конвертації в цей формат. Wendo одразу записує так через record:

const RecordConfig(
  encoder: AudioEncoder.pcm16bits,
  sampleRate: _sampleRate, // 16000
  numChannels: 1,
  androidConfig: AndroidRecordConfig(audioSource: AndroidAudioSource.voiceRecognition),
)

На Android застосунок просить джерело VOICE_RECOGNITION, створене саме для розпізнавання мовлення. Шматки PCM накопичуються в пам’яті, поки ви говорите. Коли ви зупиняєтеся, Wendo чекає 200 мс на останній шматок і відкидає записи коротші за 0,3 секунди, бо це зазвичай випадкові дотики. Потім додає попереду 44-байтовий заголовок RIFF (encodeWav у lib/components/settings/byok.dart), пише dictation.wav у кеш, розпізнає і видаляє файл у finally, хоч би як усе пройшло.

Модель лишається завантаженою між диктовками (keepModelLoaded: true), тож платити за її завантаження доводиться лише першій. Якщо ви міняєте модель або видаляєте ту, що зараз використовується, Wendo спершу звільняє контекст whisper.

Прибрати зайве з того, що повертає Whisper

Whisper не завжди повертає продиктований текст. Коли він не чує голосу, то пише позначки на кшталт [BLANK_AUDIO] або фрази в дужках типу «(тиша)». У редакторі вони не заважають, а в терміналі потрапляють у запрошення агента. _stripNonSpeech прибирає те, що стоїть у квадратних дужках великими літерами, і відкидає весь результат, якщо там тільки дужки або зірочки.

Очищений текст іде в редактор або в термінал. За замовчуванням він вставляється без Enter, щоб ви перевірили його перед відправкою, якщо тільки не ввімкнено Автонадсилання. Кожна диктовка лягає в локальну історію («Налаштування › Мовлення › Історія диктовки»), звідки її можна надіслати ще раз.

Заміни слів

Жодна маленька модель не впорається з «tmux», «kubectl» чи назвою вашого проєкту. Для цього є заміни слів: пари «що почув Whisper» → «що ви мали на увазі». Реалізація вміщається в кілька рядків:

// Заміни слів: по черзі, без урахування регістру, лише активні.
String applyReplacements(String text) {
  var out = text;
  for (final replacement in getSettings().dictationReplacements) {
    if (!replacement.enabled || replacement.from.isEmpty) continue;
    out = out.replaceAllMapped(RegExp(RegExp.escape(replacement.from), caseSensitive: false), (_) => replacement.to);
  }
  return out;
}

Вони застосовуються по черзі й без урахування регістру, їх можна вимкнути, не видаляючи, а якщо ціль порожня, слово просто зникає. Працюють однаково з локальним Whisper і з хмарою. Для власних назв із вашої роботи заміна обійдеться дешевше, ніж перехід на більшу модель.

Альтернатива: власний ключ (OpenAI або Groq)

Рушій BYOK («bring your own key») відправляє той самий WAV хмарному провайдерові з вашим ключем API. Wendo підтримує двох, вони описані в BYOK_PROVIDERS:

  • OpenAI, через /v1/audio/transcriptions, з gpt-4o-mini-transcribe як моделлю за замовчуванням.
  • Groq, через його ендпойнт, сумісний з OpenAI, з whisper-large-v3-turbo за замовчуванням.

Поле моделі можна редагувати, тож туди можна вписати іншу, яку підтримує ваш провайдер. Ключ зберігається у в’язці ключів пристрою, окремо для кожного провайдера, і на тому самому екрані застосунок пише, що він «надсилається лише» до вибраного провайдера. Запит є multipart з file, model, response_format: json і, якщо ви не лишили автовизначення, language.

Обидва варіанти існують, бо розв’язують різні задачі. Локальний Whisper приватний, працює без зв’язку і нічого не коштує за диктовку, натомість займає місце й пам’ять і залежить від потужності телефона. Хмара нічого не займає на телефоні й дає великі моделі, які на нього не вмістяться, але аудіо виходить з пристрою, потрібне з’єднання, і ви платите провайдерові. Groq запускає ту саму large-v3-turbo, яку Wendo пропонує локально, тільки на своїх серверах, а не на вашому телефоні. OpenAI додає власні моделі розпізнавання.

Наша порада: почніть з локальної base і кількох замін. Якщо ви диктуєте багато, телефон ледве тягне великі моделі, а те, що аудіо виходить з пристрою, вас не турбує, переходьте на BYOK.

Спробувати

Диктування є частиною Wendo, якого ще немає в магазинах застосунків. Вмикається воно мікрофоном на панелі термінала чи редактора або довгим натисканням кнопки клавіатури, це жест за замовчуванням. Якщо комп’ютер ще не налаштований для розмови з агентом з телефона, почніть зі статті як користуватися Claude Code з телефона, а щоб сесія жила, поки ви диктуєте, прочитайте мінімальний tmux для кодингових агентів.