27 Commits

Author SHA1 Message Date
subochev 5c86ed00b4 STT: очки распознают речь локально и шлют готовый текст (SttText) в телефон
- glasses: локальный ASR через общий pw.binom.asr (GlassesAsr/GlassesSttSession/SttModelProvider)
- protocol: SttText — очки присылают распознанный текст в телефон; телефон кидает его в LLM
  (старый SttAudio/StopStt оставлен фолбэком для старых очей)
- phone: SttStreamer делегирует общему ASR; GlassesServer роутит SttText в LLM
- lib-core: AudioPrep общий
2026-09-04 03:08:26 +03:00
subochev 0b3f9ff975 Mirror: медленный list(500) не роняет каталог и не дёргается в цикле
- отдельный defaultMirrorHttpClient (connect 15с / request 90с) + 1 повтор на list():
  тяжёлый GET /api/mirror?limit=N на сервере дольше общего 30с-таймаута и подёргивается (503);
- каталог: зеркала грузятся в отдельном try — сбой/таймаут не чистит уже загруженный каталог,
  просто временно без бейджей;
- детали: убрали тяжёлый list(limit=500) каждые 3 с (progress зеркала на сервере не живой, 0
  до конца) — в цикле опрашивается только дешёвый mirrorByItem.
2026-09-02 23:02:56 +03:00
subochev f656f43e10 Сохраняем прогресс просмотра и возобновляем с позиции 2026-09-02 20:55:00 +03:00
subochev b2e2d2697d Добавляем индикатор рассинхронизации звука и видео в статус-панели очков 2026-09-02 19:27:11 +03:00
subochev e18b82642f audio-sync: учитывать латентность линка очки→телефон
Очки штампуют sentTs в PlaybackPosition; телефон мерит one-way
латентность (receivedTs - sentTs, EWMA) и добавляет её к мастер-позиции
в sync(), снимая системный перекос, из-за которого аудио недокручивало
на величину транспортной задержки. Плюс правки тестов под
HARD_SEEK_THRESHOLD_MS=5000 (остались незакомиченными с прошлого фикса).
2026-08-30 05:08:37 +03:00
subochev 1aa389a3a0 fix(log): разблокировать спул лог-коллектора при устаревших таймстемпах
Раньше одна пачка в спуле с таймстемпом вне окна приёма Loki (≈7 суток)
топила весь пуш: Loki отвечал 400 на весь batch, спул никогда не чистился,
и новые логи (в т.ч. телефона) не доходили. Теперь flush() выгребает ВСЕ
готовые пачки за тик (FIFO) и отбрасывает записи вне окна Loki, а при
неудаче отправки спула буфер не трогается (ждёт следующий тик). Добавлены
тесты на очистку устаревших записей и FIFO-ретрай спула.
2026-08-30 03:30:39 +03:00
subochev 6cd4a8de43 feat: управление яркостью/WiFi очков через Mercury + накопленные доработки
- Mercury (app-phone): управление яркостью (с авто-выкл. перед
  применением) и WiFi (скан/подключение/список сетей с паролем);
  добавлено право BLUETOOTH_ADVERTISE в манифест и runtime-запрос
  (без него GATT-сервер не рекламируется → очки не подключаются)
- логирование (lib-core core.log): BatchingLogCollector + LogSink +
  LogSpool + LokiLogSink; цепочка очки→телефон→Loki; lib-core log()
  переадресован в коллектор
- STT: Qwen3-ASR через pw.binom.asr:asr-qwen3-android, убран in-house
  whisper-фоллбек
