feat(pine): 1.3.0 manifests — openwakeword member + live node status on the launcher page
Some checks failed
Demo images / Build & push demo images (push) Failing after 1m40s
Some checks failed
Demo images / Build & push demo images (push) Failing after 1m40s
pine-openwakeword manifest (wyoming-openwakeword 2.1.0, :10400, /custom model dir for the future Yo Archy model). Pine 1.3.0: launcher page gains a live node-status card fed by /api/pine/status via a same-origin nginx proxy, and copy for the new intents / Claude fallback / mesh announcements. Catalogs, app-session config, drift ids regenerated; release catalog embeds 56 manifests (unsigned until the ceremony). Framework PT test plan in docs/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e074a117d2
commit
184257390d
@ -373,8 +373,8 @@
|
|||||||
{
|
{
|
||||||
"id": "pine",
|
"id": "pine",
|
||||||
"title": "Pine",
|
"title": "Pine",
|
||||||
"version": "1.2.0",
|
"version": "1.3.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.",
|
"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",
|
"icon": "/assets/img/app-icons/pine.svg",
|
||||||
"author": "Archipelago",
|
"author": "Archipelago",
|
||||||
"category": "home",
|
"category": "home",
|
||||||
|
|||||||
70
apps/pine-openwakeword/manifest.yml
Normal file
70
apps/pine-openwakeword/manifest.yml
Normal file
@ -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
|
||||||
@ -1,8 +1,8 @@
|
|||||||
app:
|
app:
|
||||||
id: pine
|
id: pine
|
||||||
name: Pine
|
name: Pine
|
||||||
version: "1.2.0"
|
version: "1.3.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.
|
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
|
category: home
|
||||||
|
|
||||||
# The user-facing launcher (app_id + container both "pine", matching the
|
# The user-facing launcher (app_id + container both "pine", matching the
|
||||||
@ -30,6 +30,7 @@ app:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- app_id: pine-whisper
|
- app_id: pine-whisper
|
||||||
- app_id: pine-piper
|
- app_id: pine-piper
|
||||||
|
- app_id: pine-openwakeword
|
||||||
- storage: 128Mi
|
- storage: 128Mi
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
@ -92,6 +93,15 @@ app:
|
|||||||
ssl_certificate_key /etc/nginx/tls.key;
|
ssl_certificate_key /etc/nginx/tls.key;
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.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; }
|
location / { try_files $uri $uri/ /index.html; }
|
||||||
}
|
}
|
||||||
- path: /var/lib/archipelago/pine/index.html
|
- path: /var/lib/archipelago/pine/index.html
|
||||||
@ -156,9 +166,16 @@ app:
|
|||||||
<div class="card status">
|
<div class="card status">
|
||||||
<div class="row"><b>Whisper</b><span>speech-to-text ready on <code>:10300</code></span></div>
|
<div class="row"><b>Whisper</b><span>speech-to-text ready on <code>:10300</code></span></div>
|
||||||
<div class="row"><b>Piper</b><span>text-to-speech ready on <code>:10200</code></span></div>
|
<div class="row"><b>Piper</b><span>text-to-speech ready on <code>:10200</code></span></div>
|
||||||
|
<div class="row"><b>Wake word</b><span>“Hey Jarvis” on the speaker (openWakeWord on <code>:10400</code>)</span></div>
|
||||||
<div class="row"><b>Speaker</b><span>put it in pairing mode — ring LED blinking yellow</span></div>
|
<div class="row"><b>Speaker</b><span>put it in pairing mode — ring LED blinking yellow</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card status">
|
||||||
|
<div class="row"><b>Node</b><span id="ns-node">checking…</span></div>
|
||||||
|
<div class="row"><b>Bitcoin</b><span id="ns-btc">—</span></div>
|
||||||
|
<div class="row"><b>Peers</b><span id="ns-peers">—</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card insecure" id="insecure">
|
<div class="card insecure" id="insecure">
|
||||||
This page isn’t running over HTTPS, so the browser blocks Bluetooth.
|
This page isn’t running over HTTPS, so the browser blocks Bluetooth.
|
||||||
Open it via its <b>https://…:10380</b> address (accept the self-signed
|
Open it via its <b>https://…:10380</b> address (accept the self-signed
|
||||||
@ -177,9 +194,13 @@ app:
|
|||||||
<p class="ha">After WiFi joins, one manual step remains — pair the
|
<p class="ha">After WiFi joins, one manual step remains — pair the
|
||||||
speaker in Home Assistant: <b>Settings → Devices & services →
|
speaker in Home Assistant: <b>Settings → Devices & services →
|
||||||
Add Wyoming Protocol</b>, host = the speaker’s IP, port
|
Add Wyoming Protocol</b>, host = the speaker’s IP, port
|
||||||
<b>10700</b>. Whisper, Piper and the Assist pipeline are wired up
|
<b>10700</b>. Whisper, Piper, openWakeWord and the Assist pipeline
|
||||||
automatically when Pine installs. Wake word: <b>“Hey Jarvis.”</b>
|
are wired up automatically when Pine installs. Wake word:
|
||||||
You can also ask node things like <i>“what’s the block height?”</i></p>
|
<b>“Hey Jarvis.”</b> Ask node things like <i>“what’s the block
|
||||||
|
height?”</i>, <i>“how many peers?”</i>, <i>“is the node
|
||||||
|
synced?”</i> or <i>“what’s my lightning balance?”</i> — 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.</p>
|
||||||
<p class="ha">Troubleshooting: if it hears you (LED reacts) but answers
|
<p class="ha">Troubleshooting: if it hears you (LED reacts) but answers
|
||||||
are silent, unplug and replug the speaker — an interrupted answer can
|
are silent, unplug and replug the speaker — an interrupted answer can
|
||||||
wedge its audio output until it reboots.</p>
|
wedge its audio output until it reboots.</p>
|
||||||
@ -304,6 +325,39 @@ app:
|
|||||||
try { device?.gatt?.disconnect(); } catch {}
|
try { device?.gatt?.disconnect(); } catch {}
|
||||||
} finally { btn.disabled = false; }
|
} 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);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
60
docs/pine-voice-release-test-plan.md
Normal file
60
docs/pine-voice-release-test-plan.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Framework PT test plan — Pine voice epic (pre-release gate)
|
||||||
|
|
||||||
|
Target node: **framework-pt** (`100.65.115.109`, LAN 192.168.1.249). Run after
|
||||||
|
BOTH agents' work is merged, with the dev binary sideloaded and the signed
|
||||||
|
catalog (pine 1.3.0 + pine-openwakeword) published. Every ❑ must pass before
|
||||||
|
the release ritual starts. Items marked **(user)** need a human in the room.
|
||||||
|
|
||||||
|
## A. Deploy / prerequisites
|
||||||
|
- ❑ A1 Dev binary sideloaded, `archipelago` service active, no crash-loop in journal.
|
||||||
|
- ❑ A2 nginx self-heal added `location /api/pine/status` to every server block; `nginx -t` passes; nginx reloaded.
|
||||||
|
- ❑ A3 Signed catalog with pine 1.3.0 + pine-openwakeword live at the raw URL; node refreshed it (hourly sweep or "Check for updates").
|
||||||
|
|
||||||
|
## B. `/api/pine/status` endpoint
|
||||||
|
- ❑ B1 Public tier through nginx (`curl http://127.0.0.1/api/pine/status`): version, uptime, bitcoin height/sync_percent/peers, mesh peers. `lightning` null, `mesh_message` absent.
|
||||||
|
- ❑ B2 Wrong bearer token → still public-only (no balances). Correct token (from `/var/lib/archipelago/secrets/pine-status-token`) → lightning balances + latest mesh message present.
|
||||||
|
- ❑ B3 Reachable from inside the HA container via `host.containers.internal:80`.
|
||||||
|
- ❑ B4 Token file is 0600, owned by the service user.
|
||||||
|
|
||||||
|
## C. Stack / openwakeword container
|
||||||
|
- ❑ C1 Reconcile installs `pine-openwakeword` (wyoming-openwakeword 2.1.0), healthy on :10400.
|
||||||
|
- ❑ C2 Existing pine-whisper / pine-piper / pine were ADOPTED, not recreated — model data dirs untouched.
|
||||||
|
- ❑ C3 `archipelago` service restart → all four pine containers come back (crash-recovery stack spec).
|
||||||
|
- ❑ C4 UI: openwakeword listed under Services (no extra store card); Pine card shows 1.3.0.
|
||||||
|
|
||||||
|
## D. Home Assistant seeding
|
||||||
|
- ❑ D1 configuration.yaml: legacy hand-staged block (bitcoind :18332 + plaintext RPC creds) fully replaced by the bounded token-based block.
|
||||||
|
- ❑ D2 `custom_sentences/en/archy.yaml` carries all four intents.
|
||||||
|
- ❑ D3 `.storage/core.config_entries`: wyoming entry for openwakeword (:10400) + `anthropic` entry (Claude, conversation + ai_task subentries).
|
||||||
|
- ❑ D4 Pipeline: `conversation_engine = conversation.claude_conversation`, `prefer_local_intents: true`.
|
||||||
|
- ❑ D5 automations.yaml: `archy_mesh_announce` seeded.
|
||||||
|
- ❑ D6 HA restarts clean — no setup errors for anthropic / wyoming / rest / intent_script in `podman logs homeassistant`.
|
||||||
|
- ❑ D7 Sensors report real values: archy_block_height, archy_bitcoin_sync, archy_bitcoin_peers, archy_mesh_peers, archy_lightning_balance (or clean unavailable if LND absent), archy_mesh_message.
|
||||||
|
|
||||||
|
## E. Voice / intents (API level first, then live speaker)
|
||||||
|
- ❑ E1 Exact phrase "what's the block height" → answered by the LOCAL intent (correct height, no Anthropic API call in HA logs).
|
||||||
|
- ❑ E2 Fuzzy phrase (e.g. "how tall is the chain right now") → Claude routes to the ArchyBlockHeight tool; answer contains the real height.
|
||||||
|
- ❑ E3 "how many peers", "is the node synced", "what's my lightning balance" → correct spoken-length answers.
|
||||||
|
- ❑ E4 Off-topic question → Claude answers, 1–2 sentences, no markdown.
|
||||||
|
- ❑ E5 **(user)** Live speaker: "Hey Jarvis, what's the block height" → audible correct answer.
|
||||||
|
- ❑ E6 Mesh announce: new received mesh text (or manual `assist_satellite.announce` if no radio) → speaker announces sender + text; no announce storm on HA restart.
|
||||||
|
|
||||||
|
## F. Pine launcher page (1.3.0)
|
||||||
|
- ❑ F1 Page on :10380→:10381 shows the live node card (version, uptime, block, sync, peers) within ~5s.
|
||||||
|
- ❑ F2 `/node-status` proxy works (pine nginx resolves host.containers.internal at startup — container must not crash-loop).
|
||||||
|
- ❑ F3 "Connect Pine to WiFi" provisioner still intact (no JS errors on load).
|
||||||
|
|
||||||
|
## G. Cleanup / regression sweep
|
||||||
|
- ❑ G1 Both stray socat 18332 forwarders killed; sensors still work via the endpoint.
|
||||||
|
- ❑ G2 No bitcoind RPC credentials anywhere in HA config.
|
||||||
|
- ❑ G3 Pre-existing HA function intact: whisper/piper entities, PineVoice satellite pairing, other integrations.
|
||||||
|
- ❑ G4 nginx regressions: `/health`, `/bitcoin-status`, `/api/app-catalog`, `/proxy/lnd/` all still proxied post-patch.
|
||||||
|
- ❑ G5 **(user)** Mobile Home: wallet card sits directly under My Apps; desktop layout unchanged.
|
||||||
|
- ❑ G6 Other agent's changes re-verified after merge (their own checklist).
|
||||||
|
|
||||||
|
## H. Production-readiness (release ritual gate)
|
||||||
|
- ❑ H1 `cargo test` workspace green; frontend builds; drift check `--release --strict` green.
|
||||||
|
- ❑ H2 `tests/lifecycle/run-gate.sh` re-run ON .228 (stack membership changed → lifecycle gate rule applies).
|
||||||
|
- ❑ H3 Catalog regenerated → signed (ceremony) → published via gitea-ai; verified at the raw URL.
|
||||||
|
- ❑ H4 Changelog (layman-readable) + `scripts/sync-whats-new.py` + version bump; release ritual per v1.7.110 notes (push main via gitea-ai BEFORE publish; sign manifest AFTER create-release).
|
||||||
|
- ❑ H5 No secrets in any commit; frontend tarball flat + APK policy per release notes.
|
||||||
@ -373,8 +373,8 @@
|
|||||||
{
|
{
|
||||||
"id": "pine",
|
"id": "pine",
|
||||||
"title": "Pine",
|
"title": "Pine",
|
||||||
"version": "1.2.0",
|
"version": "1.3.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.",
|
"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",
|
"icon": "/assets/img/app-icons/pine.svg",
|
||||||
"author": "Archipelago",
|
"author": "Archipelago",
|
||||||
"category": "home",
|
"category": "home",
|
||||||
|
|||||||
@ -81,6 +81,7 @@ export const GENERATED_APP_TITLES: Record<string, string> = {
|
|||||||
"nostr-rs-relay": "Nostr Relay (Rust)",
|
"nostr-rs-relay": "Nostr Relay (Rust)",
|
||||||
"photoprism": "PhotoPrism",
|
"photoprism": "PhotoPrism",
|
||||||
"pine": "Pine",
|
"pine": "Pine",
|
||||||
|
"pine-openwakeword": "Pine Wake Word (openWakeWord)",
|
||||||
"pine-piper": "Pine Piper (TTS)",
|
"pine-piper": "Pine Piper (TTS)",
|
||||||
"pine-whisper": "Pine Whisper (STT)",
|
"pine-whisper": "Pine Whisper (STT)",
|
||||||
"portainer": "Portainer",
|
"portainer": "Portainer",
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export const SERVICE_NAMES = new Set([
|
|||||||
// Pine voice-assistant stack: the two Wyoming engines are backends (STT/TTS)
|
// Pine voice-assistant stack: the two Wyoming engines are backends (STT/TTS)
|
||||||
// reached by Home Assistant over host.containers.internal — the user-facing
|
// reached by Home Assistant over host.containers.internal — the user-facing
|
||||||
// card is "pine" (the setup/status launcher), so the engines go to Services.
|
// card is "pine" (the setup/status launcher), so the engines go to Services.
|
||||||
'pine-whisper', 'pine-piper',
|
'pine-whisper', 'pine-piper', 'pine-openwakeword',
|
||||||
])
|
])
|
||||||
|
|
||||||
const INTERNAL_TOOLING_NAMES = new Set([
|
const INTERNAL_TOOLING_NAMES = new Set([
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -41,6 +41,7 @@ INTERNAL_MANIFEST_IDS = {
|
|||||||
"netbird-server",
|
"netbird-server",
|
||||||
"pine-whisper",
|
"pine-whisper",
|
||||||
"pine-piper",
|
"pine-piper",
|
||||||
|
"pine-openwakeword",
|
||||||
}
|
}
|
||||||
|
|
||||||
LEGACY_STACK_CATALOG_IDS = {
|
LEGACY_STACK_CATALOG_IDS = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user