Author SHA1 Message Date
darmanandClaude Opus 5 22caa06904 mars(kittentts): patch venv .so RPATHs so numpy/onnxruntime load
The venv's python is Nix python313 (also inside the container, via the
/nix/store mount), whose loader never searches /usr/lib, so PyPI manylinux
wheels failed with "libstdc++.so.6: cannot open shared object file".
auto-patchelf the staged venv against gcc-lib + zlib; RPATHs are store
paths, so the delivered copy works in the container too. The lib paths join
the input hash so a toolchain bump rebuilds.

Verified locally with mars's exact uv/python313: construct offline from the
staged hub layout and synthesize with voice "Luna".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 15:05:01 +02:00
darmanandClaude Opus 5 812c1af1f2 mars(kittentts): install the wheel under its real filename
uv reads the version from the wheel filename, and the fetchurl store path's
hash prefix made it reject the file ("invalid version"). Expose it via a
linkFarm under kittentts-0.8.1-py3-none-any.whl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:59:01 +02:00
darmanandClaude Opus 5 e0f5c6b37b mars(kittentts): fix review round 3 — eval, delivery and stale-venv bugs
- pkgs.setpriv does not exist (mars failed to eval); setpriv is in util-linux.
- `sh` is not on a unit's PATH; call ${pkgs.runtimeShell} directly.
- Drop `env HOME="$hermesHome"`: unset shell var under `set -u`, and unneeded.
- Staging dir 0700 -> 0755: the uid-986 delivery step could not read it.
- Rebuild the staging venv whenever the stamp misses, else an input bump
  delivered the old venv under the new stamp.
