Слои записи и оверлей берут стт-статус и сессию из единого GlassesStt

This commit is contained in:
2026-09-07 04:27:05 +03:00
parent 7db0fc71de
commit e81026b9f5
5 changed files with 10 additions and 98 deletions
@@ -14,7 +14,6 @@ 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.GlassesStt import pw.binom.viewmate.glasses.stt.GlassesStt
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.MoveLayout import pw.binom.viewmate.glasses.ui.layers.MoveLayout
import pw.binom.viewmate.glasses.ui.layers.toFrame import pw.binom.viewmate.glasses.ui.layers.toFrame
@@ -135,38 +134,6 @@ class GlassesApp : Application() {
prefs.edit().putFloat("vertical", v).apply() prefs.edit().putFloat("vertical", v).apply()
} }
// Хостовые STT-события с телефона. Микрофон и распознавание владеет слой записи
// [ListeningLayout]: маршрутизирую в *верхний слой* (транспорт о слоях не знает —
// сюда заходит из [HostConnection]). Слоя записи нет — событие неактуально, игнор.
// Activity о распознавании не знает.
/** Пустой клик без голоса (SttCancel reason="user") — верхнему слою («не расслышал»). */
fun stopSttListening() {
val layer = layoutManager.current as? ListeningLayout ?: run {
log("stt", "стоп-записи без слоя записи — игнор")
return
}
layer.onSttCancelUser()
}
/** Таймаут тишины 30с с телефона (SttCancel reason="timeout") — верхнему слою. */
fun onSttTimeout() {
val layer = layoutManager.current as? ListeningLayout ?: run {
log("stt", "стт-таймаут без слоя записи — игнор")
return
}
layer.onSttTimeout()
}
/** Хостовая накопленная фраза (SttPhrase.full с телефона) — верхнему слою (живой текст). */
fun onSttPhrase(full: String) {
val layer = layoutManager.current as? ListeningLayout ?: run {
log("stt", "стт-фраза без слоя записи — игнор")
return
}
layer.onSttPhrase(full)
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
@@ -40,9 +40,6 @@ import pw.binom.viewmate.core.protocol.SetScale
import pw.binom.viewmate.core.protocol.SetVertical import pw.binom.viewmate.core.protocol.SetVertical
import pw.binom.viewmate.core.protocol.Shutdown import pw.binom.viewmate.core.protocol.Shutdown
import pw.binom.viewmate.core.protocol.ShowText import pw.binom.viewmate.core.protocol.ShowText
import pw.binom.viewmate.core.protocol.SttCancel
import pw.binom.viewmate.core.protocol.SttDone
import pw.binom.viewmate.core.protocol.SttPhrase
import pw.binom.viewmate.core.protocol.Welcome import pw.binom.viewmate.core.protocol.Welcome
import pw.binom.viewmate.core.protocol.protocolJson import pw.binom.viewmate.core.protocol.protocolJson
@@ -368,42 +365,6 @@ class HostConnection(
) )
} }
is SttPhrase -> {
// Хостовую накопленную фразу отдаю *верхнему слою* (как и SttCancel):
// микрофон/распознавание владеет слой записи, а транспорт о слоях не знает
// (роутинг — в [GlassesApp]).
GlassesApp.instance.onSttPhrase(msg.full)
log("stt", "фраза: ${msg.phrase}")
}
is SttDone -> {
log("stt", "ВЕСЬ ТЕКСТ: ${msg.full}")
}
is SttCancel -> {
when (msg.reason) {
SttCancel.REASON_CANCEL -> {
// Явная отмена диктовки двойным тапом: очки САМИ уже сделали стоп
// (POP кадра «слушаю» с ОСТАВАНИЕМ в ассистенте — микрофон закрыт,
// чат не прерван, оверлей на месте). Это всего подтверждение, что
// телефон отменил STT-сессию (stt.reset()) — состояния не трогаем.
log("stt", "отмена диктовки (двойной тап) — стоп, остался в ассистенте, оверлей на месте")
}
SttCancel.REASON_TIMEOUT -> {
// Авто-отмена по тишине 30с: пользователь молчит/ушёл — ВЫХОДИМ из
// режима (микрофон закрыть, оверлей скрыть), как exitAssistantMode.
log("stt", "тишина 30с — выход из режима общения")
GlassesApp.instance.onSttTimeout()
}
else -> {
// reason="user" — пустой клик без голоса: не выходим из режима,
// стоп-записи с остаованием в ассистенте (idle), «не расслышал».
log("stt", "отменено — стоп, остаюсь в ассистенте (не расслышал)")
GlassesApp.instance.stopSttListening()
}
}
}
is Shutdown -> { is Shutdown -> {
addMessage("Shutdown: закрываю приложение") addMessage("Shutdown: закрываю приложение")
// как в pair-view: finish() Activity очков // как в pair-view: finish() Activity очков
@@ -411,6 +372,9 @@ class HostConnection(
// страховка: процесс завершится сам, но kill убирает зависшие потоки (RayNeo) // страховка: процесс завершится сам, но kill убирает зависшие потоки (RayNeo)
android.os.Process.killProcess(android.os.Process.myPid()) android.os.Process.killProcess(android.os.Process.myPid())
} }
// Неподдерживаемое host-сообщение — молча игнор (в т.ч. legacy phone-STT: очки не ждут).
else -> Unit
} }
} }
@@ -209,8 +209,8 @@ class MainActivity : BaseEventActivity() {
// Мозг ассистента (стейт/эффекты) — в движке общения [GlassesApp.assistantEngine]: // Мозг ассистента (стейт/эффекты) — в движке общения [GlassesApp.assistantEngine]:
// слои AssistantLayout / ListeningLayout упираются в него, кино — в MoveLayout. // слои AssistantLayout / ListeningLayout упираются в него, кино — в MoveLayout.
// Хостовые STT-события (SttPhrase/SttCancel) маршрутизирует [GlassesApp] в верхний // Activity о распознавании не знает: запись самозамкнута в [ListeningLayout] (локальная
// слой — Activity о распознавании не знает. // стт), host-STT-события очки не обрабатывают (молча игнор в HostConnection).
/** /**
* Звуки касания трекпада играет сам SDK (GestureDetectorWithSound, SoundPool: * Звуки касания трекпада играет сам SDK (GestureDetectorWithSound, SoundPool:
@@ -30,7 +30,7 @@ data class AssistantState(
val stack: List<NavFrame> = listOf(NavFrame.Film), val stack: List<NavFrame> = listOf(NavFrame.Film),
/** «Думаю»: фраза отправлена, ждём ответ (ShowText). Контент кадра ассистента. */ /** «Думаю»: фраза отправлена, ждём ответ (ShowText). Контент кадра ассистента. */
val thinking: Boolean = false, val thinking: Boolean = false,
/** Накопленные фразы (SttPhrase, full). Контент кадра «слушаю». */ /** Живой текст записи (локальное распознавание в [ListeningLayout]). Контент кадра «слушаю». */
val sttText: String = "", val sttText: String = "",
/** Ответ ассистента (ShowText). Контент кадра ассистента. */ /** Ответ ассистента (ShowText). Контент кадра ассистента. */
val assistantText: String = "", val assistantText: String = "",
@@ -13,9 +13,9 @@ import kotlin.coroutines.resume
/** /**
* Слой записи — ЕДИНСТВЕННЫЙ, кто знает про микрофон и распознавание. Он сам открывает * Слой записи — ЕДИНСТВЕННЫЙ, кто знает про микрофон и распознавание. Он сам открывает
* микрофон ([stt.session.start] в [onEnter]), закрывает ([onExit]) и сам принимает * микрофон ([stt.session.start] в [onEnter]), закрывает ([onExit]) и сам принимает
* стт-решение: клик распознаёт накопленное ([stt.session.send]), результат/события * стт-решение: клик распознаёт накопленное ([stt.session.send]), локальный результат
* (локальный [onSttResult], хостовые [onSttPhrase]/[onSttCancelUser]/[onSttTimeout]) слой * ([onSttResult]) слой переваривает сам и сообщает вызывающему факт исхода кэлбэками
* переваривает сам и сообщает вызывающему факт исхода кэлбэками [onSuccess]/[onCancel]. * [onSuccess]/[onCancel]. Хостовых входов нет — в локальной архитектуре слой самозамкнут.
* *
* Для всего остального (кино, чат, ассистент) «текст откуда-то берётся» — никто кроме этого * Для всего остального (кино, чат, ассистент) «текст откуда-то берётся» — никто кроме этого
* слоя не в курсе микрофона и распознавания. Зависимость — единый объект [GlassesStt] (весь * слоя не в курсе микрофона и распознавания. Зависимость — единый объект [GlassesStt] (весь
@@ -118,7 +118,7 @@ class ListeningLayout(
* - пусто + клик (без голоса) → отмена ([onCancel]) + POP слоя (не расслышал). * - пусто + клик (без голоса) → отмена ([onCancel]) + POP слоя (не расслышал).
* Текст дальше «уходит от куда-то» (в LLM на телефон) — слой это не видит. * Текст дальше «уходит от куда-то» (в LLM на телефон) — слой это не видит.
*/ */
fun onSttResult(text: String, auto: Boolean) { private fun onSttResult(text: String, auto: Boolean) {
sttText.value = text sttText.value = text
when { when {
text.isNotBlank() -> { text.isNotBlank() -> {
@@ -137,23 +137,4 @@ class ListeningLayout(
} }
} }
} }
/** Хостовая накопленная фраза (SttPhrase.full): обновить живой текст слоя. */
fun onSttPhrase(full: String) {
sttText.value = full
}
/** Хостовая отмена (SttCancel, reason="user"): отмена, остаюсь в ассистенте (POP слоя). */
fun onSttCancelUser() {
log("stt", "хост: отмена (user) → стоп, остаюсь в ассистенте (не расслышал)")
onCancel()
ctx.close()
}
/** Хостовый таймаут (SttCancel, reason="timeout"): отмена, остаюсь в ассистенте (POP слоя). */
fun onSttTimeout() {
log("stt", "хост: тишина 30с → отмена, остаюсь в ассистенте")
onCancel()
ctx.close()
}
} }