From 42271ce933350ae2b29b6d085e40b97a49ded187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Mon, 24 Aug 2026 16:07:28 -0700 Subject: [PATCH 1/5] feat(glyph): add AudioMuse-AI via oci-containers Three containers (postgres, flask, worker) on a shared podman network. Persistent data under /var/lib/audiomuse/. The flask API is exposed on port 8000 for the navidrome plugin and setup wizard. After deploying, visit http://glyph:8000 to complete the setup wizard and connect AudioMuse-AI to navidrome. Entire-Checkpoint: 2548eb6544f7 (cherry picked from commit 74c8af1b1696d8772ba78304c9cba437fd4630a6) --- hosts/glyph/secrets/audiomuse-env.age | 7 ++ hosts/glyph/services/audiomuse.nix | 105 ++++++++++++++++++++++++++ hosts/glyph/services/default.nix | 1 + lib/secrets/glyph.nix | 1 + 4 files changed, 114 insertions(+) create mode 100644 hosts/glyph/secrets/audiomuse-env.age create mode 100644 hosts/glyph/services/audiomuse.nix diff --git a/hosts/glyph/secrets/audiomuse-env.age b/hosts/glyph/secrets/audiomuse-env.age new file mode 100644 index 00000000..275c351a --- /dev/null +++ b/hosts/glyph/secrets/audiomuse-env.age @@ -0,0 +1,7 @@ +age-encryption.org/v1 +-> ssh-ed25519 rSr+rA Xv5Z5uJ2taK1fC1adStPVMyhJDeynSwoe89k4Fo/Enw +zrKo7M7UJ3hdaKCMp7B8fImRA1VIVyv2zrBmqW2psS0 +-> ssh-ed25519 3EWhnQ 8BWG7aLna2EboJ63F2iISv/yRNWksn3bXiekgHFCSj4 +/VZUGmL+20q3+/HHgq8wUdgP6uhOvPoDhJkaJMiLuqQ +--- v69QvzRzgmi08DMSTg+f87/GqVrtSeppJiH7RptDVoE +ñôÏVÕ&ÀYäÆ3ö¡KÐz] õÁÖ5ÖÒ!Øa¥ \ No newline at end of file diff --git a/hosts/glyph/services/audiomuse.nix b/hosts/glyph/services/audiomuse.nix new file mode 100644 index 00000000..3fe01d4e --- /dev/null +++ b/hosts/glyph/services/audiomuse.nix @@ -0,0 +1,105 @@ +{ + config, + pkgs, + ... +}: { + age.secrets.audiomuse-env.file = ./../secrets/audiomuse-env.age; + + systemd.tmpfiles.rules = [ + "d /var/lib/audiomuse 0755 root root - -" + "d /var/lib/audiomuse/postgres 0700 root root - -" + "d /var/lib/audiomuse/temp-flask 0755 root root - -" + "d /var/lib/audiomuse/plugins-flask 0755 root root - -" + "d /var/lib/audiomuse/temp-worker 0755 root root - -" + "d /var/lib/audiomuse/plugins-worker 0755 root root - -" + ]; + + # Shared podman network so containers can reach each other by hostname. + systemd.services.init-audiomuse-network = { + description = "Create audiomuse podman network"; + after = ["network.target"]; + wantedBy = ["multi-user.target"]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + ${pkgs.podman}/bin/podman network inspect audiomuse >/dev/null 2>&1 || \ + ${pkgs.podman}/bin/podman network create audiomuse + ''; + }; + + virtualisation.oci-containers.containers = { + audiomuse-postgres = { + image = "postgres:15-alpine"; + environmentFiles = [config.age.secrets.audiomuse-env.path]; + environment = { + TZ = config.time.timeZone; + POSTGRES_USER = "audiomuse"; + POSTGRES_DB = "audiomusedb"; + }; + volumes = ["/var/lib/audiomuse/postgres:/var/lib/postgresql/data"]; + extraOptions = [ + "--network=audiomuse" + "--network-alias=postgres" + ]; + }; + + audiomuse-flask = { + image = "ghcr.io/neptunehub/audiomuse-ai:latest"; + environmentFiles = [config.age.secrets.audiomuse-env.path]; + environment = { + SERVICE_TYPE = "flask"; + TZ = config.time.timeZone; + POSTGRES_USER = "audiomuse"; + POSTGRES_DB = "audiomusedb"; + POSTGRES_HOST = "postgres"; + POSTGRES_PORT = "5432"; + TEMP_DIR = "/app/temp_audio"; + }; + ports = ["8000:8000"]; + volumes = [ + "/var/lib/audiomuse/temp-flask:/app/temp_audio" + "/var/lib/audiomuse/plugins-flask:/app/plugin/installed" + ]; + extraOptions = ["--network=audiomuse"]; + }; + + audiomuse-worker = { + image = "ghcr.io/neptunehub/audiomuse-ai:latest"; + environmentFiles = [config.age.secrets.audiomuse-env.path]; + environment = { + SERVICE_TYPE = "worker"; + TZ = config.time.timeZone; + POSTGRES_USER = "audiomuse"; + POSTGRES_DB = "audiomusedb"; + POSTGRES_HOST = "postgres"; + POSTGRES_PORT = "5432"; + TEMP_DIR = "/app/temp_audio"; + }; + volumes = [ + "/var/lib/audiomuse/temp-worker:/app/temp_audio" + "/var/lib/audiomuse/plugins-worker:/app/plugin/installed" + ]; + extraOptions = ["--network=audiomuse"]; + }; + }; + + # Ensure network exists before any container starts, and postgres before app containers. + systemd.services = { + "podman-audiomuse-postgres" = { + after = ["init-audiomuse-network.service"]; + requires = ["init-audiomuse-network.service"]; + }; + "podman-audiomuse-flask" = { + after = ["init-audiomuse-network.service" "podman-audiomuse-postgres.service"]; + requires = ["init-audiomuse-network.service" "podman-audiomuse-postgres.service"]; + }; + "podman-audiomuse-worker" = { + after = ["init-audiomuse-network.service" "podman-audiomuse-postgres.service"]; + requires = ["init-audiomuse-network.service" "podman-audiomuse-postgres.service"]; + }; + }; + + networking.firewall.allowedTCPPorts = [8000]; +} diff --git a/hosts/glyph/services/default.nix b/hosts/glyph/services/default.nix index db05bb11..2f7d3802 100644 --- a/hosts/glyph/services/default.nix +++ b/hosts/glyph/services/default.nix @@ -7,6 +7,7 @@ imports = [ ./alloy.nix ./attic.nix + ./audiomuse.nix ./db.nix ./avahi.nix ./dns.nix diff --git a/lib/secrets/glyph.nix b/lib/secrets/glyph.nix index d28ea350..0e1dba28 100644 --- a/lib/secrets/glyph.nix +++ b/lib/secrets/glyph.nix @@ -14,4 +14,5 @@ in { "hosts/glyph/secrets/user-password.age".publicKeys = keys; "hosts/glyph/secrets/obsidian-auth-token.age".publicKeys = keys; "hosts/glyph/secrets/navidrome-env.age".publicKeys = keys; + "hosts/glyph/secrets/audiomuse-env.age".publicKeys = keys; } From 1f38721e5996717ca10e287ba7d030988113f06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Mon, 24 Aug 2026 16:19:29 -0700 Subject: [PATCH 2/5] refactor(glyph): use native postgres for audiomuse Replace the audiomuse postgres container with the existing native services.postgresql instance. Add the audiomuse DB/user to db.nix and include it in postgresqlBackup. Both app containers switch to --network=host so they reach postgres on 127.0.0.1 directly, removing the need for a custom podman network. Entire-Checkpoint: d794d06ab601 --- hosts/glyph/services/audiomuse.nix | 83 ++++++++---------------------- hosts/glyph/services/db.nix | 8 ++- 2 files changed, 28 insertions(+), 63 deletions(-) diff --git a/hosts/glyph/services/audiomuse.nix b/hosts/glyph/services/audiomuse.nix index 3fe01d4e..04ca58df 100644 --- a/hosts/glyph/services/audiomuse.nix +++ b/hosts/glyph/services/audiomuse.nix @@ -1,50 +1,17 @@ -{ - config, - pkgs, - ... -}: { +{config, ...}: let + dataDir = "/var/lib/audiomuse"; +in { age.secrets.audiomuse-env.file = ./../secrets/audiomuse-env.age; systemd.tmpfiles.rules = [ - "d /var/lib/audiomuse 0755 root root - -" - "d /var/lib/audiomuse/postgres 0700 root root - -" - "d /var/lib/audiomuse/temp-flask 0755 root root - -" - "d /var/lib/audiomuse/plugins-flask 0755 root root - -" - "d /var/lib/audiomuse/temp-worker 0755 root root - -" - "d /var/lib/audiomuse/plugins-worker 0755 root root - -" + "d ${dataDir} 0755 root root - -" + "d ${dataDir}/temp-flask 0755 root root - -" + "d ${dataDir}/plugins-flask 0755 root root - -" + "d ${dataDir}/temp-worker 0755 root root - -" + "d ${dataDir}/plugins-worker 0755 root root - -" ]; - # Shared podman network so containers can reach each other by hostname. - systemd.services.init-audiomuse-network = { - description = "Create audiomuse podman network"; - after = ["network.target"]; - wantedBy = ["multi-user.target"]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - ${pkgs.podman}/bin/podman network inspect audiomuse >/dev/null 2>&1 || \ - ${pkgs.podman}/bin/podman network create audiomuse - ''; - }; - virtualisation.oci-containers.containers = { - audiomuse-postgres = { - image = "postgres:15-alpine"; - environmentFiles = [config.age.secrets.audiomuse-env.path]; - environment = { - TZ = config.time.timeZone; - POSTGRES_USER = "audiomuse"; - POSTGRES_DB = "audiomusedb"; - }; - volumes = ["/var/lib/audiomuse/postgres:/var/lib/postgresql/data"]; - extraOptions = [ - "--network=audiomuse" - "--network-alias=postgres" - ]; - }; - audiomuse-flask = { image = "ghcr.io/neptunehub/audiomuse-ai:latest"; environmentFiles = [config.age.secrets.audiomuse-env.path]; @@ -52,17 +19,16 @@ SERVICE_TYPE = "flask"; TZ = config.time.timeZone; POSTGRES_USER = "audiomuse"; - POSTGRES_DB = "audiomusedb"; - POSTGRES_HOST = "postgres"; + POSTGRES_DB = "audiomuse"; + POSTGRES_HOST = "127.0.0.1"; POSTGRES_PORT = "5432"; TEMP_DIR = "/app/temp_audio"; }; - ports = ["8000:8000"]; volumes = [ - "/var/lib/audiomuse/temp-flask:/app/temp_audio" - "/var/lib/audiomuse/plugins-flask:/app/plugin/installed" + "${dataDir}/temp-flask:/app/temp_audio" + "${dataDir}/plugins-flask:/app/plugin/installed" ]; - extraOptions = ["--network=audiomuse"]; + extraOptions = ["--network=host"]; }; audiomuse-worker = { @@ -72,32 +38,27 @@ SERVICE_TYPE = "worker"; TZ = config.time.timeZone; POSTGRES_USER = "audiomuse"; - POSTGRES_DB = "audiomusedb"; - POSTGRES_HOST = "postgres"; + POSTGRES_DB = "audiomuse"; + POSTGRES_HOST = "127.0.0.1"; POSTGRES_PORT = "5432"; TEMP_DIR = "/app/temp_audio"; }; volumes = [ - "/var/lib/audiomuse/temp-worker:/app/temp_audio" - "/var/lib/audiomuse/plugins-worker:/app/plugin/installed" + "${dataDir}/temp-worker:/app/temp_audio" + "${dataDir}/plugins-worker:/app/plugin/installed" ]; - extraOptions = ["--network=audiomuse"]; + extraOptions = ["--network=host"]; }; }; - # Ensure network exists before any container starts, and postgres before app containers. systemd.services = { - "podman-audiomuse-postgres" = { - after = ["init-audiomuse-network.service"]; - requires = ["init-audiomuse-network.service"]; - }; "podman-audiomuse-flask" = { - after = ["init-audiomuse-network.service" "podman-audiomuse-postgres.service"]; - requires = ["init-audiomuse-network.service" "podman-audiomuse-postgres.service"]; + after = ["postgresql.service"]; + requires = ["postgresql.service"]; }; "podman-audiomuse-worker" = { - after = ["init-audiomuse-network.service" "podman-audiomuse-postgres.service"]; - requires = ["init-audiomuse-network.service" "podman-audiomuse-postgres.service"]; + after = ["postgresql.service"]; + requires = ["postgresql.service"]; }; }; diff --git a/hosts/glyph/services/db.nix b/hosts/glyph/services/db.nix index 5da67060..7e9626cf 100644 --- a/hosts/glyph/services/db.nix +++ b/hosts/glyph/services/db.nix @@ -12,7 +12,7 @@ port = 5432; max_connections = 150; }; - ensureDatabases = ["atticd" "grafana" "open-webui" "pocketid"]; + ensureDatabases = ["atticd" "audiomuse" "grafana" "open-webui" "pocketid"]; ensureUsers = [ { name = "mu"; @@ -22,6 +22,10 @@ name = "atticd"; ensureDBOwnership = true; } + { + name = "audiomuse"; + ensureDBOwnership = true; + } { name = "grafana"; ensureDBOwnership = true; @@ -39,6 +43,6 @@ services.postgresqlBackup = { enable = true; - databases = ["atticd" "grafana" "open-webui" "pocketid" "windmill"]; + databases = ["atticd" "audiomuse" "grafana" "open-webui" "pocketid" "windmill"]; }; } From a0f73a3dc2180fb9add7df8f983977219e08f71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Mon, 24 Aug 2026 16:20:01 -0700 Subject: [PATCH 3/5] fix(glyph): set NUMBA_CACHE_DIR in audiomuse worker Without this, numba emits 'cannot cache function: no locator available' inside the Podman container and silently skips all tracks. Pointing the cache at a subdir of the already-mounted temp volume gives numba a writable, persistent location. Entire-Checkpoint: d3b3e5bbdd47 --- hosts/glyph/services/audiomuse.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/hosts/glyph/services/audiomuse.nix b/hosts/glyph/services/audiomuse.nix index 04ca58df..ac435c7d 100644 --- a/hosts/glyph/services/audiomuse.nix +++ b/hosts/glyph/services/audiomuse.nix @@ -42,6 +42,7 @@ in { POSTGRES_HOST = "127.0.0.1"; POSTGRES_PORT = "5432"; TEMP_DIR = "/app/temp_audio"; + NUMBA_CACHE_DIR = "/app/temp_audio/.numba_cache"; }; volumes = [ "${dataDir}/temp-worker:/app/temp_audio" From 2f6792001678db94b5e70f67249f4d56c6e7e519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Wed, 2 Sep 2026 15:06:14 -0700 Subject: [PATCH 4/5] feat(glyph): wire audiomuseai into navidrome Enable the audiomuseai navidrome plugin and agent now that the AudioMuse-AI backend is running. Entire-Checkpoint: 429c92e2b778 --- hosts/glyph/services/navidrome.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hosts/glyph/services/navidrome.nix b/hosts/glyph/services/navidrome.nix index 792f4450..77cf78a8 100644 --- a/hosts/glyph/services/navidrome.nix +++ b/hosts/glyph/services/navidrome.nix @@ -32,6 +32,7 @@ in { environmentFile = config.age.secrets.navidrome-env.path; plugins = [ pkgs.navidromePlugins.apple-music + pkgs.navidromePlugins.audiomuseai nd-lyrics ]; settings = { @@ -41,7 +42,7 @@ in { Scanner.Schedule = "@every 1h"; RecentlyAddedByModTime = true; Prometheus.Enabled = true; - Agents = "apple-music,lastfm,deezer"; + Agents = "apple-music,lastfm,deezer,audiomuseai"; LyricsPriority = ".ttml,.yaml,.yml,.elrc,.lrc,.srt,.txt,embedded,nd-lyrics"; }; }; From 586f2aeaa78f4b0c0eb925f638deafd82e3a2a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Thu, 3 Sep 2026 12:25:06 -0700 Subject: [PATCH 5/5] fix(glyph): move open-terminal off port 8000 AudioMuse-AI's flask container binds host port 8000 directly via --network=host (its internal port is hardcoded, not configurable). open-terminal was already publishing 8000:8000, so it silently won the port and answered with its own 404 for any AudioMuse-AI request instead of connection being refused. Move open-terminal to 8001, which is trivially remappable, to free 8000 for AudioMuse-AI. Entire-Checkpoint: 9d33df0e6848 --- hosts/glyph/services/open-terminal.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts/glyph/services/open-terminal.nix b/hosts/glyph/services/open-terminal.nix index 411efbaa..be247498 100644 --- a/hosts/glyph/services/open-terminal.nix +++ b/hosts/glyph/services/open-terminal.nix @@ -8,7 +8,7 @@ virtualisation.oci-containers.backend = "podman"; virtualisation.oci-containers.containers.open-terminal = { image = "ghcr.io/open-webui/open-terminal:latest"; - ports = ["8000:8000"]; + ports = ["8001:8000"]; volumes = ["open-terminal:/home/user"]; environmentFiles = [config.age.secrets.open-terminal-env.path]; environment = {