- sketches/index.html: все экраны одним документом в порядке 1·… (14–19 — бывшие approved) - approved/ (4 файла групп + 3 макета + README) удалён как дубль - стили вынесены в sketches/style.css - README, THEMES, BORROW-FROM-ASSISTENT, MARKDOWN-SOURCE, MIC-ASR-SEARCH приведены к фактическому состоянию
15 KiB
Микрофон и распознавание: результаты поиска
Статус: реализовано (см.
src/jvmMain/.../desktop/voice/).mic-api+asr-vosk+vadподключены вgradle/libs.versions.toml, кнопка микрофона в чате активна:VoiceController(микрофон → VAD → распознавание),VoskVoiceRecognizer(Vosk-модель иsilero_vad.onnxлежат внутри артефактов — качать нечего). Ниже — исходный поиск, из которого это выросло.
Задача была: найти готовые библиотеки для работы с микрофоном и распознавания,
прежде чем брать код из ai/assistent. Код не пишем — только поиск.
Ответ короткий: и то, и другое уже есть у нас, готовое и опубликованное.
Из ai/assistent брать код микрофона не нужно вообще.
1. Микрофон: библиотека есть — mic-kmp
Репозиторий: https://git.binom.pw/subochev/mic-kmp Каталог: https://git.binom.pw/subochev/libraries (раздел «📦 Микрофон»)
KMP-библиотека: поток микрофона как Flow<ByteArray> (s16le mono PCM).
Три файла, вот и вся библиотека:
| Файл | Что делает |
|---|---|
mic-api/src/commonMain/.../MicAudio.kt |
интерфейс MicDevice + объект MicAudio.record(...) |
mic-api/src/jvmMain/.../MicJvm.kt |
JVM — javax.sound.sampled (TargetDataLine) |
mic-api/src/androidMain/.../MicAndroid.kt |
Android — AudioRecord |
Таргеты: jvm и android. JVM-таргет — ровно то, что нужно десктопу.
JVM-код собран на JvmTarget.JVM_17.
Опубликована в Nexus caffeine, версия 1.0.0:
pw.binom.mic:mic-api:1.0.0(KMP metadata)pw.binom.mic:mic-api-jvm:1.0.0(JVM — наше)pw.binom.mic:mic-api-android:1.0.0
Проверено: maven-metadata.xml отдаёт HTTP 200, latest = 1.0.0.
Использование — одна строка:
MicAudio.record(sampleRate = 16000, channels = 1, chunkMillis = 100)
.onEach { chunk: ByteArray -> /* s16le mono PCM, ~100 мс */ }
Чем лучше кода из assistent: отмена структурная. Остановил корутину-коллектор —
микрофон освободился сам, в finally. Не надо помнить про close(). В assistent
(MicrophoneRecorder, 83 строки) — ручное управление через start()/stop(),
и связка «канал + флаг recording + внешний TargetDataLine». Наш вариант чище.
Вывод: копировать MicrophoneRecorder из assistent НЕ надо — подключить mic-kmp.
2. Распознавание: библиотека есть — asr-kmp
Репозиторий: https://git.binom.pw/subochev/asr-kmp Каталог: https://git.binom.pw/subochev/libraries (раздел «🎙 ASR»)
KMP, таргеты jvm и android (плюс native: linux, macos, mingw). Потоковый интерфейс — приём аудио кусками и промежуточный текст по ходу:
interface Stt : AutoCloseable {
fun feed(samples: FloatArray) // порция 16 кГц моно PCM, [-1, 1]
fun partialText(): String? // промежуточный текст (растёт по ходу)
fun endOfInput() // конец ввода
fun finalText(): String // итог
override fun close()
}
Есть partialText — текст появляется по мере речи, а не после конца записи.
Для нашей кнопки микрофона это именно то поведение, которое нужно.
Движки (все опубликованы в caffeine)
| Движок | Координаты (группа pw.binom.asr) |
Версии в Nexus |
|---|---|---|
asr-api — интерфейс |
asr-api / -jvm / -android |
до 4 |
asr-vosk — Vosk |
asr-vosk / -jvm / -android |
есть |
asr-qwen3 — Qwen3-ASR |
asr-qwen3 / -jvm |
есть |
asr-whisper — Whisper |
asr-whisper / -jvm / -android |
есть |
asr-openai — удалённое (OpenAI-совместимое) |
asr-openai |
есть |
Под капотом нейронок — sherpa-onnx (com.github.k2-fsa.sherpa-onnx, v1.13.6),
для JVM отдельно тянется нативная библиотека sherpa-onnx-native-lib-linux-x64.
Про модели (важно)
- Qwen3-ASR — модель уже на нашем статик-сервере, качать из-за бугра не надо:
http://static.binom.pw/models/qwen3_06/(~940 МБ: encoder 175M, decoder 721M, frontend 43M). - Whisper small int8 — модели на статике НЕТ, качать самостоятельно.
- Vosk (
vosk-model-small-ru-0.22) — входит в комплект, распаковывается сам. Самый простой путь для первого запуска: ничего качать не надо.
3. Рядом есть ещё три полезные библиотеки
Из того же каталога subochev/libraries:
VAD — детектор речи, vad-kmp
https://git.binom.pw/subochev/vad-kmp — «VAD — Silero», KMP jvm+android, модель внутри артефакта (2.3 МБ, ничего качать не надо).
VadInstance(16000).use { vad ->
val prob = vad.processingWindow(windowPcmFloats) // FloatArray → [0..1]
}
Координаты: pw.binom.voice.vad:vad-jvm, версия 2.
Зачем: определять, говорит человек или молчит — не гнать тишину в распознавание.
Есть и второй VAD
pw.binom.ai.voice:vad — «детектор речи (VAD) — vad / vad-jvm». Два разных VAD,
при выборе разобраться, какой свежее.
OpenAI-клиент
pw.binom.openai:api / api-jvm + ktor-impl (HTTP-движок). Нужен для asr-openai
(распознавание на удалённом сервере, а не локальной нейронкой).
Speaker-эмбеддинги и text-embedding
subochev/embedding-kmp— voice embedding (кто говорит).subochev/text-embedding-kmp— text embedding (SigLIP2). Нам сейчас не нужно, но пусть будет известно.
4. Как это собрано вместе — пример view-mate
Репо: /root/WORK/view-mate (очки RayNeo X2), модули app-glasses, app-phone, app-host, lib-core.
Важно: view-mate не подключает mic-kmp/asr-kmp как библиотеки — у него
своя реализация SttMicStream на android.media.AudioRecord
(app-glasses/.../stt/SttMicStream.kt). То есть mic-kmp — это вынесенная
и обобщённая версия того же подхода.
Из полезного в SttMicStream.kt — комментарий на строке 229:
AudioRecord.read(byte[], int, inFrameCount) возвращает число кадров, а не байт.
Классические грабли Android-записи.
5. Чем пользуется сам ai/assistent
Важно для понимания разницы подходов:
chat-server/.../stt/SttClientImpl.kt — это не локальное распознавание.
Это HTTP-клиент к OpenAI-совместимому API:
client.post("$base/audio/transcriptions") { ... } // multipart, как у OpenAI
Настройки через переменные окружения:
STT_API_KEY— без него голосовой адаптер выключен;STT_URL— по умолчаниюhttps://api.openai.com/v1;STT_MODEL— по умолчаниюwhisper-1.
Вывод: у assistent распознавание — отправка файла на сервер. У нас
asr-kmp умеет и локальные нейронки (Qwen3, Whisper, Vosk), и удалённое (asr-openai).
Наш вариант шире, а mic-kmp (поток) + asr-kmp (поток) + vad-kmp (детектор речи)
складываются в готовую цепочку: микрофон → детектор речи → распознавание → текст.
6. Итог: что делать
| Вопрос | Ответ |
|---|---|
| Та отдельная библиотечка для микрофона, которую мы делали — существует? | Да. mic-kmp, опубликована в caffeine версии 1.0.0, jvm-таргет есть. |
| Библиотечки распознавания с нейронками где? | asr-kmp — Qwen3, Whisper, Vosk, удалённое. Всё в caffeine. Плюс каталог: subochev/libraries. |
| Есть ли где-то микрофон в каталоге библиотек? | Да, первым разделом — «📦 Микрофон». |
Брать ли код микрофона из ai/assistent? |
Нет. Подключаем mic-kmp — чище, с автоосвобождением, без внешних библиотек. |
| Нужны ли сторонние зависимости для звука? | Нет. Всё на стандартной Java (javax.sound.sampled) и sherpa-onnx для нейронок. |
Код не писан — только поиск, как и просили.
7. Что в итоге подключено
| Слой | Артефакт | Где в коде |
|---|---|---|
| Микрофон | pw.binom.mic:mic-api:1.0.0 |
voice/VoiceController.kt (MicSource → MicAudio.record) |
| Детектор речи | pw.binom.voice.vad:vad:2 |
voice/VoiceRecognizer.kt (VadInstance) |
| Распознавание | pw.binom.asr:asr-vosk:4 (+ asr-api:4) |
voice/VoiceRecognizer.kt (VoskVoiceRecognizer) |
| Сборка тракта | — | voice/VoiceController.kt (VoiceState: IDLE/PREPARING/RECORDING/RECOGNIZING) |
| Склейка с UI | — | ui/AppState.kt (voiceState, voicePartial, composerDraft), ui/screens/ChatPane.kt |
| Прогрев до skia | — | voice/VoiceNative.kt (warmUp() из main()) |
Тесты: voice/AudioTest.kt (s16le→float, нарезка окон VAD + реальная загрузка
silero_vad.onnx), voice/VoiceControllerTest.kt (фейковые микрофон и
распознаватель), voice/VoskVoiceRecognizerIT.kt (настоящие Vosk + VAD на
тишине), voice/RealSpeechIT.kt (настоящая русская речь после инициализации
skia — см. ниже).
8. ⚠️ Конфликт skiko и нативных библиотек (найдено 28.09.2026)
Симптом. Приложение Compose Desktop роняется с SIGSEGV (не исключение —
падение всего JVM) при первом нажатии микрофона. В hs_err_pid:
C [libc.so.6+0x184fdc]
C [libstdc++.so.6+0x15d820] std::ostream::_M_insert<double>
C [jnaNNNN.tmp+0x6838e7] kaldi::ParseOptions::RegisterSpecific(...)
j org.vosk.LibVosk.vosk_model_new(...)
То же самое — с ONNX (OrtSession.createSession → std::ostream::_M_insert):
C++-библиотека пишет в свой лог-стрим, а стрим оказывается невалидным.
Причина — порядок загрузки. skiko (нативный рендер Compose, грузит
~/.skiko/<hash>/libskiko-linux-x64.so) и C++-стек распознавания (Vosk/Kaldi,
onnxruntime) конфликтуют, если второй инициализируется ПОСЛЕ первого. Первое
создание модели/сессии после инициализации skia валит JVM.
Проверено пробниками (позже удалены):
| Порядок | Результат |
|---|---|
| ONNX → skia (raster) → ONNX | ок |
| skia (Compose-текст) → ONNX | SIGSEGV |
| skia (Compose-текст) → Vosk | SIGSEGV |
| Vosk → skia (Compose-текст) → Vosk | ок |
Фикс. VoiceNative.warmUp() вызывается в main() до application {}:
создаёт и закрывает Stt.vosk(...) и VadInstance(...). После этого и свежий
распознаватель, и свежий VAD, созданные уже после отрисовки окна, работают.
Побочный плюс — модель Vosk (~46 МБ, ~2 с) грузится на старте, а не при первом
клике. Если прогрев не удался, AppState.voiceAvailable = false — кнопка
микрофона заблокирована (лучше «недоступно», чем краш).
Следствие для тестов. jvmTest форкает JVM на каждый тест-класс
(forkEvery = 1): иначе skiko-тесты (ThemeRenderTest) отравляют
нативные тесты голоса в том же JVM. RealSpeechIT проверяет ровно
продуктовый порядок: warmUp() → рендер Compose → создание распознавателя →
распознавание реального сэмпла (src/jvmTest/resources/voice/raz-dva-tri.wav
→ «раз два три четыре пять»).
Заметки на будущее:
- Создание распознавателя стоит секунды (загрузка модели Vosk), поэтому
распаковка zip в tmp кэшируется самим
asr-vosk, а в UI есть состояние «загружаю модель». VadInstance.close()не идемпотентен (второйclose()роняет JVM через ONNXdouble free) — закрывать ровно один раз.