feat(pine): add Pine voice-assistant app package (manifests + icon)

Package the PineVoice/Wyoming local voice assistant as ONE store app "Pine"
that runs three rootless containers:

- pine-whisper — Wyoming faster-whisper STT (docker.io/rhasspy/wyoming-whisper
  3.4.1, model base-int8/en), publishes :10300
- pine-piper   — Wyoming Piper TTS (2.2.2, voice en_GB-alba-medium),
  publishes :10200
- pine         — nginx launcher serving a self-contained setup/status page
  (WiFi provisioning + Home Assistant wiring instructions), the Open target

The two engines publish their Wyoming ports so Home Assistant reaches them via
host.containers.internal. All rootless (cap-drop=ALL, no_new_privileges),
manifest-driven — no hardcoded podman/installer. The engines are internal stack
members (added to the drift-check INTERNAL_MANIFEST_IDS allowlist); only "pine"
carries a catalog entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-21 05:14:32 -04:00
parent 146a3bc738
commit 1730d02003
5 changed files with 330 additions and 0 deletions

View File

@ -0,0 +1,70 @@
app:
id: pine-piper
name: Pine Piper (TTS)
version: "2.2.2"
description: Wyoming-protocol Piper text-to-speech engine. Internal Pine voice-assistant stack member — gives Home Assistant Assist a natural voice for spoken responses on the PineVoice satellite.
category: home
# Hyphen name matches the runtime references (stack member table / startup
# order) + the live container, so on an existing node the orchestrator ADOPTS
# the running engine rather than recreating it (downloaded voices under /data
# preserved).
container_name: pine-piper
container:
image: docker.io/rhasspy/wyoming-piper:2.2.2
pull_policy: if-not-present
network: archy-net
network_aliases: [pine-piper]
# The image entrypoint already binds tcp://0.0.0.0:10200; this arg only
# picks the voice (mirrors the pine ha-stack.yml compose command).
custom_args: ["--voice", "en_GB-alba-medium"]
dependencies:
- storage: 1Gi
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 # downloads the voice into /data on first run
no_new_privileges: true
network_policy: isolated
ports:
# Published so Home Assistant (on the pasta net) can reach the engine via
# host.containers.internal:10200 (the Wyoming integration endpoint).
- host: 10200
container: 10200
protocol: tcp
volumes:
- type: bind
source: /var/lib/archipelago/pine-piper
target: /data
options: [rw]
environment: []
health_check:
type: tcp
endpoint: localhost:10200
interval: 30s
timeout: 5s
retries: 5
start_period: 60s # first start downloads the voice
metadata:
author: Rhasspy / Home Assistant
icon: /assets/img/app-icons/pine.svg
website: https://github.com/rhasspy/wyoming-piper
repo: https://github.com/rhasspy/wyoming-piper
license: MIT
tags:
- home
- voice
- text-to-speech
- wyoming

View File

@ -0,0 +1,70 @@
app:
id: pine-whisper
name: Pine Whisper (STT)
version: "3.4.1"
description: Wyoming-protocol faster-whisper speech-to-text engine. Internal Pine voice-assistant stack member — turns speech captured by a PineVoice satellite into text for Home Assistant Assist.
category: home
# Hyphen name matches the runtime references (stack member table / startup
# order) + the live container, so on an existing node the orchestrator ADOPTS
# the running engine rather than recreating it (downloaded models under /data
# preserved).
container_name: pine-whisper
container:
image: docker.io/rhasspy/wyoming-whisper:3.4.1
pull_policy: if-not-present
network: archy-net
network_aliases: [pine-whisper]
# The image entrypoint already binds tcp://0.0.0.0:10300; these args only
# pick the model + language (mirrors the pine ha-stack.yml compose command).
custom_args: ["--model", "base-int8", "--language", "en"]
dependencies:
- storage: 2Gi
resources:
memory_limit: 2Gi
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 # downloads the whisper model into /data on first run
no_new_privileges: true
network_policy: isolated
ports:
# Published so Home Assistant (on the pasta net) can reach the engine via
# host.containers.internal:10300 (the Wyoming integration endpoint).
- host: 10300
container: 10300
protocol: tcp
volumes:
- type: bind
source: /var/lib/archipelago/pine-whisper
target: /data
options: [rw]
environment: []
health_check:
type: tcp
endpoint: localhost:10300
interval: 30s
timeout: 5s
retries: 5
start_period: 60s # first start downloads the model
metadata:
author: Rhasspy / Home Assistant
icon: /assets/img/app-icons/pine.svg
website: https://github.com/rhasspy/wyoming-faster-whisper
repo: https://github.com/rhasspy/wyoming-faster-whisper
license: MIT
tags:
- home
- voice
- speech-to-text
- wyoming

177
apps/pine/manifest.yml Normal file
View File

