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