Voice-over phase 1: VoxCPM2 GGUF TTS server on Dankclaw CPU + voice-registry shim + omp speech role #2

Open
opened 2026-10-17 22:51:38 -07:00 by dank · 1 comment
Owner

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).

> 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).
Author
Owner

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.

## 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.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dank/serverAppConfigs#2