Очки: история диалога в свайпе + распознавание в LLM только по клику (касяки 1-3)
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# TASK-glasses-chat-fix.md — три кассия по голосовому диалогу (отчёт теста 23.08)
|
||||
|
||||
## Контекст
|
||||
Режим общения с ассистентом: очки (app-glasses) → BT SPP → телефон (app-phone) → STT (Whisper+VAD) →
|
||||
LLM → ShowText. Пользовательский тест от 23.08 нашёл три кассия. Корневые причины уже установлены
|
||||
расследованием — ниже точные правки. Не трогать: аппаратное ускорение GPU на телефоне, скорость —
|
||||
проверено, не трогать.
|
||||
|
||||
## Кассий №3 (приоритет): VAD-пауза = самоотправка в LLM
|
||||
**Корень:** `PhoneApp.ensureStt()` (app-phone, ~строка 355): колбэк `onPhrase` SttStreamer делает
|
||||
`assistantChannel.send(phrase)` — каждая VAD-фраза (сегмент речи, отделённый паузой ~0.25–20с)
|
||||
немедленно улетает в `assistantLoop` → LLM. Человек говорит, замолчал на секунду — VAD вырезал
|
||||
сегмент, и запрос уже ушёл в нейронку.
|
||||
|
||||
**Правка:**
|
||||
1. В `PhoneApp.ensureStt` убрать из `onPhrase` отправку в `assistantChannel` (оставить только
|
||||
`broadcast(SttPhrase(...))` — экрану очков показывать распознавание по-прежнему нужно).
|
||||
2. Отправка в `assistantChannel` — только по `StopStt(cancel=false)` из хаба
|
||||
(`GlassesServer.handle`, ветка `is StopStt`): `val full = stt?.finish() ?: ""; stt?.reset()`,
|
||||
затем (если `full.isNotBlank()`) — `assistantChannel.send(full.trim())` и бродкаст.
|
||||
Т.е. единственный путь текста в LLM — повторный клик пользователя.
|
||||
3. В логах сохранить читаемость: `log("stt", ...)` что фраза в LLM отправлена только по клику.
|
||||
|
||||
Проверка логики: VAD-сегменты во время речи НИКОГДА не уходят в LLM; пустая пауза — тоже нет.
|
||||
|
||||
## Кассий №2: пустой клик → висящее «думаю...» 10+ с → выкидывание из режима
|
||||
**Корень:** клик без голоса → `StopStt(cancel=false)` → телефон шлёт `SttDone("")` + LLM-цикл
|
||||
получает пустоту (после правки №3 вообще не получит). Очки в `HostConnection`: `SttDone` →
|
||||
`listening=false`, а `thinking` сбрасывается только на `ShowText`/`SttCancel` — а их нет (пустая
|
||||
фраза в LLM не уходит). Оверлей висит в «думаю...» до автo-отмены тишины 30с (SttCancel) →
|
||||
`listening=false, thinking=false` → оверлей прячется → пользователь видит кино (пауза).
|
||||
«Выкинуло из режима» + долгое «думаю» объяснено.
|
||||
|
||||
**Правка:**
|
||||
1. Телефон: `StopStt(cancel=false)` с пустым `full` — НЕ шлёт `SttDone`, а бродкастит `SttCancel`
|
||||
(«фразы не было») и `stt?.reset()` — очки возвращаются в «слушаю» (см. п.2).
|
||||
2. Очки `HostConnection`: `SttCancel` — сброс `thinking=false`, `listening` = true (вернуться в
|
||||
«слушаю», микрофон открыть заново — старт `sttMic` вызвать из обработчика через
|
||||
`GlassesApp.instance.mainActivity`). Заголовок оверлея автоматически вернётся к «слушаю...»
|
||||
(`sttText` пустой — это OK, он при SttCancel не сбрасывается: оставил бы накопленное, если было).
|
||||
Реализация — в `MainActivity`: новый хук `onSttCancelledBackToListening()` (по аналогии
|
||||
`startAssistantMode` без очистки стtText).
|
||||
3. В `AssistantGestures.overlayHeader` поведение не менять (логика «слушаю...» уже корректна).
|
||||
|
||||
Проверка: клик без голоса → очки мгновенно в «слушаю...», без «думаю» и без выхода из режима.
|
||||
|
||||
## Кассий №1: в свайпе видна только последняя пара сообщений, история диалога не доступна
|
||||
**Корень:** протокол передаёт очки только последний ответ (`ShowText.text`) и текущую
|
||||
распознаваемую фразу (`sttText`). История сессии живёт в SQLite телефона
|
||||
(`ChatDao.messagesForSession`) — очкам её никто не отдаёт. Скроллить в оверлее нечего.
|
||||
|
||||
**Правка:**
|
||||
1. `lib-core/.../protocol/HostToGlasses.kt`: новое сообщение
|
||||
`@SerialName("chat_history") data class ChatHistoryMsg(val sessionId: String?, val messages: List<ChatHistoryItem>)`,
|
||||
где `ChatHistoryItem(val role: String /*"user"|"assistant"|"tool"*/, val content: String)`
|
||||
(только user/assistant; tool-сообщения и toolCalls не нужны очкам — фильтр на телефоне).
|
||||
2. `GlassesServer` (tелефон): на `StopStt(cancel=false)` с непустым full — ПОСЛЕ отправки в
|
||||
LLM-цикл (или на `Welcome` — при подключении очков, если сессия активна) бродкаст
|
||||
`ChatHistoryMsg` с историей текущей/активной сессии (`chatDao.messagesForSession`,
|
||||
`chatDao.latestSessionId()`), роль user/assistant, content (tool-контент не передавать).
|
||||
Точка отправки — после показа ответа, чтобы в истории был и ответ ассистента:
|
||||
простейший вариант — бродкаст истории в `assistantLoop` сразу после `broadcast(ShowText(answer))`.
|
||||
3. Очки: `GlassesApp` — `chatHistory = MutableStateFlow<List<ChatEntry>>(emptyList())`
|
||||
(`ChatEntry(role, text)`); `HostConnection` на `ChatHistoryMsg` — `filter { role == "user" ||
|
||||
role == "assistant" }`, сохранить. Новый `ShowText` не должен стирать историю (это последнее
|
||||
сообщение, оно уже в истории).
|
||||
4. `AssistantOverlay`: вместо пары «sttText + assistantText» — список `chatHistory`:
|
||||
«я: ...» (user, правее/одним цветом) и «ассистент: ...» (вторым цветом), один
|
||||
`verticalScroll` на весь список, автоскролл к концу при новом `ShowText`
|
||||
(`LaunchedEffect(lastHistoryVersion) { scrollState.animateScrollTo(scrollState.maxValue) }`).
|
||||
Режимы «слушаю»/«думаю» заголовок — как сейчас; свайпы — те же `ScrollAnswer` дельты на общий
|
||||
скролл (механизм `answerScrollDelta` сохранить, теперь он скроллит список, не одно сообщение).
|
||||
5. `AssistantGestures` — без изменений (матрица жестов не меняется).
|
||||
|
||||
## Ограничения
|
||||
- Код только Kotlin. Не трогать: `LocalLlm.kt` (GPU-конфиг, uses-native-library), скорость,
|
||||
клавиатуру телефона, автоименование сессий.
|
||||
- Протокол — backward-совместимый: новые сообщения с `@SerialName`, старые клиенты не падают
|
||||
(ксеролайн — ignore-неизвестные в `protocolJson`?). Проверить флаг `ignoreUnknownKeys`
|
||||
в `ProtocolJson`/`protocolJson` — если не включён, включить (очки новой версии → телефон старой
|
||||
и обратно).
|
||||
- Unit-тесты: жесты не менять, но добавить/поправить, если `AssistantState` меняется.
|
||||
- Сборка: `./gradlew :app-glasses:assembleDebug :app-phone:assembleDebug :lib-core:build` —
|
||||
компиляция и тесты без ошибок.
|
||||
|
||||
## Определяющий тест (прогон на устройстве позже)
|
||||
1. Кликаю → говорю → пауза 2с → продолжаю → клик. LLM получает ВСЮ фразу целиком, один раз.
|
||||
(Логи: одна `ВЕСЬ ТЕКСТ`, один `получена фраза`.)
|
||||
2. Клик без голоса → «слушаю...» без «думаю», остаюсь в режиме.
|
||||
3. Несколько реплик подряд → свайп показывает все предыдущие пары я/ассистент, автоскролл к концу.
|
||||
Reference in New Issue
Block a user