- audio-sync: P-регулятор с насыщением вместо фиксированной скорости
- media-split: очки только video, телефон только audio (UI-чипы)
- Components: форматирование размера через Locale.ROOT
2026-08-29 23:06:52 +03:00
subochev 6d22f56bd8 Регулятор «Высота» экрана очков (0..1, с учётом масштаба): SetVertical по протоколу, offset Y в кино/коннект/оверлее, слайдер на пульте 2026-08-25 02:11:01 +03:00
subochev 7557d7229f Очки: отмена слушания двойным тапом — SttCancel reason=cancel 2026-08-24 03:01:55 +03:00
subochev e24caaa684 Очки: разграничение SttCancel (user/timeout) — выход из режима при тишине 30с, микрофон закрывается при потере фокуса 2026-08-23 21:17:43 +03:00
subochev 706cc10d1e Очки: история диалога в свайпе + распознавание в LLM только по клику (касяки 1-3) 2026-08-23 20:15:21 +03:00
subochev 28d0f9b68d Агент: медиа-тулсет (search/info/play) на Jellyfin + скилл media
- MEDIA_TOOLSET + MediaToolExecutor: search (поиск с лимитом), info (детали), play (проверки+запуск)
- JellyfinMediaSource: обёртка JellyfinClient (search/item) с защитой userId
- agentPlayMedia: проверки (очки на связи, видео скачано на очки, звук на телефоне) → watch с позицией
- PhoneActions.watch(startPositionMs), JellyfinItem.ProductionYear
- скилл app-phone/skills/media.md (frontmatter: name/description)
- MediaToolsTest: 22 кейса — всего 142 теста зелёные
2026-08-22 21:42:53 +03:00
subochev bbf7a020d4 Ассистент Порфирий: тула времени, сессии, чат-UI, голосовой цикл; BT-транспорт очки↔телефон
- BT (RFCOMM SPP 00001105) вместо WiFi: BtGlassesTransport/BtServerTransport, реконнект 3с
- PlaybackService (foreground, WAKE_LOCK) против морозов HONOR
- Ассистент: AgentTools (TimeTool ISO 8601), TOOL_CALL-протокол, сессии SQLite (UUID)
- Чат-UI: создание/сброс/переключение сессий, автообновление ленты 2с
- LlmClient на прямом OkHttp (Koog давал пустой content на длинных историях)
- Голосовой цикл: тройной тап на очках → STT → активная сессия → ShowText
- Тесты: AgentToolsTest (3), GlassesServerTest переписан (6) — зелёные
2026-08-22 16:31:04 +03:00
subochev ee9700c558 CICD: сборка APK на release → F-Droid; versionCode из тега; фикс скролла каталога и масштаба
build / Android APK to F-Droid (release) Failing after 36s
2026-08-21 19:27:02 +03:00
Hermes Agent 1304c6d921 feat: голосовой ввод на очках — TripleClick + PCM → телефон → текст на очки
Протокол: SttAudio (PCM s16 16k mono кусками), SttPhrase/SttDone/SttCancel
(host→очки), StopStt(cancel) (очки→host).

Очки: TripleClick — безусловный вызов (кино: пауза + затемнение + слушаем);
во время распознавания Click — завершить (весь текст в консоль, затемнение
уходит, видео на паузе), DoubleClick — отмена; VAD-тишина 30с — автоотмена.
Микрофон (AudioRecord 16k) → SttAudio; на эмуляторе — PCM из filesDir/samples
(куски 3200 + пауза 200мс). Оверлей текста в Binocular (полупрозрачный фон,
текст по центру, в оба глаза). RECORD_AUDIO в манифесте. DEBUG-ресивер
(am broadcast DEBUG_GESTURE type=triple_click|click|double_click).

Телефон: SttStreamer — стриминговый VAD (Silero fp32, окна 512) → фразы →
Whisper-small int8 (executor, не блокирует приём) → SttPhrase на очки;
StopStt → SttDone (полный текст) / SttCancel; watchdog 30с тишины.

Фиксы (найдены на эмуляторе): IllegalThreadStateException — двойной старт
watchdog-потока (thread{}.apply{start()}); decode входящих — try/catch с
логом (не роняет сессию).

Проверено на эмуляторах (5554 очки + 5556 телефон, forward 8080):
triple_click → фразы «Раз, два» и «3 4 5» на очках (оверлей) → click →
«ВЕСЬ ТЕКСТ: Раз, два 3 4 5»; 30с-тишина → «отменено». Тесты: lib-core,
phone, glasses — зелёные.
2026-08-20 04:12:28 +03:00
Hermes Agent 2844d6a3ac feat: разделение контента — на телефон только аудио, на очки только видео
Телефон качает audio-N.ogg (audioKeys вместо fileKeys, стадия «звук»),
очки получают DownloadFiles только с video.mkv (buildGlassesFileSpecs).
Watch проверяет готовность обоих: телефон — audioReady(), очки — как
было; отказ с подсказкой «Скачать на телефон/очки». Тесты: lib-core 19,
phone 36.
2026-08-20 01:41:43 +03:00
Hermes Agent 896185244b fix(app-glasses): scale на паузе — принудительная перерисовка кадра
На паузе ExoPlayer не шлёт кадры — поверхность меняла размер/позицию,
но кадр оставался старым (часть видео не видно). requestRedraw():
updateTexImage + drawFrame для всех outputs — вызывается при смене
scale (LaunchedEffect) и в surfaceChanged. Подтверждено на железе.
2026-08-20 00:52:21 +03:00
Hermes Agent e29bc5032d feat: автоподхват звука после рестарта телефона (очки играют — звук молчит)
Сценарий (железо): телефон перезапущен, очки продолжают играть — звук
не создавался (плеер живёт только по watch). Решение:
- PlaybackPosition теперь несёт itemId+audioIndex (дефолты — совместимость)
- очки: MovieController хранит itemId/audioIndex (playVideo/close), шлёт в позиции
- телефон: onPlaybackPosition → autostartAudio: если itemId есть, аудио не
  активно и не было неудачи — mirrorByItem → resolveAudioSource → play(source,
  positionMs); lastFailed защита от спама каждые 5с
