- agent/predict.py: HTTPServer bind 127.0.0.1 -> 0.0.0.0, иначе route.ts
(контейнер планшета) не достукивается до tablet-agent по имени в сети coolify
- route.ts: historyToAnthropicMessages приводит не-объект/null из
tc.function.arguments к {} — Anthropic 400 (tool_use.input must be object)
на legacy-истории от Groq (arguments: "null")
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYsVrPyJ5phJeVsAGciS1v
- промпт: перечислены ВСЕ 15+ tools, разрешено уверенно управлять домом
- route.ts: живой контекст (время + снимок устройств из HA) в системный промпт
для LLM-пути (defensive, с таймаутом)
- новые tools: control_climate (кондей/термостат), control_tv, set_scene
(night/movie/morning/away), all_off, ha_service (универсальный HA с whitelist)
- env: CLIMATE_ENTITY, TV_ENTITY (см. RUNBOOK)
- погода-hero: насыщенный indigo/violet градиент, крупная анимация справа
(убирает воид), температура 100px, стеклянные чипы деталей
- карточки заметно светлее (surface-3 glass) — явно поднимаются над фоном
- ambient крупнее и насыщеннее (indigo/cyan/violet) — фон живой, не чёрный
- карточки: полупрозрачный градиент + backdrop-blur + inset-подсветка +
глубокие тени + верхняя градиентная кромка (эффект стекла над aurora)
- Plus Jakarta Sans для заголовков/крупных чисел (выразительнее Inter)
- космическая indigo/aurora палитра, третий ореол глубины
- микро-интеракции (hover-подъём, press-scale) + prefers-reduced-motion
- lib/agent-local.ts + route.ts: команды-действия исполняет локальная intent-служба
(под AGENT_URL, с fallback на LLM); инфо-запросы остаются у LLM
- lib/tools/lights.ts: голосовое управление светом через HA (control_light)
- app/globals.css + page.tsx: космическая indigo/aurora тема (токены, kiosk-safe)
- agent/: sidecar-служба (Dockerfile, predict.py), веса монтируются томом
- RUNBOOK.md: вывод в продакшен на мини-ПК
- route.ts: replace @anthropic-ai/sdk with groq-sdk, rewrite chat loop
- voice-tool-schemas.ts: convert from Anthropic format to OpenAI/Groq function tools
- voice-history.ts: extend HistoryMessage type to include tool role, simplify cache stubs
No prompt caching (Groq does not support it), tool calling preserved.
docker --env-file не поддерживает многострочные значения и не парсит
кавычки. Сырой JSON service-account ломается на newline'ах в private_key
поле → docker пытается парсить '-----END PRIVATE KEY-----' как имя
переменной и валится с 'contains whitespaces'.
Решение: base64 GOOGLE_SA_JSON_B64 (одна строка ASCII, никаких кавычек).
Старая GOOGLE_SA_JSON оставлена как fallback. Третий fallback на файл —
для локальной разработки.
Service Account ключ больше не распространяется через git/image —
читается из env GOOGLE_SA_JSON, которая выставлена в /opt/digital-home/
tablet.env на сервере.
- google-sa.json удалён из git tracking (git rm --cached)
- Добавлен в .gitignore + .dockerignore
- На сервере после git pull файл будет удалён с диска;
/api/calendar читает credentials из env (env-first fallback уже был
в коде). Это критично если bundle/image куда-то утечёт — ключ Google
больше не лежит внутри.
Безопасность:
- Rate-limit на /api/voice/chat (20/мин per cookie/IP, env VOICE_RATE_LIMIT).
Защищает от случайных циклов и утечки PIN.
- Усечение user prompt'а до 4000 символов в /api/voice/chat.
- Tool-loop защита от циклов: если LLM дважды просит тот же tool с теми же
args — прерываем (раньше мог уйти в бесконечный цикл при tool error'ах).
Чистка кода:
- lib/debug.ts — vlog/vwarn/verror гейтят браузерные логи за
NEXT_PUBLIC_VOICE_DEBUG=1 (или localStorage 'voice-debug=1').
Серверные console.log оставлены — полезны в Docker logs.
- lib/audio-wav.ts — вынесена дублированная floatToWav из VoiceController.
- Удалены orphan компоненты FocusCard.tsx и CountdownCard.tsx
(не подключены, отвергнуты по UX-фидбеку).
Resilience:
- WakeWordDetector: drop-on-busy в onChunk — на медленных устройствах
(Android, бюджетный CPU) backlog inference больше не копится.
- voice-history fallback на /tmp/voice-history если /data не примонтирован
(локальная разработка / нестандартная конфигурация).
VAD на iPad валился с «no available backend found / RangeError».
onnxruntime-web 1.24 ships только threaded WASM сборки, которые
требуют SharedArrayBuffer. iOS Safari его не даёт без cross-origin
isolation — отсюда и ошибка.
Добавляем заголовки:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless
credentialless (а не require-corp) выбрали чтобы не ломать внешние
ресурсы без CORP-заголовка (Google Fonts, wttr.in). Поддержка:
Chrome 96+, Firefox 110+, Safari 16.4+.
iOS Safari блокирует <audio>.play() даже после silent-WAV unlock —
каждый new Audio() считается новым элементом без gesture.
Решение: при тапе кнопки в VoiceController создаём общий
AudioContext (под user-gesture) и пробуждаем его. VoiceOverlay
теперь играет TTS через этот ctx (decodeAudioData + BufferSource).
HTMLAudioElement остаётся fallback'ом если ctx недоступен.
decodeAudioData в Safari исторически callback, в Chrome — Promise:
используем оба варианта.
Android Chrome требует user-gesture для <audio>.play(). Wake-word
триггерит TTS «сам», без тапа, поэтому play() тихо отвергался.
При тапе на кнопку микрофона теперь проигрываем 1мс silent WAV →
браузер помечает страницу как разрешённую для autoplay в текущей
сессии. Дальше TTS-ответы Cosmo/Lusya играют без проблем.
В VoiceOverlay логируем причину если play() всё ещё отвергнут.
Параллельный getUserMedia от MicVAD конфликтует со stream'ом wake-word —
видимо Chrome применяет AGC/NS по-разному и wake получает «глухое» аудио.
Score упал с 0.988 до 0.093 — wake перестал срабатывать.
Возвращаемся: VAD создаётся ПОСЛЕ первого wake (~1-2с пауза),
но cancel/onSpeechEnd теперь только pause (не destroy), так что
повторные wake мгновенные.
В логах было видно: между wake-trigger и реальным VAD recording
проходило 1-2с (Loading VAD... → finished loading → started micVAD).
Каждый cancel дополнительно destroy'ил VAD, и следующий wake снова
ждал инициализацию.
Теперь:
- VAD создаётся один раз в paused-режиме сразу после wake.start()
(в фоне, не блокирует UI).
- На каждый wake → vad.start() мгновенно.
- onSpeechEnd → vad.pause() (был implicit pause; явно ставим).
- voice-cancel → vad.pause(), а не destroy. Wake продолжает слушать.
- destroy только при полном выключении ассистента.
Из ORT-ошибки:
onnx::Flatten_0 index 1 Got: 16 Expected: 25.
Стоковые openWakeWord модели тренируются на окне 16, наша cosmo.onnx —
на 25. Меняем EMB_WINDOW. Окно для embedding-буфера тоже подвинули,
чтобы хватало для classifier'а.
В overlay появляется крестик в правом верхнем углу. Тап = эмитит
voice-cancel → VoiceController прерывает активный VAD-захват и сам
overlay закрывается. Wake-word, если был активен, продолжает слушать
в фоне.
Текущая сборка ~7 минут — npm ci качает все 183 пакета с нуля + next
build без incremental cache на каждом push.
- Dockerfile: BuildKit cache mount на /root/.npm (npm install) и
.next/cache (Next.js incremental TS/swc/webpack cache).
- workflow: DOCKER_BUILDKIT=1 чтобы cache mounts работали.
- .dockerignore: убираем node_modules/.next/.git/docs из контекста —
меньше копирования + чище инвалидация слоёв.
- Поднимаем NODE_OPTIONS heap до 4G в build-стадии (TS падал с OOM).
Первый билд после этого коммита заполнит cache — второй и дальше
будут значительно быстрее (deps install ~5-10с вместо 60-90с,
next build тоже ускорится за счёт incremental).
openWakeWord pipeline на onnxruntime-web прямо на планшете. Цепочка:
mic (16kHz, AudioWorklet) → melspectrogram.onnx → embedding_model.onnx
(sliding 76-frame window, stride 8) → cosmo.onnx → score 0..1.
Триггер при score≥0.5 → запускается тот же VAD-flow что и push-to-talk.
- public/wake/ — cosmo.onnx (custom-trained на голос Даниила) +
melspectrogram.onnx + embedding_model.onnx (~2.9MB вместе).
- lib/wake-word.ts — WakeWordDetector class. ort грузится через
<script src=/vad/ort.wasm.min.js> на клиенте — обход проблемы next-swc
с парсингом import.meta.url в onnxruntime-web .mjs билдах.
- VoiceController: тап = активация (нужен для AudioContext user-gesture),
далее непрерывное слушание wake-word; на детект → MicVAD флоу.
Долгий тап = выкл. Ручной тап остаётся как fallback.
После деплоя Python-агент на .103 не нужен — можно архивировать
home-voice-assistant. На .103 остаётся только ElevenLabs прокси :8888.
В public/vad/ были только asyncify-варианты, а onnxruntime-web по дефолту
просит ort-wasm-simd-threaded.{mjs,wasm} → 404 → MicVAD init falls.
- Положили ort-wasm-simd-threaded.{mjs,wasm} рядом.
- ortConfig forces numThreads=1, чтобы не требовать SharedArrayBuffer
(нет COOP/COEP headers и не хотим их вешать на весь сайт).
- Раздельный getUserMedia probe перед VAD init, чтобы отличить отказ
по микрофону от ошибки VAD/wasm в UI-сообщении.
Шаг 2 миграции: убираем зависимость от Python-агента для базового
голосового сценария. Тап на круглую кнопку-микрофон в правом нижнем
углу → MicVAD (Silero v5) ловит речь → автостоп по тишине → /api/voice/stt
→ /api/voice/chat → ответ через SSE и TTS как раньше.
- components/VoiceController.tsx — push-to-talk UI + MicVAD orchestration
- VoiceOverlay теперь слушает window CustomEvent('voice-local'), чтобы
орб моргал ещё до round-trip на сервер (wake/listening мгновенно).
- public/vad/ — silero v5/legacy onnx + ort wasm + audio worklet,
раздаются через baseAssetPath: '/vad/' (не зависит от внешнего CDN,
важно если планшет без интернета или с RU-блоком).
Что осталось от home-voice-assistant: только wake-word. После Шага 3
(onnxruntime-web + перенос openwakeword .onnx) Python-агент уйдёт целиком.
Шаг 1 миграции голосового стека из home-voice-assistant в сам tablet:
- /api/voice/chat — Claude Haiku 4.5 с tool-loop (max 4 раунда), prompt
caching на system + старой истории, история в /data/voice-history/.
Эмитит command/response/error в voice-bus → орб моргает как раньше.
- /api/voice/stt — Groq whisper-large-v3-turbo, multipart или raw audio.
- lib/voice-text.ts — порт clean_for_speech (без pymorphy3, время в
именительном падеже) и strip_fillers + RESET_PATTERNS.
- lib/voice-executors.ts — tool executors через loopback fetch на
существующие /api/voice/tools/* и /api/voice/timer.
- Поддержка ANTHROPIC_PROXY/GROQ_PROXY (fallback на HTTPS_PROXY).
После деплоя нужны GROQ_API_KEY и ANTHROPIC_API_KEY в tablet.env.
Шаги 2 (push-to-talk в браузере) и 3 (wake-word) — отдельно.
Focus/Countdown не лежат по high-density. На Home вернул старый
weather-hero: WeatherAnimation (фон + иконка), feelsLike/humidity/wind,
76px display-цифра (чуть крупнее прежних 64). FocusCard и CountdownCard
файлы оставляем для будущего, но на главной не подключаем. Убрал
сопутствующие state/fetch (tramNext, countdowns, nextEvent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>