diff --git a/app-catalog/catalog.json b/app-catalog/catalog.json index 1cbf9dd6..145f7cd3 100644 --- a/app-catalog/catalog.json +++ b/app-catalog/catalog.json @@ -373,8 +373,8 @@ { "id": "pine", "title": "Pine", - "version": "1.2.0", - "description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper) and text-to-speech (Piper) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud.", + "version": "1.3.0", + "description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node — block height, sync, peers, Lightning balance — and, when a Claude API key is set, anything else.", "icon": "/assets/img/app-icons/pine.svg", "author": "Archipelago", "category": "home", diff --git a/apps/pine-openwakeword/manifest.yml b/apps/pine-openwakeword/manifest.yml new file mode 100644 index 00000000..03910469 --- /dev/null +++ b/apps/pine-openwakeword/manifest.yml @@ -0,0 +1,70 @@ +app: + id: pine-openwakeword + name: Pine Wake Word (openWakeWord) + version: "2.1.0" + description: Wyoming-protocol openWakeWord wake-word engine. Internal Pine voice-assistant stack member — lets Assist pipelines run wake-word detection on the node (groundwork for the custom "Yo Archy" wake word; stock models like "ok nabu" ship with the image). + category: home + + # Hyphen name matches the runtime references (stack member table / startup + # order) so the orchestrator adopts a matching running container instead of + # recreating it. + container_name: pine-openwakeword + + container: + image: docker.io/rhasspy/wyoming-openwakeword:2.1.0 + pull_policy: if-not-present + network: archy-net + network_aliases: [pine-openwakeword] + # The image entrypoint binds tcp://0.0.0.0:10400. Preload the stock + # "ok nabu" model; /custom is where a trained custom model (yo_archy) + # drops in later — the engine picks up new .tflite files on restart. + custom_args: ["--preload-model", "ok_nabu", "--custom-model-dir", "/custom"] + + dependencies: + - storage: 512Mi + + resources: + memory_limit: 512Mi + + security: + # cap-drop=ALL is applied by the orchestrator. A plain Python Wyoming server + # on an unprivileged port needs no added capabilities. + capabilities: [] + readonly_root: false + no_new_privileges: true + network_policy: isolated + + ports: + # Published so Home Assistant (on the pasta net) can reach the engine via + # host.containers.internal:10400 (the Wyoming integration endpoint). + - host: 10400 + container: 10400 + protocol: tcp + + volumes: + - type: bind + source: /var/lib/archipelago/pine-openwakeword + target: /custom + options: [rw] + + environment: [] + + health_check: + type: tcp + endpoint: localhost:10400 + interval: 30s + timeout: 5s + retries: 5 + start_period: 30s + + metadata: + author: Rhasspy / Home Assistant + icon: /assets/img/app-icons/pine.svg + website: https://github.com/rhasspy/wyoming-openwakeword + repo: https://github.com/rhasspy/wyoming-openwakeword + license: MIT + tags: + - home + - voice + - wake-word + - wyoming diff --git a/apps/pine/manifest.yml b/apps/pine/manifest.yml index 4f690234..3984cd3f 100644 --- a/apps/pine/manifest.yml +++ b/apps/pine/manifest.yml @@ -1,8 +1,8 @@ app: id: pine name: Pine - version: "1.2.0" - description: A private voice assistant for your home. Pine runs speech-to-text (Whisper) and text-to-speech (Piper) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. + version: "1.3.0" + description: A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node — block height, sync, peers, Lightning balance — and, when a Claude API key is set, anything else. category: home # The user-facing launcher (app_id + container both "pine", matching the @@ -30,6 +30,7 @@ app: dependencies: - app_id: pine-whisper - app_id: pine-piper + - app_id: pine-openwakeword - storage: 128Mi resources: @@ -92,6 +93,15 @@ app: ssl_certificate_key /etc/nginx/tls.key; root /usr/share/nginx/html; index index.html; + # Live node facts for the status card — proxied to the node's + # public status tier so the (https) page can fetch same-origin. + location = /node-status { + proxy_pass http://host.containers.internal:80/api/pine/status; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_connect_timeout 5s; + proxy_read_timeout 10s; + } location / { try_files $uri $uri/ /index.html; } } - path: /var/lib/archipelago/pine/index.html @@ -156,9 +166,16 @@ app:
:10300:10200:10400)After WiFi joins, one manual step remains — pair the speaker in Home Assistant: Settings → Devices & services → Add Wyoming Protocol, host = the speaker’s IP, port - 10700. Whisper, Piper and the Assist pipeline are wired up - automatically when Pine installs. Wake word: “Hey Jarvis.” - You can also ask node things like “what’s the block height?”
+ 10700. Whisper, Piper, openWakeWord and the Assist pipeline + are wired up automatically when Pine installs. Wake word: + “Hey Jarvis.” Ask node things like “what’s the block + height?”, “how many peers?”, “is the node + synced?” or “what’s my lightning balance?” — and when a + Claude API key is set on the node, anything else gets answered by + Claude. New mesh messages are announced on the speaker too.Troubleshooting: if it hears you (LED reacts) but answers are silent, unplug and replug the speaker — an interrupted answer can wedge its audio output until it reboots.
@@ -304,6 +325,39 @@ app: try { device?.gatt?.disconnect(); } catch {} } finally { btn.disabled = false; } }); + + // Live node status card — public tier of /api/pine/status via the + // same-origin /node-status proxy. Best-effort: failures just show + // "unavailable" and retry on the next tick. + const nsNode = document.getElementById("ns-node"); + const nsBtc = document.getElementById("ns-btc"); + const nsPeers = document.getElementById("ns-peers"); + async function refreshNodeStatus() { + try { + const r = await fetch("/node-status", { cache: "no-store" }); + if (!r.ok) throw new Error(String(r.status)); + const s = await r.json(); + const up = Math.floor((s.uptime_seconds || 0) / 3600); + nsNode.textContent = `Archipelago ${s.version || "?"} — up ${up}h`; + if (s.bitcoin && s.bitcoin.height != null) { + const pct = s.bitcoin.sync_percent; + nsBtc.textContent = `block ${s.bitcoin.height}` + + (pct != null ? (pct >= 99.99 ? " — synced" : ` — ${pct}% synced`) : ""); + } else { + nsBtc.textContent = "not running"; + } + const btcPeers = s.bitcoin && s.bitcoin.peers != null ? s.bitcoin.peers : "?"; + const meshPeers = s.mesh ? s.mesh.peers : 0; + nsPeers.textContent = `${btcPeers} bitcoin` + + (s.mesh && s.mesh.enabled ? `, ${meshPeers} mesh` : ""); + } catch { + nsNode.textContent = "node status unavailable"; + nsBtc.textContent = "—"; + nsPeers.textContent = "—"; + } + } + refreshNodeStatus(); + setInterval(refreshNodeStatus, 30000);