Слои записи и оверлей берут стт-статус и сессию из единого GlassesStt
This commit is contained in:
@@ -12,10 +12,7 @@ import pw.binom.viewmate.core.log.BatchingLogCollector
|
||||
import pw.binom.viewmate.core.log.FileLogSpool
|
||||
import pw.binom.viewmate.core.log.SystemClock
|
||||
import pw.binom.viewmate.glasses.gestures.NavFrame
|
||||
import pw.binom.viewmate.glasses.stt.GlassesAsr
|
||||
import pw.binom.viewmate.glasses.stt.GlassesSttSession
|
||||
import pw.binom.viewmate.glasses.stt.LocalGlassesRecognizer
|
||||
import pw.binom.viewmate.glasses.stt.SttModelDownloader
|
||||
import pw.binom.viewmate.glasses.stt.GlassesStt
|
||||
import pw.binom.viewmate.glasses.ui.layers.AssistantEngine
|
||||
import pw.binom.viewmate.glasses.ui.layers.ListeningLayout
|
||||
import pw.binom.viewmate.glasses.ui.layers.MoveLayout
|
||||
@@ -57,16 +54,8 @@ class GlassesApp : Application() {
|
||||
lateinit var downloadManager: GlassesDownloadManager
|
||||
private set
|
||||
|
||||
/** Модель Qwen3-ASR в filesDir/models: скачивает недостающее с зеркала при старте. */
|
||||
lateinit var sttModel: SttModelDownloader
|
||||
private set
|
||||
|
||||
/** Локальный Qwen3-ASR распознаватель очков (нативка лениво, один раз на процесс). */
|
||||
lateinit var asr: GlassesAsr
|
||||
private set
|
||||
|
||||
/** Локальная STT-сессия очков: микрофон копит PCM, распознавание на клике, 30s-лимит. */
|
||||
lateinit var sttSession: GlassesSttSession
|
||||
/** Весь стек распознавания очков (скачивание + статус + сессия микрофона) — в одном объекте. */
|
||||
lateinit var stt: GlassesStt
|
||||
private set
|
||||
|
||||
/**
|
||||
@@ -85,7 +74,7 @@ class GlassesApp : Application() {
|
||||
* Движок ассистента: внятный стейт диалога (фазы/тексты/история) + его жестовые эффекты.
|
||||
* Владелец стейта, живёт всё время приложения; слои [AssistantLayout]/[ListeningLayout]
|
||||
* и оверлей дотягиваются до него, протокол/lifecycle пишут через [assistantEngine].
|
||||
* Зависимости от [GlassesApp] (sttSession/sttModel/hostConnection/rootLayer) — лениво, в методах.
|
||||
* Зависимости от [GlassesApp] (stt/hostConnection/rootLayer) — лениво, в методах.
|
||||
*/
|
||||
val assistantEngine = AssistantEngine(this)
|
||||
|
||||
@@ -110,7 +99,7 @@ class GlassesApp : Application() {
|
||||
// Стейт диалога ассистента (thinking/assistantText/chatHistory/answerScrollDelta + idle-hint
|
||||
// sttNoSpeech) — в [assistantEngine], владелец стейта ассистента. Микрофон и распознавание
|
||||
// (sttText, старт/стоп стт-сессии) владеет слой [ListeningLayout]; «модель не скачалась» —
|
||||
// из sttModel.status. Движок микрофоном не владеет.
|
||||
// из stt.status. Движок микрофоном не владеет.
|
||||
|
||||
/**
|
||||
* Стек навигации очков (ЛIFO), снизу вверх: корень [NavFrame.Film] (двойным кликом не
|
||||
@@ -210,19 +199,12 @@ class GlassesApp : Application() {
|
||||
pw.binom.viewmate.core.logCollector = logCollector
|
||||
log("app", "лог-коллектор запущен (пачки → телефон, спул: filesDir/logspool)")
|
||||
|
||||
// Модель Qwen3-ASR: если локально отсутствует — докачивать с зеркала (idempotent, по Range).
|
||||
sttModel = SttModelDownloader(File(filesDir, "models"), bgScope)
|
||||
sttModel.ensure()
|
||||
|
||||
// Локальное распознавание на очках: копит микрофонный PCM, распознаёт Qwen3-ом на клике
|
||||
// (нативка лениво) и 30s-лимит на тишину. Куда уходит готовый текст (onResult) регистрирует
|
||||
// владелец — слой записи (ListeningLayout.onEnter); приложение результат к mainActivity не шьёт.
|
||||
asr = GlassesAsr(File(filesDir, "models"))
|
||||
sttSession = GlassesSttSession(
|
||||
filesDir = filesDir,
|
||||
scope = bgScope,
|
||||
recognizer = LocalGlassesRecognizer(asr, sttModel),
|
||||
)
|
||||
// Весь стек распознавания очков (скачивание + статус + сессия микрофона) — в одном объекте.
|
||||
// Модель если отсутствует докачивается фоновой корутиной стта (idempotent, по Range). Куда
|
||||
// уходит готовый текст (onResult) регистрирует владелец — слой записи (ListeningLayout.onEnter);
|
||||
// приложение результат к mainActivity не шьёт.
|
||||
stt = GlassesStt(filesDir, bgScope)
|
||||
stt.ensure()
|
||||
|
||||
// Проекция стека layout-ов в [navStack] (для оверлея): стек правды — layoutManager,
|
||||
// navStack — лишь его зеркало. Начальная проекция (корень = Film).
|
||||
|
||||
@@ -65,7 +65,7 @@ fun AssistantOverlay(engine: AssistantEngine) {
|
||||
// «Модель не скачалась» выводится из состояния догрузчика (отдельного логического флага в
|
||||
// движке нет): распознавание локально, поэтому модель не READY = распознавание не готово —
|
||||
// объясняем что происходит (качается / не скачалась).
|
||||
val modelStatus by app.sttModel.status.collectAsState()
|
||||
val modelStatus by app.stt.status.collectAsState()
|
||||
val state = AssistantState(
|
||||
stack = navStack,
|
||||
thinking = thinking,
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package pw.binom.viewmate.glasses.ui.layers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import pw.binom.viewmate.glasses.Gesture
|
||||
import pw.binom.viewmate.glasses.Layout
|
||||
import pw.binom.viewmate.glasses.LayoutContext
|
||||
import pw.binom.viewmate.glasses.gestures.AssistantMode
|
||||
import pw.binom.viewmate.glasses.stt.GlassesSttSession
|
||||
import pw.binom.viewmate.glasses.stt.SttModelStatus
|
||||
import pw.binom.viewmate.glasses.stt.GlassesStt
|
||||
import pw.binom.viewmate.glasses.ui.AssistantOverlay
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@@ -26,8 +24,7 @@ import kotlin.coroutines.resume
|
||||
class AssistantLayout(
|
||||
private val ctx: LayoutContext,
|
||||
private val engine: AssistantEngine,
|
||||
private val sttSession: GlassesSttSession,
|
||||
private val modelStatus: StateFlow<SttModelStatus>,
|
||||
private val stt: GlassesStt,
|
||||
) : Layout {
|
||||
|
||||
@Composable
|
||||
@@ -70,7 +67,7 @@ class AssistantLayout(
|
||||
* Возвращает распознанный текст ([onSuccess]) или null ([onCancel], любая отмена) —
|
||||
* слой записи завершает ожидание этими кэлбэками.
|
||||
*/
|
||||
private suspend fun listen() = ListeningLayout.listen(ctx, sttSession, modelStatus)
|
||||
private suspend fun listen() = ListeningLayout.listen(ctx, stt)
|
||||
|
||||
/** Вызвать запись: ланчить suspend-функцию [listen] в [engine.scope] и по её результату продолжить ход. */
|
||||
private fun openListening() {
|
||||
|
||||
+16
-20
@@ -2,33 +2,30 @@ package pw.binom.viewmate.glasses.ui.layers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import pw.binom.viewmate.glasses.Gesture
|
||||
import pw.binom.viewmate.glasses.Layout
|
||||
import pw.binom.viewmate.glasses.LayoutContext
|
||||
import pw.binom.viewmate.glasses.log
|
||||
import pw.binom.viewmate.glasses.stt.GlassesSttSession
|
||||
import pw.binom.viewmate.glasses.stt.SttModelStatus
|
||||
import pw.binom.viewmate.glasses.stt.GlassesStt
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Слой записи — ЕДИНСТВЕННЫЙ, кто знает про микрофон и распознавание. Он сам открывает
|
||||
* микрофон ([GlassesSttSession.start] в [onEnter]), закрывает ([onExit]) и сам принимает
|
||||
* стт-решение: клик распознаёт накопленное ([GlassesSttSession.send]), результат/события
|
||||
* микрофон ([stt.session.start] в [onEnter]), закрывает ([onExit]) и сам принимает
|
||||
* стт-решение: клик распознаёт накопленное ([stt.session.send]), результат/события
|
||||
* (локальный [onSttResult], хостовые [onSttPhrase]/[onSttCancelUser]/[onSttTimeout]) слой
|
||||
* переваривает сам и сообщает вызывающему факт исхода кэлбэками [onSuccess]/[onCancel].
|
||||
*
|
||||
* Для всего остального (кино, чат, ассистент) «текст откуда-то берётся» — никто кроме этого
|
||||
* слоя не в курсе микрофона и распознавания. Зависимости — конкретные стт-сущности
|
||||
* ([sttSession] и состояние готовности модели [modelStatus]), а не весь [GlassesApp];
|
||||
* слоя не в курсе микрофона и распознавания. Зависимость — единый объект [GlassesStt] (весь
|
||||
* стек распознавания: скачивание, статус, сессия микрофона), а не весь [GlassesApp];
|
||||
* исхода двух видов: успех — [onSuccess] с текстом, ЛЮБАЯ отмена (двойной тап, тишина,
|
||||
* модель не готова) — [onCancel]; выхода из режима слой не делает — это уже эффект вызывающего.
|
||||
*/
|
||||
class ListeningLayout(
|
||||
private val ctx: LayoutContext,
|
||||
private val sttSession: GlassesSttSession,
|
||||
private val modelStatus: StateFlow<SttModelStatus>,
|
||||
private val stt: GlassesStt,
|
||||
private val onSuccess: (String) -> Unit,
|
||||
private val onCancel: () -> Unit,
|
||||
) : Layout {
|
||||
@@ -36,14 +33,13 @@ class ListeningLayout(
|
||||
companion object {
|
||||
suspend fun listen(
|
||||
ctx: LayoutContext,
|
||||
sttSession: GlassesSttSession,
|
||||
modelStatus: StateFlow<SttModelStatus>,
|
||||
stt: GlassesStt,
|
||||
) = suspendCancellableCoroutine { context ->
|
||||
ctx.open { c ->
|
||||
context.invokeOnCancellation {
|
||||
c.close()
|
||||
}
|
||||
ListeningLayout(c, sttSession, modelStatus, onSuccess = { text ->
|
||||
ListeningLayout(c, stt, onSuccess = { text ->
|
||||
context.resume(text)
|
||||
}, onCancel = {
|
||||
context.resume(null)
|
||||
@@ -55,7 +51,7 @@ class ListeningLayout(
|
||||
/** Накопленный распознанный текст — живой стейт слоя (оверлей читает через вершину стека). */
|
||||
val sttText = MutableStateFlow("")
|
||||
|
||||
private val modelReady: Boolean get() = modelStatus.value.ready
|
||||
private val modelReady: Boolean get() = stt.status.value.ready
|
||||
|
||||
/** Прозрачный слой: сам ничего не рисует — оверлей ассистента (поверх) показывает индикатор и фразу. */
|
||||
@Composable
|
||||
@@ -69,15 +65,15 @@ class ListeningLayout(
|
||||
sttText.value = ""
|
||||
// Регистрирую себя обработчиком готового текста: слой сам заводит свой результат,
|
||||
// сессия лишь порождает текст — куда он идёт, знает только владелец (этот слой).
|
||||
sttSession.onResult = { text, auto -> onSttResult(text, auto) }
|
||||
sttSession.start()
|
||||
stt.session.onResult = { text, auto -> onSttResult(text, auto) }
|
||||
stt.session.start()
|
||||
log("stt", "слои: вхожу в режим записи, стартую сессию + микрофон")
|
||||
}
|
||||
|
||||
/** Слой закрыт: микрофон закрыть + безопасная отмена ожидания (resume однократен, повтор — no-op). */
|
||||
override fun onExit() {
|
||||
sttSession.onResult = null
|
||||
sttSession.stop()
|
||||
stt.session.onResult = null
|
||||
stt.session.stop()
|
||||
onCancel()
|
||||
}
|
||||
|
||||
@@ -90,10 +86,10 @@ class ListeningLayout(
|
||||
Gesture.Click ->
|
||||
if (modelReady) {
|
||||
log("stt", "клик → распознаю накопленное")
|
||||
sttSession.send()
|
||||
stt.session.send()
|
||||
} else {
|
||||
log("stt", "клик: модель не готова — повторю старт записи")
|
||||
sttSession.start()
|
||||
stt.session.start()
|
||||
}
|
||||
|
||||
Gesture.DoubleClick ->
|
||||
@@ -115,7 +111,7 @@ class ListeningLayout(
|
||||
|
||||
/**
|
||||
* Обработчик результата ЛОКАЛЬНОГО распознавания. Слой сам заводит свой результат:
|
||||
* регистрируется на [GlassesSttSession.onResult] в [onEnter], сбрасывает в [onExit] —
|
||||
* регистрируется на [stt.session.onResult] в [onEnter], сбрасывает в [onExit] —
|
||||
* извне (телефон/LLM-путь) [onSttResult] не вызывают. Слой сам решает исход:
|
||||
* - текст не пуст → успех ([onSuccess]) + POP слоя;
|
||||
* - пусто + авто (тишина 30с) → отмена ([onCancel]) + POP слоя;
|
||||
|
||||
@@ -84,7 +84,7 @@ class MoveLayout(
|
||||
}
|
||||
app.hostConnection.sendPlaybackPositionNow()
|
||||
ctx.open { c ->
|
||||
AssistantLayout(c, app.assistantEngine, app.sttSession, app.sttModel.status)
|
||||
AssistantLayout(c, app.assistantEngine, app.stt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user