Макеты — один 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
  приведены к фактическому состоянию
This commit is contained in:
2026-09-28 10:07:57 +03:00
parent 960cd60120
commit 27f349550c
25 changed files with 2416 additions and 4448 deletions
+81
View File
@@ -1,5 +1,12 @@
# Микрофон и распознавание: результаты поиска
> **Статус: реализовано** (см. `src/jvmMain/.../desktop/voice/`).
> `mic-api` + `asr-vosk` + `vad` подключены в `gradle/libs.versions.toml`,
> кнопка микрофона в чате активна: `VoiceController` (микрофон → VAD →
> распознавание), `VoskVoiceRecognizer` (Vosk-модель и `silero_vad.onnx` лежат
> внутри артефактов — качать нечего). Ниже — исходный поиск, из которого это
> выросло.
**Задача была:** найти готовые библиотеки для работы с микрофоном и распознавания,
прежде чем брать код из `ai/assistent`. **Код не пишем** — только поиск.
@@ -172,3 +179,77 @@ client.post("$base/audio/transcriptions") { ... } // multipart, как у Open
| Нужны ли сторонние зависимости для звука? | **Нет.** Всё на стандартной 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`) — закрывать ровно один раз.