Files
agentik-desktop/MIC-ASR-SEARCH.md
T
subochev 27f349550c Макеты — один sketches/index.html; approved/ удалён
- sketches/index.html: все экраны одним документом в порядке 1·… (14–19 — бывшие approved)
- approved/ (4 файла групп + 3 макета + README) удалён как дубль
- стили вынесены в sketches/style.css
- README, THEMES, BORROW-FROM-ASSISTENT, MARKDOWN-SOURCE, MIC-ASR-SEARCH
  приведены к фактическому состоянию
2026-09-28 10:07:57 +03:00

15 KiB
Raw Blame History

Микрофон и распознавание: результаты поиска

Статус: реализовано (см. 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 через ONNX double free) — закрывать ровно один раз.