# Спецификация 02: SttEngine / VoskEngine и ModelStore Источники: Ф-1.1 (активация кнопкой, офлайн, пока экран открыт), ОВ-2 (Vosk, малая ru-модель), tech-req 5 (`VoiceInput` — service в процессе приложения; грамматика JSON-словарь), решение гейта 1.3 №1/№4. Единицы 2–3. ## 1. SttEngine — интерфейс (для тестов и подмены) ```kotlin interface SttEngine { /** Прослушивание; onResult — финальные гипотезы (до 3) в тишине. */ fun startListening(onResult: (List) -> Unit) fun stopListening() fun shutdown() } ``` - колбэк — в главном потоке; повторный `startListening` без `stopListening` игнорируется; `shutdown` освобождает нативные ресурсы (вызывается при уходе с экрана/закрытии приложения). ## 2. VoskEngine — реализация - зависимость `com.alphacephei:vosk-android:0.3.47` (Maven Central; JNI-библиотеки в AAR — рост APK ≈ 45 МБ debug без минификации); - `org.vosk.Model` инициализируется из каталога модели (§3) в IO-потоке (не главном); `SpeechService(recognizer, 16000f)` — микрофон 16 кГц; остановка — `stop()` + `shutdown()` (errata 2026-10-19: методов stopListening/destroy в API 0.3.47 нет); `RecognitionListener` — 5 методов: команды приходят в `onResult` (после паузы речи), `onFinalResult` — только при остановке сервиса, `onPartialResult`/`onError`/`onTimeout` игнорируются (потеря аудио — ждём следующую фразу); - **грамматика (решение гейта 1.3 №4):** при создании — попытка `Recognizer(model, 16000f, grammarJson)`; grammarJson строится из шаблонов Ф-1.2 + числительные 0–999 (продукции: сотни/десятки/ единицы, «с половиной», «на», «секунд|секунды», «рпэ»); формат — GOF `setGrammar` Vosk; при отказе инициализации с грамматикой (малая модель не гарантирует поддержку) — автоматический откат на свободное распознавание (`Recognizer(model, 16000f)`), лишнее отсекает парсер (спец 01, Ф-1.3); выбранный режим — в поле `grammarApplied` (диагностика в настройках); - n-best: `setMaxAlternatives(3)`, если доступно в API vosk-android; иначе список из одного текста; финальный результат — разбор JSON (`text` / `alternatives[].text`); - микрофон/разрешение: движок не запрашивает `RECORD_AUDIO` сам — старт при отсутствии разрешения → `SecurityException` перехват → `startListening` возвращает false-статус (UI запрашивает до старта, спец 05); - только офлайн: сетевых вызовов нет; движения речи не сохраняются (аудио не пишется на диск). ## 3. ModelStore — доставка модели (решение гейта 1.3 №1) ```kotlin class ModelStore(context, settings) { fun modelDir(): File // filesDir/models/vosk-ru fun isDownloaded(): Boolean // каталог + am/ присутствуют suspend fun download(onProgress: (Int) -> Unit): Result fun sizeMb(): Long? // для статуса настроек } ``` - модель `vosk-model-small-ru-0.22` (~45 МБ zip): URL — настройка `Voice.ModelUrl`, умолчание `https://alphacephei.com/vosk/models/vosk-model-small-ru-0.22.zip` (можно заменить на файл сервера владельца); - загрузка OkHttp (зависимость уже есть), zip → `cacheDir`; распаковка `ZipInputStream` c защитой от zip-slip (пути нормализуются, выход за каталог отбрасывается); распакованный каталог приобретает финальное имя атомарной заменой (временный суффикс → rename); - идемпотентность: `download` при готовой модели — no-op (`isDownloaded` → успех без сети); повторная загрузка (смена URL) заменяет каталог; - прогресс — проценты по `Content-Length`; ошибки — `Result.failure` (сеть/битый zip/нет места), текст для UI — спец 05; - модель НЕ входит в APK и git (в `filesDir`; бэкап системы — `allowBackup=false` уже в манифесте). ## 4. Тесты 1. ModelStore: распаковка корректного zip (фикстура-архив с файлами-маркерами `am/`, `graph/`) → `isDownloaded=true`, структура каталога; zip-slip (запись `../../evil`) — файл отброшен, исключения нет; битый zip → failure; повторный download — no-op. Чистые JVM-тесты на временных каталогах (`@TempDir`), OkHttp — через `MockWebServer` (добавить testImplementation). 2. VoskEngine — тонкая обёртка над нативным API, unit-тестами не покрывается (нативные библиотеки); разбор JSON-результата (`text`/`alternatives`) — выделен в чистую функцию `VoskResult.parse(json)` и тестируется на JVM. 3. Интеграция движка с моделью — ручной смок владельца (критерий приёмки 2 module_reqs).