diff --git a/core/archipelago/src/api/rpc/package/config.rs b/core/archipelago/src/api/rpc/package/config.rs index b6457bb0..6c8036f3 100644 --- a/core/archipelago/src/api/rpc/package/config.rs +++ b/core/archipelago/src/api/rpc/package/config.rs @@ -496,6 +496,12 @@ pub(super) fn all_container_names(package_id: &str) -> Vec { "netbird-dashboard".into(), "netbird-server".into(), ], + // Pine voice-assistant stack: launcher + the two Wyoming engines. + "pine" => vec![ + "pine".into(), + "pine-whisper".into(), + "pine-piper".into(), + ], "nostr-vpn" => vec![ "nostr-vpn".into(), "archy-nostr-vpn".into(), diff --git a/core/archipelago/src/api/rpc/package/dependencies.rs b/core/archipelago/src/api/rpc/package/dependencies.rs index 6c0cd962..dae8e601 100644 --- a/core/archipelago/src/api/rpc/package/dependencies.rs +++ b/core/archipelago/src/api/rpc/package/dependencies.rs @@ -595,6 +595,9 @@ pub(super) fn needs_archy_net(package_id: &str) -> bool { | "nbxplorer" | "fedimint" | "fedimint-gateway" + | "pine" + | "pine-whisper" + | "pine-piper" ) } @@ -626,6 +629,7 @@ pub(super) fn startup_order(package_id: &str) -> &'static [&'static str] { &["archy-btcpay-db", "archy-nbxplorer", "btcpay-server"] } "netbird" => &["netbird-server", "netbird-dashboard", "netbird"], + "pine" => &["pine-whisper", "pine-piper", "pine"], "penpot" | "penpot-frontend" => &[ "penpot-postgres", "penpot-valkey", diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 88a21eb4..bb860f2c 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -294,6 +294,9 @@ impl RpcHandler { if package_id == "netbird" { return self.install_netbird_stack().await; } + if package_id == "pine" { + return self.install_pine_stack().await; + } // Dependency checks. Prefer the scanner's cached package state so a // congested Podman API does not turn an already-running dependency into // a false install failure. Fall back to a bounded direct Podman probe diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index 85e3f1aa..f3ef4e8a 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -744,6 +744,15 @@ fn netbird_stack_app_ids() -> &'static [&'static str] { &["netbird-server", "netbird-dashboard", "netbird"] } +fn pine_stack_app_ids() -> &'static [&'static str] { + // Dependency/startup order: the two Wyoming engines (STT + TTS) first — they + // own their downloaded model/voice under /data and publish 10300/10200 — + // then the user-facing launcher ("pine", the nginx that serves the setup / + // status page + is the Open target). Mirrors the pine startup_order in + // dependencies.rs + the stack member table in app_ops.rs. + &["pine-whisper", "pine-piper", "pine"] +} + fn indeedhub_stack_app_ids() -> &'static [&'static str] { // Dependency order: backends + their generated secrets first, then the api // (owns indeedhub-jwt; reads the db/minio secrets the backends materialised), @@ -1907,6 +1916,37 @@ impl RpcHandler { "netbird manifests not available on this node — the signed catalog must provide apps/netbird-*/manifest.yml (legacy hardcoded installer removed in #20 ph4)" ) } + + /// Install the Pine voice-assistant stack (Whisper STT + Piper TTS + the + /// setup/status launcher). Manifest-driven only, like netbird: render the + /// 3-member stack from apps/pine-*/manifest.yml via the orchestrator + /// (archy-net + network_aliases, published Wyoming ports 10300/10200 so + /// Home Assistant reaches the engines via host.containers.internal, the + /// launcher's setup page written via `files:`). The manifests use the exact + /// live container names, so on an existing node this ADOPTS the running + /// stack rather than recreating it (downloaded models/voices preserved). + /// + /// There is no in-Rust hardcoded fallback: the signed catalog always ships + /// apps/pine-*/manifest.yml. If the orchestrator doesn't know these app_ids + /// and no running stack exists to adopt, install errors rather than + /// silently diverging from the manifest contract. + pub(super) async fn install_pine_stack(&self) -> Result { + if let Some(orchestrated) = + install_stack_via_orchestrator(self, "pine", pine_stack_app_ids()).await? + { + return Ok(orchestrated); + } + + if let Some(adopted) = + adopt_stack_if_exists("pine", "pine", &["pine-whisper", "pine-piper", "pine"]).await? + { + return Ok(adopted); + } + + anyhow::bail!( + "pine manifests not available on this node — the signed catalog must provide apps/pine-*/manifest.yml" + ) + } } #[cfg(test)] diff --git a/core/archipelago/src/app_ops.rs b/core/archipelago/src/app_ops.rs index a9fc7acf..311fce8b 100644 --- a/core/archipelago/src/app_ops.rs +++ b/core/archipelago/src/app_ops.rs @@ -50,6 +50,7 @@ pub fn stack_member_app_ids(package_id: &str) -> &'static [&'static str] { &["archy-btcpay-db", "archy-nbxplorer", "btcpay-server"] } "netbird" => &["netbird-server", "netbird-dashboard", "netbird"], + "pine" => &["pine-whisper", "pine-piper", "pine"], // The legacy umbrella id maps to the split stack (the orchestrator's // umbrella alias handles this too; listing it here keeps the RPC // layer's fan-out explicit). @@ -75,7 +76,14 @@ pub fn address_caching_dependents(package_id: &str) -> &'static [&'static str] { /// `app_id` is a member (RPC ops on "mempool" hold the "mempool" lock while /// they drive archy-mempool-web), otherwise the app itself. fn owning_package(app_id: &str) -> &str { - const STACKS: &[&str] = &["immich", "indeedhub", "btcpay-server", "netbird", "mempool"]; + const STACKS: &[&str] = &[ + "immich", + "indeedhub", + "btcpay-server", + "netbird", + "mempool", + "pine", + ]; for stack in STACKS { if stack_member_app_ids(stack).contains(&app_id) { return stack; diff --git a/core/archipelago/src/container/docker_packages.rs b/core/archipelago/src/container/docker_packages.rs index cd9e6592..976d37eb 100644 --- a/core/archipelago/src/container/docker_packages.rs +++ b/core/archipelago/src/container/docker_packages.rs @@ -57,6 +57,8 @@ impl DockerPackageScanner { "indeedhub-build_ffmpeg-worker_1", "netbird-server", "netbird-dashboard", + "pine-whisper", + "pine-piper", "buildx_buildkit_default", ]; diff --git a/core/archipelago/src/crash_recovery.rs b/core/archipelago/src/crash_recovery.rs index e52eb01f..5f925dc8 100644 --- a/core/archipelago/src/crash_recovery.rs +++ b/core/archipelago/src/crash_recovery.rs @@ -766,6 +766,9 @@ fn should_auto_start_stopped_container(name: &str, include_stack_members: bool) | "netbird-server" | "netbird-dashboard" | "netbird" + | "pine-whisper" + | "pine-piper" + | "pine" ) } @@ -827,6 +830,20 @@ fn stack_recovery_specs() -> &'static [StackRecoverySpec] { containers: &["netbird-server", "netbird-dashboard", "netbird"], anchor: "netbird-server", }, + StackRecoverySpec { + name: "pine", + network: "archy-net", + aliases: &[ + ("pine-whisper", "pine-whisper"), + ("pine-piper", "pine-piper"), + ("pine", "pine"), + ], + containers: &["pine-whisper", "pine-piper", "pine"], + // The launcher only depends on the two engines; whisper is the + // heaviest/first member, so treat it as the stack anchor (its + // presence means the stack was really installed, not orphan debris). + anchor: "pine-whisper", + }, ] }