@ -0,0 +1,177 @@
app:
id: pine
name: Pine
version: "1.0.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.
container_name: pine
container:
image: docker.io/library/nginx:1.27-alpine
pull_policy: if-not-present
network: archy-net
network_aliases: [pine]
dependencies:
- app_id: pine-whisper
- app_id: pine-piper
- storage: 128Mi
resources:
memory_limit: 64Mi
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 +
# NET_BIND_SERVICE for the privileged port.
capabilities: [CHOWN, DAC_OVERRIDE, SETGID, SETUID, NET_BIND_SERVICE]
readonly_root: false
no_new_privileges: true
network_policy: isolated
ports:
- host: 10380
container: 80
protocol: tcp
volumes:
- type: bind
source: /var/lib/archipelago/pine/index.html
target: /usr/share/nginx/html/index.html
options: [ro]
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/index.html
overwrite: true
content: |
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pine — private voice assistant</title>
<style>
:root { color-scheme: dark; }
body { margin: 0; font: 16px/1.6 system-ui, sans-serif;
background: #0f1512; color: #e7efe9; }
.wrap { max-width: 720px; margin: 0 auto; padding: 40px 24px 80px; }
header { display: flex; align-items: center; gap: 16px; margin-bottom: 8px; }
header svg { width: 56px; height: 56px; flex: 0 0 auto; }
h1 { font-size: 28px; margin: 0; }
.tag { color: #8fbfa5; font-size: 14px; margin: 2px 0 0; }
h2 { font-size: 18px; margin: 32px 0 8px; color: #b7e0c8; }
ol { padding-left: 22px; } li { margin: 6px 0; }
code { background: #1c2620; padding: 2px 6px; border-radius: 5px;
font-size: 14px; color: #cfe9d9; }
.card { background: #16201b; border: 1px solid #24332b;
border-radius: 12px; padding: 16px 20px; margin: 16px 0; }
.row { display: flex; gap: 12px; align-items: baseline; }
.row b { min-width: 96px; color: #9fd3b6; }
a { color: #7fd6a6; }
footer { margin-top: 40px; color: #6d8578; font-size: 13px; }
</style>
</head>
<body>
<div class="wrap">
<header>
<svg viewBox="0 0 512 512" aria-hidden="true"><g fill="#7fd6a6">
<rect x="236" y="396" width="40" height="72" rx="6"/>
<path d="M256 44 L336 168 L296 168 L256 108 L216 168 L176 168 Z"/>
<path d="M256 150 L360 300 L300 300 L256 236 L212 300 L152 300 Z"/>
<path d="M256 262 L392 430 L120 430 L256 262 Z"/>
</g></svg>
<div>
<h1>Pine</h1>
<p class="tag">A private voice assistant that runs on your node.</p>
</div>
</header>
<p>Pine gives Home Assistant a local voice: your PineVoice speaker
hears you, <b>Whisper</b> turns speech into text on this node, and
<b>Piper</b> speaks the reply back — nothing leaves your home.</p>
<div class="card">
<div class="row"><b>Whisper (STT)</b><span>listening on
<code>host.containers.internal:10300</code></span></div>
<div class="row"><b>Piper (TTS)</b><span>listening on
<code>host.containers.internal:10200</code></span></div>
<div class="row"><b>Speaker</b><span>Wyoming satellite on
<code>&lt;speaker-ip&gt;:10700</code></span></div>
</div>
<h2>1 · Get the speaker on WiFi</h2>
<ol>
<li>Web-Bluetooth provisioning needs a secure/localhost page, so
run the setup tool from the <code>pine</code> repo on a laptop:
<code>python3 -m http.server 8377 --bind 127.0.0.1</code>, then
open <code>http://localhost:8377</code> in Chrome.</li>
<li>Put the speaker in provisioning mode (ring LED blinking
yellow), enter your WiFi, and press the centre button when the
page asks for authorization.</li>
<li>Success: the log shows <code>✓ PROVISIONED</code> and the
speaker chimes.</li>
</ol>
<h2>2 · Wire it into Home Assistant</h2>
<ol>
<li>In Home Assistant: <b>Settings → Devices &amp; services</b>.
Add the two <b>Wyoming Protocol</b> integrations —
<code>host.containers.internal:10300</code> (Whisper) and
<code>:10200</code> (Piper).</li>
<li>Add the speaker as a third Wyoming device at
<code>&lt;speaker-ip&gt;:10700</code>.</li>
<li>Build an <b>Assist</b> pipeline: STT = faster-whisper,
TTS = Piper, then set it as the speaker's pipeline.</li>
<li>Say the wake word (<b>“Hey Jarvis”</b>) or press the speaker's
centre button, and talk to your home.</li>
</ol>
<footer>Pine · Whisper + Piper run locally on this Archipelago node.
No cloud, no account.</footer>
</div>
</body>
</html>
health_check:
type: tcp
endpoint: localhost:80
interval: 30s
timeout: 5s
retries: 5
start_period: 10s
interfaces:
main:
name: Pine
description: Set up and check your private voice assistant
type: ui
port: 10380
protocol: http
path: /
metadata:
author: Archipelago
icon: /assets/img/app-icons/pine.svg
website: https://github.com/rhasspy/wyoming
repo: https://github.com/rhasspy/wyoming
license: MIT
category: home
launch:
open_in_new_tab: true
tags:
- home
- voice
- assistant
- privacy

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Pine">
<rect width="512" height="512" fill="#ffffff"/>
<g fill="#000000">
<!-- trunk -->
<rect x="236" y="396" width="40" height="72" rx="6"/>
<!-- three tiers of the evergreen, top to bottom -->
<path d="M256 44 L336 168 L296 168 L256 108 L216 168 L176 168 Z"/>
<path d="M256 150 L360 300 L300 300 L256 236 L212 300 L152 300 Z"/>
<path d="M256 262 L392 430 L120 430 L256 262 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 508 B

View File

@ -39,6 +39,8 @@ INTERNAL_MANIFEST_IDS = {
"indeedhub-relay",
"netbird-dashboard",
"netbird-server",
"pine-whisper",
"pine-piper",
}
LEGACY_STACK_CATALOG_IDS = {