diff --git a/app-catalog/catalog.json b/app-catalog/catalog.json index d42cc11d..4688db41 100644 --- a/app-catalog/catalog.json +++ b/app-catalog/catalog.json @@ -373,7 +373,7 @@ { "id": "pine", "title": "Pine", - "version": "1.0.0", + "version": "1.1.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.", "icon": "/assets/img/app-icons/pine.svg", "author": "Archipelago", diff --git a/apps/pine/manifest.yml b/apps/pine/manifest.yml index ee0cf4f6..9387384c 100644 --- a/apps/pine/manifest.yml +++ b/apps/pine/manifest.yml @@ -1,14 +1,15 @@ app: id: pine name: Pine - version: "1.0.0" + version: "1.1.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. category: home # The user-facing launcher (app_id + container both "pine", matching the # runtime references + the live container so the orchestrator adopts it). A - # tiny nginx that serves the setup / status page for the voice stack. The two - # Wyoming engines (pine-whisper, pine-piper) are internal stack members. + # tiny nginx that serves the "Connect Pine to WiFi" provisioner page for the + # voice stack. The two Wyoming engines (pine-whisper, pine-piper) are internal + # stack members. container_name: pine container: @@ -16,6 +17,15 @@ app: pull_policy: if-not-present network: archy-net network_aliases: [pine] + # The provisioner uses Web Bluetooth (Improv-over-BLE) to push WiFi creds to + # the PineVoice speaker. navigator.bluetooth only exists in a SECURE CONTEXT + # (https or localhost), so the launcher terminates TLS with a self-signed + # cert — otherwise the "Connect Pine to WiFi" button is inert on the LAN. + # Idempotent: kept as-is when crt+key already exist. Mirrors the netbird + # secure-context fix (#15). + generated_certs: + - crt: /var/lib/archipelago/pine/tls.crt + key: /var/lib/archipelago/pine/tls.key dependencies: - app_id: pine-whisper @@ -27,7 +37,7 @@ app: security: # cap-drop=ALL is applied by the orchestrator. nginx (master as root, drops - # workers) binds :80 inside the container — needs the worker-drop caps + + # workers) binds :443 inside the container — needs the worker-drop caps + # NET_BIND_SERVICE for the privileged port. capabilities: [CHOWN, DAC_OVERRIDE, SETGID, SETUID, NET_BIND_SERVICE] readonly_root: false @@ -36,10 +46,22 @@ app: ports: - host: 10380 - container: 80 + container: 443 protocol: tcp volumes: + - type: bind + source: /var/lib/archipelago/pine/nginx.conf + target: /etc/nginx/conf.d/default.conf + options: [ro] + - type: bind + source: /var/lib/archipelago/pine/tls.crt + target: /etc/nginx/tls.crt + options: [ro] + - type: bind + source: /var/lib/archipelago/pine/tls.key + target: /etc/nginx/tls.key + options: [ro] - type: bind source: /var/lib/archipelago/pine/index.html target: /usr/share/nginx/html/index.html @@ -47,12 +69,19 @@ app: environment: [] - # The setup / status page, written to the host before create and served - # read-only. Web-Bluetooth WiFi provisioning of the speaker only works from a - # secure/localhost context (not this LAN page), so the page documents that - # flow rather than embedding it — the live provisioner lives in the `pine` - # Gitea repo (index.html), run from a laptop on localhost. files: + - path: /var/lib/archipelago/pine/nginx.conf + overwrite: true + content: | + server { + listen 443 ssl; + server_name _; + ssl_certificate /etc/nginx/tls.crt; + ssl_certificate_key /etc/nginx/tls.key; + root /usr/share/nginx/html; + index index.html; + location / { try_files $uri $uri/ /index.html; } + } - path: /var/lib/archipelago/pine/index.html overwrite: true content: | @@ -61,26 +90,42 @@ app: - Pine — private voice assistant + Pine — connect your speaker @@ -92,61 +137,163 @@ app: -
-

Pine

-

A private voice assistant that runs on your node.

-
+

Pine

+

Connect your speaker — everything stays on your node.

-

Pine gives Home Assistant a local voice: your PineVoice speaker - hears you, Whisper turns speech into text on this node, and - Piper speaks the reply back — nothing leaves your home.

- -
-
Whisper (STT)listening on - host.containers.internal:10300
-
Piper (TTS)listening on - host.containers.internal:10200
-
SpeakerWyoming satellite on - <speaker-ip>:10700
+
+
Whisperspeech-to-text ready on :10300
+
Pipertext-to-speech ready on :10200
+
Speakerput it in pairing mode — ring LED blinking yellow
-

1 · Get the speaker on WiFi

-
    -
  1. Web-Bluetooth provisioning needs a secure/localhost page, so - run the setup tool from the pine repo on a laptop: - python3 -m http.server 8377 --bind 127.0.0.1, then - open http://localhost:8377 in Chrome.
  2. -
  3. Put the speaker in provisioning mode (ring LED blinking - yellow), enter your WiFi, and press the centre button when the - page asks for authorization.
  4. -
  5. Success: the log shows ✓ PROVISIONED and the - speaker chimes.
  6. -
+
+ This page isn’t running over HTTPS, so the browser blocks Bluetooth. + Open it via its https://…:10380 address (accept the self-signed + certificate) and the button below will work. +
-

2 · Wire it into Home Assistant

-
    -
  1. In Home Assistant: Settings → Devices & services. - Add the two Wyoming Protocol integrations — - host.containers.internal:10300 (Whisper) and - :10200 (Piper).
  2. -
  3. Add the speaker as a third Wyoming device at - <speaker-ip>:10700.
  4. -
  5. Build an Assist pipeline: STT = faster-whisper, - TTS = Piper, then set it as the speaker's pipeline.
  6. -
  7. Say the wake word (“Hey Jarvis”) or press the speaker's - centre button, and talk to your home.
  8. -
+
+ + + + + +
Ready. Click the button, then pick “PineVoice” in the Bluetooth popup.
+
-
Pine · Whisper + Piper run locally on this Archipelago node. - No cloud, no account.
+

After WiFi joins, add the speaker to Home Assistant: + Settings → Devices & services → Add Wyoming Protocol, host = + the speaker’s IP, port 10700, then pick an Assist pipeline using + Whisper + Piper. Wake word: “Hey Jarvis.”

+ + health_check: type: tcp - endpoint: localhost:80 + endpoint: localhost:443 interval: 30s timeout: 5s retries: 5 @@ -155,10 +302,10 @@ app: interfaces: main: name: Pine - description: Set up and check your private voice assistant + description: Connect your speaker to WiFi and check the voice assistant type: ui port: 10380 - protocol: http + protocol: https path: / metadata: diff --git a/neode-ui/public/catalog.json b/neode-ui/public/catalog.json index d42cc11d..4688db41 100644 --- a/neode-ui/public/catalog.json +++ b/neode-ui/public/catalog.json @@ -373,7 +373,7 @@ { "id": "pine", "title": "Pine", - "version": "1.0.0", + "version": "1.1.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.", "icon": "/assets/img/app-icons/pine.svg", "author": "Archipelago", diff --git a/releases/app-catalog.json b/releases/app-catalog.json index 4949f2b3..0606434e 100644 --- a/releases/app-catalog.json +++ b/releases/app-catalog.json @@ -3845,6 +3845,12 @@ "app": { "category": "home", "container": { + "generated_certs": [ + { + "crt": "/var/lib/archipelago/pine/tls.crt", + "key": "/var/lib/archipelago/pine/tls.key" + } + ], "image": "docker.io/library/nginx:1.27-alpine", "network": "archy-net", "network_aliases": [ @@ -3868,13 +3874,18 @@ "environment": [], "files": [ { - "content": "\n\n\n \n \n Pine — private voice assistant\n \n\n\n
\n
\n \n \n \n \n \n \n
\n

Pine

\n

A private voice assistant that runs on your node.

\n
\n
\n\n

Pine gives Home Assistant a local voice: your PineVoice speaker\n hears you, Whisper turns speech into text on this node, and\n Piper speaks the reply back — nothing leaves your home.

\n\n
\n
Whisper (STT)listening on\n host.containers.internal:10300
\n
Piper (TTS)listening on\n host.containers.internal:10200
\n
SpeakerWyoming satellite on\n <speaker-ip>:10700
\n
\n\n

1 · Get the speaker on WiFi

\n
    \n
  1. Web-Bluetooth provisioning needs a secure/localhost page, so\n run the setup tool from the pine repo on a laptop:\n python3 -m http.server 8377 --bind 127.0.0.1, then\n open http://localhost:8377 in Chrome.
  2. \n
  3. Put the speaker in provisioning mode (ring LED blinking\n yellow), enter your WiFi, and press the centre button when the\n page asks for authorization.
  4. \n
  5. Success: the log shows ✓ PROVISIONED and the\n speaker chimes.
  6. \n
\n\n

2 · Wire it into Home Assistant

\n
    \n
  1. In Home Assistant: Settings → Devices & services.\n Add the two Wyoming Protocol integrations —\n host.containers.internal:10300 (Whisper) and\n :10200 (Piper).
  2. \n
  3. Add the speaker as a third Wyoming device at\n <speaker-ip>:10700.
  4. \n
  5. Build an Assist pipeline: STT = faster-whisper,\n TTS = Piper, then set it as the speaker's pipeline.
  6. \n
  7. Say the wake word (“Hey Jarvis”) or press the speaker's\n centre button, and talk to your home.
  8. \n
\n\n
Pine · Whisper + Piper run locally on this Archipelago node.\n No cloud, no account.
\n
\n\n\n", + "content": "server {\n listen 443 ssl;\n server_name _;\n ssl_certificate /etc/nginx/tls.crt;\n ssl_certificate_key /etc/nginx/tls.key;\n root /usr/share/nginx/html;\n index index.html;\n location / { try_files $uri $uri/ /index.html; }\n}\n", + "overwrite": true, + "path": "/var/lib/archipelago/pine/nginx.conf" + }, + { + "content": "\n\n\n \n \n Pine — connect your speaker\n \n\n\n
\n
\n \n \n \n \n \n \n

Pine

\n

Connect your speaker — everything stays on your node.

\n
\n\n
\n
Whisperspeech-to-text ready on :10300
\n
Pipertext-to-speech ready on :10200
\n
Speakerput it in pairing mode — ring LED blinking yellow
\n
\n\n
\n This page isn’t running over HTTPS, so the browser blocks Bluetooth.\n Open it via its https://…:10380 address (accept the self-signed\n certificate) and the button below will work.\n
\n\n
\n \n \n \n \n \n
Ready. Click the button, then pick “PineVoice” in the Bluetooth popup.
\n
\n\n

After WiFi joins, add the speaker to Home Assistant:\n Settings → Devices & services → Add Wyoming Protocol, host =\n the speaker’s IP, port 10700, then pick an Assist pipeline using\n Whisper + Piper. Wake word: “Hey Jarvis.”

\n
\n\n \n\n\n", "overwrite": true, "path": "/var/lib/archipelago/pine/index.html" } ], "health_check": { - "endpoint": "localhost:80", + "endpoint": "localhost:443", "interval": "30s", "retries": 5, "start_period": "10s", @@ -3884,11 +3895,11 @@ "id": "pine", "interfaces": { "main": { - "description": "Set up and check your private voice assistant", + "description": "Connect your speaker to WiFi and check the voice assistant", "name": "Pine", "path": "/", "port": 10380, - "protocol": "http", + "protocol": "https", "type": "ui" } }, @@ -3912,7 +3923,7 @@ "name": "Pine", "ports": [ { - "container": 80, + "container": 443, "host": 10380, "protocol": "tcp" } @@ -3932,8 +3943,32 @@ "no_new_privileges": true, "readonly_root": false }, - "version": "1.0.0", + "version": "1.1.0", "volumes": [ + { + "options": [ + "ro" + ], + "source": "/var/lib/archipelago/pine/nginx.conf", + "target": "/etc/nginx/conf.d/default.conf", + "type": "bind" + }, + { + "options": [ + "ro" + ], + "source": "/var/lib/archipelago/pine/tls.crt", + "target": "/etc/nginx/tls.crt", + "type": "bind" + }, + { + "options": [ + "ro" + ], + "source": "/var/lib/archipelago/pine/tls.key", + "target": "/etc/nginx/tls.key", + "type": "bind" + }, { "options": [ "ro" @@ -3945,7 +3980,7 @@ ] } }, - "version": "1.0.0" + "version": "1.1.0" }, "pine-piper": { "manifest": { @@ -4606,7 +4641,7 @@ } }, "schema": 1, - "signature": "42f1f5092e82302f62b7aae5ff3799b3ea91fcf46b499d3f888fe5b86c9d4781277a5d3ff5f7b84516a5cfada529ad07738951d79908a16326fbea9ddbc62d0a", + "signature": "ee660b635640c9ac91d203330a7593132842bbd5650883183110f9829dfda417ab172211364860d7dd3f52ada19745850fad66f424a03faf27465cf3ff740301", "signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur", "updated": "2026-07-21" }