- Тесты: 13 lib-core (сериализация + старый формат), 4 glasses
  (contentId/close), 70/0 phone; железо: рестарт телефона → «audio sync»
  через ~5с (diff 483), подтверждено пользователем
2026-08-19 21:45:23 +03:00
Hermes Agent 8072d0c95a feat(app-phone): трекбар-таймлайн в пульте (seek на любую позицию)
- MediaCommand.SEEK (абсолютный seek на seekMs, поле в протоколе уже было)
- MovieController (очки): SEEK → player.seekTo(seekMs.clamp(0,duration)); чистая seekTargetMs
- GlassesScreen (пульт): material3 Slider под статус-карточкой (виден при durationMs>0),
  локальный стейт драга — время под слайдером следует за пальцем (sliderValue ?: positionMs),
  после отпускания цель держится до подтверждения позиции от очков (±2.5с, таймаут 10с),
  звук: SEEK → audio.seekTo(seekMs)
- Тесты 70/0 phone, 37/0 glasses (MovieControllerSeekTest, сериализация SEEK)
2026-08-19 21:35:20 +03:00
Hermes Agent 64f0efde37 fix: детект обрыва связи ≤5с (ping 15с→5с) — пауза при обрыве обязательна
Требование пользователя (проверено на железе): при потере связи видео
обязательно ставится на паузу и быстро. Было: ping 15с + таймаут 15с на
обоих концах — обрыв замечался до ~35с (видео убегало, звук вставал —
рассинхрон). Стало: pingIntervalMillis 5с (lib-core Ktor), pingPeriod/
timeoutMillis 5с (app-phone OkHttp) — детект ≤10с.
Железо: обрыв → «связь потеряна — пауза» → «связь восстановлена —
продолжаю» → diff 139. Визуально подтверждено пользователем.
2026-08-19 21:21:43 +03:00
Hermes Agent 49dac8b33f fix(app-phone): звук при watch (диагностика+подстраховка mirrorByItem) + качка не воскресает после удаления
- onWatch: явные логи в каждой точке отказа (watch result / audio source / уже играет / не скачано / mediaDir пуст), свежий mirrorByItem если files не догрузился, звук стартует с позиции очков при уже идущем видео
- resolveAudioSource → resolveAudioSourceDetailed (AudioSourceResult с причиной null)
- DownloadManager: поколения generations — delete() инвалидирует старую корутину (не воскрешает качку), ensureActive перед .part/rename, jobs.remove(itemId, job)
- Тесты 62/0 (в т.ч. поколения — поймали null != 0L)

Инфраструктурный блокер (не код): SeaweedFS на Server3 76.66 в IO-шторме (load ~184, wa 20%, чужой yt-dlp на хосте) — большие GET с s3.binom.pw висят/рвутся с ~14:00; качка на очки FAILED по этой причине.
2026-08-19 17:19:53 +03:00
Hermes Agent 6069400b88 feat(app-phone): полноценный UI телефона — каталог Jellyfin, детали, пульт очков, реальное скачивание с S3
- 4 экрана по макету sketches/phone-ui: Каталог (табы Фильмы/Сериалы/Папки, поиск, бейджи зеркала), Детали (озвучки, статус конвертации, чипы скачивания), Очки (пульт, статус), Чат-заглушка
- S3 SigV4 presign (S3Presigner): mirror-client read-only креды; watch() и DownloadManager подписывают URL (бакет приватный)
- DownloadManager: реальное скачивание video+audio с прогрессом, readTimeout=0 (длинные файлы)
- PhoneActions.watch: параметр presign (app-host работает без изменений)
- TESTING.md: блок телефонных кейсов TP-1..6
2026-08-19 09:13:56 +03:00
Hermes Agent 298d8e0b69 feat: сетевая часть для app-phone (WS-сервер) и app-glasses (WS-клиент), PhoneState/PhoneActions → lib-core
lib-core:
- core/phone/: PhoneState, PhoneActions, GlassesSender (интерфейс) — перенесены из app-host
- core/net/: GlassesWsClient — KMP WS-клиент очков (реконнект, Hello, send GlassesToHost)
- GlassesWsClientTest + PhoneActionsTest (перенесён)

