Moved from dank/command-center#3 (filed here by mistake of repo — this is fleet infra, not agent config).
Research 2026-10-17 (scouts SpeachesDeploy + TtsLandscape2026, live fleet census).
Goal
Custom (cloned) voice for the omp tts tool, fully local, zero production impact.
Why not the alternatives
Upstream Speaches: Kokoro+Piper only, no cloning (stalled since v0.9.0-rc.3). openedai-speech archived 2026-01-04; XTTS-v2 = 920 Elo graveyard + CPML non-commercial weights.
GPU slots measured empty: 3090 22752/24576 MiB (frank-zealot); 2× R9700 ~33.9/34.2 GB each (frank-archon). Qwen3-TTS 1.7B + vLLM-Omni (the only true /v1/audio/voices clone registry) is the GPU endgame when a card frees.
llama.cpp-omnillama-tts-server DOES expose OpenAI /v1/audio/speech but voice= is reserved; cloning needs per-request reference_audio (base64 WAV) — invisible to the omp tts schema → needs a ~30-line voice-registry shim (voice name → stored ref wav → inject reference_audio).
Scope
Build llama.cpp-omni (llama-tts-server target) on Dankclaw; GGUFs from openbmb/VoxCPM2 (Apache-2.0): BaseLM-F16 ~3 GB (Q8_0 ~1.5 GB) + Acoustic-F16 1.83 GB.
Quadlet podman service :8880 on Dankclaw (CPU; unified memory, ~19 Gi available + reclaimable cache, 439 GB disk free).
Voice shim :8890: voice=<name> → reference_audio from voices/<name>.wav; clone registration = drop a WAV.
Measure RTF on the Ryzen AI Max+ 395 CPU (4090 published 0.25–0.38; CPU unmeasured).
Acceptance
omp tts call with voice=frank returns a WAV audibly resembling the reference; stock kokoro fallback still resolves.
Refs: full landscape report tts-landscape-report.md (session memory dir).
> Moved from dank/command-center#3 (filed here by mistake of repo — this is fleet infra, not agent config).
Research 2026-10-17 (scouts SpeachesDeploy + TtsLandscape2026, live fleet census).
## Goal
Custom (cloned) voice for the omp `tts` tool, fully local, zero production impact.
## Why not the alternatives
- Upstream Speaches: Kokoro+Piper only, **no cloning** (stalled since v0.9.0-rc.3). `openedai-speech` archived 2026-01-04; XTTS-v2 = 920 Elo graveyard + CPML non-commercial weights.
- GPU slots measured empty: 3090 22752/24576 MiB (frank-zealot); 2× R9700 ~33.9/34.2 GB each (frank-archon). Qwen3-TTS 1.7B + vLLM-Omni (the only true `/v1/audio/voices` clone registry) is the GPU endgame when a card frees.
- `llama.cpp-omni` `llama-tts-server` DOES expose OpenAI `/v1/audio/speech` but `voice=` is reserved; cloning needs per-request `reference_audio` (base64 WAV) — invisible to the omp tts schema → needs a ~30-line voice-registry shim (voice name → stored ref wav → inject `reference_audio`).
## Scope
1. Build llama.cpp-omni (`llama-tts-server` target) on Dankclaw; GGUFs from openbmb/VoxCPM2 (Apache-2.0): BaseLM-F16 ~3 GB (Q8_0 ~1.5 GB) + Acoustic-F16 1.83 GB.
2. Quadlet podman service `:8880` on Dankclaw (CPU; unified memory, ~19 Gi available + reclaimable cache, 439 GB disk free).
3. Voice shim `:8890`: `voice=<name>` → `reference_audio` from `voices/<name>.wav`; clone registration = drop a WAV.
4. `models.yml`: provider `api: openai-speech`, `auth: none`, baseUrl `http://192.168.0.143:8890/v1`; `modelRoles.speech` → it; `retry.fallbackChains.speech` keeps `local/kokoro`.
5. Measure RTF on the Ryzen AI Max+ 395 CPU (4090 published 0.25–0.38; CPU unmeasured).
## Acceptance
omp `tts` call with `voice=frank` returns a WAV audibly resembling the reference; stock kokoro fallback still resolves.
Refs: full landscape report `tts-landscape-report.md` (session memory dir).
30 GiB available — the frank-dragoon KV-pool cut (524,288 → 200,704 positions, 2026-10-17) freed it
439 GB disk free
439 G free (1.9 T root, 76 % used) — unchanged
CPU
16 threads, idle
ports 8880 / 8890
both free (nothing listening on 88xx or 809x)
GPU slots empty
unchanged: 3090 held by frank-zealot, both R9700s by frank-archon → CPU remains the only honest answer
Upstream contract, verified against tools/omni/voxcpm2/README.md (tc-mb/llama.cpp-omni, master)
llama-tts-server serves POST /v1/audio/speech with model, input, voice, response_format (wav|pcm), reference_audio, seed, cfg_value, inference_timesteps, max_steps, temperature; plus POST /v1/audio/speech/stream (chunked WAV), POST /v1/voxcpm2/init (dynamic load with n_gpu_layers), GET /v1/audio/speech/models.
voice is documented as "(reserved)" and cloning is reference_audio = base64 WAV. So scope item 3 (the ~30-line voice-registry shim) is confirmed necessary, not speculative.
Build targets are voxcpm2-cli and llama-tts-server; both build out of the same cmake tree.
Three corrections to make before anyone builds
openbmb/VoxCPM2 publishes PyTorch weights, not GGUFs. The GGUF pair comes from either the vendored converter (convert_voxcpm2_to_gguf.py --model model.safetensors --vae audiovae.pth --config config.json, needs torch + safetensors + gguf — do it in a container, not against the host Python) or the third-party DennisHuang648/VoxCPM2-GGUF. Choose deliberately: the converter is Apache-2.0 and auditable, the pre-converted repo is a trust decision. Note the metadata namespace moved — current files use voxcpm.*, and acoustic GGUFs exported with voxcpm2.* must be regenerated, so a stale download is a load failure.
"CPU" has to be said out loud. The loader takes n_gpu_layers (-1 = all). On this box CPU-only means n_gpu_layers 0; left at the default the service silently lands on the gfx1151 iGPU — the same device frank-dragoon runs on, which quietly breaks the "zero production impact" promise. Put it in the quadlet command and assert it.
Q8_0 is a two-step detour, not a flag: F32 convert → llama-quantize … Q8_0 (only BaseLM is quantized; Acoustic stays F16 at 1.83 GB). Budget ~5 GB disk and roughly 2.5–3.5 GB RSS at Q8_0.
Still unmeasured — and it gates everything else
CPU RTF on the Ryzen AI Max+ 395 is unknown; the published 0.25–0.38 is RTX 4090 F16. The README's own math gives the shape of the problem: VoxCPM2 emits ~160 ms of audio per decode step and CFM runs inference_timesteps=10 per step, so a 10 s clip is ≈63 steps × 10 diffusion iterations on 16 CPU threads. Measure that with voxcpm2-cli --cpubefore writing the quadlet and the shim — if CPU RTF lands above ~1.5, the whole phase-1 shape is wrong and the answer is a queue, not a faster shim.
The fork is optional: mainline llama.cpp already clones, but only as a CLI
Checked the host's own mainline checkout (~/GitPackages/llama-master @ 5fc4f3c, 0.5.0-dev — the build frank-zealot ran before the PrismML swap) instead of trusting the headline:
tools/tts exists: llama-tts does Qwen3-TTS voice cloning with --tts-speaker-file speaker.wav|mp3 (official ggml-org/Qwen3-TTS-12Hz-1.7B-Base-GGUF, 10 languages via --tts-lang), and also Kyutai Pocket-TTS on the same speaker-file contract (language is a property of the weights).
tools/server contains zero occurrences of audio/speech, and llama-server --help prints no tts/audio/voice flag. The "voice cloning landed in mainline" story is about the tool, not a server route.
So phase 1 is a two-way choice, and the voice-registry shim is required in both arms:
arm
runtime
weights
cost
A (as written)
llama.cpp-omni → llama-tts-server
VoxCPM2 GGUF (convert ourselves, or trust DennisHuang648/VoxCPM2-GGUF)
native /v1/audio/speech + streaming, at the price of a third loader fork in a fleet whose two existing fork locks are payload-format-forced, not stylistic
B (new)
mainline llama-tts behind the same shim
official ggml-org Qwen3-TTS GGUF, 1.7B
no fork, no conversion trust decision; pays a process-per-request (or persistent-worker) wrapper, and gets no streaming until upstream adds the route
Scope item 5 therefore has to measure bothvoxcpm2-cli --cpu and llama-tts -ngl 0 on the same 10-second text before the build order is fixed. If mainline Qwen3-TTS lands at acceptable CPU RTF, arm B deletes a permanent maintenance lock and I would take it.
## Census 2026-10-18: premises re-checked against the live host, contract verified upstream
Read the issue today after the IOMMU window (which changed the RAM picture this plan was sized on). Nothing is built yet.
### Host capacity (Dankclaw, uptime 25 min, load 0.29)
| premise (2026-10-17) | live today |
|---|---|
| "~19 Gi available + reclaimable cache" | **30 GiB available** — the frank-dragoon KV-pool cut (524,288 → 200,704 positions, 2026-10-17) freed it |
| 439 GB disk free | 439 G free (1.9 T root, 76 % used) — unchanged |
| CPU | 16 threads, idle |
| ports 8880 / 8890 | both free (nothing listening on 88xx or 809x) |
| GPU slots empty | unchanged: 3090 held by frank-zealot, both R9700s by frank-archon → **CPU remains the only honest answer** |
### Upstream contract, verified against `tools/omni/voxcpm2/README.md` (tc-mb/llama.cpp-omni, master)
- `llama-tts-server` serves `POST /v1/audio/speech` with `model, input, voice, response_format (wav|pcm), reference_audio, seed, cfg_value, inference_timesteps, max_steps, temperature`; plus `POST /v1/audio/speech/stream` (chunked WAV), `POST /v1/voxcpm2/init` (dynamic load with `n_gpu_layers`), `GET /v1/audio/speech/models`.
- **`voice` is documented as "(reserved)"** and cloning is `reference_audio` = base64 WAV. So scope item 3 (the ~30-line voice-registry shim) is confirmed necessary, not speculative.
- Build targets are `voxcpm2-cli` and `llama-tts-server`; both build out of the same cmake tree.
### Three corrections to make before anyone builds
1. **`openbmb/VoxCPM2` publishes PyTorch weights, not GGUFs.** The GGUF pair comes from either the vendored converter (`convert_voxcpm2_to_gguf.py --model model.safetensors --vae audiovae.pth --config config.json`, needs torch + safetensors + gguf — do it in a container, not against the host Python) or the third-party `DennisHuang648/VoxCPM2-GGUF`. Choose deliberately: the converter is Apache-2.0 and auditable, the pre-converted repo is a trust decision. Note the metadata namespace moved — current files use `voxcpm.*`, and acoustic GGUFs exported with `voxcpm2.*` must be regenerated, so a stale download is a load failure.
2. **"CPU" has to be said out loud.** The loader takes `n_gpu_layers` (`-1` = all). On this box CPU-only means `n_gpu_layers 0`; left at the default the service silently lands on the **gfx1151 iGPU — the same device frank-dragoon runs on**, which quietly breaks the "zero production impact" promise. Put it in the quadlet command and assert it.
3. **Q8_0 is a two-step detour, not a flag**: F32 convert → `llama-quantize … Q8_0` (only BaseLM is quantized; Acoustic stays F16 at 1.83 GB). Budget ~5 GB disk and roughly 2.5–3.5 GB RSS at Q8_0.
### Still unmeasured — and it gates everything else
CPU RTF on the Ryzen AI Max+ 395 is unknown; the published **0.25–0.38 is RTX 4090 F16**. The README's own math gives the shape of the problem: VoxCPM2 emits ~160 ms of audio per decode step and CFM runs `inference_timesteps=10` per step, so a 10 s clip is ≈63 steps × 10 diffusion iterations on 16 CPU threads. Measure that with `voxcpm2-cli --cpu` **before** writing the quadlet and the shim — if CPU RTF lands above ~1.5, the whole phase-1 shape is wrong and the answer is a queue, not a faster shim.
### The fork is optional: mainline llama.cpp already clones, but only as a CLI
Checked the host's own mainline checkout (`~/GitPackages/llama-master` @ `5fc4f3c`, 0.5.0-dev — the build `frank-zealot` ran before the PrismML swap) instead of trusting the headline:
* `tools/tts` exists: `llama-tts` does **Qwen3-TTS** voice cloning with `--tts-speaker-file speaker.wav|mp3` (official `ggml-org/Qwen3-TTS-12Hz-1.7B-Base-GGUF`, 10 languages via `--tts-lang`), and also **Kyutai Pocket-TTS** on the same speaker-file contract (language is a property of the weights).
* `tools/server` contains **zero** occurrences of `audio/speech`, and `llama-server --help` prints no tts/audio/voice flag. The "voice cloning landed in mainline" story is about the **tool**, not a server route.
So phase 1 is a two-way choice, and the voice-registry shim is required in both arms:
| arm | runtime | weights | cost |
|---|---|---|---|
| **A (as written)** | `llama.cpp-omni` → `llama-tts-server` | VoxCPM2 GGUF (convert ourselves, or trust `DennisHuang648/VoxCPM2-GGUF`) | native `/v1/audio/speech` + streaming, at the price of a **third loader fork** in a fleet whose two existing fork locks are payload-format-forced, not stylistic |
| **B (new)** | mainline `llama-tts` behind the same shim | official `ggml-org` Qwen3-TTS GGUF, 1.7B | no fork, no conversion trust decision; pays a process-per-request (or persistent-worker) wrapper, and gets no streaming until upstream adds the route |
Scope item 5 therefore has to measure **both** `voxcpm2-cli --cpu` and `llama-tts -ngl 0` on the same 10-second text before the build order is fixed. If mainline Qwen3-TTS lands at acceptable CPU RTF, arm B deletes a permanent maintenance lock and I would take it.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Research 2026-10-17 (scouts SpeachesDeploy + TtsLandscape2026, live fleet census).
Goal
Custom (cloned) voice for the omp
ttstool, fully local, zero production impact.Why not the alternatives
openedai-speecharchived 2026-01-04; XTTS-v2 = 920 Elo graveyard + CPML non-commercial weights./v1/audio/voicesclone registry) is the GPU endgame when a card frees.llama.cpp-omnillama-tts-serverDOES expose OpenAI/v1/audio/speechbutvoice=is reserved; cloning needs per-requestreference_audio(base64 WAV) — invisible to the omp tts schema → needs a ~30-line voice-registry shim (voice name → stored ref wav → injectreference_audio).Scope
llama-tts-servertarget) on Dankclaw; GGUFs from openbmb/VoxCPM2 (Apache-2.0): BaseLM-F16 ~3 GB (Q8_0 ~1.5 GB) + Acoustic-F16 1.83 GB.:8880on Dankclaw (CPU; unified memory, ~19 Gi available + reclaimable cache, 439 GB disk free).:8890:voice=<name>→reference_audiofromvoices/<name>.wav; clone registration = drop a WAV.models.yml: providerapi: openai-speech,auth: none, baseUrlhttp://192.168.0.143:8890/v1;modelRoles.speech→ it;retry.fallbackChains.speechkeepslocal/kokoro.Acceptance
omp
ttscall withvoice=frankreturns a WAV audibly resembling the reference; stock kokoro fallback still resolves.Refs: full landscape report
tts-landscape-report.md(session memory dir).Census 2026-10-18: premises re-checked against the live host, contract verified upstream
Read the issue today after the IOMMU window (which changed the RAM picture this plan was sized on). Nothing is built yet.
Host capacity (Dankclaw, uptime 25 min, load 0.29)
Upstream contract, verified against
tools/omni/voxcpm2/README.md(tc-mb/llama.cpp-omni, master)llama-tts-serverservesPOST /v1/audio/speechwithmodel, input, voice, response_format (wav|pcm), reference_audio, seed, cfg_value, inference_timesteps, max_steps, temperature; plusPOST /v1/audio/speech/stream(chunked WAV),POST /v1/voxcpm2/init(dynamic load withn_gpu_layers),GET /v1/audio/speech/models.voiceis documented as "(reserved)" and cloning isreference_audio= base64 WAV. So scope item 3 (the ~30-line voice-registry shim) is confirmed necessary, not speculative.voxcpm2-cliandllama-tts-server; both build out of the same cmake tree.Three corrections to make before anyone builds
openbmb/VoxCPM2publishes PyTorch weights, not GGUFs. The GGUF pair comes from either the vendored converter (convert_voxcpm2_to_gguf.py --model model.safetensors --vae audiovae.pth --config config.json, needs torch + safetensors + gguf — do it in a container, not against the host Python) or the third-partyDennisHuang648/VoxCPM2-GGUF. Choose deliberately: the converter is Apache-2.0 and auditable, the pre-converted repo is a trust decision. Note the metadata namespace moved — current files usevoxcpm.*, and acoustic GGUFs exported withvoxcpm2.*must be regenerated, so a stale download is a load failure.n_gpu_layers(-1= all). On this box CPU-only meansn_gpu_layers 0; left at the default the service silently lands on the gfx1151 iGPU — the same device frank-dragoon runs on, which quietly breaks the "zero production impact" promise. Put it in the quadlet command and assert it.llama-quantize … Q8_0(only BaseLM is quantized; Acoustic stays F16 at 1.83 GB). Budget ~5 GB disk and roughly 2.5–3.5 GB RSS at Q8_0.Still unmeasured — and it gates everything else
CPU RTF on the Ryzen AI Max+ 395 is unknown; the published 0.25–0.38 is RTX 4090 F16. The README's own math gives the shape of the problem: VoxCPM2 emits ~160 ms of audio per decode step and CFM runs
inference_timesteps=10per step, so a 10 s clip is ≈63 steps × 10 diffusion iterations on 16 CPU threads. Measure that withvoxcpm2-cli --cpubefore writing the quadlet and the shim — if CPU RTF lands above ~1.5, the whole phase-1 shape is wrong and the answer is a queue, not a faster shim.The fork is optional: mainline llama.cpp already clones, but only as a CLI
Checked the host's own mainline checkout (
~/GitPackages/llama-master@5fc4f3c, 0.5.0-dev — the buildfrank-zealotran before the PrismML swap) instead of trusting the headline:tools/ttsexists:llama-ttsdoes Qwen3-TTS voice cloning with--tts-speaker-file speaker.wav|mp3(officialggml-org/Qwen3-TTS-12Hz-1.7B-Base-GGUF, 10 languages via--tts-lang), and also Kyutai Pocket-TTS on the same speaker-file contract (language is a property of the weights).tools/servercontains zero occurrences ofaudio/speech, andllama-server --helpprints no tts/audio/voice flag. The "voice cloning landed in mainline" story is about the tool, not a server route.So phase 1 is a two-way choice, and the voice-registry shim is required in both arms:
llama.cpp-omni→llama-tts-serverDennisHuang648/VoxCPM2-GGUF)/v1/audio/speech+ streaming, at the price of a third loader fork in a fleet whose two existing fork locks are payload-format-forced, not stylisticllama-ttsbehind the same shimggml-orgQwen3-TTS GGUF, 1.7BScope item 5 therefore has to measure both
voxcpm2-cli --cpuandllama-tts -ngl 0on the same 10-second text before the build order is fixed. If mainline Qwen3-TTS lands at acceptable CPU RTF, arm B deletes a permanent maintenance lock and I would take it.