- Add diffutils: `cmp` in the fast path was missing, so every boot re-delivered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:51:05 +02:00
luna bef5215bea mars(kittentts): review round 2 — setpriv delivery, staged hub tree, ordering fix
1: stageVenv declared with the other path vars before the early-exit
reads it (set -u no longer kills the self-heal path on later boots).
2: delivery is rm+cp as the container uid via setpriv — no mv -Tf
rename-replace on a non-empty directory (that failed every
re-provision); chown -R step gone with it.
3: network-online.target back in after= — this unit CAN download at
boot, so uv must not run pre-network.
4: root never writes into hermesHome at all. refs/main, snapshots and
venv staging happen under root-owned staging; delivery drops to uid 986
via setpriv, rm -rf + cp -a from the staging sources. Symlink-takeover
of refs/install targets and the rm/cp race on the delivered tree are
structurally gone.
Minor: sanity check now constructs KittenTTS against the staged hub
tree, not just imports; fetchurls pinned to the commit sha matching
refs/main; fast-path comment matches behavior (staging venv skips
rebuild on damaged delivery).
2026-09-19 12:46:14 +00:00
luna b2982c47a1 mars(kittentts): review fixes — root-owned staging, offline cache resolved, full-input stamp
Review round 1 fixes:
1+2 (security): build+stamp+uv-cache moved out of hermesHome into a
0700 root-owned staging dir (/var/lib/hermes-kittentts). Root executes
python only from the staging venv after an offline import check;
finished artifacts are copied into hermesHome and the stamp compares
delivered copy vs staging byte-for-byte. No root code path follows
anything the container can write, so no stamp symlink-takeover and no
planted-wheel-in-cache execution.
3: HF snapshot dir is the real commit sha with refs/main written, so
hf_hub_download resolves the seeded cache offline (was 'kitten', never
found).
4: container env pins HF_HOME=/opt/data/kittentts-hf + HF_HUB_OFFLINE=1.
5: stamp keyed on the full input set (requirements+wheel+stub+models),
not requirements alone; deletion of a delivered file drops out of the
fast path and re-provisions cheaply.
6: orders after/requires hermes-agent-prepare-dirs.
2026-09-19 12:33:55 +00:00
luna 524315f495 mars: add KittenTTS voice provider (CPU-only, offline mini model)
tts.provider: kittentts, voice Luna, mini-0.8 model (80M params) per
darman. CPU-only onnxruntime inference — no GPU on mars, validated ~1x
realtime on a weaker dev box. Upstream's misaki[en]+spacy declaration is
deliberately not honored (pulls torch→CUDA, 5.6 GB); a .pth-registered
stub satisfies the dead misaki import, failing loudly if it is ever
actually used. HF model cache pre-seeded from hash-pinned store paths so
HF_HUB_OFFLINE=1 runs with zero boot-time network. Provisioner shaped by
the Mnemosyne review: uv venv --clear, root never executes python from
the venv, ownership to the container uid before anything imports, stamp
checked against venv+model files.
2026-09-19 12:14:53 +00:00
darman 25e17f163e Merge pull request 'mars: provision the Mnemosyne memory provider for Hermes' (#6) from feat/mars-hermes-mnemosyne into master
Reviewed-on: #6
2026-09-19 02:16:35 +02:00
darmanandClaude Opus 5 9b96d3c6f8 mars(mnemosyne): fix env build and plugin symlink target
- fetchPypi: sdists are published underscore-named, so the hyphenated
  pname 404'd.
- sitePackages is relative; prefix the env path so the unit's -d check
  and the symlink point at the store.
- hermes_memory_provider ships in mnemosyne-memory, not mnemosyne-hermes;
  move the import check accordingly.
- Drop the unused /opt/data/mnemosyne-env mount, the orphaned
  requirements.txt, and trim comments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:09:14 +02:00
5 changed files with 369 additions and 154 deletions
+238 -50
View File
@@ -74,13 +74,41 @@ let
builtins.readFile ./gitea-pr-review-prompt.md
);
# Mnemosyne memory provider (local SQLite, third-party plugin not bundled
# with the official image). Fully built as a Nix derivation — see
# pkgs/mnemosyne-env.nix — and mounted READ-ONLY into the container at a
# fixed path. The oneshot near the bottom of this file only writes the
# plugin symlink Docker needs at $HERMES_HOME/plugins/mnemosyne.
# Mnemosyne memory provider: third-party plugin, not in the image.
mnemosyneEnv = pkgs.callPackage ../../pkgs/mnemosyne-env.nix { };
# KittenTTS voice provider inputs (CPU-only; model + wheel hash-pinned).
# Provisioning unit near the bottom of this file; background on why the
# deps deviate from upstream's declaration lives in
# ./kittentts/requirements.txt + ./kittentts/kitten-misaki-stub.py.
kittenttsWheel = pkgs.fetchurl {
url = "https://github.com/KittenML/KittenTTS/releases/download/0.8.1/kittentts-0.8.1-py3-none-any.whl";
sha256 = "sha256-SCpDbE8fMZIVNxA3bkWf82iVF+vNp8KwUeL9QYe0GFE=";
};
# uv parses the version out of the filename; the store hash prefix breaks it.
kittenttsWheelFile = "${pkgs.linkFarm "kittentts-wheel" {
"kittentts-0.8.1-py3-none-any.whl" = kittenttsWheel;
}}/kittentts-0.8.1-py3-none-any.whl";
kittenttsRuntimeLibs = "${pkgs.stdenv.cc.cc.lib}/lib ${pkgs.zlib}/lib";
kittenttsReqs = pkgs.writeText "kittentts-requirements.txt" (
builtins.readFile ./kittentts/requirements.txt
);
kittenttsStub = pkgs.writeText "kitten-tts-stub.py" (
builtins.readFile ./kittentts/kitten-misaki-stub.py
);
kittenttsModelOnnx = pkgs.fetchurl {
url = "https://huggingface.co/KittenML/kitten-tts-mini-0.8/resolve/c02725660cea441db4c383af69f1f26f5cd00947/kitten_tts_mini_v0_8.onnx";
sha256 = "sha256-D1u65PxIAMmNvFRKh+z6eVEN4vuCItsw0S5b/pF335E=";
};
kittenttsModelVoices = pkgs.fetchurl {
url = "https://huggingface.co/KittenML/kitten-tts-mini-0.8/resolve/c02725660cea441db4c383af69f1f26f5cd00947/voices.npz";
sha256 = "sha256-QK0mOJUrd7ey8wEn4mCOFp/GndJWtTvYqqNAmjMZPEI=";
};
kittenttsModelConfig = pkgs.fetchurl {
url = "https://huggingface.co/KittenML/kitten-tts-mini-0.8/resolve/c02725660cea441db4c383af69f1f26f5cd00947/config.json";
sha256 = "sha256-axYLybGeJOyyHoS8FPin2iH99H7HLUJFC8XPUUthgEo=";
};
# Wire event names (X-GitHub-Event) each route accepts — NOT the
# subscription names the gitea hooks in services/dev/gitea.nix use. The two
# namespaces collide; see the long comment on the route unit below.
@@ -235,15 +263,6 @@ in
"${hermesHome}:/opt/data"
"${dropboxDir}:/opt/data/dropbox"
# Mnemosyne memory provider — a Nix-built python env, mounted :ro.
# Nothing fetched at boot, nothing writable from inside the container.
# The plugin symlink the oneshot at the bottom of this file writes
# points at the canonical store path (site-packages passthru), which
# is visible inside thanks to the existing /nix/store ro mount, so
# this /opt/data restatement is a readability alias, not a load
# bearing path.
"${mnemosyneEnv}:/opt/data/mnemosyne-env:ro"
# luna's Obsidian vault, synced with CouchDB on jupiter by
# livesync-bridge.nix. Under /opt/data so she can write notes, not just
# read them; the bridge runs as this same uid/gid so no chown is needed.
@@ -287,6 +306,11 @@ in
# Authentik rejects. Safe to trust any peer: 9119 is already scoped to
# loopback + tailscale0 only.
FORWARDED_ALLOW_IPS = "*";
# KittenTTS: point huggingface_hub at the pre-seeded offline cache and
# forbid network — no model drift, no boot-time fetch (review #4).
HF_HOME = "/opt/data/kittentts-hf";
HF_HUB_OFFLINE = "1";
};
environmentFiles = [ config.sops.templates."hermes-agent.env".path ];
cmd = [ "gateway" "run" ];
@@ -435,26 +459,9 @@ in
'';
};
# ---- Mnemosyne memory provider ----------------------------------------
# The provider's Python closure (mnemosyneEnv, callPackage'd
# pkgs/mnemosyne-env.nix above) is a READ-ONLY nix store path mounted into
# the container — nothing fetched at boot, nothing inside the container can
# write to it, and the mono-repo reproducibility story applies. Hermes only
# needs one mutable pickup point: $HERMES_HOME/plugins/mnemosyne, the
# symlink its discovery scan looks for. Both the plugin wrapper and its
# sibling `mnemosyne` core package live in that env's single site-packages,
# so one link covers them.
#
# Path handling: the symlink target is spelled in the CONTAINER's path
# space (/opt/data/...), because Hermes resolves the plugin from inside the
# container — the same host/container mismatch the webhook prompts already
# navigate via containerHome. Host-side the identical literal resolves
# onto the same store path through hermesHome's bind mount.
#
# Failure posture: after=, not requires= — a failed link write leaves the
# container running with whatever memory.provider falls back to Hermes's
# built-in memory, not a dead bot. The ro mount itself is evaluated at
# build time, so there is nothing provisionable to drift at runtime.
# Hermes discovers memory providers under $HERMES_HOME/plugins; the target
# is a store path, readable in the container via the /nix/store ro mount.
# wantedBy, not requiredBy: a failure leaves Hermes on built-in memory.
systemd.services.hermes-agent-mnemosyne-plugin = {
description = "Link Mnemosyne provider into the Hermes plugin dir";
before = [ "podman-hermes-agent.service" ];
@@ -468,30 +475,211 @@ in
};
script = ''
set -euo pipefail
pluginsDir=${hermesHome}/plugins
pluginDir=$pluginsDir/mnemosyne
# Target is the STORE path itself, not a /opt/data restatement: the
# container already ro-mounts /nix/store for git/tea (see the volumes
# list), so the identical literal resolves on both sides of the bind
# mount. Using the canonical store path directly not the
# /opt/data/mnemosyne-env mount keeps one truth and still works
# whether Hermes resolves the link inside the container or host-side
# during debugging.
target="${mnemosyneEnv.sitePackages}/hermes_memory_provider"
if [ ! -d "$target" ]; then
echo "hermes_memory_provider not found in the mnemosyne env unit bug, not transient" >&2
exit 1
fi
target=${mnemosyneEnv}/${mnemosyneEnv.sitePackages}/hermes_memory_provider
[ -d "$target" ] || { echo "$target missing" >&2; exit 1; }
mkdir -p "$pluginsDir"
chown ${hermesUid}:${hermesGid} "$pluginsDir"
# Atomic swap: write to a temp name, rename over the old link. `-T`
# errors loudly if the target turned into a directory for any reason.
ln -sfn "$target" "$pluginDir.new"
mv -Tf "$pluginDir.new" "$pluginDir"
chown -h ${hermesUid}:${hermesGid} "$pluginDir"
'';
};
# ---- KittenTTS voice provider ------------------------------------------
# CPU-only onnxruntime TTS (no GPU on mars), mini model per darman. The
# upstream `misaki[en]` declaration is deliberately not honored — it pulls
# torch→CUDA (5.6 GB verified); the runtime phonemizes with espeak-ng only,
# so the dead `from misaki import en, espeak` import is satisfied by a
# .pth-registered stub (kitten-misaki-stub.py) that fails loudly if misaki
# is ever actually used.
#
# Provisioner invariants (shaped by the Mnemosyne + KittenTTS review rounds):
# - Build in a ROOT-OWNED staging dir (/var/lib/hermes-kittentts, 0755 so
# the uid-986 delivery step can read it; nothing in it is secret):
# stamp, staging venv, uv cache and staging model copies all live there.
# Root never reads or executes anything the container can write — the
# container cannot symlink-takeover the stamp (review #1) or plant a
# wheel in the uv cache (review #2); only FINISHED artifacts are copied
# into hermesHome, and the stamp compares the delivered copy against
# staging byte-for-byte.
# - Root runs python ONLY from the staging venv (never from the delivered
# uid-986-owned tree in hermesHome) — after a root-side offline import
# check; a failing build aborts before anything is delivered.
# - HF model cache delivered to hermesHome from staging (refs/main ->
# snapshots/<real commit sha>), and the container env pins
# HF_HOME=/opt/data/kittentts-hf + HF_HUB_OFFLINE=1: zero boot-time
# network, no drift.
# - Idempotency stamp keyed on the FULL input set (requirements + wheel +
# stub + model files), not just requirements (review #5); a deleted
# delivered file falls out of the fast path and re-provisions cheaply.
# - Order after prepare-dirs (review #6) — mirrors the mnemosyne unit.
#
# Trust boundary: the DELIVERED venv lives inside hermesHome
# (HERMES_WRITE_SAFE_ROOT), so luna can alter her own TTS engine — and a
# deleted/modified copy just triggers a fresh delivery from the root-owned
# staging area on next boot (self-heals instead of wedging). Deliberate: it's
# her voice, not her jail — the webhook filter scripts remain the only
# write-protected-but-load-bearing items.
systemd.services.hermes-agent-kittentts-provision = {
description = "Provision KittenTTS voice provider (side venv + offline HF cache)";
before = [ "podman-hermes-agent.service" ];
wantedBy = [ "podman-hermes-agent.service" ];
wants = [ "network-online.target" ];
# after prepare-dirs (review #6): on a fresh state dir this unit must not
# create hermesHome root-owned before prepare-dirs sets the ownership
# layout — same ordering contract the mnemosyne unit has. PLUS
# network-online ordering (review2 #3): this unit CAN download at boot
# (unlike mnemosyne's store-path build), so uv must not run before the
# network is actually up.
after = [
"network-online.target"
"hermes-agent-prepare-dirs.service"
];
requires = [ "hermes-agent-prepare-dirs.service" ];
# diffutils: `cmp` in the fast path; missing, it silently re-delivers every boot.
path = [ pkgs.uv pkgs.coreutils pkgs.diffutils pkgs.util-linux pkgs.auto-patchelf pkgs.patchelf ];
serviceConfig = {
Type = "oneshot";
TimeoutStartSec = 600;
};
script = ''
set -euo pipefail
venv=${hermesHome}/kittentts-venv
hubDir=${hermesHome}/kittentts-hf/hub/models--KittenML--kitten-tts-mini-0.8
# Real upstream commit SHA as snapshot dir: hf_hub_download resolves
# refs/main -> snapshots/<sha>; "kitten" (review #3) is never found
# offline and silently triggers a re-download.
modelSha=c02725660cea441db4c383af69f1f26f5cd00947
snap=$hubDir/snapshots/$modelSha
# REVIEW #1/#2: nothing root touches lives in hermesHome. Stamp, staging
# venv and uv cache live root-owned under /var/lib/hermes-kittentts (a
# path the container can not pathwrite or symlinks into its own tree);
# the FINISHED staging venv and model files are the only things copied
# into hermesHome, and only after being validated. Root doesn't follow
# any uid-986-writable path while running as root.
stageDir=/var/lib/hermes-kittentts
# Root-side staging of the DELIVERED TREE (venv + full HF hub layout
# including refs/main) everything root writes lives here.
# review2 #4: root never writes into hermesHome; delivery happens as
# the container uid via setpriv, copying from these root-owned sources.
stageVenv=$stageDir/venv
stageHub=$stageDir/kittentts-hf
stageSnap=$stageDir/hf-model
# Input key: requirements + wheel + stub + model files + resolved script.
# Review #5 a miss on the old requirements-only stamp let a changed
# wheel/stub/model URL keep running the stale install forever.
inputHash=$({ cat ${kittenttsReqs} ${kittenttsWheel} ${kittenttsStub} \
${kittenttsModelOnnx} ${kittenttsModelVoices} ${kittenttsModelConfig}
echo ${kittenttsRuntimeLibs}; } | sha256sum | cut -d' ' -f1)
stampFile=$stageDir/provision.stamp
# Idempotent early exit: stamp matches the full input hash, staging venv
# validated, delivered copy intact check runs below.
if [ -f "$stampFile" ] && [ "$(cat "$stampFile")" = "$inputHash" ] \
&& [ -x "$stageVenv/bin/python" ] \
&& [ -f "$stageSnap/config.json" ]; then
# Delivered artifacts in hermesHome must ALSO match the staging copy:
# luna can rewrite her copy, that's fine but then the missing file
# forces a re-provision (cheap copy, not a rebuild) so deletions
# cannot wedge the gateway without a voice.
if [ -x "$venv/bin/python" ] \
&& cmp -s "$stageSnap/config.json" "$snap/config.json" 2>/dev/null \
&& cmp -s "$stageSnap/kitten_tts_mini_v0_8.onnx" "$snap/kitten_tts_mini_v0_8.onnx" 2>/dev/null \
&& cmp -s "$stageSnap/voices.npz" "$snap/voices.npz" 2>/dev/null; then
exit 0
fi
fi
# ---- staging venv + staged hub tree: root-owned path, uv cache
# included. Root runs python from HERE (container can write nothing in
# this tree); the FINISHED result is copied into hermesHome AS THE
# CONTAINER USER via setpriv (review2 #4) root never writes into
# hermesHome, so no uid-986-controlled path is ever followed while
# running as root; symlink-takeover of stamps/refs/install targets is
# structurally impossible. ----
mkdir -p "$stageDir" "$stageSnap" "$stageVenv" "$stageHub"
# 0755: the setpriv'd uid-986 delivery below must be able to read it.
chmod 0755 "$stageDir"
install -m 0444 ${kittenttsModelConfig} "$stageSnap/config.json"
install -m 0444 ${kittenttsModelOnnx} "$stageSnap/kitten_tts_mini_v0_8.onnx"
install -m 0444 ${kittenttsModelVoices} "$stageSnap/voices.npz"
# Skip the venv REBUILD when staging is still valid (review2 minor:
# damaged delivery should be a copy, not a rebuild) but only for the
# CURRENT inputs, else a bump delivers the old venv under a new stamp.
if [ "$(cat "$stampFile" 2>/dev/null)" != "$inputHash" ] \
|| ! [ -x "$stageVenv/bin/python" ] \
|| ! [ -f "$stageVenv/lib/python3.13/site-packages/kitten_tts_stub.py" ]; then
rm -rf "$stageVenv"
UV_CACHE_DIR=$stageDir/uv-cache \
uv venv "$stageVenv" --python ${pkgs.python313}/bin/python3 --quiet
UV_CACHE_DIR=$stageDir/uv-cache \
uv pip install --python "$stageVenv/bin/python" --quiet \
--requirement ${kittenttsReqs}
# kittentts --no-deps: its overlay of spacy/misaki[en] is what drags in
# the CUDA tree; the requirements freeze already covers its real needs.
UV_CACHE_DIR=$stageDir/uv-cache \
uv pip install --python "$stageVenv/bin/python" --quiet --no-deps \
${kittenttsWheelFile}
# Dead-import shim: .pth auto-loads kitten_tts_stub at interpreter
# start so `from misaki import en, espeak` resolves without the real
# misaki.en.
siteDir=$("$stageVenv/bin/python" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')
cp ${kittenttsStub} "$siteDir/kitten_tts_stub.py"
printf 'import kitten_tts_stub\n' > "$siteDir/zz-kitten-stub.pth"
# PyPI manylinux .so's (numpy, onnxruntime) need libstdc++, which the
# Nix loader never finds, on the host or in the container.
auto-patchelf --paths "$stageVenv" --libs ${kittenttsRuntimeLibs}
fi
# Stage the full delivered hub tree (exact hf_hub_download layout:
# refs/main -> snapshots/<sha>; review1 #3) under root-owned staging.
stageModelDir=$stageHub/hub/models--KittenML--kitten-tts-mini-0.8
mkdir -p "$stageModelDir/refs" "$stageModelDir/snapshots/$modelSha"
printf '%s' "$modelSha" > "$stageModelDir/refs/main"
install -m 0444 ${kittenttsModelOnnx} "$stageModelDir/snapshots/$modelSha/kitten_tts_mini_v0_8.onnx"
install -m 0444 ${kittenttsModelVoices} "$stageModelDir/snapshots/$modelSha/voices.npz"
install -m 0444 ${kittenttsModelConfig} "$stageModelDir/snapshots/$modelSha/config.json"
# No blobs/ indirection: kittentts reads paths RETURNED by
# hf_hub_download, which serves the resolved snapshot file directly
# (prefer-dir layout works offline for fully-materialized files).
# Root-side sanity: the staged interpreter must construct the model
# END-TO-END offline (review2 minor import alone doesn't exercise
# hf_hub_download; a broken cache layout must fail HERE, not in the
# gateway). Points HF_HOME at the staged hub tree itself.
HF_HOME=$stageHub \
HF_HUB_OFFLINE=1 \
PHONEMIZER_ESPEAK_LIBRARY="$("$stageVenv/bin/python" -c 'import espeakng_loader,pathlib;print(pathlib.Path(espeakng_loader.get_library_path()))')" \
PHONEMIZER_ESPEAK_DATA_PATH="$("$stageVenv/bin/python" -c 'import espeakng_loader,pathlib;print(pathlib.Path(espeakng_loader.get_data_path()))')" \
"$stageVenv/bin/python" -c 'from kittentts import KittenTTS; KittenTTS("KittenML/kitten-tts-mini-0.8"); print("kittentts offline build ok")' >/dev/null
# ---- deliver AS THE CONTAINER USER (review2 #4): root never writes
# into hermesHome, so no symlink race and no `chown` step. setpriv
# drops to uid 986, rm -rf's the old delivered copies and copies the
# fresh staging tree in. cp-as-986 also fixes review2 #2: rm+cp in one
# step, no mv -Tf rename-replace on a non-empty directory.
setpriv --reuid=${hermesUid} --regid=${hermesGid} --clear-groups \
${pkgs.runtimeShell} -c '
set -eu
rm -rf "$1" "$2"
cp -a "$3" "$1"
cp -a "$4" "$2"
' _ \
"${hermesHome}/kittentts-venv" \
"${hermesHome}/kittentts-hf" \
"$stageVenv" \
"$stageHub"
# Stamp LAST, root-owned outside hermesHome luna can delete it (which
# forces a cheap re-delivery on next boot), not tamper via symlink.
printf '%s' "$inputHash" > "$stampFile"
'';
};
}
@@ -0,0 +1,34 @@
# Runtime shim for KittenTTS on a CPU-only, disk-constrained host.
#
# kittentts/onnx_model.py opens with `from misaki import en, espeak` — a dead import:
# every code path that produces audio from text goes through phonemizer-fork's
# EspeakBackend (espeak-ng via espeakng-loader); misaki.en/espeak are never called.
# If misaki[en] were installed for real, those two imports would be a hook for the
# whole spacy → thinc → torch → CUDA chain (~5.6 GB verified). This stub registers
# `misaki.en` and `misaki.espeak` as import-time-only placeholders instead.
#
# Written into site-packages by the derivation (see pkgs/kittentts-env.nix) under
# `sitecustomize.py`-style auto-load — actually via a `kitten_tts_stub.py` + a `.pth`
# pointing at it, so any Python process in this env gets the stub registered before
# any kittentts import. If KittenTTS upstream ever starts USING misaki, this shim
# will fail loudly at import of the missing attributes (better than silent distortion),
# and the fix becomes "install the real misaki[en]" — a deliberate, reviewed change.
import sys
import types
_misaki = sys.modules.get("misaki")
if _misaki is None:
# Avoid registering a fake parent before the real misaki loads — the base misaki
# (addict/regex only) is installed normally, so usually already here.
import misaki # noqa: F401 (raises if base misaki is missing — loud, not silent)
_misaki = sys.modules["misaki"]
if getattr(_misaki, "en", None) is None:
_stub = types.ModuleType("misaki.en")
sys.modules.setdefault("misaki.en", _stub)
_misaki.en = _stub
if getattr(_misaki, "espeak", None) is None:
_stub2 = types.ModuleType("misaki.espeak")
sys.modules.setdefault("misaki.espeak", _stub2)
_misaki.espeak = _stub2
+69
View File
@@ -0,0 +1,69 @@
# Pinned requirements for the KittenTTS side-venv on mars (validated freeze, CPU-only).
#
# DEPENDENCY DISCIPLINE (important — this is NOT the upstream dependency set):
# `kittentts` declares `misaki[en]` + spacy, which transitively pull torch → the whole
# CUDA runtime (~5.6 GB with nvidia-* wheels, verified in a clean install on this box).
# NONE of that is reachable at runtime: onnx_model.py imports `from misaki import en, espeak`
# at module load but generate() phonemizes exclusively through `phonemizer-fork` +
# `espeakng-loader` (espeak-ng via EspeakBackend); misaki.en/espeak glyphs are parsed but
# never called in the ONNX path. mars has no GPU and a modest disk budget, so:
#
# - install kittentts with --no-deps and enumerate its deps manually,
# - install base `misaki` (NOT misaki[en]) — only addict/regex,
# - the misaki.en import is satisfied by a stub registered in sitecustomize
# (kitten-misaki-stub.py, written into site-packages by the derivation).
#
# Freeze captured 2026-09-19 from a verified CPU-only venv (python 3.13, i5-4460,
# generation ~= 1x realtime with the mini model, HF_HUB_OFFLINE=1).
#
# kittentts itself comes from a GitHub release wheel (0.8.1), not PyPI (PyPI's 0.1.3 is
# the old v0.1-lineage); see pkgs/kittentts-env.nix.
addict==2.4.0
anyio==4.15.1
attrs==26.1.0
babel==2.18.0
certifi==2026.7.22
cffi==2.1.1
click==8.5.0
cloudpickle==3.1.2
csvw==4.1.0
dlinfo==2.0.0
docopt==0.6.2
espeakng-loader==0.2.4
filelock==4.0.1
flatbuffers==25.12.19
fsspec==2026.9.0
h11==0.16.0
hf-xet==1.6.0
httpcore==1.0.9
httpx==0.28.1
huggingface-hub==1.32.0
idna==3.20
isodate==0.7.2
joblib==1.6.0
jsonschema-specifications==2025.9.1
jsonschema==4.26.0
language-tags==1.3.1
misaki==0.9.4
num2words==0.5.14
numpy==2.5.3
onnxruntime==1.30.0
packaging==26.3
phonemizer-fork==3.3.2
protobuf==7.36.2
pycparser==3.0
pyparsing==3.3.2
python-dateutil==2.9.0.post0
pyyaml==6.0.3
rdflib==7.6.0
referencing==0.37.0
regex==2026.9.10
rfc3986==1.5.0
rpds-py==2026.6.3
segments==2.4.0
six==1.17.0
soundfile==0.14.0
termcolor==3.3.0
tqdm==4.70.1
typing-extensions==4.16.0
uritemplate==4.2.0
-46
View File
@@ -1,46 +0,0 @@
# Pinned requirements for a Mnemosyne side-venv on mars.
#
# Hermes vendors its own Python (the official image's venv) and deliberately
# stays minimal: no pip module inside it, PEP 668 external-management on top.
# Installing provider packages straight into that interpreter would fight the
# image on every rebuild, so Mnemosyne (and its plugin wrapper) live in their
# own venv instead — see the provisioning unit in hosts/mars/hermes-agent.nix.
#
# Freeze captured 2026-09-19 from a verified container-side install of
# `mnemosyne-memory[embeddings]` + `mnemosyne-hermes` — side venv at
# $HERMES_HOME/mnemosyne-venv, activated via $HERMES_HOME/plugins/mnemosyne.
# Versions pinned exactly; transitive deps frozen for reproducibility
# (onnxruntime/numpy drift under a long-lived SQLite state dir is what a
# freeze is here to prevent).
#
anyio==4.15.0
certifi==2026.7.22
charset-normalizer==3.5.1
click==8.5.0
fastembed==0.8.0
filelock==3.32.5
flatbuffers==25.12.19
fsspec==2026.7.0
h11==0.16.0
hf-xet==1.6.0
httpcore==1.0.9
httpx==0.28.1
huggingface-hub==1.32.0
idna==3.19
loguru==0.7.3
mmh3==5.3.0
mnemosyne-hermes==0.5.0
mnemosyne-memory==3.15.1
numpy==2.5.3
onnxruntime==1.30.0
packaging==26.3
pillow==12.3.0
protobuf==7.36.1
py-rust-stemmers==0.1.8
pyyaml==6.0.3
requests==2.34.2
sqlite-vec==0.1.9
tokenizers==0.23.2
tqdm==4.70.0
typing-extensions==4.16.0
urllib3==2.7.0
+28 -58
View File
@@ -1,86 +1,56 @@
# Mnemosyne memory provider for Hermes on mars — packaged for real (Nix).
# Mnemosyne memory provider for Hermes. Built here rather than pip-installed:
# the image's Python has no pip and is PEP 668 managed.
#
# Why derivations instead of a runtime side-venv: the official Hermes image
# vendors its own Python and stays off-limits to pip (no pip module, PEP 668),
# and a runtime venv built host-side breaks twice over inside the container:
# the venv's pyvenv.cfg records a /nix/store python home the container never
# mounts, and a plugins symlink with an absolute host path points nowhere
# from /opt/data. Building here means nothing is fetched at boot, nothing
# under the provider's control is writable from inside the container, and
# the closure is as reproducible as the rest of the host.
#
# Package set (one shared site-packages — the plugin wrapper imports its
# sibling `mnemosyne` core package, so withPackages, not separate envs):
#
# mnemosyne-memory core engine: SQLite/FTS5 storage, recall, tools.
# Base deps only (PyYAML); the optional extras (llm,
# embeddings via fastembed/onnxruntime, mcp, sync) are
# deliberately NOT pulled — recall uses the bundled FTS5
# lexical path, and the heavyweight ML stack (~hundreds of
# MB, live network on first vector use) buys nothing for
# a first deployment. Adding the embeddings extra later
# is pinning fastembed + sqlite-vec here.
# mnemosyne-hermes the wrapper Hermes discovers under $HERMES_HOME/plugins
# (installs itself as package `hermes_memory_provider`).
# Hard dependency: mnemosyne-memory, PyYAML.
#
# Platform note: both sdists are pure Python (build no C extensions), so
# nothing here constrains the host arch beyond the interpreter itself.
# Core deps only: the embeddings extra (fastembed/onnxruntime) is optional at
# runtime, and recall falls back to FTS5.
{
python3,
fetchPypi,
}:
let
python = python3;
mnemosyneMemory = python.pkgs.buildPythonPackage rec {
mnemosyneMemory = python3.pkgs.buildPythonPackage rec {
pname = "mnemosyne-memory";
version = "3.15.1";
pyproject = true;
src = fetchPypi {
inherit pname version;
sha256 = "sha256-lspUMxc0pUSkhSUrNdiiO5OJ1NMC/S853EYSanXtXKM=";
pname = "mnemosyne_memory";
inherit version;
hash = "sha256-lspUMxc0pUSkhSUrNdiiO5OJ1NMC/S853EYSanXtXKM=";
};
build-system = with python.pkgs; [ setuptools ];
build-system = with python3.pkgs; [ setuptools ];
dependencies = with python3.pkgs; [ pyyaml ];
# Base dependency set — everything else in the upstream metadata is an
# optional extra (llm / embeddings / mcp / sync / test / dev) and is not
# installed; see the file-level comment.
dependencies = with python.pkgs; [ pyyaml ];
doCheck = false; # upstream tests want a live Hermes + LLM key present
pythonImportsCheck = [ "mnemosyne" ];
doCheck = false; # tests want a live Hermes + LLM key
# Ships the Hermes plugin package too, not just the core.
pythonImportsCheck = [
"mnemosyne"
"hermes_memory_provider"
];
};
mnemosyneHermes = python.pkgs.buildPythonPackage rec {
mnemosyneHermes = python3.pkgs.buildPythonPackage rec {
pname = "mnemosyne-hermes";
version = "0.5.0";
pyproject = true;
src = fetchPypi {
inherit pname version;
sha256 = "sha256-CzEvnUw5oPFtT5bHQQ/GBdy2C/E7qShQn32irIRYKqw=";
pname = "mnemosyne_hermes";
inherit version;
hash = "sha256-CzEvnUw5oPFtT5bHQQ/GBdy2C/E7qShQn32irIRYKqw=";
};
build-system = with python.pkgs; [ setuptools ];
# The wrapper declares `mnemosyne-memory[embeddings]>=3.11.1` on PyPI, but
# the embeddings extra is only consulted when vector recall is enabled
# (see above) — pass the core dependency explicitly rather than dragging
# in onnxruntime for nothing.
dependencies = [ mnemosyneMemory ] ++ (with python.pkgs; [ pyyaml ]);
build-system = with python3.pkgs; [ setuptools ];
# Upstream asks for mnemosyne-memory[embeddings]; see the header.
dependencies = [ mnemosyneMemory ] ++ (with python3.pkgs; [ pyyaml ]);
doCheck = false;
pythonImportsCheck = [ "hermes_memory_provider" ];
pythonImportsCheck = [ "mnemosyne_hermes" ];
};
# The exposed value is the python env itself (a store path mounted :ro).
# Hermes only needs the site-packages dir inside it; `sitePackages` is a
# passthru the python interpreter derivation (and hence withPackages envs)
# exposes, so the caller uses `${env.sitePackages}` instead of guessing
# the python version in a path literal.
in
python.withPackages (_: [ mnemosyneMemory mnemosyneHermes ])
python3.withPackages (_: [
mnemosyneMemory
mnemosyneHermes
])