6.2 KiB
6.2 KiB
Спецификация 02: SttEngine / VoskEngine и ModelStore
Источники: Ф-1.1 (активация кнопкой, офлайн, пока экран открыт),
ОВ-2 (Vosk, малая ru-модель), tech-req 5 (VoiceInput — service
в процессе приложения; грамматика JSON-словарь), решение гейта 1.3
№1/№4. Единицы 2–3.
1. SttEngine — интерфейс (для тестов и подмены)
interface SttEngine {
/** Прослушивание; onResult — финальные гипотезы (до 3) в тишине. */
fun startListening(onResult: (List<String>) -> 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 (продукции: сотни/десятки/ единицы, «с половиной», «на», «секунд|секунды», «рпэ»); формат — GOFsetGrammarVosk; при отказе инициализации с грамматикой (малая модель не гарантирует поддержку) — автоматический откат на свободное распознавание (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)
class ModelStore(context, settings) {
fun modelDir(): File // filesDir/models/vosk-ru
fun isDownloaded(): Boolean // каталог + am/ присутствуют
suspend fun download(onProgress: (Int) -> Unit): Result<File>
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; распаковкаZipInputStreamc защитой от zip-slip (пути нормализуются, выход за каталог отбрасывается); распакованный каталог приобретает финальное имя атомарной заменой (временный суффикс → rename); - идемпотентность:
downloadпри готовой модели — no-op (isDownloaded→ успех без сети); повторная загрузка (смена URL) заменяет каталог; - прогресс — проценты по
Content-Length; ошибки —Result.failure(сеть/битый zip/нет места), текст для UI — спец 05; - модель НЕ входит в APK и git (в
filesDir; бэкап системы —allowBackup=falseуже в манифесте).
4. Тесты
- ModelStore: распаковка корректного zip (фикстура-архив с
файлами-маркерами
am/,graph/) →isDownloaded=true, структура каталога; zip-slip (запись../../evil) — файл отброшен, исключения нет; битый zip → failure; повторный download — no-op. Чистые JVM-тесты на временных каталогах (@TempDir), OkHttp — черезMockWebServer(добавить testImplementation). - VoskEngine — тонкая обёртка над нативным API, unit-тестами не
покрывается (нативные библиотеки); разбор JSON-результата
(
text/alternatives) — выделен в чистую функциюVoskResult.parse(json)и тестируется на JVM. - Интеграция движка с моделью — ручной смок владельца (критерий приёмки 2 module_reqs).