Skip to content

feat(voxcpm2): ultimate_clone request option and on-disk encoded prompt cache - #982

Open
ersensari wants to merge 4 commits into
0xShug0:mainfrom
ersensari:voxcpm2-ultimate-clone
Open

ersensari wants to merge 4 commits into
0xShug0:mainfrom
ersensari:voxcpm2-ultimate-clone

Conversation

@ersensari

Copy link
Copy Markdown
Contributor

What

Stacked on #981. This branch contains #981's three commits; only the last commit, feat(voxcpm2): ultimate_clone request option, on-disk encoded prompt cache, is new here. It touches session.cpp next to #981's streaming changes, so it does not apply to main on its own. Once #981 is merged, I'll rebase this branch.

  1. ultimate_clone request option. With ultimate_clone=true, the voice reference itself is used as the continuation prompt, and reference_text is its transcript. This is upstream VoxCPM2's prompt_wav_path + prompt_text used together with reference_wav_path.
    • Server voice presets can carry the option, so OpenAI-shaped clients get it by sending only voice=<name>.
    • Today a reference_text sent with voice_ref has no prompt audio and is silently unused, because reference_text is an alias of prompt_text. Callers believed they were giving the model the transcript, but they were running a reference-only (controllable) clone.
    • The output is identical to passing the reference again as audio_input together with prompt_text.
    • With ultimate_clone=true and no transcript, the request fails with a clear error.
  2. voxcpm2.prompt_cache_dir session option. Encoded prompts are kept on disk.
    • The file name is an FNV-1a hash of the clip(s), the transcript and the AudioVAE storage type. A changed input is a cache miss, and requests cannot name a path.
    • Each file is checked for magic, version and size. An unreadable or truncated file is encoded again and replaced: it is written to .tmp, then renamed.

Why: a voice assistant sends one reply as several requests, one per sentence or group of sentences. With a fixed seed, each request still started from nothing, so voice and pace drifted between them. Using the ultimate clone (reference = prompt) keeps every request on the same voice. The cost is re-encoding the prompt for every request: ~3.2 s on CPU.

Validation

Same build as #981, CPU, voxcpm2-q8_0.gguf, Ryzen 9 9900X, 12 threads. Re-run on this branch, rebased on main @ ccdbe76:

audiocpp_cli --task clon --family voxcpm2 --model voxcpm2-q8_0.gguf --backend cpu --threads 12 \
  --request-option seed=13 --request-option ultimate_clone=true \
  --session-option voxcpm2.prompt_cache_dir=/tmp/pc \
  --voice-ref ref.wav --reference-text "<transcript of ref.wav>" \
  --text "Good morning. I checked the schedule, and your first meeting starts at nine." --out ult.wav --log
run voxcpm2.prompt_encode_ms session.wall_ms output sha256
1st (miss, file written: voxcpm2-prompt-6a967775f30d3dea.bin, 88 KB) 3217.6 9053 3bfcc3f0…
2nd (hit) 0.98 5858 3bfcc3f0… (identical)

Also checked during development:

  • A truncated cache file was re-encoded and replaced.
  • ultimate_clone=true without a transcript gives a clear error.
  • Server presets with "request_options": {"ultimate_clone": "true"} and prompt_cache_dir on a volume: after a restart, all 9 voices are warm again from disk.

I compared plain clone, continuation and ultimate clone across multi-request replies by ear, and ultimate clone gave the most consistent voice from one request to the next.

On CUDA (RTX 5060 Ti, sm_120), this branch serves 9 preset voices in a self-hosted voice assistant (in use since 2026-10-19).

Known limitations

  • The cache directory is never pruned. Each entry is small (~90 KB for a ~7 s reference).
  • With ultimate_clone, a (style) / control instruction does not apply. As in upstream VoxCPM2, control instructions only work in controllable-clone mode, so a caller that wants a style should leave ultimate_clone off for that request.

