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.
This commit is contained in:
2026-09-19 12:09:03 +00:00
parent 32dd2abdce
commit 76ebc8c822
3 changed files with 216 additions and 0 deletions
@@ -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