Files
vllm-omni/vllm_omni/model_executor/models/indextts2/indextts2_talker.py
T
2026-08-11 23:00:35 +00:00

1286 lines
55 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""IndexTTS2 Stage 0: GPT-2 AR Talker with vLLM-native PagedAttention.
Predicts mel codes autoregressively and collects hidden_states as latent
for Stage 1 (S2Mel + BigVGAN).
"""
from __future__ import annotations
import json
import math
import re
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Any
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from vllm.compilation.decorators import support_torch_compile
from vllm.config import VllmConfig
from vllm.distributed import get_pp_group
from vllm.logger import init_logger
from vllm.model_executor.layers.logits_processor import LogitsProcessor
from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.model_executor.models.gpt2 import GPT2Block
from vllm.model_executor.models.utils import (
is_pp_missing_parameter,
make_empty_intermediate_tensors_factory,
make_layers,
maybe_prefix,
)
from vllm.sequence import IntermediateTensors
from vllm_omni.data_entry_keys import OmniPayload
from vllm_omni.model_executor.models.output_templates import OmniOutput
from vllm_omni.utils.speaker_cache import get_speaker_cache
from .configuration_indextts2 import (
INDEXTTS25_MAX_DURATION_FACTOR,
INDEXTTS25_MIN_DURATION_FACTOR,
IndexTTS2Config,
)
from .gpt.conformer_encoder import ConformerEncoder
from .gpt.embeddings import LearnedPositionEmbeddings
from .gpt.perceiver import PerceiverResampler
from .preprocess_utils import (
compute_fbank,
load_campplus,
load_qwen_emotion,
load_reference_audio,
load_semantic_codec,
load_wav2vec2,
resolve_model_file,
wav2vec_extract,
)
logger = init_logger(__name__)
@dataclass(frozen=True)
class IndexTTSConditioningPolicy:
prefix_tokens: int
uses_conformer_perceiver: bool
uses_campplus_projection: bool
uses_language_embedding: bool
def resolve_indextts_conditioning_policy(model_type: str) -> IndexTTSConditioningPolicy:
if model_type == "indextts2_5":
return IndexTTSConditioningPolicy(
prefix_tokens=3,
uses_conformer_perceiver=False,
uses_campplus_projection=True,
uses_language_embedding=True,
)
return IndexTTSConditioningPolicy(
prefix_tokens=34,
uses_conformer_perceiver=True,
uses_campplus_projection=False,
uses_language_embedding=False,
)
def _normalize_indextts_checkpoint_key(name: str) -> str:
"""Remove FSDP wrapper path components from native checkpoint keys."""
return name.replace("_fsdp_wrapped_module.", "")
def _require_exact_indextts_prefill_span(
*,
actual_span_len: int,
scheduled_span_len: int,
model_type: str,
lang: str,
text_token_count: int,
) -> None:
"""Reject prompt-sizing drift before it can move/remove ``start_mel``."""
if actual_span_len == scheduled_span_len:
return
raise ValueError(
"IndexTTS prefill embedding span mismatch: "
f"scheduled_span_len={scheduled_span_len}, "
f"actual_span_len={actual_span_len}, "
f"model_type={model_type}, lang={lang}, "
f"text_token_count={text_token_count}. "
"IndexTTS requires exact placeholder sizing and does not support "
"chunked prefill; keep enable_chunked_prefill=false."
)
def _find_most_similar_cosine(query: torch.Tensor, matrix: torch.Tensor) -> int:
sims = F.cosine_similarity(query.float(), matrix.float(), dim=1)
return int(torch.argmax(sims).item())
@support_torch_compile(
dynamic_arg_dims={
"input_ids": 0,
"positions": 0,
"intermediate_tensors": 0,
"inputs_embeds": 0,
}
)
class IndexTTS2TalkerForConditionalGeneration(nn.Module):
"""vLLM-native GPT-2 AR talker for IndexTTS2.
Stage 0 of the two-stage pipeline. Predicts mel codes (8194 vocab)
and accumulates hidden_states as latent for Stage 1 S2Mel decoder.
"""
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
self.vllm_config = vllm_config
self.model_path = vllm_config.model_config.model
self.config: IndexTTS2Config = vllm_config.model_config.hf_config # type: ignore[assignment]
gpt_cfg = self.config.gpt
self.conditioning_policy = resolve_indextts_conditioning_policy(self.config.model_type)
self.use_gpt_latent = bool(getattr(self.config, "use_gpt_latent", True))
self.model_dim = gpt_cfg["model_dim"]
self.num_layers = gpt_cfg["layers"]
self.num_heads = gpt_cfg["heads"]
self.max_mel_tokens = gpt_cfg["max_mel_tokens"]
self.max_text_tokens = gpt_cfg["max_text_tokens"]
self.number_mel_codes = gpt_cfg["number_mel_codes"]
self.start_mel_token = gpt_cfg["start_mel_token"]
self.stop_mel_token = gpt_cfg["stop_mel_token"]
self.number_text_tokens = gpt_cfg.get("number_text_tokens", 12000)
self.condition_num_latent = gpt_cfg.get("condition_num_latent", 32)
# --- Flags for vLLM-Omni framework ---
self.have_multimodal_outputs = True
self.has_preprocess = True
self.has_postprocess = True
self.requires_raw_input_tokens = True
self.enable_update_additional_information = True
# Stage 1 consumes the hidden-state stream only in the legacy
# ``use_gpt_latent`` mode. IndexTTS 2.5 transports mel codes plus
# reference conditioning, so asking the generic runner to include
# hidden states would add a synchronous [num_tokens, model_dim] D2H
# copy on every decode step without adding anything to the payload.
self.omni_pooler_payload_include_hidden = self.use_gpt_latent
# S2Mel consumes the complete sequence only at request end. In latent
# mode the runner retains aligned code/latent deltas on GPU; no-latent
# mode reuses the CPU output-token history and retains only conditioning
# metadata here.
self.omni_payload_at_request_end = self.config.model_type == "indextts2_5"
self.omni_request_end_token_ids = ()
self._speaker_cache = get_speaker_cache()
self.gpu_resident_buffer_keys: set[tuple[str, str]] = {
("codes", "mel"),
("meta", "mel_start_offset"),
("meta", "mel_code_count"),
}
if self.use_gpt_latent:
self.gpu_resident_buffer_keys.update(
{
("hidden_states", "latent"),
("meta", "latent_acc"),
}
)
# --- GPT-2 transformer (vLLM-native with PagedAttention) ---
# Build a GPT2Config-like object for GPT2Block
from transformers import GPT2Config as HFGpt2Config
max_seq_len = self.max_mel_tokens + self.max_text_tokens + self.conditioning_policy.prefix_tokens + 3
gpt2_config = HFGpt2Config(
vocab_size=self.number_mel_codes,
n_positions=max_seq_len,
n_ctx=max_seq_len,
n_embd=self.model_dim,
n_layer=self.num_layers,
n_head=self.num_heads,
activation_function="gelu_new",
layer_norm_epsilon=1e-5,
)
# Store for GPT2Block construction
cache_config = vllm_config.cache_config
quant_config = vllm_config.quant_config
self.start_layer, self.end_layer, self.h = make_layers(
gpt2_config.n_layer,
lambda prefix: GPT2Block(gpt2_config, cache_config, quant_config, prefix=prefix),
prefix=f"{prefix}.h",
)
self.ln_f = nn.LayerNorm(self.model_dim, eps=gpt2_config.layer_norm_epsilon)
self.final_norm = nn.LayerNorm(self.model_dim, eps=gpt2_config.layer_norm_epsilon)
self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
["hidden_states"], self.model_dim
)
# --- Embeddings ---
self.text_embedding = nn.Embedding(self.number_text_tokens + 1, self.model_dim)
self.mel_embedding = nn.Embedding(self.number_mel_codes, self.model_dim)
self.text_pos_embedding = LearnedPositionEmbeddings(self.max_text_tokens + 2, self.model_dim)
self.mel_pos_embedding = LearnedPositionEmbeddings(self.max_mel_tokens + 2 + 1, self.model_dim)
if self.conditioning_policy.uses_campplus_projection:
from .tokenizer_v2_5 import LANGUAGE_DICT
self.spk_emb_proj = nn.Linear(192, self.model_dim)
self.lang_embedding = nn.Embedding(len(LANGUAGE_DICT) + 1, self.model_dim)
else:
self.speed_emb = nn.Embedding(2, self.model_dim)
self.emo_layer = nn.Linear(self.model_dim, self.model_dim)
self.emovec_layer = nn.Linear(1024, self.model_dim)
# --- Mel head (logits) ---
if get_pp_group().is_last_rank:
self.mel_head = ParallelLMHead(
self.number_mel_codes,
self.model_dim,
bias=True,
quant_config=quant_config,
prefix=maybe_prefix(prefix, "mel_head"),
)
else:
self.mel_head = None # type: ignore[assignment]
self.logits_processor = LogitsProcessor(self.number_mel_codes)
# --- Conditioning modules (speaker / emotion) ---
cond_cfg = gpt_cfg.get("condition_module", {})
emo_cond_cfg = gpt_cfg.get("emo_condition_module", {})
if self.conditioning_policy.uses_conformer_perceiver:
self.conditioning_encoder = ConformerEncoder(
input_size=1024,
output_size=cond_cfg.get("output_size", 512),
linear_units=cond_cfg.get("linear_units", 2048),
attention_heads=cond_cfg.get("attention_heads", 8),
num_blocks=cond_cfg.get("num_blocks", 6),
input_layer=cond_cfg.get("input_layer", "conv2d2"),
)
self.perceiver_encoder = PerceiverResampler(
self.model_dim,
dim_context=cond_cfg.get("output_size", 512),
ff_mult=cond_cfg.get("perceiver_mult", 2),
heads=cond_cfg.get("attention_heads", 8),
num_latents=self.condition_num_latent,
)
self.emo_conditioning_encoder = ConformerEncoder(
input_size=1024,
output_size=emo_cond_cfg.get("output_size", 512),
linear_units=emo_cond_cfg.get("linear_units", 1024),
attention_heads=emo_cond_cfg.get("attention_heads", 4),
num_blocks=emo_cond_cfg.get("num_blocks", 4),
input_layer=emo_cond_cfg.get("input_layer", "conv2d2"),
)
self.emo_perceiver_encoder = PerceiverResampler(
1024,
dim_context=emo_cond_cfg.get("output_size", 512),
ff_mult=emo_cond_cfg.get("perceiver_mult", 2),
heads=emo_cond_cfg.get("attention_heads", 4),
num_latents=1,
)
# Padding masks for perceiver cross-attention
if self.conditioning_policy.uses_conformer_perceiver:
self.cond_mask_pad = nn.ConstantPad1d((self.condition_num_latent, 0), True)
self.emo_cond_mask_pad = nn.ConstantPad1d((1, 0), True)
# --- Lazy-loaded external models ---
self._w2v_stat: torch.Tensor | None = None
self._emo_matrix: torch.Tensor | None = None
self._spk_matrix: torch.Tensor | None = None
self._text_tokenizer: Any = None
# Initialize embeddings per GPT-2 convention
for emb in [self.text_embedding, self.mel_embedding]:
emb.weight.data.normal_(mean=0.0, std=0.02)
if hasattr(self, "speed_emb"):
self.speed_emb.weight.data.normal_(mean=0.0, std=0.0)
# ------------------------------------------------------------------
# vLLM required hooks
# ------------------------------------------------------------------
def embed_input_ids(self, input_ids: torch.Tensor, **_: Any) -> torch.Tensor:
return self.mel_embedding(input_ids)
def _extract_mel_offsets(self, positions: torch.Tensor, kwargs: dict[str, Any]) -> torch.Tensor:
"""Build per-token mel_start_offset from model_intermediate_buffer."""
info_dicts = kwargs.get("model_intermediate_buffer") or kwargs.get("runtime_additional_information") or []
if "runtime_additional_information" in kwargs and "model_intermediate_buffer" not in kwargs:
logger.warning_once("runtime_additional_information is deprecated, use model_intermediate_buffer")
if not info_dicts:
logger.warning_once("[OFFSET] no model_intermediate_buffer available; defaulting mel offsets to 0")
return torch.zeros_like(positions)
offsets = []
for info in info_dicts:
if isinstance(info, dict):
meta = info.get("meta", {})
val = meta.get("mel_start_offset", 0)
offsets.append(int(val) if not isinstance(val, int) else val)
else:
offsets.append(0)
return torch.tensor(offsets, dtype=positions.dtype, device=positions.device)
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
**kwargs: Any,
) -> torch.Tensor | IntermediateTensors:
"""AR transformer forward.
During prefill: inputs_embeds is set by preprocess(), input_ids ignored.
During decode: inputs_embeds is mel_embedding(input_ids) + mel_pos.
"""
if get_pp_group().is_first_rank:
if inputs_embeds is None:
logger.warning("[FORWARD] inputs_embeds=None — fallback decode path")
mel_start_offset = self._extract_mel_offsets(positions, kwargs)
mel_pos = positions - mel_start_offset
mel_pos = torch.clamp(mel_pos, min=0)
inputs_embeds = self.mel_embedding(input_ids) + self.mel_pos_embedding.emb(mel_pos)
hidden_states = inputs_embeds
else:
assert intermediate_tensors is not None
hidden_states = intermediate_tensors["hidden_states"]
for layer in self.h[self.start_layer : self.end_layer]:
hidden_states = layer(hidden_states)
if not get_pp_group().is_last_rank:
return IntermediateTensors({"hidden_states": hidden_states})
return self.final_norm(self.ln_f(hidden_states))
def compute_logits(
self,
hidden_states: torch.Tensor | OmniOutput,
sampling_metadata: Any = None,
) -> torch.Tensor | None:
if isinstance(hidden_states, OmniOutput):
hidden_states = hidden_states.text_hidden_states
if hidden_states is None or self.mel_head is None:
return None
# `LogitsProcessor` never reads `lm_head.bias`: the unquantized apply()
# path is `F.linear(x, layer.weight, bias)` where `bias` comes from the
# `embedding_bias` argument. IndexTTS's mel head is `Linear(..., bias=True)`
# upstream, so the bias has to be passed explicitly or logits silently
# drop it.
return self.logits_processor(
self.mel_head,
hidden_states,
embedding_bias=getattr(self.mel_head, "bias", None),
)
# ------------------------------------------------------------------
# Omni multimodal output plumbing
# ------------------------------------------------------------------
def make_omni_output(self, model_outputs: torch.Tensor | OmniOutput, **kwargs: Any) -> OmniOutput:
"""Build the per-step Stage 1 payload from the intermediate buffer."""
if isinstance(model_outputs, OmniOutput):
return model_outputs
hidden = model_outputs
info_dicts = kwargs.get("model_intermediate_buffer")
if info_dicts is None:
info_dicts = kwargs.get("runtime_additional_information") or []
if "runtime_additional_information" in kwargs and "model_intermediate_buffer" not in kwargs:
logger.warning_once("runtime_additional_information is deprecated, use model_intermediate_buffer")
device = hidden.device
latent_dim = hidden.shape[-1]
_zero_codes = torch.zeros(0, 1, dtype=torch.long, device=device) if self.use_gpt_latent else None
_zero_latent = torch.zeros(0, latent_dim, dtype=hidden.dtype, device=device) if self.use_gpt_latent else None
# Per-request lists: to_payload_element dispatches lists via
# element[idx], giving correct per-request mapping even during
# mixed prefill+decode steps. Zero-length placeholders for prefill
# requests are harmless — torch.cat ignores them in accumulation.
#
# Latent-mode exports are appendable deltas for the existing
# full-payload accumulator: codes.mel is a 2-D row [1, 1], while
# hidden_states.latent is the aligned one-row [1, D] slice from
# meta.latent_acc. No-latent mode only uses meta_list.
mel_codes_list: list[torch.Tensor] = []
latent_list: list[torch.Tensor] = []
meta_list: list[dict[str, Any]] = []
for info in info_dicts:
if not isinstance(info, dict):
if self.use_gpt_latent:
assert _zero_codes is not None
mel_codes_list.append(_zero_codes)
assert _zero_latent is not None
latent_list.append(_zero_latent)
meta_list.append({})
continue
info_meta = info.get("meta", {})
# mel_len now comes from the int counter in meta, not from
# code_seq.shape[0] (codes.mel is a single-token delta).
mel_len = int(info_meta.get("mel_code_count", 0))
if self.use_gpt_latent:
codes = info.get("codes", {})
mc = codes.get("mel")
if isinstance(mc, torch.Tensor) and mc.numel() > 0 and mel_len > 0:
code_seq = mc.reshape(-1)
mel_codes_list.append(code_seq[-1:].reshape(1, 1).contiguous())
else:
assert _zero_codes is not None
mel_codes_list.append(_zero_codes)
lat_acc = info_meta.get("latent_acc")
if isinstance(lat_acc, torch.Tensor) and lat_acc.numel() > 0 and mel_len > 0:
lat_seq = lat_acc.reshape(-1, lat_acc.shape[-1]) if lat_acc.ndim != 2 else lat_acc
idx = min(max(mel_len - 1, 0), int(lat_seq.shape[0]) - 1)
latent_list.append(lat_seq[idx : idx + 1].contiguous())
else:
hs = info.get("hidden_states", {})
lat = hs.get("latent")
if isinstance(lat, torch.Tensor) and lat.numel() > 0:
lat_seq = lat.reshape(-1, lat.shape[-1]) if lat.ndim != 2 else lat
latent_list.append(lat_seq[-1:].contiguous())
else:
assert _zero_latent is not None
latent_list.append(_zero_latent)
# Scalar metadata is kept with REPLACE semantics by the generic
# full-payload accumulator. Emit the policy on every step so the
# final payload contract never depends on which decode row carried
# the one-time conditioning tensors.
req_meta: dict[str, Any] = {"use_gpt_latent": self.use_gpt_latent}
if mel_len == 1:
s_ref = info_meta.get("S_ref")
ref_mel = info_meta.get("ref_mel")
style = info_meta.get("style")
req_meta["duration_factor"] = info_meta.get("duration_factor", 1.0)
if isinstance(s_ref, torch.Tensor):
req_meta["S_ref"] = s_ref
if isinstance(ref_mel, torch.Tensor):
req_meta["ref_mel"] = ref_mel
if isinstance(style, torch.Tensor):
req_meta["style"] = style
meta_list.append(req_meta)
if self.use_gpt_latent:
if not any(t.numel() > 0 for t in mel_codes_list):
return OmniOutput(text_hidden_states=hidden, multimodal_outputs={})
mm: OmniPayload = {
"codes": {"mel": mel_codes_list},
}
mm["hidden_states"] = {"latent": latent_list}
else:
# vLLM already materializes sampled token IDs on CPU for scheduler
# bookkeeping and stop-token handling. Stage 1 consumes the complete
# sequence only after request completion, so retain the conditioning
# metadata once and read the finished request.output_token_ids there
# instead of snapshotting a duplicate GPU code row every step.
if not any(meta_list):
return OmniOutput(text_hidden_states=hidden, multimodal_outputs={})
mm = {}
if any(meta_list):
mm["meta"] = meta_list
return OmniOutput(text_hidden_states=hidden, multimodal_outputs=mm)
# ------------------------------------------------------------------
# preprocess / postprocess
# ------------------------------------------------------------------
def preprocess(
self,
input_ids: torch.Tensor,
input_embeds: torch.Tensor | None,
**info_dict: Any,
) -> tuple[torch.Tensor, torch.Tensor, dict[str, Any]]:
"""Build prompt embeddings for prefill; compute mel embeddings for decode.
IndexTTS 2 prefill uses 32 speaker latents plus two duration tokens.
IndexTTS 2.5 uses one CAMPPlus speaker token plus two zero tokens.
Decode: ``mel_embedding(token) + mel_pos_embedding(step)``.
"""
span_len = int(input_ids.shape[0])
meta = info_dict.get("meta", {})
# --- Decode path: span_len==1 and mel_start_offset already set ---
if span_len == 1 and isinstance(meta, dict) and meta.get("mel_start_offset") is not None:
return self._preprocess_decode(input_ids, info_dict, meta)
# --- Prefill path ---
return self._preprocess_prefill(input_ids, input_embeds, info_dict, span_len)
def _preprocess_decode(
self,
input_ids: torch.Tensor,
info_dict: dict[str, Any],
meta: dict[str, Any],
) -> tuple[torch.Tensor, torch.Tensor, dict[str, Any]]:
"""Lightweight decode step: embed token + track code count."""
# Track code count via int counter instead of accumulating a full tensor
# (the connector reconstructs the full sequence from per-step deltas).
prev_count = meta.get("mel_code_count", 0)
if not isinstance(prev_count, int):
prev_count = int(prev_count)
decode_step = prev_count + 1
# Only keep the current token; no O(T) history accumulation.
new_codes = input_ids.detach()
mel_pos = input_ids.new_full((1,), decode_step)
embeds = self.mel_embedding(input_ids) + self.mel_pos_embedding.emb(mel_pos)
return (
input_ids,
embeds,
{
"codes": {"mel": new_codes},
"meta": {"mel_code_count": decode_step},
},
)
def _preprocess_prefill(
self,
input_ids: torch.Tensor,
input_embeds: torch.Tensor | None,
info_dict: dict[str, Any],
span_len: int,
) -> tuple[torch.Tensor, torch.Tensor, dict[str, Any]]:
"""Full conditioning pipeline — runs once during prefill."""
additional_information = info_dict.get("additional_information")
if isinstance(additional_information, dict):
merged: dict[str, Any] = {k: v for k, v in info_dict.items() if k != "additional_information"}
for k, v in additional_information.items():
merged.setdefault(k, v)
info_dict = merged
device = input_ids.device
def _first(key, default=None):
v = info_dict.get(key)
return v[0] if isinstance(v, list) and v else default
duration_factor = float(_first("duration_factor", 1.0))
# Direct Omni callers bypass the serving adapter, so retain the
# model-side guard as the final fail-fast validation layer.
if not (INDEXTTS25_MIN_DURATION_FACTOR <= duration_factor <= INDEXTTS25_MAX_DURATION_FACTOR):
raise ValueError("IndexTTS 2.5 duration_factor must be between 0.5 and 2.0")
text = _first("text")
if not text:
raise ValueError("Missing additional_information.text for IndexTTS2 talker.")
voice_path = _first("voice")
if voice_path is None:
raise ValueError("IndexTTS2 requires voice (reference audio path) in additional_information.")
emo_audio_path = _first("emo_audio")
emo_text = _first("emo_text")
use_emo_text = bool(_first("use_emo_text", False))
emo_vector = _first("emo_vector")
emo_alpha = float(_first("emo_alpha", 1.0))
use_random = bool(_first("use_random", False))
lang = str(_first("lang", "zh"))
text_normalization = bool(_first("text_normalization", True))
_raw_emo_voice = _first("emo_voice_name")
emo_voice_name = str(_raw_emo_voice).strip().lower() if _raw_emo_voice else None
# --- Speaker cache lookup ---
_voice_name = _first("voice_name")
_voice_created_at = int(_first("voice_created_at", 0))
_speaker_cache_key = None
_cached = None
if _voice_name:
_speaker_cache_key = self._speaker_cache.make_cache_key(_voice_name, "indextts2", _voice_created_at)
_cached = self._speaker_cache.get(_speaker_cache_key)
# --- Load audio and extract features ---
# Keep the speaker-cache hit path truly lazy: cached speaker artifacts are
# sufficient for conditioning and same-speaker emotion reuse, so avoid
# loading/resampling reference audio and touching the Wav2Vec2 singleton
# unless a miss (or an uncached separate emotion audio) actually needs it.
wav_16k: torch.Tensor | None = None
w2v_model = None
w2v_proc = None
if _cached is not None:
s_ref = _cached["S_ref"].to(device)
style = _cached["style"].to(device)
ref_mel = _cached["ref_mel"].to(device)
spk_cond_emb = _cached["spk_cond_emb"].to(device)
else:
wav_16k, wav_22k = load_reference_audio(voice_path, device, mode="speaker")
w2v_model, w2v_proc = load_wav2vec2(self.model_path, device)
self._ensure_w2v_stat_loaded(device)
spk_cond_emb = wav2vec_extract(wav_16k, w2v_model, w2v_proc, device, self._w2v_stat)
if self.conditioning_policy.uses_campplus_projection:
# Official 2.5 keeps W2V-BERT features as the reference
# semantic sequence; EnhancedCodec is only used for generated
# semantic codes in Stage 1.
s_ref = spk_cond_emb
else:
semantic_codec = load_semantic_codec(
self.model_path,
self.config.semantic_codec,
device,
)
with torch.no_grad():
_, s_ref = semantic_codec.quantize(spk_cond_emb)
campplus = load_campplus(self.model_path, device)
fbank = compute_fbank(wav_16k, device)
with torch.no_grad():
style = campplus(fbank) # [1, 192]
ref_mel = self._compute_mel_22k(wav_22k, device) # [1, 80, T_ref]
if _speaker_cache_key is not None:
self._speaker_cache.put(
_speaker_cache_key,
{
"S_ref": s_ref.detach().cpu().contiguous(),
"style": style.detach().cpu().contiguous(),
"ref_mel": ref_mel.detach().cpu().contiguous(),
"spk_cond_emb": spk_cond_emb.detach().cpu().contiguous(),
},
)
if self.conditioning_policy.uses_campplus_projection:
model_dtype = self.spk_emb_proj.weight.dtype
else:
model_dtype = next(self.conditioning_encoder.parameters()).dtype
spk_cond_emb = spk_cond_emb.to(device=device, dtype=model_dtype)
if self.conditioning_policy.uses_conformer_perceiver:
spk_lens = torch.tensor([spk_cond_emb.shape[1]], device=device, dtype=torch.long)
speech_cond, mask = self.conditioning_encoder(spk_cond_emb, spk_lens)
conds_mask = self.cond_mask_pad(mask.squeeze(1))
conds = self.perceiver_encoder(speech_cond, conds_mask)
emo_vec = self._compute_emotion_vector(
wav_16k=wav_16k,
speaker_audio_path=voice_path,
emo_audio_path=emo_audio_path,
emo_voice_name=emo_voice_name,
main_text=text,
use_emo_text=use_emo_text,
emo_text=emo_text,
emo_vector=emo_vector,
emo_alpha=emo_alpha,
use_random=use_random,
style=style,
spk_cond_emb=spk_cond_emb,
w2v_model=w2v_model,
w2v_proc=w2v_proc,
device=device,
)
if self.conditioning_policy.uses_campplus_projection:
speaker_token = self.spk_emb_proj(style.to(dtype=model_dtype)).unsqueeze(1)
zero_tokens = torch.zeros(
speaker_token.shape[0],
2,
self.model_dim,
device=device,
dtype=speaker_token.dtype,
)
conds_prefix = torch.cat(
[speaker_token + emo_vec.unsqueeze(1), zero_tokens],
dim=1,
)
else:
speed_zero = torch.zeros(1, device=device, dtype=torch.long)
speed_one = torch.ones(1, device=device, dtype=torch.long)
duration_emb = self.speed_emb(speed_zero)
duration_emb_half = self.speed_emb(speed_one)
conds_with_emo = conds + emo_vec.unsqueeze(1)
conds_prefix = torch.cat(
[conds_with_emo, duration_emb_half.unsqueeze(1), duration_emb.unsqueeze(1)],
dim=1,
)
text_tokens, lang_id = self._tokenize_text(
text,
device,
lang=lang,
text_normalization=text_normalization,
)
text_emb = self.text_embedding(text_tokens) + self.text_pos_embedding(text_tokens)
if lang_id is not None:
text_emb = text_emb + self.lang_embedding(lang_id).unsqueeze(1)
start_mel = self.mel_embedding(torch.tensor([[self.start_mel_token]], device=device, dtype=torch.long))
start_mel_pos = self.mel_pos_embedding.get_fixed_embedding(0, device)
start_mel = start_mel + start_mel_pos # [1, 1, D]
inputs_embeds = torch.cat([conds_prefix, text_emb, start_mel], dim=1).squeeze(0)
mel_start_offset = int(conds_prefix.shape[1]) + int(text_emb.shape[1])
prefill_mel_index = int(inputs_embeds.shape[0]) - 1
_require_exact_indextts_prefill_span(
actual_span_len=int(inputs_embeds.shape[0]),
scheduled_span_len=span_len,
model_type=str(getattr(self.config, "model_type", "indextts2")),
lang=lang,
text_token_count=int(text_tokens.shape[1]),
)
input_ids_out = torch.full_like(input_ids, self.start_mel_token)
info_update: dict[str, Any] = {
"meta": {
"mel_start_offset": mel_start_offset,
"prefill_mel_index": prefill_mel_index,
"mel_code_count": 0,
"S_ref": s_ref.cpu().contiguous(),
"ref_mel": ref_mel.cpu().contiguous(),
"style": style.cpu().contiguous(),
"use_gpt_latent": self.use_gpt_latent,
"duration_factor": duration_factor,
},
"codes": {"mel": torch.zeros(0, dtype=torch.long, device=device)},
}
if self.use_gpt_latent:
info_update["meta"]["latent_acc"] = torch.zeros(
0,
self.model_dim,
dtype=inputs_embeds.dtype,
device=device,
)
info_update["hidden_states"] = {
"latent": torch.zeros(
0,
self.model_dim,
dtype=inputs_embeds.dtype,
device=device,
)
}
return input_ids_out, inputs_embeds, info_update
def postprocess(
self,
hidden_states: torch.Tensor,
multimodal_outputs: Any = None,
**kwargs: Any,
) -> dict[str, Any]:
"""Store current decode-step hidden state for Stage 1.
Only the current row is kept; the connector's full-payload accumulator
reconstructs the complete latent sequence from per-step deltas emitted
by make_omni_output.
"""
if not self.use_gpt_latent:
return {}
if hidden_states.shape[0] != 1:
meta = kwargs.get("meta", {})
prefill_mel_index = int(meta.get("prefill_mel_index", hidden_states.shape[0] - 1))
prefill_mel_index = min(max(prefill_mel_index, 0), hidden_states.shape[0] - 1)
return {"meta": {"latent_acc": hidden_states[prefill_mel_index : prefill_mel_index + 1].detach()}}
return {"meta": {"latent_acc": hidden_states.detach()}}
def _compute_mel_22k(self, wav_22k: torch.Tensor, device: torch.device) -> torch.Tensor:
"""Compute 80-band mel spectrogram at 22.05kHz for Stage 1."""
s2mel_cfg = self.config.s2mel.get("preprocess_params", {})
sr = s2mel_cfg.get("sr", 22050)
spect = s2mel_cfg.get("spect_params", {})
n_fft = spect.get("n_fft", 1024)
hop_length = spect.get("hop_length", 256)
win_length = spect.get("win_length", 1024)
n_mels = spect.get("n_mels", 80)
# mel_spectrogram is pure torch with per-device caches — run on the
# wav's device (GPU after load_reference_audio) instead of CPU.
wav = wav_22k.float()
if wav.ndim == 1:
wav = wav.unsqueeze(0)
from .s2mel.modules.audio import mel_spectrogram as mel_fn
mel = mel_fn(wav, n_fft, n_mels, sr, hop_length, win_length, 0, None)
return mel.to(device=device) # [1, 80, T]
# ------------------------------------------------------------------
# Emotion conditioning
# ------------------------------------------------------------------
def _compute_emotion_vector(
self,
*,
wav_16k: torch.Tensor | None,
speaker_audio_path: Any,
emo_audio_path: str | None,
emo_voice_name: str | None,
main_text: str,
use_emo_text: bool,
emo_text: str | None,
emo_vector: Any,
emo_alpha: float,
use_random: bool,
style: torch.Tensor,
spk_cond_emb: torch.Tensor,
w2v_model: Any,
w2v_proc: Any,
device: torch.device,
) -> torch.Tensor:
"""Full emotion control logic aligned with official infer_v2.py. Returns [1, D]."""
# --- Mutual exclusion: text/vector guidance clears emotion reference audio ---
# Mirrors official infer_v2.py: use_emo_text is evaluated before explicit
# vectors, so text guidance wins if both are supplied by an internal caller.
if use_emo_text:
if emo_text is None:
emo_text = main_text
emo_vector = self._predict_emotion_from_text(emo_text, device)
if emo_vector is None:
raise RuntimeError("IndexTTS use_emo_text requested but QwenEmotion could not be loaded")
if use_emo_text or emo_vector is not None:
emo_audio_path = None
# --- Validate emo_vector (user-provided or text-predicted) ---
if emo_vector is not None:
if isinstance(emo_vector, torch.Tensor):
emo_vector = emo_vector.tolist()
elif isinstance(emo_vector, np.ndarray):
emo_vector = emo_vector.tolist()
if not isinstance(emo_vector, list) or len(emo_vector) != len(self._DESIRED_ORDER):
raise ValueError(
"IndexTTS2 emo_vector must contain 8 values in order: "
"happy, angry, sad, afraid, disgusted, melancholic, surprised, calm"
)
if any(isinstance(v, bool) for v in emo_vector):
raise ValueError("IndexTTS2 emo_vector values must be numeric")
try:
emo_vector = [float(v) for v in emo_vector]
except (TypeError, ValueError) as exc:
raise ValueError("IndexTTS2 emo_vector values must be numeric") from exc
if not all(math.isfinite(v) for v in emo_vector):
raise ValueError("IndexTTS2 emo_vector values must be finite")
if not all(0.0 <= v <= 1.2 for v in emo_vector):
raise ValueError("IndexTTS2 emo_vector values must be between 0.0 and 1.2")
# Alpha pre-scaling (official infer_v2.py:414-418):
# when emo_alpha != 1.0, scale emo_vector down before overlay.
emo_vector_scale = max(0.0, min(1.0, emo_alpha))
if emo_vector_scale != 1.0:
emo_vector = [int(x * emo_vector_scale * 10000) / 10000 for x in emo_vector]
# --- Path B: emo_vector (8-dim) → spk_matrix cosine lookup ---
emovec_mat = None
weight_sum = 0.0
if emo_vector is not None:
weight_sum = sum(emo_vector)
vec = torch.tensor(emo_vector, device=device, dtype=torch.float32)
emovec_mat = self._compute_emo_from_distribution(vec, style, device, use_random=use_random)
# --- Audio-based emotion with alpha blending ---
if emo_audio_path is None:
# No separate emotion audio: reuse speaker w2v-bert features instead of
# reloading + re-extracting the same audio (saves ~0.5s per prefill).
# Official code resamples orig→16k directly for emotion vs orig→22k→16k
# for speaker; the resulting w2v features are near-identical.
effective_alpha = 1.0
emo_cond_emb = spk_cond_emb
else:
effective_alpha = emo_alpha
# emotion audio cache (enabled when emo_voice_name is provided)
_emo_cache_key = None
_emo_cached = None
if emo_voice_name:
_emo_cache_key = self._speaker_cache.make_cache_key(emo_voice_name, "indextts2_emo", 0)
_emo_cached = self._speaker_cache.get(_emo_cache_key)
if _emo_cached is not None:
emo_cond_emb = _emo_cached["spk_cond_emb"].to(device)
logger.debug("[EMO] cache HIT for %s", emo_voice_name)
else:
emo_wav_16k, _ = load_reference_audio(emo_audio_path, device, mode="emotion")
if w2v_model is None or w2v_proc is None:
w2v_model, w2v_proc = load_wav2vec2(self.model_path, device)
self._ensure_w2v_stat_loaded(device)
emo_cond_emb = wav2vec_extract(emo_wav_16k, w2v_model, w2v_proc, device, self._w2v_stat)
if _emo_cache_key is not None:
self._speaker_cache.put(
_emo_cache_key,
{
"spk_cond_emb": emo_cond_emb.detach().cpu().contiguous(),
},
)
logger.debug("[EMO] cache MISS, cached for %s", emo_voice_name)
emovec = self._merge_emovec(spk_cond_emb, emo_cond_emb, effective_alpha)
# --- emo_vector overlay (official infer_v2.py:560-561) ---
# emovec_mat is raw feat2.pt weighted sum [1, model_dim], NOT projected.
# Mix: emovec_mat contributes the emotion prototype; (1-sum)*emovec
# preserves the audio-based emotion as a "base color".
if emovec_mat is not None:
emovec = emovec_mat.to(dtype=emovec.dtype) + (1.0 - weight_sum) * emovec
return emovec
_MELANCHOLIC_WORDS = frozenset(
{
"低落",
"melancholy",
"melancholic",
"depression",
"depressed",
"gloomy",
}
)
_DESIRED_ORDER = ["高兴", "愤怒", "悲伤", "恐惧", "反感", "低落", "惊讶", "自然"]
def _predict_emotion_from_text(self, text: str, device: torch.device) -> list[float] | None:
"""Use QwenEmotion CausalLM to predict 8-dim emotion vector from text (aligned with official)."""
model, tokenizer = load_qwen_emotion(
self.model_path,
device,
trust_remote_code=self.vllm_config.model_config.trust_remote_code,
)
if model is None or tokenizer is None:
return None
messages = [
{"role": "system", "content": "文本情感分类"},
{"role": "user", "content": text},
]
prompt_text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
enable_thinking=False,
)
model_inputs = tokenizer([prompt_text], return_tensors="pt").to(model.device)
with torch.no_grad():
generated_ids = model.generate(
**model_inputs,
max_new_tokens=512,
pad_token_id=tokenizer.eos_token_id,
)
output_ids = generated_ids[0][len(model_inputs.input_ids[0]) :].tolist()
try:
index = len(output_ids) - output_ids[::-1].index(151668)
except ValueError:
index = 0
content_str = tokenizer.decode(output_ids[index:], skip_special_tokens=True)
try:
content = json.loads(content_str)
except json.JSONDecodeError:
content = {
m.group(1): float(m.group(2)) for m in re.finditer(r'([^\s":.,]+?)"?\s*:\s*([\d.]+)', content_str)
}
text_lower = text.lower()
if any(w in text_lower for w in self._MELANCHOLIC_WORDS):
content["悲伤"], content["低落"] = content.get("低落", 0.0), content.get("悲伤", 0.0)
max_score, min_score = 1.2, 0.0
emo_vector = []
for cn_key in self._DESIRED_ORDER:
val = content.get(cn_key, 0.0)
if isinstance(val, str):
try:
val = float(val)
except ValueError:
val = 0.0
emo_vector.append(max(min_score, min(max_score, val)))
if all(v <= 0.0 for v in emo_vector):
emo_vector[7] = 1.0 # default calm
return emo_vector
def _compute_emo_from_distribution(
self,
emo_dist: torch.Tensor,
style: torch.Tensor,
device: torch.device,
use_random: bool = False,
) -> torch.Tensor:
"""From 8-dim emotion distribution + CAMPPlus style, build emovec_mat via spk_matrix cosine lookup.
Returns [1, model_dim] (feat2.pt rows are already model_dim).
"""
self._ensure_matrix_loaded("_emo_matrix", "emo_matrix", "Emotion matrix")
self._ensure_matrix_loaded("_spk_matrix", "spk_matrix", "Speaker matrix")
if self._emo_matrix is None or self._spk_matrix is None:
raise RuntimeError("IndexTTS emotion control requires both emotion and speaker matrices")
weight_vector = emo_dist.to(device=device, dtype=torch.float32)
if weight_vector.ndim == 2:
weight_vector = weight_vector.squeeze(0) # [8]
if use_random:
indices = [int(torch.randint(0, sub.shape[0], (1,), device=device).item()) for sub in self._spk_matrix]
else:
indices = [_find_most_similar_cosine(style, sub.to(device)) for sub in self._spk_matrix]
emo_rows = torch.cat(
[sub.to(device)[idx].unsqueeze(0) for idx, sub in zip(indices, self._emo_matrix)], dim=0
) # [8, model_dim]
return (weight_vector.unsqueeze(1) * emo_rows).sum(0).unsqueeze(0) # [1, model_dim]
def _compute_audio_emo_vec(self, cond_emb: torch.Tensor) -> torch.Tensor:
"""Conformer → Perceiver → emovec_layer → emo_layer. Input: [1, T, 1024], output: [1, D]."""
emo_dtype = next(self.emo_conditioning_encoder.parameters()).dtype
cond_emb = cond_emb.to(device=cond_emb.device, dtype=emo_dtype)
cond_lens = torch.tensor([cond_emb.shape[1]], device=cond_emb.device, dtype=torch.long)
emo_cond, emo_mask = self.emo_conditioning_encoder(cond_emb, cond_lens)
emo_conds_mask = self.emo_cond_mask_pad(emo_mask.squeeze(1))
emo_percept = self.emo_perceiver_encoder(emo_cond, emo_conds_mask) # [1, 1, 1024]
emo_vec_syn = self.emovec_layer(emo_percept.squeeze(1)) # [1, D]
return self.emo_layer(emo_vec_syn) # [1, D]
def _merge_emovec(
self,
spk_cond_emb: torch.Tensor,
emo_cond_emb: torch.Tensor,
alpha: float,
) -> torch.Tensor:
"""Alpha-blend speaker and emotion audio vectors: base + alpha * (emo - base)."""
if emo_cond_emb is spk_cond_emb or alpha == 1.0:
return self._compute_audio_emo_vec(emo_cond_emb)
if alpha == 0.0:
return self._compute_audio_emo_vec(spk_cond_emb)
base_vec = self._compute_audio_emo_vec(spk_cond_emb)
emo_vec = self._compute_audio_emo_vec(emo_cond_emb)
return base_vec + alpha * (emo_vec - base_vec) # [1, D]
# ------------------------------------------------------------------
# Text tokenizer
# ------------------------------------------------------------------
def _tokenize_text(
self,
text: str,
device: torch.device,
*,
lang: str = "zh",
text_normalization: bool = True,
) -> tuple[torch.Tensor, torch.Tensor | None]:
"""Tokenize text and add the checkpoint start/stop text tokens."""
start_text = 0
stop_text = 1
lang_id: int | None = None
if self.conditioning_policy.uses_language_embedding:
from .text_processing_v2_5 import prepare_indextts25_text
token_ids, lang_id = prepare_indextts25_text(
text,
lang=lang,
model_dir=self.model_path,
tokenizer_file=self.config.tokenizer_file,
text_normalization=text_normalization,
)
token_ids = [token_id for token_id in token_ids if token_id not in {start_text, stop_text}]
else:
if self._text_tokenizer is None:
from .tokenizer import IndexTTS2Tokenizer
bpe_path = resolve_model_file(self.model_path, "bpe.model")
if bpe_path is None:
raise FileNotFoundError(f"BPE model not found in {self.model_path}")
self._text_tokenizer = IndexTTS2Tokenizer(bpe_path, model_dir=self.model_path)
token_ids = self._text_tokenizer.encode(text, add_special_tokens=False)
# The 2.5 tokenizer begins with the explicit ``<|lang|>`` token.
# Official prepare_gpt_inputs filters any existing wrapper tokens,
# then adds start_text=0 and stop_text=1 around that tokenizer output.
token_ids = [start_text] + token_ids + [stop_text]
token_tensor = torch.tensor([token_ids], device=device, dtype=torch.long)
lang_tensor = torch.tensor([lang_id], device=device, dtype=torch.long) if lang_id is not None else None
return token_tensor, lang_tensor
# ------------------------------------------------------------------
# Lazy loading helpers
# ------------------------------------------------------------------
def _ensure_w2v_stat_loaded(self, device: torch.device) -> None:
if self._w2v_stat is not None:
return
stat_path = resolve_model_file(self.model_path, self.config.w2v_stat)
if stat_path is not None:
raw = torch.load(stat_path, map_location="cpu", weights_only=True)
if isinstance(raw, dict):
self._w2v_stat = torch.stack([raw["mean"], raw["var"]]) # [2, 1024]
else:
self._w2v_stat = raw
else:
raise FileNotFoundError(f"Wav2Vec2-BERT stat file not found in {self.model_path!r}")
def _ensure_matrix_loaded(self, attr: str, cfg_key: str, label: str) -> None:
if getattr(self, attr) is not None:
return
path = resolve_model_file(self.model_path, getattr(self.config, cfg_key))
if path is not None:
raw = torch.load(path, map_location="cpu", weights_only=True)
setattr(self, attr, torch.split(raw, list(self.config.emo_num)))
else:
raise FileNotFoundError(f"{label} not found in {self.model_path!r}")
# ------------------------------------------------------------------
# Weight loading
# ------------------------------------------------------------------
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Load weights from IndexTTS2 checkpoint (gpt.pth).
Bypasses vllm's default .pt iterator because the model directory
contains raw-tensor files (feat1.pt, feat2.pt) that are not state
dicts and would crash ``pt_weights_iterator``.
Weight mapping from gpt.pth checkpoint → vLLM model params:
gpt.h.{i}.* → h.{i}.* (strip ``gpt.`` prefix)
gpt.ln_f.* → ln_f.*
final_norm.* → final_norm.*
text_head.* → (skipped)
<everything else> → identity
"""
_ = weights # don't iterate — raw .pt files crash the default iterator
ckpt_path = resolve_model_file(self.model_path, self.config.gpt_checkpoint)
if ckpt_path is None:
raise FileNotFoundError(f"IndexTTS2 GPT checkpoint {self.config.gpt_checkpoint!r} not found")
state = torch.load(ckpt_path, map_location="cpu", weights_only=True)
prefix_map = {
"gpt.h.": "h.",
"gpt.ln_f.": "ln_f.",
"final_norm.": "final_norm.",
"text_head.": "_skip_text_head.",
}
params_dict = dict(self.named_parameters(remove_duplicate=False))
loaded_params: set[str] = set()
for checkpoint_name, loaded_weight in state.items():
name = _normalize_indextts_checkpoint_key(checkpoint_name)
# Skip attention masks
if ".attn.bias" in name or ".attn.masked_bias" in name:
continue
# Skip wpe (null position embeddings)
if ".wpe." in name or ".wte." in name:
continue
# Remap checkpoint name to model name
mapped_name = name
for old_prefix, new_prefix in prefix_map.items():
if name.startswith(old_prefix):
mapped_name = new_prefix + name[len(old_prefix) :]
break
# Skip unmapped / intentionally skipped weights
if mapped_name.startswith("_skip_"):
continue
if is_pp_missing_parameter(mapped_name, self):
continue
if mapped_name not in params_dict:
logger.debug(
"Skipping unrecognized weight: %s → %s",
checkpoint_name,
mapped_name,
)
continue
param = params_dict[mapped_name]
# GPT-2 Conv1D → Linear transpose
for conv1d_name in ["c_attn", "c_proj", "c_fc"]:
if conv1d_name not in mapped_name:
continue
if not mapped_name.endswith(".weight"):
continue
loaded_weight = loaded_weight.t()
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(mapped_name)
logger.info(
"Loaded %d weights for IndexTTS2TalkerForConditionalGeneration",
len(loaded_params),
)
required_prefixes = [
"text_embedding.",
"mel_embedding.",
"text_pos_embedding.",
"mel_pos_embedding.",
"emo_layer.",
"emovec_layer.",
"emo_conditioning_encoder.",
"emo_perceiver_encoder.",
"h.",
"ln_f.",
"final_norm.",
]
if self.conditioning_policy.uses_campplus_projection:
required_prefixes.extend(
[
"spk_emb_proj.",
"lang_embedding.",
]
)
else:
required_prefixes.extend(
[
"speed_emb.",
"conditioning_encoder.",
"perceiver_encoder.",
]
)
if self.mel_head is not None:
required_prefixes.append("mel_head.")
missing_prefixes = [
prefix for prefix in required_prefixes if not any(name.startswith(prefix) for name in loaded_params)
]
if missing_prefixes:
raise RuntimeError(
f"{self.config.model_type} GPT checkpoint did not load required parameter groups: "
+ ", ".join(missing_prefixes)
)
# Ensure all sub-modules on correct device (some nn.Parameter created
# with torch.Tensor() end up on CPU even when model is on CUDA).
target_device = next((p.device for p in self.h.parameters()), torch.device("cpu"))
modules = [
self.emo_conditioning_encoder,
self.emo_perceiver_encoder,
]
if self.conditioning_policy.uses_campplus_projection:
modules.extend([self.spk_emb_proj, self.lang_embedding])
else:
modules.extend([self.conditioning_encoder, self.perceiver_encoder])
for mod in modules:
mod.to(target_device)
self._warmup_external_models(target_device)
return loaded_params
def _warmup_external_models(self, device: torch.device) -> None:
"""Preload external models to eliminate first-request latency."""
try:
load_wav2vec2(self.model_path, device)
if not self.conditioning_policy.uses_campplus_projection:
load_semantic_codec(
self.model_path,
self.config.semantic_codec,
device,
)
load_campplus(self.model_path, device)
self._ensure_w2v_stat_loaded(device)
logger.info(
"External models preloaded: Wav2Vec2, %sCAMPPlus",
"RepCodec, " if not self.conditioning_policy.uses_campplus_projection else "",
)
except Exception as e:
raise RuntimeError("Failed to preload required IndexTTS Stage 0 models") from e