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",
|
||||
"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",
|
||||
|
||||
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:
|
||||
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:
|
||||
<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>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>
|
||||
|
||||
<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">
|
||||
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
|
||||
@ -177,9 +194,13 @@ app:
|
||||
<p class="ha">After WiFi joins, one manual step remains — pair the
|
||||
speaker in Home Assistant: <b>Settings → Devices & services →
|
||||
Add Wyoming Protocol</b>, host = the speaker’s IP, port
|
||||
<b>10700</b>. Whisper, Piper and the Assist pipeline are wired up
|
||||
automatically when Pine installs. Wake word: <b>“Hey Jarvis.”</b>
|
||||
You can also ask node things like <i>“what’s the block height?”</i></p>
|
||||
<b>10700</b>. Whisper, Piper, openWakeWord and the Assist pipeline
|
||||
are wired up automatically when Pine installs. Wake word:
|
||||
<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
|
||||
are silent, unplug and replug the speaker — an interrupted answer can
|
||||
wedge its audio output until it reboots.</p>
|
||||
@ -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);
|
||||
</script>
|
||||
</body>
|
||||
</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",
|
||||
"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",
|
||||
|
||||
@ -81,6 +81,7 @@ export const GENERATED_APP_TITLES: Record<string, string> = {
|
||||
"nostr-rs-relay": "Nostr Relay (Rust)",
|
||||
"photoprism": "PhotoPrism",
|
||||
"pine": "Pine",
|
||||
"pine-openwakeword": "Pine Wake Word (openWakeWord)",
|
||||
"pine-piper": "Pine Piper (TTS)",
|
||||
"pine-whisper": "Pine Whisper (STT)",
|
||||
"portainer": "Portainer",
|
||||
|
||||
@ -29,7 +29,7 @@ export const SERVICE_NAMES = new Set([
|
||||
// Pine voice-assistant stack: the two Wyoming engines are backends (STT/TTS)
|
||||
// reached by Home Assistant over host.containers.internal — the user-facing
|
||||
// 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([
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -41,6 +41,7 @@ INTERNAL_MANIFEST_IDS = {
|
||||
"netbird-server",
|
||||
"pine-whisper",
|
||||
"pine-piper",
|
||||
"pine-openwakeword",
|
||||
}
|
||||
|
||||
LEGACY_STACK_CATALOG_IDS = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user