Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Runtime tags: safetensors is the default model loading path. `GGUF 16/Q8/Q4` mea
| **vibevoice_asr** | ASR | auto | VibeVoice ASR | GGUF 16/Q8 |
| **voxtral_realtime** | ASR | auto | Voxtral-Mini-4B-Realtime-2602 | GGUF 16/Q8/Q4, Stream |
| **voxcpm2** | TTS, Clone, Design, Ctrl | ar, da, de, el, en, es, fi, fr, he, hi, id, it, ja, km, ko, lo, ms, my, nl, no, pl, pt, ru, sv, sw, th, tl, tr, vi, zh | VoxCPM2-2B, 48 kHz | GGUF 16/Q8, Stream |
| **index_tts2** | TTS, Clone, Ctrl | zh, en | IndexTTS-2 | GGUF 16/Q8 |
| **index_tts2** | TTS, Clone, Ctrl | zh, en, ja, es, ar | IndexTTS-2, IndexTTS-2.5 (variant) | GGUF 16/Q8 |
| **irodori_tts** | TTS, Clone, Design, Ctrl | ja | Irodori-TTS-v4-Small, Irodori-TTS-500M-v3, Irodori-TTS-600M-v3-VoiceDesign | GGUF 16/Q8 |
| **moss_tts_nano** | TTS, Clone | auto | MOSS-TTS-Nano-100M | GGUF 16/Q8 |
| **moss_tts_local** | TTS, Clone, Ctrl | auto, optional language hint | MOSS-TTS-Local-Transformer-v1.5 | GGUF 16/Q8 |
Expand Down
2 changes: 1 addition & 1 deletion docs/gguf.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Status labels:
| `htdemucs` | Done | Pass | --- | Pass | Pass (drift) |
| `hviske_asr` | Done | Pass | --- | --- | Pass |
| `inflect_v2` | Done | Pass | Pass | --- | --- |
| `index_tts2` | Done | Pass | Pass | Pass (drift) | Pass (ASR match, drift) |
| `index_tts2` | Done (v2 + v2.5 variant) | Pass | Pass | Pass (drift) | Pass (ASR match, drift) |
| `irodori_tts` | Done | Pass | --- | Pass | Pass (ASR match, drift) |
| `kroko_asr` | Done | Pass | --- | --- | Pass |
| `marblenet_vad` | Bundled (tiny model) | Pass | --- | --- | --- |
Expand Down
84 changes: 84 additions & 0 deletions docs/tts.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
| 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) |
| IndexTTS2 | `index_tts2` | `tts` | [IndexTTS2](#indextts2) |
| IndexTTS2.5 | `index_tts2` (variant `2.5`) | `tts` | [IndexTTS2.5](#indextts25) |
| Irodori-TTS | `irodori_tts` | `tts`, `vdes` | [Irodori-TTS](#irodori-tts) |
| GLM-TTS | `glm_tts` | `tts`, `clon` | [GLM-TTS](#glm-tts) |
| Inflect Micro v2 | `inflect_v2` | `tts` | [Inflect v2](#inflect-v2) |
Expand Down Expand Up @@ -546,6 +547,89 @@ audiocpp_cli --task tts --family index_tts2 --model /path/to/IndexTTS-2 --backen
| `--session-option index_tts2.emotion_text_max_new_tokens=<n>` | tokens | `256` | Maximum generated tokens for emotion-text classification. |
| `--session-option index_tts2.weight_context_mb=<n>` | MB | `32` | Shared ggml weight metadata context size. |

## IndexTTS2.5

IndexTTS2.5 is IndexTeam/bilibili's multilingual zero-shot TTS model (released 2026-07): a 0.8B GPT (autoregressive) + DiT CFM + BigVGAN stack that keeps IndexTTS2's timbre-emotion decoupling and adds Japanese, Spanish, and Arabic on top of Chinese and English. It requires a speaker reference through the framework `--voice-ref` path. Inline `<文字|发音>` pronunciation overrides (pinyin, CMU phonemes, or kana) are supported. Upstream weights live at [IndexTeam/IndexTTS-2.5](https://huggingface.co/IndexTeam/IndexTTS-2.5); the reference implementation is [index-tts/index-tts](https://github.com/index-tts/index-tts) branch `indextts-2.5`.

IndexTTS2.5 is implemented as a variant of the `index_tts2` family rather than a separate family: both variants share the audio features, wav2vec2bert, Qwen emotion, style encoder, BigVGAN vocoder, S2Mel, and the GPT decode/cache code, while the tokenizer (SentencePiece vs multilingual tiktoken), GPT speaker conditioning (conditioning encoder + perceiver vs CAMPPlus `spk_emb_proj` + `lang_embedding`), and the semantic-codec decode path (v2.5 adds a 2x nearest upsample + `up` conv) are selected per variant from the model config `version` field (`"2.5"`). All IndexTTS2 session options (`index_tts2.*`) apply to both variants.

| Field | Value |
|---|---|
| Family | `index_tts2` (the `2.5` variant is selected from the model config `version` field; no separate family) |
| Model directory | `models/IndexTTS2.5-GGUF` (default GGUF package `index_tts2_5_q8_0`; `index_tts2_5_f16` and `index_tts2_5_orig` also available) |
| Task | `tts`, `clon` |
| Modes | `offline` |
| Languages | `zh`, `en`, `ja`, `es`, `ar` |
| Voice input | Required reference WAV through `--voice-ref` |
| Built-in voices | Not exposed |

Voice clone:

```bash
audiocpp_cli --task clon --family index_tts2 --model /path/to/IndexTTS2.5-GGUF --backend cuda --text "Hello from IndexTTS2.5." --voice-ref /path/to/reference.wav --out out.wav
```

Emotion text:

```bash
audiocpp_cli --task tts --family index_tts2 --model /path/to/IndexTTS2.5-GGUF --backend cuda --text "今天的演示会更有情绪。" --voice-ref /path/to/reference.wav --emotion "你吓死我了!你是鬼吗?" --request-option emotion_alpha=0.6 --out out.wav
```

The `lang` request option selects the text language (`auto`, `zh`, `en`, `ja`, `es`, `ar`, or any tokenizer language code). The default `auto` picks `zh` when the text contains Han characters and `en` otherwise, so mixed Japanese/Spanish/Arabic text should set `--request-option lang=ja|es|ar` explicitly.

Emotion conditioning supports all three IndexTTS2 paths: an emotion reference WAV through `--audio`, an explicit `emotion_vector`, and Qwen-based emotion-text classification through `--emotion` / `use_emotion_text`. Known limitation: the NeMo text normalizers for Japanese and Spanish are not ported, so ja/es input text is passed through without upstream-style normalization.

License: IndexTTS-2.5 weights are distributed under the bilibili Model Use License, which is not OSI-approved. It requires separate commercial authorization when monthly active users exceed 100 million or annual revenue exceeds 1 billion RMB, and it forbids using model outputs to improve other AI models. Check the upstream repository for the full terms before redistribution or commercial use.

| Option | Values | Default | Meaning |
|---|---|---:|---|
| `--voice-ref` | WAV path | required | Reference speaker audio. |
| `--request-option lang=<code>` | `auto`, `zh`, `en`, `ja`, `es`, `ar`, ... | `auto` | Text language hint; `auto` infers `zh` when the text contains Han characters, otherwise `en`. |
| `--emotion` | text | not set | Emotion-text conditioning through the framework style field. |
| `--request-option emotion_alpha=<float>` | float in `[0, 1]` | `1.0` | Blend strength for explicit emotion conditioning. |
| `--request-option emotion_vector=<v0,...,v7>` | 8 floats | not set | Explicit emotion vector. |
| `--request-option use_emotion_text=true|false` | bool | `false` | Infer emotion from text. |
| `--request-option use_random_emotion=true|false` | bool | `false` | Use random emotion weights in the emotion mixer. |
| `--request-option interval_silence_ms=<n>` | milliseconds | `200` | Silence inserted between generated text chunks. |
| `--text-chunk-size` | characters | not set | Optional framework outer text chunk size. When omitted, IndexTTS2.5 keeps its internal tokenizer segmentation. |
| `--text-chunk-mode` | `default`, `tag_aware`, `japanese`, `endline` | `default` | Framework chunking mode used only when `--text-chunk-size` is set. |
| `--max-tokens` | integer | `1500` | Maximum generated GPT mel tokens. |
| `--temperature` | float | `0.8` | GPT sampling temperature. |
| `--top-p` | float | `0.8` | GPT nucleus sampling limit. |
| `--top-k` | integer | `30` | GPT top-k sampling limit. |
| `--repetition-penalty` | float | `10.0` | GPT repetition penalty. |
| `--do-sample` | `true`, `false` | `true` | Enable stochastic GPT sampling. |
| `--request-option length_penalty=<float>` | float | `0.0` | GPT beam-search length penalty. |
| `--request-option num_beams=<n>` | integer | `3` | GPT beam count. |
| `--session-option index_tts2.mem_saver=true|false` | bool | `false` | Release staged reference and conditioning graphs after request phases. |
| `--session-option index_tts2.weight_type=native|f32|f16|bf16|q8_0` | enum | `native` | Matmul weight storage type. |
| `--session-option index_tts2.conv_weight_type=native|f32|f16` | enum | `native` | Convolution weight storage type. |
| `--session-option index_tts2.speaker_cache_slots=<n>` | integer slots | `1` | Prepared speaker-reference cache slots; set `0` to disable reuse. |
| `--session-option index_tts2.emotion_cache_slots=<n>` | integer slots | `1` | Prepared emotion-reference cache slots; set `0` to disable reuse. |
| `--session-option index_tts2.emotion_text_cache_slots=<n>` | integer slots | `1` | Emotion-text weight cache slots; set `0` to disable reuse. |
| `--session-option index_tts2.gpt_graph_arena_mb=<n>` | MB | model default | GPT graph arena size. |
| `--session-option index_tts2.s2mel_graph_arena_mb=<n>` | MB | model default | S2Mel graph arena size. |
| `--session-option index_tts2.reference_graph_arena_mb=<n>` | MB | model default | Reference encoder and codec graph arena size. |
| `--session-option index_tts2.emotion_text_prefill_graph_arena_mb=<n>` | MB | model default | Emotion-text prefill graph arena size. |
| `--session-option index_tts2.emotion_text_decode_graph_arena_mb=<n>` | MB | model default | Emotion-text cached-step graph arena size. |
| `--session-option index_tts2.emotion_text_max_new_tokens=<n>` | tokens | `256` | Maximum generated tokens for emotion-text classification. |
| `--session-option index_tts2.weight_context_mb=<n>` | MB | `32` | Shared ggml weight metadata context size. |

### Converting From Upstream Weights

`tools/convert_index_tts2_5.py` turns an official `IndexTeam/IndexTTS-2.5` snapshot (the `.pth` checkpoints) into the Safetensors staging layout the engine expects, and prints (or runs) the matching `audiocpp_gguf` command. The w2v-bert-2.0, CAMPPlus, and BigVGAN checkpoints are auto-detected under `<model-dir>/hf_cache/` (run the official inference once to populate it) and each has an explicit override flag:

```bash
python tools/convert_index_tts2_5.py \
--model-dir /path/to/IndexTTS-2.5 \
--output-dir /path/to/staging \
--run-converter /path/to/audiocpp_gguf --type q8_0
```

Pass `--native-dir /path/to/IndexTTS-2.5-native` to also emit a directly loadable native Safetensors model directory (hardlinked from the staging files), no GGUF conversion required.

The script repackages the checkpoints the loader needs (unwraps the `s2mel.pth`/`codec.pth` container keys, prefixes CAMPPlus tensors with `speaker_encoder.`, strips BigVGAN's `generator.` prefix, wraps the `feat1/feat2.pt` matrices as a single `tensor`) and assembles the sidecar `root/` (config, tiktoken vocabulary, auxiliary model configs) that gets embedded into the GGUF. The staged `config.yaml` has its `version` field normalized to `"2.5"` (the official snapshot ships `version: 2.0`); the engine uses that field to select the IndexTTS2 family variant.

## Irodori-TTS

Irodori-TTS is Japanese TTS under `--family irodori_tts`. v4 Small is the preferred GGUF-first package and supports no-reference speech, reference-conditioned speech, and caption-based voice design in one checkpoint. The older 500M v3 and 600M v3 VoiceDesign packages remain supported for existing users. See [Irodori-TTS](models/irodori_tts.md) for v3/v4 differences, GGUF variants, options, and compatibility aliases.
Expand Down
18 changes: 18 additions & 0 deletions include/engine/models/index_tts2/gpt.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ struct IndexTTS2GptWeights {
engine::modules::LinearWeights emotion_vec_projection;
engine::modules::LinearWeights emotion_layer;
std::vector<float> speed_embedding_values;
// v2.5 campplus speaker conditioning (spk_cond_mode="campplus" in the
// official model_v2.py): projects the 192-dim CAMPPlus speaker/style
// embedding into a GPT speaker token.
engine::modules::LinearWeights spk_emb_proj;
// v2.5: row of this table is added to every text embedding during prefill.
engine::core::TensorValue lang_embedding;
std::vector<IndexTTS2Gpt2LayerWeights> gpt_layers;
engine::modules::NormWeights gpt_final_norm;
engine::modules::NormWeights final_norm;
Expand All @@ -109,8 +115,14 @@ struct IndexTTS2GptGeneration {

struct IndexTTS2GptGenerationRequest {
std::vector<int32_t> text_tokens;
// v2 speaker conditioning: wav2vec2bert semantic features of the reference.
std::vector<float> speaker_semantic;
int64_t speaker_frames = 0;
// v2.5 speaker conditioning: 192-dim CAMPPlus speaker embedding, projected
// by spk_emb_proj inside the prefill graph.
std::vector<float> speaker_style;
// v2.5: row of the GPT lang_embedding table added to every text embedding.
int32_t lang_id = 0;
std::vector<float> emotion_semantic;
int64_t emotion_frames = 0;
std::vector<float> emotion_vector;
Expand All @@ -125,6 +137,12 @@ struct IndexTTS2GptGenerationRequest {
uint32_t seed = 0;
};

// Mirrors the valid_mask filtering in the official v2.5 prepare_gpt_inputs:
// any start/stop text tokens in the segment (including the trailing pad
// appended by the tokenizer) are dropped before the start/stop pair is
// re-added around it.
std::vector<int32_t> align_index_tts2_gpt_text_tokens(const std::vector<int32_t> & text_tokens);

std::shared_ptr<const IndexTTS2GptWeights> load_index_tts2_gpt_weights(
const IndexTTS2Assets & assets,
ggml_backend_t backend,
Expand Down
4 changes: 4 additions & 0 deletions include/engine/models/index_tts2/request.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

namespace engine::models::index_tts2 {

// Normalizes the "lang" request option (v2.5): trims, lowercases, and maps
// "auto" to an empty string (tokenizer-side language inference).
std::string normalize_index_tts2_lang(const std::string & value);

IndexTTS2Request parse_index_tts2_request(const runtime::TaskRequest & request);

} // namespace engine::models::index_tts2
2 changes: 2 additions & 0 deletions include/engine/models/index_tts2/semantic_codec.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ struct IndexTTS2SemanticCodecWeights {
engine::modules::Conv1dWeights quantizer_out;
IndexTTS2VocosBackboneWeights decoder_backbone;
engine::modules::LinearWeights decoder_projection;
// v2.5 only: conv applied after the 2x nearest upsample in the decode path.
engine::modules::Conv1dWeights up;
};

struct IndexTTS2SemanticCodecOutput {
Expand Down
1 change: 1 addition & 0 deletions include/engine/models/index_tts2/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class IndexTTS2Session final
const EmotionState & emotion);
runtime::AudioBuffer synthesize_segment(
const std::vector<int32_t> & text_tokens,
int32_t lang_id,
const SpeakerState & speaker,
const EmotionState & emotion,
const std::vector<float> & emotion_vector,
Expand Down
57 changes: 55 additions & 2 deletions include/engine/models/index_tts2/tokenizer_text.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,92 @@
#include <unordered_map>
#include <vector>

namespace llama_tokenizer_vendor {
struct BpeVocabulary;
} // namespace llama_tokenizer_vendor

namespace engine::models::index_tts2 {

struct IndexTTS2TextEncoding {
// v2.5: resolved language code; empty for v2.
std::string lang;
std::string normalized_text;
// v2 SentencePiece pieces; empty for v2.5.
std::vector<std::string> pieces;
std::vector<int32_t> token_ids;
// v2: piece strings per segment; v2.5: processed text per segment.
std::vector<std::vector<std::string>> segments;
std::vector<std::vector<int32_t>> segment_token_ids;
};

// Variant-aware IndexTTS2 text tokenizer. v2 keeps the SentencePiece bpe.model
// behavior; v2.5 uses the multilingual tiktoken/BPE vocabulary with language
// special tokens and language-id handling. The variant is selected from the
// model config version, not from probing tokenizer files.
class IndexTTS2TextTokenizer {
public:
explicit IndexTTS2TextTokenizer(std::shared_ptr<const IndexTTS2Assets> assets);

IndexTTS2Variant variant() const noexcept {
return variant_;
}

std::string normalize_english(const std::string & text) const;
std::string normalize_chinese(const std::string & text) const;
std::string normalize_text(const std::string & text) const;

// v2: SentencePiece encode of the normalized text.
// v2.5: raw tiktoken encode with allowed_special="all"; does not apply any
// text normalization. Special tokens present in the text are recognized
// directly.
std::vector<int32_t> encode(const std::string & text) const;

// v2 only: normalize then tokenize helpers kept for parity/debug.
std::string normalize_text(const std::string & text) const;
std::vector<std::string> tokenize_to_pieces(const std::string & text) const;

// v2.5 only: returns the id of an exact token text (e.g. "<|zh|>"), or -1
// when unknown.
int32_t special_token_id(const std::string & token_text) const;

// v2.5 only: maps a language code to the GPT lang_embedding row, following
// the LANGUAGES order of indextts/utils/tokenizer.py (en=0, zh=1, ...).
// Unknown codes map to "common".
static int32_t lang_to_id(const std::string & lang);

// v2: normalize -> SentencePiece encode -> segment pieces by token budget.
// v2.5: normalize -> case rules -> pronunciation annotations ->
// special-token name uppercasing -> segment by token budget. Each segment
// is encoded as encode("<|{lang}|> " + segment) plus a trailing pad token
// id 1. When lang is empty, it is inferred (Han -> zh, else en).
IndexTTS2TextEncoding encode_for_inference(
const std::string & text,
int max_text_tokens_per_segment) const;
int max_text_tokens_per_segment,
const std::string & lang = "") const;

private:
// v2 SentencePiece path.
IndexTTS2TextEncoding encode_for_inference_v2(
const std::string & text,
int max_text_tokens_per_segment) const;
int32_t piece_to_id(const std::string & piece) const;
std::string id_to_piece(int32_t id) const;
std::vector<std::vector<std::string>> split_segments(
const std::vector<std::string> & pieces,
int max_text_tokens_per_segment) const;

// v2.5 tiktoken path.
IndexTTS2TextEncoding encode_for_inference_v2_5(
const std::string & text,
int max_text_tokens_per_segment,
const std::string & lang) const;

std::shared_ptr<const IndexTTS2Assets> assets_;
IndexTTS2Variant variant_ = IndexTTS2Variant::kV2;
// v2 SentencePiece model.
std::vector<engine::tokenizers::SentencePiecePiece> pieces_;
std::unordered_map<std::string, int32_t> piece_to_id_;
// v2.5 tiktoken vocabulary.
std::shared_ptr<llama_tokenizer_vendor::BpeVocabulary> vocab_;
};

} // namespace engine::models::index_tts2
21 changes: 21 additions & 0 deletions include/engine/models/index_tts2/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,28 @@
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

namespace engine::models::index_tts2 {

// Model variant, selected from the model config "version" field ("2.5" ->
// kV2_5, anything else -> kV2). Variant branches must be driven by this value,
// never by probing weight tensor names in hot paths.
enum class IndexTTS2Variant {
kV2,
kV2_5,
};

inline IndexTTS2Variant index_tts2_variant_from_version(std::string_view version) {
return version == "2.5" ? IndexTTS2Variant::kV2_5 : IndexTTS2Variant::kV2;
}

// Rows in the v2.5 GPT lang_embedding table. indextts/utils/tokenizer.py
// defines 106 language codes (including "common"); the checkpoint table has
// one extra unused row.
constexpr int64_t kIndexTTS2LangEmbeddingRows = 107;

struct IndexTTS2GptConfig {
int64_t model_dim = 1280;
int64_t max_mel_tokens = 1815;
Expand Down Expand Up @@ -132,6 +150,9 @@ struct IndexTTS2Request {
std::string text;
std::optional<runtime::AudioBuffer> speaker_audio = std::nullopt;
std::optional<runtime::AudioBuffer> emotion_audio = std::nullopt;
// Text language hint (v2.5 only); empty means auto (zh when the text
// contains Han characters, otherwise en).
std::string lang;
float emotion_alpha = 1.0F;
std::optional<std::vector<float>> emotion_vector = std::nullopt;
bool use_emotion_text = false;
Expand Down
Loading
Loading