# Микрофон и распознавание: результаты поиска > **Статус: реализовано** (см. `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` (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`. **Использование — одна строка:** ```kotlin 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). Потоковый интерфейс — приём аудио кусками и **промежуточный текст по ходу**: ```kotlin 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 МБ, ничего качать не надо). ```kotlin 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: ```kotlin 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 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//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`) — закрывать ровно один раз.