app-phone (Android):
- GlassesServer: Ktor WS-сервер (CIO) 0.0.0.0:8080/ws/glasses, принимает очки, Welcome из стейта
- PhoneApp (Application): клиенты, стейт, действия, сервер
- PhoneConfig: креды Jellyfin/Mirror
- MainActivity: статус сервера (порт, счётчик подключений)
- GlassesServerTest: hello→welcome, gesture

app-glasses (Android):
- GlassesApp (Application), HostConnection: WS-клиент к телефону, Hello, реконнект, обработка HostToGlasses (PlayVideo/SetMode и т.д.), StateFlow для UI
- MainActivity: статус подключения

app-host: PhoneActions/PhoneState удалены (теперь lib-core), адаптирован под GlassesSender

171 тест, 0 провалов; app-host жив (catalog/watch/status работают через lib-core)
2026-08-11 15:06:07 +03:00
Hermes Agent f0f1f71575 feat: app-host → телефон-эмулятор (Jellyfin + mirror + стейт + команды)
- Config.kt: env > application.properties > дефолты (креды не в коде)
- PhoneState.kt: единый стейт — mode, currentItem, audioIndex, mirrorStatus, activeSessionId, сессии (эмуляция), downloaded
- PhoneActions.kt: catalog (с бейджами зеркал), open, watch (PlayVideo на очки), makeMirror, mirrorStatus, download/delete, sessions/selectSession, status
- Main.kt: новые консольные команды (catalog/open/watch/mirror/dl/rm/sessions/session/status)
- GlassesHub: Welcome берёт mode/sessionId из стейта, glasses_off → пауза
- JellyfinClient.item() — элемент по id (для open/watch)
- 58 тестов, 130 всего, все зелёные; живые проверки: catalog (реальный Jellyfin), watch (PlayVideo с s3-url), status (стейт обновляется)
2026-08-11 14:37:43 +03:00
Hermes Agent d95d289d29 feat: HTTP-клиенты Jellyfin + media-mirror в lib-core (KMP, Ktor)
media/JellyfinClient: users, rootItems, items, search, allItems, posterUrl (X-Emby-Token)
media/MirrorClient: createMirror, mirrorByItem (404→null), list, delete, scan (X-API-Key)
media/HttpClient: defaultHttpClient (ContentNegotiation+json, таймауты, ApiClientException)
Ktor 3.3.0 (совместим с Kotlin 2.2; 3.5.2 требует Kotlin 2.3 — не берём)

17 тестов (MockEngine): пути, методы, заголовки, JSON, 201/404, round-trip DTO.
Проверено на живых серверах: Jellyfin (6 users, корень, поиск 'дюна'), mirror (by-item done + url, список done)
2026-08-11 12:59:44 +03:00
Hermes Agent 9271517b3c feat: WS-протокол очки↔телефон + app-host (JVM Ktor-сервер)
lib-core (commonMain):
- Messages.kt: GlassesToHost (hello, gesture, glasses_status, playback_position, glasses_off)
- HostToGlasses.kt: Welcome, SetMode, MediaCommandMsg, PlayVideo, AssistantStateMsg, ShowText, ChatSessionMsg
- AssistantState enum (IDLE/LISTENING/THINKING/SPEAKING)
- round-trip тесты сериализации обоих направлений

app-host (переведён с Android на JVM):
- Ktor WS-сервер 0.0.0.0:8080/ws/glasses — очки подключаются как WS-клиенты
- GlassesHub: broadcast всем очкам, счётчик подключений (StateFlow)
- консольный ввод: play/mode/state/text/session/cmd/exit — команды от «телефона»
- логирование всех сообщений обоих направлений
- HostServerTest: hello→welcome, счётчик подключений

Проверено: сборка зелёная, 21 тест 0 провалов, живой WS-цикл (hello→welcome, gesture, статус, позиция, glasses_off)
2026-08-11 12:35:30 +03:00
Hermes Agent 5268a12d20 view-mate: болванка + концепт + эскизы + сценарии (единый коммит, история очищена от hprof/build) 2026-08-11 11:34:50 +03:00