Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ad2baa7
[WIP] Add VoxCPM v1 — lightweight VoxCPM TTS support (0.5B / 1.5B)
jasonchen31 Aug 16, 2026
11f37a7
fix(voxcpm1): resolve partialy pure noise output by fixing synthesize…
jasonchen31 Aug 17, 2026
59d6185
# Commit: Fix VoxCPM1 Voice Quality (Pure Noise → Intelligible Speech)
jasonchen31 Aug 17, 2026
fdb06c2
Merge branch '0xShug0:main' into main
jasonchen31 Aug 17, 2026
43b12a9
Merge branch '0xShug0:main' into main
jasonchen31 Aug 17, 2026
36567b7
# VoxCPM1 GGUF Self-Contained Loading
jasonchen31 Aug 17, 2026
bc80b8b
Fix VoxCPM1 issues: sample rate (V1.5) and early stopping (V1 0.5B)
jasonchen31 Aug 17, 2026
0ec6230
Merge branch '0xShug0:main' into main
jasonchen31 Aug 18, 2026
85e5859
**feat(voxcpm1): enable voice clone & streaming support (parity with …
jasonchen31 Aug 18, 2026
a8fef3d
fix(voxcpm1): load real RoPE longrope factors and align min_tokens floor
jasonchen31 Aug 18, 2026
7f224ab
Merge branch '0xShug0:main' into main
jasonchen31 Aug 18, 2026
65ea6a1
fix(voxcpm1): align voice-clone conditioning and VAE encoder with gol…
jasonchen31 Aug 19, 2026
0801a84
Merge branch '0xShug0:main' into main
jasonchen31 Aug 19, 2026
00ea405
fix(voxcpm1): route --voice-ref through prompt path so V1 cloning works
jasonchen31 Aug 19, 2026
51c4452
Merge branch 'main' of https://github.com/jasonchen31/audio.cpp into …
jasonchen31 Aug 19, 2026
57b6498
refactor: release tensor_source framework changes back to upstream
jasonchen31 Aug 19, 2026
2b65a0d
fix(voxcpm1): Add Webui support for VoxCPM v1 (0.5B)
jasonchen31 Aug 19, 2026
1d9c59b
perf(voxcpm1): release text-length-scaled VRAM after each request
jasonchen31 Aug 19, 2026
6e4f09d
Merge branch '0xShug0:main' into main
jasonchen31 Aug 19, 2026
1d6576c
remove local
jasonchen31 Aug 19, 2026
f125922
Merge branch 'main' of https://github.com/jasonchen31/audio.cpp into …
jasonchen31 Aug 19, 2026
063cbe3
Merge remote-tracking branch 'upstream/main'
jasonchen31 Aug 19, 2026
5b949e7
generate new index.html based on merged code
jasonchen31 Aug 19, 2026
064bd91
Merge remote-tracking branch 'upstream/main'
jasonchen31 Aug 20, 2026
47a7e7e
fix(vpxcpm1): release the last framework change. udpate index.html fo…
jasonchen31 Aug 20, 2026
730fb37
remove loacal file
jasonchen31 Aug 20, 2026
03e18ca
add missing file
jasonchen31 Aug 20, 2026
a252b6d
fix(voxcpm1/2): remove strange stream blocking setting.
jasonchen31 Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -734,17 +734,38 @@ audiocpp_add_model(voxcpm2
SOURCES
src/models/voxcpm2/assets.cpp
src/models/voxcpm2/audiovae.cpp
src/models/voxcpm2/config_gguf.cpp
src/models/voxcpm2/generator.cpp
src/models/voxcpm2/gguf_metadata.cpp
src/models/voxcpm2/loader.cpp
src/models/voxcpm2/minicpm.cpp
src/models/voxcpm2/session.cpp
src/models/voxcpm2/tokenizer_gguf.cpp
src/models/voxcpm2/tokenizer_text.cpp
INCLUDES
engine/models/voxcpm2/loader.h
LOADERS
engine::models::voxcpm2::make_voxcpm2_loader
)

audiocpp_add_model(voxcpm1
SOURCES
src/models/voxcpm2/assets.cpp
src/models/voxcpm2/audiovae.cpp
src/models/voxcpm2/config_gguf.cpp
src/models/voxcpm2/generator.cpp
src/models/voxcpm2/gguf_metadata.cpp
src/models/voxcpm2/loader.cpp
src/models/voxcpm2/minicpm.cpp
src/models/voxcpm2/session.cpp
src/models/voxcpm2/tokenizer_gguf.cpp
src/models/voxcpm2/tokenizer_text.cpp
INCLUDES
engine/models/voxcpm2/loader.h
LOADERS
engine::models::voxcpm2::make_voxcpm1_loader
)

audiocpp_add_model(vibevoice
SOURCES
src/models/vibevoice/assets.cpp
Expand Down
50 changes: 50 additions & 0 deletions docs/tts.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
| NeuTTS | `neutts` | `tts` | [NeuTTS](#neutts) |
| OmniVoice | `omnivoice` | `tts` | [OmniVoice](#omnivoice), [full guide](models/omnivoice.md) |
| PocketTTS | `pocket_tts` | `tts` | [PocketTTS](#pockettts) |
| VoxCPM1 | `voxcpm1` | `tts` | [VoxCPM1](#voxcpm1) |
| VoxCPM2 | `voxcpm2` | `tts`, `vdes` | [VoxCPM2](#voxcpm2) |
| Higgs Audio v3 TTS | `higgs_audio_tts` | `tts` | [Higgs Audio v3 TTS](#higgs-audio-v3-tts) |
| Fish Audio S2 Pro | `fish_audio` | `tts` | [Fish Audio S2 Pro](#fish-audio-s2-pro) |
Expand Down Expand Up @@ -422,6 +423,55 @@ audiocpp_cli --task tts --family pocket_tts --model models/pocket-tts --backend
| `--text-chunk-size` | integer chars | `256` | Long-form chunk size. |
| `--session-option pocket_tts.voice_state_cache_slots=<n>` | integer slots | `4` | Prepared voice-state cache slots; set `0` to disable reuse. |

## VoxCPM1

VoxCPM1 supports offline and streaming TTS plus short-reference voice cloning. It reuses the VoxCPM2 runtime tree with a GGUF tensor-adaptation layer that understands the OpenBMB folded AudioVAE weights, so the same `--family voxcpm1` path serves both the 16 kHz 0.5B model and the 44.1 kHz 1.5B variants.

| Field | Value |
|---|---|
| Family | `voxcpm1` |
| Model directory | `models/VoxCPM1-GGUF` (0.5B), `models/VoxCPM1.5-GGUF` (1.5B) |
| Task | `tts` |
| Modes | `offline`, `streaming` |
| Languages | Model auto-handles supported languages |
| Voice input | Optional reference WAV; optional transcript through `--reference-text` |
| Built-in voices | Not exposed |

Text to speech:

```bash
audiocpp_cli --task tts --family voxcpm1 --model models/VoxCPM1-GGUF/voxcpm-0.5b-q8_0-audiovae-f16.gguf --backend cpu --text "Hello from VoxCPM1." --out out.wav
```

1.5B variant (44.1 kHz output):

```bash
audiocpp_cli --task tts --family voxcpm1 --model models/VoxCPM1.5-GGUF/voxcpm1.5-q8_0.gguf --backend cpu --text "Hello from VoxCPM1." --out out.wav
```

Voice clone:

```bash
audiocpp_cli --task tts --family voxcpm1 --model models/VoxCPM1-GGUF/voxcpm-0.5b-q8_0-audiovae-f16.gguf --backend cpu --text "Hello from VoxCPM1." --voice-ref assets/resources/b.wav --out out.wav
```

Streaming output:

```bash
audiocpp_cli --task tts --family voxcpm1 --model models/VoxCPM1-GGUF/voxcpm-0.5b-q8_0-audiovae-f16.gguf --backend cpu --mode streaming --text "Hello from VoxCPM1." --request-option retry_badcase=false --out out.wav
```

| Option | Values | Default | Meaning |
|---|---:|---:|---|
| `--voice-ref` | WAV path | not set | Reference speaker audio. |
| `--reference-text` | text | empty string | Transcript for the reference audio (clone prompting). |
| `--mode` | `offline`, `streaming` | `offline` | Full-output or streaming run mode; streaming requires `retry_badcase=false`. |
| `--session-option voxcpm1.mem_saver=true\|false` | bool | `false` | Use tighter graph workspaces and release MiniCPM/AudioVAE request graphs after completion to reduce resident VRAM. |
| `--session-option voxcpm1.prompt_cache_slots=<n>` | integer | `1` | Prompt and prompt-audio embedding cache slots. Set to `0` to disable prompt caching. |
| `--max-tokens` | integer | `4096` | Maximum generated AR tokens. |
| `--num-inference-steps` | integer | `10` | Flow matching steps. |
| `--guidance-scale` | float | `2.0` | CFG strength. |

## VoxCPM2

VoxCPM2 supports plain TTS, voice design, controllable voice cloning, and an ultimate-clone style that uses both prompt audio and transcript. The CLI expresses voice design with the same text convention as the upstream examples: put the voice/style description in parentheses at the start of `--text`.
Expand Down
5 changes: 4 additions & 1 deletion include/engine/models/voxcpm2/assets.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "engine/framework/assets/resource_bundle.h"
#include "engine/framework/assets/tensor_source.h"
#include "engine/models/voxcpm2/tokenizer_gguf.h"

#include <cstdint>
#include <filesystem>
Expand Down Expand Up @@ -85,15 +86,17 @@ struct VoxCPM2Config {
int64_t max_length = 8192;
std::string device = "cuda";
std::string dtype = "bfloat16";
bool v1 = false;
};

struct VoxCPM2Assets {
assets::ResourceBundle resources;
VoxCPM2Config config;
std::shared_ptr<const assets::TensorSource> model_weights;
std::shared_ptr<const assets::TensorSource> audiovae_weights;
std::shared_ptr<const VoxCPM1GgufTokenizer> gguf_tokenizer;
};

std::shared_ptr<const VoxCPM2Assets> load_voxcpm2_assets(const std::filesystem::path & model_path);
std::shared_ptr<const VoxCPM2Assets> load_voxcpm2_assets(const std::filesystem::path & model_path, bool is_v1);

} // namespace engine::models::voxcpm2
1 change: 1 addition & 0 deletions include/engine/models/voxcpm2/audiovae.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class VoxCPM2AudioVAEDecoderRuntime final {
const std::string &prompt_text,
const std::optional<runtime::AudioBuffer> &reference_audio);
void release_runtime_memory();
void release_encoder_graph();

private:
class Impl;
Expand Down
18 changes: 18 additions & 0 deletions include/engine/models/voxcpm2/config_gguf.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#pragma once

#include "engine/models/voxcpm2/assets.h"
#include "engine/framework/assets/tensor_source.h"

#include <memory>
#include <optional>
#include <string>

namespace engine::models::voxcpm2 {

// Load VoxCPM1 config from GGUF metadata
VoxCPM2Config load_voxcpm1_config_from_gguf(const engine::assets::TensorSource & source);

// Check if GGUF has VoxCPM1 config metadata
bool has_voxcpm1_config_metadata(const engine::assets::TensorSource & source);

} // namespace engine::models::voxcpm2
1 change: 1 addition & 0 deletions include/engine/models/voxcpm2/generator.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class VoxCPM2FeatureGeneratorRuntime final {
const std::function<void(const VoxCPM2StreamingChunk &)>
&chunk_callback = nullptr);
void release_runtime_memory();
void release_text_length_memory();

private:
class Impl;
Expand Down
47 changes: 47 additions & 0 deletions include/engine/models/voxcpm2/gguf_metadata.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#pragma once

#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

struct gguf_context;

namespace engine::assets {
class TensorSource;
}

namespace engine::models::voxcpm2 {

// Reads GGUF KV metadata (tokenizer.ggml.*, voxcpm_*) directly from the file
// backing a TensorSource. Only meaningful for GGUF sources: for any other
// source type valid() is false and all accessors return nullopt (optional_*)
// or throw (require_*). This keeps VoxCPM schema knowledge out of the
// framework TensorSource interface.
class GgufMetadataReader {
public:
explicit GgufMetadataReader(const engine::assets::TensorSource & source);
~GgufMetadataReader();

GgufMetadataReader(const GgufMetadataReader &) = delete;
GgufMetadataReader & operator=(const GgufMetadataReader &) = delete;

bool valid() const noexcept { return gguf_ != nullptr; }

[[nodiscard]] std::optional<std::string> optional_string(std::string_view key) const;
[[nodiscard]] std::optional<uint32_t> optional_u32(std::string_view key) const;
[[nodiscard]] std::optional<std::vector<std::string>> optional_string_array(std::string_view key) const;
[[nodiscard]] std::optional<std::vector<int32_t>> optional_i32_array(std::string_view key) const;
[[nodiscard]] std::optional<std::vector<float>> optional_f32_array(std::string_view key) const;

[[nodiscard]] std::string require_string(std::string_view key) const;
[[nodiscard]] uint32_t require_u32(std::string_view key) const;
[[nodiscard]] std::vector<std::string> require_string_array(std::string_view key) const;
[[nodiscard]] std::vector<int32_t> require_i32_array(std::string_view key) const;

private:
struct gguf_context * gguf_ = nullptr;
};

} // namespace engine::models::voxcpm2
1 change: 1 addition & 0 deletions include/engine/models/voxcpm2/loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ class VoxCPM2LoadedModel final : public runtime::ILoadedVoiceModel {
std::unique_ptr<VoxCPM2LoadedModel>
load_voxcpm2_model(const std::filesystem::path &model_path);
std::shared_ptr<runtime::IVoiceModelLoader> make_voxcpm2_loader();
std::shared_ptr<runtime::IVoiceModelLoader> make_voxcpm1_loader();

} // namespace engine::models::voxcpm2
2 changes: 2 additions & 0 deletions include/engine/models/voxcpm2/minicpm.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ class VoxCPM2TextEmbeddingRuntime final {
~VoxCPM2TextEmbeddingRuntime();

std::vector<float> embed_token(int32_t token_id);
void release_runtime_memory();

private:
class Impl;
Expand All @@ -150,6 +151,7 @@ class VoxCPM2PromptPrefillRuntime final {
~VoxCPM2PromptPrefillRuntime();

VoxCPM2PromptPrefillOutput run(const VoxCPM2PromptPrefillInput &input);
void release_runtime_memory();

private:
class Impl;
Expand Down
39 changes: 39 additions & 0 deletions include/engine/models/voxcpm2/tokenizer_gguf.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#pragma once

#include "engine/models/voxcpm2/types.h"
#include "engine/framework/assets/tensor_source.h"

#include <memory>
#include <string>
#include <vector>

namespace engine::models::voxcpm2 {

// Forward declaration
struct VoxCPM2TextPrompt;

// GGUF-native tokenizer that reads tokenizer metadata directly from GGUF
class VoxCPM1GgufTokenizer {
public:
struct Impl;

explicit VoxCPM1GgufTokenizer(std::shared_ptr<const engine::assets::TensorSource> gguf_source);

std::vector<int32_t> encode(const std::string & text) const;
VoxCPM2TextPrompt build_prompt(const std::string & text) const;
int32_t audio_start_token_id() const noexcept;
int32_t audio_end_token_id() const noexcept;
int32_t reference_audio_start_token_id() const noexcept;
int32_t reference_audio_end_token_id() const noexcept;
int32_t bos_token_id() const noexcept;
int32_t eos_token_id() const noexcept;
int32_t unk_token_id() const noexcept;

// Check if the GGUF source has tokenizer metadata
static bool has_tokenizer_metadata(const engine::assets::TensorSource & source);

private:
std::shared_ptr<Impl> impl_;
};

} // namespace engine::models::voxcpm2
6 changes: 4 additions & 2 deletions include/engine/models/voxcpm2/tokenizer_text.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#pragma once

#include "engine/models/voxcpm2/assets.h"
#include "engine/models/voxcpm2/types.h"

#include <cstdint>
Expand All @@ -10,6 +9,9 @@

namespace engine::models::voxcpm2 {

// Forward declaration
struct VoxCPM2Assets;

class VoxCPM2TextTokenizer {
public:
struct Impl;
Expand All @@ -27,4 +29,4 @@ class VoxCPM2TextTokenizer {
std::shared_ptr<const Impl> impl_;
};

} // namespace engine::models::voxcpm2
} // namespace engine::models::voxcpm2
73 changes: 73 additions & 0 deletions include/engine/models/voxcpm2/tokenizer_wrapper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#pragma once

#include "engine/models/voxcpm2/tokenizer_text.h"
#include "engine/models/voxcpm2/tokenizer_gguf.h"
#include "engine/models/voxcpm2/types.h"

#include <memory>
#include <variant>

namespace engine::models::voxcpm2 {

// Wrapper that can hold either VoxCPM2TextTokenizer (JSON-based) or VoxCPM1GgufTokenizer (GGUF-based)
class VoxCPM2TokenizerWrapper {
public:
VoxCPM2TokenizerWrapper() = default;
explicit VoxCPM2TokenizerWrapper(std::shared_ptr<const VoxCPM2TextTokenizer> tokenizer)
: tokenizer_(std::move(tokenizer)) {}
explicit VoxCPM2TokenizerWrapper(std::shared_ptr<const VoxCPM1GgufTokenizer> tokenizer)
: tokenizer_(std::move(tokenizer)) {}

VoxCPM2TextPrompt build_prompt(const std::string & text) const {
if (std::holds_alternative<std::shared_ptr<const VoxCPM2TextTokenizer>>(tokenizer_)) {
return std::get<std::shared_ptr<const VoxCPM2TextTokenizer>>(tokenizer_)->build_prompt(text);
} else {
return std::get<std::shared_ptr<const VoxCPM1GgufTokenizer>>(tokenizer_)->build_prompt(text);
}
}

int32_t audio_start_token_id() const noexcept {
if (std::holds_alternative<std::shared_ptr<const VoxCPM2TextTokenizer>>(tokenizer_)) {
return std::get<std::shared_ptr<const VoxCPM2TextTokenizer>>(tokenizer_)->audio_start_token_id();
} else {
return std::get<std::shared_ptr<const VoxCPM1GgufTokenizer>>(tokenizer_)->audio_start_token_id();
}
}

int32_t audio_end_token_id() const noexcept {
if (std::holds_alternative<std::shared_ptr<const VoxCPM2TextTokenizer>>(tokenizer_)) {
return std::get<std::shared_ptr<const VoxCPM2TextTokenizer>>(tokenizer_)->audio_end_token_id();
} else {
return std::get<std::shared_ptr<const VoxCPM1GgufTokenizer>>(tokenizer_)->audio_end_token_id();
}
}

int32_t reference_audio_start_token_id() const noexcept {
if (std::holds_alternative<std::shared_ptr<const VoxCPM2TextTokenizer>>(tokenizer_)) {
return std::get<std::shared_ptr<const VoxCPM2TextTokenizer>>(tokenizer_)->reference_audio_start_token_id();
} else {
return std::get<std::shared_ptr<const VoxCPM1GgufTokenizer>>(tokenizer_)->reference_audio_start_token_id();
}
}

int32_t reference_audio_end_token_id() const noexcept {
if (std::holds_alternative<std::shared_ptr<const VoxCPM2TextTokenizer>>(tokenizer_)) {
return std::get<std::shared_ptr<const VoxCPM2TextTokenizer>>(tokenizer_)->reference_audio_end_token_id();
} else {
return std::get<std::shared_ptr<const VoxCPM1GgufTokenizer>>(tokenizer_)->reference_audio_end_token_id();
}
}

bool empty() const noexcept {
return std::holds_alternative<std::monostate>(tokenizer_);
}

private:
std::variant<
std::monostate,
std::shared_ptr<const VoxCPM2TextTokenizer>,
std::shared_ptr<const VoxCPM1GgufTokenizer>
> tokenizer_;
};

} // namespace engine::models::voxcpm2
Loading
Loading