97 lines
6.2 KiB
Markdown
97 lines
6.2 KiB
Markdown
# Спецификация 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<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)
|
||
|
||
```kotlin
|
||
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).
|