В 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-агент уйдёт целиком.
Tablet touch targets were cramped (30px +/- buttons). Swap to modal UX:
- New TimerModal component handles both 'control existing' and
'create new'. 84px countdown, 56px min button height, 3×2 adjust
grid (-5m, -1m, -10s, +10s, +1m, +5m), big destructive cancel/stop.
Create view has 7 duration presets (1..60 min), label input with
shortcut chips (Чайник, Паста, Яйца…), gradient 'Запустить' CTA.
- TimerHomeWidget cards are now full tap targets — open control modal
on press. + button in header opens create modal. Inline buttons
removed. Subtle hint text 'тап чтобы изменить'.
- Ticking countdown inside modal stays fresh via shared timers state:
while modal is open we look up the current timer in active[] by id
and re-pass to modal on every tick. When timer disappears
(cancelled / expired >30s), modal auto-closes.
Bug: после перезагрузки страницы оверлей «Таймер прозвенел» открывался
снова и снова. Две причины:
- dismissTimer в TimerWidget удалял таймер только из локального
useState, но /data/tablet-timers.json оставался нетронутым. После
reload таймер возвращался в список и firedRef (которая пустая после
reload) снова триггерила alarm.
- lib/timers.ts держал просроченные таймеры 30 минут, давая им шанс
повторно сработать при каждом reload в этом окне.
Фикс:
- dismissTimer теперь POST /api/voice/timer {action:cancel, id} через
cookie auth (endpoint с прошлого коммита принимает и cookie, и bearer).
- Retention в listActive снижена до 30 секунд — этого хватает чтобы
клиент увидел свежий звонок; старше = самоудаление.
- TimerWidget клиентский фильтр тоже 30 секунд.
Two fixes:
1) Overlay was hiding mid-TTS because dismiss timer used
text.length * 80ms — ElevenLabs speaks slower, so the audio got
cut off. Now scheduleDismiss is only called from playTTS's
onEnded callback (plus 4s lingering after audio finishes).
2) After response, the Python script silently re-entered record()
for follow-ups but the overlay disappeared, so the user had to
re-wake every turn. Added a new 'listening' event — Python
emits it just before each followup record(), tablet shows the
orb pulsing at medium intensity with 'жду' status and the last
response text preserved below.
Safety: any state now arms a 60s auto-close in case Python dies
and never emits idle.
UI:
- Replace Notes column on Home bento with TimerHomeWidget. Shows all
active timers as stacked cards with big 30px countdowns, per-timer
+1/-1 minute buttons and cancel. Colors: indigo default, amber in
last 10s, red when expired. Empty state suggests voice command.
- Existing chip TimerWidget (bottom-right) kept for ambient view on
other tabs — redundant on Home, but harmless.
API:
- /api/voice/timer accepts cookie OR bearer (browser widget cancel
works with user's auth_token cookie; Python script uses bearer).
- New action 'adjust' — shifts endsAt by delta_seconds. Clamps so
endsAt never goes into the past.
- Cancel now supports {label} in addition to {id} (fuzzy substring
match, most-recently-started wins). Emits timer_cancel with id+label
so clients can refresh.
- findByLabel / adjustTimer helpers in lib/timers.ts.
Adds the infrastructure for Claude tool use + visual timer.
Tablet API surface (all bearer-authed with VOICE_API_KEY, middleware bypassed):
- /api/voice/tools/weather — current + short forecast via Open-Meteo
- /api/voice/tools/transport — tram arrivals by direction / route filter
- /api/voice/tools/events — Google Calendar today/week
- /api/voice/tools/notes — notes + shopping lists
- /api/voice/timer — start (with seconds+label), cancel; GET list (cookie ok)
Active timers persisted at /data/tablet-timers.json
UI:
- VoiceOverlay stripped to minimal Siri look: no agent emoji/name, just the
pulsing orb (3-layer radial gradient, independent breath animations),
subtle status label on wake only, transcription/response text centered.
Agents distinguished by orb color (Cosmo indigo/violet, Люся pink).
- TimerWidget: bottom-right chip stack with countdown, progress bar, turns
amber in last 10s. On expiry, fires fullscreen alarm overlay with beep
(WebAudio osc) + Остановить button.
Other:
- lib/timers.ts — persistent timer store in /data
- lib/voice-tools.ts — shared bearer-auth helper
- middleware — bypass list now covers /api/voice/tools/* and /api/voice/timer
Stage 2 of voice integration — centralizes TTS on the tablet so the
Python satellite no longer needs ElevenLabs credentials or mpv.
- app/api/voice/tts — POST {text, agent}, proxies to ElevenLabs
streaming endpoint with flash_v2_5 default, returns audio/mpeg.
Per-agent voice id via COSMO_TTS_VOICE / LUSYA_TTS_VOICE env.
- VoiceOverlay — on response/error events fetches TTS and plays via
HTMLAudioElement; on wake event stops playback (barge-in). Dismiss
timer extended by text length so long responses do not cut off.
- Autoplay caveat: browser may block first playback until user taps
anywhere on the page (FKB: enable Force Autoplay to bypass).
Adds the tablet side of voice assistant integration. External Python
script (openWakeWord + Groq STT + OpenClaw) will POST state transitions
to /api/voice/event with a bearer token, and the tablet shows a
fullscreen overlay with Siri-style animated blob + current agent +
recognized text / response text.
- lib/voice-bus.ts — in-process EventEmitter singleton, preserved
across hot reloads via globalThis
- app/api/voice/event — POST, bearer-auth via VOICE_API_KEY env,
validates event kind, broadcasts on voiceBus
- app/api/voice/stream — GET, SSE endpoint, per-connection listener
with 15s keep-alive ping and abort-signal cleanup
- components/VoiceOverlay — full-screen overlay, 3-layer pulsing
Siri blob, per-agent palette (cosmo indigo/violet, lusya pink/rose),
auto-dismiss timeouts (wake=20s safety, response=6s, error=4s),
auto-reconnect on SSE drop
- middleware bypasses /api/voice/event so the script does not need
a user auth cookie
- VoiceOverlay mounted in HomePageInner outside tab routing so it
appears on every view
- WeatherDayModal now accepts the full forecast array and an onChange
callback; supports horizontal drag (framer-motion) plus prev/next
chevrons and a dot-indicator. Drag > 60px switches day; style uses
semantic tokens (shadow-xl, surface-1).
- NotesTab list items wrap each note in a motion.button with drag=x,
constrained to -80px. Below it a gradient+trash reveal layer. Drag
past 60px opens the existing confirmDelete modal.
- HomePageInner adds a night-shift overlay (fixed, mixBlendMode multiply,
rgba(255,120,40,0.12)) active 22:00-06:00, auto-checked each minute,
fades in/out over 800ms. No user toggle yet — fully automatic.
CSS grid items default min-width to min-content; the tram widget 3-col
subgrid plus its баbadges and long затем text forced its cell wider
than 1.1fr, collapsing the outer layout on tablet. Fixes:
- outer bento row → gridTemplateColumns: minmax(0, 1fr) minmax(0, 1.1fr)
- events+notes row same treatment
- TransportWidget inner subgrids: 58px → 52px badge, 1fr → minmax(0, 1fr)
- Cell: minWidth: 0, overflow: hidden, затем text trimmed with ellipsis
and short м suffix (5м instead of 5 мин)
- big number 32→28px, badge 22→20px to fit in denser columns
The big Доброе утро line on Home ate one row for marginal value.
Moved it into TopBar as a center column via 3-col grid (1fr auto 1fr),
so it appears on all tabs and leaves the Home body denser.
- Removes the date label from the greeting row (user request — тoo dense
on tablet and redundant with TopBar clock).
- Scrolling: drops overflowY: auto on the inner Events/Notes cards and
removes flex: 1 / minHeight: 0 from their grid. On iPad-class touch
nested scroll containers fought the root scroll; now only the root
scrolls, which the browser handles natively.
- TopBar: replaces hardcoded rgba(255,255,255,X) on sensor icons, chip
background, chip border and header bottom-border with semantic tokens
(--text-tertiary, --surface-2, --border-subtle, --hairline) so the
thermometer/humidity/wind icons are visible in light theme.
- introduces semantic CSS tokens (--surface-1/2/3, --border-subtle/strong,
--hairline, --shadow-sm/md/lg/xl) with distinct dark and light values;
fixes broken light theme caused by hardcoded rgba(255,255,255,X)
- drops glassmorphism on cards — solid var(--surface-1) with 1px border
and layered shadows; glass kept only for aurora page background
- introduces .card/.card-raised/.card-hero utility classes
- Home page restructured into a bento grid:
* greeting row with inline day/date
* hero weather (64px number, large icon, ощущается/влажность/ветер)
next to the tram widget (1fr 1.1fr)
* forecast as a single hairline-separated band (no per-day cards)
* events+notes in a 2-column grid; events card combines today and
tomorrow with a divider; notes card styled via surface tokens
- TransportWidget repainted to use tokens, larger numbers (32px for the
next arrival), imminent highlight uses color-mix against surface-2
Weather hint (оденьтесь потеплее / не забудьте зонт) was pushing the
home screen past one viewport on the tablet — removed the block and its
helper fn. New tram color palette per user preference.
Restructures the tram widget: instead of one card per stop (showing all
routes at that stop), now one row per route (23, 27, 39) with two
columns — → Лента (в центр) and → Дыбенко (от центра). Each cell shows
the next arrival prominently plus the following 1-2 pickups inline.
Adds a live transit widget on the home screen showing upcoming trams
at both directions of the stop: toward Новочеркасская (stopID 16226)
and toward пр. Большевиков (stopID 16354).
- /api/transport proxies the СПб ORGP endpoint /stop/{id}/arriving
(DataTables POST format, JSON response with route number + minutes).
No auth required, free.
- TransportWidget renders two glassmorphism cards with route badges,
minutes-to-arrival, wheelchair indicator; imminent (<=2 min) arrivals
get a colored highlight. Filters to trams 23/27/39; refreshes every 30s.
- Route colors: 23 blue, 27 amber, 39 purple.
Native browser confirm() looked out of place on the dashboard. Replaced
with a glassmorphism modal matching the rest of the UI — trash icon,
note title preview, Cancel/Delete buttons with proper styling.
- calendar API: today/week ranges use Moscow time (UTC+3) instead of UTC — previously today events did not appear until 03:00 MSK
- settings tab: add -webkit-overflow-scrolling: touch + touchAction pan-y for tablet scroll
- NotesTab: add date picker (pinDate) in editor header + date badge in list
- home: pinnedNotes now filters by pinDate (today or future), falls back to latest
- notes/auth: storage moved from /tmp to /data (falls back to /tmp if /data missing)
- deploy workflow: mount /opt/digital-home/smart-home-tablet-data:/data so notes survive redeploys