🤖 Generated with Claude Code

ersensari and others added 4 commits October 19, 2026 13:31
…e count

The decoder graph capacity doubled from patch_size until the request fit and
the whole capacity was computed, so a 5.6 s phrase (140 latent frames) decoded
a 256-frame (10.24 s) graph. The decoder is causal, so the padding never
changes the kept samples; it only costs compute.

Without voxcpm2.audiovae_latent_capacity the graph is now built at the
request's frame count (rounded to patch_size) and the allocator is kept across
rebuilds. Setting the option keeps the old reservation behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing windows off the generator thread

Streaming decoded every window inside the generator's chunk callback, on the
session backend, so generation stopped while the AudioVAE ran. On a 12-core
Zen 5 CPU the decoder is ~40% of a request (offline 5.6 s of 20.7 s for 18.7 s
of audio), and the generator scales poorly past ~6 threads (OpenMP barrier
waits are ~20% of a perf profile), so the two serialized stages left cores idle.

New session option voxcpm2.audiovae_threads (CPU only, default 0 = unchanged):
the AudioVAE gets its own CPU ExecutionContext with that many threads, and
streaming requests hand windows to a worker thread in patch order. Only the
worker touches the StreamingAudioController and the event sink, and it is
joined before the request returns, so events stay sequential. A failing sink
(client gone) stops generation at the next patch and the error is rethrown.

Output is bit-identical to the shared-backend path (offline, streaming, clone).

Also documents voxcpm2.audiovae_latent_capacity's new meaning (previous commit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Snake (x + sin(a*x)^2 / (a + 1e-9)) repeated alpha to the activation's full
[1, C, T] size, ran scale_bias over that full tensor, then five more
full-size passes; the sample-rate condition repeated its scale and bias the
same way. ggml_mul / ggml_div broadcast [1, C, 1] directly, so the repeats go,
and on the CPU backend Snake is one ggml_map_custom2 pass (the same float
operations in the same order, rows split across the graph's threads; the
pattern kokoro_tts already uses). Other backends keep the graph form.

Output is bit-identical (offline, 3 and 6 sentences). AudioVAE decode on a
Ryzen 9 9900X, 12 threads, 17.4 s of audio: 5.41 s -> 3.61 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cache

ultimate_clone=true uses the voice reference itself as the continuation
prompt, with reference_text as its transcript - upstream VoxCPM2's
prompt_wav_path + prompt_text together with reference_wav_path. Server voice
presets can carry it, so OpenAI-shaped clients get it with voice=<name>.
Without it a reference_text sent with voice_ref has no prompt audio and is
unused (reference_text is an alias of prompt_text). Output is identical to
passing the reference again as audio_input with prompt_text.

voxcpm2.prompt_cache_dir (session option) keeps encoded prompts on disk.
The file name is a hash of the clip(s), transcript and AudioVAE storage type,
so a changed input is a miss and requests cannot name a path; an unreadable
file is encoded again and replaced (written to .tmp, then renamed). Ultimate
clone encoding is ~3.2 s on a Ryzen 9 9900X; a disk hit is ~1 ms, same audio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@0xShug0

0xShug0 commented Oct 19, 2026

Copy link
Copy Markdown
Owner

@ersensari I'm not sure ultimate_clone is really required. The existing interface already supports it:

Inputs Behavior
--voice-ref ref.wav Reference-only voice cloning; no transcript required
--voice-ref ref.wav --reference-text "..." Still reference-only cloning; transcript is unused without --audio
--voice-ref ref.wav --audio ref.wav --reference-text "..." Uses the recording as both voice reference and continuation prompt

For this part, we only need a documentation fix to correct the example and clarify the roles of these inputs.

@0xShug0

0xShug0 commented Oct 19, 2026

Copy link
Copy Markdown
Owner

@ersensari #981 has been merged. Please rebase the PR while keeping the exec_plan approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants