VEU.WathBody/1-docs/4-modules/m11-voice-input/api_specs/02-stt-model.md

6.2 KiB
Raw Blame History

Спецификация 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 (продукции: сотни/десятки/ единицы, «с половиной», «на», «секунд|секунды», «рпэ»); формат — 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)

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; распаковка 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).