Pocket-TTS adapter
The Pocket adapter is a private, fail-closed production binding. It is not an online model manager: it downloads nothing, compiles nothing, and reads only a manifest that provisioning wrote.
pocket-tts==2.1.0 is a required dependency and needs no separate install.
Installing it does not fetch or authorize any model or voice.
Single-branch render path
Every renderable voice asset is a compiled .safetensors speaker embedding.
WAV prompts are compiled during provisioning, so the adapter has no
audio-encoding branch: preflight_pocket validates a bounded safetensors
header, and synthesize loads a conditioning state and nothing else.
The conditioning state is derived once per engine, not once per segment. A worker holds a reusable engine and processes its batch serially, so re-deriving per segment repeated a file load for every chunk of text.
The path passed to get_state_for_audio_prompt is always a Path, never a
str. Upstream calls download_if_necessary only for str input, so a Path
cannot reach the network even before the allowlist intervenes.
The local manifest
Every render reads a local manifest. Normally load_voice() writes it; an
operator can instead point KENKUI_POCKET_MANIFEST at one they maintain. It
contains:
- canonical absolute owner-controlled model root and selected local YAML path;
- immutable model revision and exact package version
2.1.0; - every model-tree file as a safe relative POSIX path, exact positive byte size, and lowercase SHA-256 (no extra or missing tree entries);
- canonical absolute compiled speaker-embedding path and its SHA-256;
- voice provenance, license identifier, rights statement, and explicit
commercial_use_allowedboolean; - expected sample rate, CPU device, and bounded timeout; and
- matching public voice content fingerprint, metadata, and compatible model revision.
No environment variable, cache entry, provider default, language shortcut, URL, or Hugging Face identifier can substitute for this declaration. The selected YAML must itself be in the manifest. Unsafe roots/files, symlinks, hardlinks, permissions, hashes, sizes, remote markers, path forms, extra files, malformed embedding headers, mismatched metadata, missing package, or wrong package version fail closed with stable Pocket model/voice errors.
Offline worker behavior
Preflight reads and verifies every declared byte without importing the inference
package. Actual construction is allowed only after the scheduler's spawned-worker
handshake. The child repeats preflight, copies a read-only private snapshot, sets
HF_HUB_OFFLINE=1, HF_HUB_DISABLE_TELEMETRY=1, and
TRANSFORMERS_OFFLINE=1, denies Python socket audit events, and replaces the
provider downloader with an allow-list that accepts only declared snapshot
files. The voice is passed as a local Path.
Each bounded spawned worker creates one private model snapshot and model instance, then reuses them serially for its assigned segment batch. Each voice's conditioning state is loaded once on first use and reused for that worker's later segments. No model is constructed in the parent, workers do not share model state, and each worker removes its snapshot when it exits. Provider exceptions are sanitized as stable load, voice, inference, or invalid-audio errors.
Package inspection basis
The adapter targets these inspected 2.1.0 interfaces:
TTSModel.load_model(config=<local Path>, quantize=False);model.sample_rateandmodel.device;model.get_state_for_audio_prompt(<local Path>); andmodel.generate_audio(state, text)returning the tensor subsequently checked and converted to bounded mono PCM.
The inspected PyPI wheel SHA-256 was
7b8f01d3e52aa7df84887b711994586bdc875e024a8b40a15f757feeeb29f752.
Wheel/source inspection is not inference acceptance.
Provisioning boundary
Provisioning downloads model and voice assets explicitly. Character analysis
has a separate LiteLLM network boundary, but neither asset downloads nor
character analysis run in synthesis workers. tests/test_import_boundaries.py statically asserts that
no module under _tts, _execution, _audio, _domain, or _epub imports
the provisioning module.
Because the renderer replaces download_if_necessary with an allowlist
accepting only local absolute paths, a stock pocket-tts config full of hf://
URLs would be rejected at load time. Provisioning therefore writes a derived
config with every weight reference rewritten to a local path, and a test
asserts no remote scheme survives into it.
Verification
CI exercises the adapter with test doubles, and native FFmpeg acceptance with
generated audio. Real inference, which downloads assets and renders a real
M4B, is an opt-in local test behind KENKUI_RUN_PROVISIONING_REAL=1, because
it needs hundreds of megabytes of model weights.
A gated-compiled embedding is valid under the ungated model, confirmed by transcription against a negative control. Incompatibility here fails silently — a mismatched pairing still yields well-formed audio — so the discriminating signal is whether the output contains words. A genuinely mismatched control (italian embedding, english weights) ran to the generation limit without EOS and transcribed to nothing; the gated-compiled embedding under ungated weights transcribed the full sentence verbatim. Speaker-identity fidelity was not assessed.
Kenkui still pins conservatively: the compiling engine's revision goes into the
voice's compatible_model_revisions and the revision check rejects a mismatch.
Relaxing that is now an evidenced follow-up.
One engine, many voices
An engine config carries a collection of voice assets rather than one. The engine constructs a single model and derives a conditioning state per voice on first use, keyed by asset digest.
That is what makes a cast affordable: a language engine is roughly 225 MB of weights against roughly 6.5 MB per speaker embedding, so a ten-voice cast costs about 65 MB more than a single voice, not ten models. One model per worker is also required — the scheduler rejects any worker reporting more than one engine initialisation.
Each synthesis task names the digest of the voice that renders it. A worker that had to guess would produce well-formed audio in the wrong voice, which no automated check would catch.
Every voice in one cast must share an engine and revision. A cast spanning languages is refused rather than silently loading a second engine per worker.