windextts 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- windextts/__init__.py +3 -0
- windextts/__main__.py +5 -0
- windextts/attention.py +154 -0
- windextts/cli.py +188 -0
- windextts/config.py +326 -0
- windextts/data/mel_basis_hifigan.pt +0 -0
- windextts/data/seamless_frontend.npz +0 -0
- windextts/download.py +199 -0
- windextts/frontend/__init__.py +1 -0
- windextts/frontend/audio_utils.py +236 -0
- windextts/frontend/mel.py +158 -0
- windextts/frontend/normalizer.py +417 -0
- windextts/frontend/segmenter.py +158 -0
- windextts/frontend/tokenizer.py +272 -0
- windextts/inference.py +748 -0
- windextts/models/__init__.py +1 -0
- windextts/models/bigvgan.py +434 -0
- windextts/models/campplus.py +411 -0
- windextts/models/codec.py +474 -0
- windextts/models/emo_conditioning.py +466 -0
- windextts/models/gpt.py +1255 -0
- windextts/models/length_regulator.py +223 -0
- windextts/models/qwen3.py +451 -0
- windextts/models/qwen_emotion.py +178 -0
- windextts/models/s2mel_cfm.py +498 -0
- windextts/models/s2mel_dit.py +788 -0
- windextts/models/w2v2_bert.py +366 -0
- windextts/profiler.py +540 -0
- windextts/server.py +235 -0
- windextts/webui.py +504 -0
- windextts/weights.py +185 -0
- windextts-0.1.0.dist-info/METADATA +218 -0
- windextts-0.1.0.dist-info/RECORD +37 -0
- windextts-0.1.0.dist-info/WHEEL +5 -0
- windextts-0.1.0.dist-info/entry_points.txt +5 -0
- windextts-0.1.0.dist-info/licenses/LICENSE +202 -0
- windextts-0.1.0.dist-info/top_level.txt +1 -0
windextts/__init__.py
ADDED
windextts/__main__.py
ADDED
windextts/attention.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Adaptive SDPA (Scaled Dot-Product Attention) backend selection.
|
|
2
|
+
|
|
3
|
+
Design goal: **zero external attention dependency** in the core inference path.
|
|
4
|
+
We use only ``torch.nn.functional.scaled_dot_product_attention`` (SDPA) with
|
|
5
|
+
explicit backend pinning via ``torch.nn.attention.sdpa_kernel``. No ``flash_attn``
|
|
6
|
+
import, no triton — works on Windows with stock torch wheels.
|
|
7
|
+
|
|
8
|
+
Backend policy (verified on torch 2.8.0+cu128, A10G):
|
|
9
|
+
- prefill (seq > 1, e.g. GPT conditioning / DiT): ``CUDNN_ATTENTION``.
|
|
10
|
+
Ships inside the torch CUDA wheel (cross-platform, no extra install).
|
|
11
|
+
Measured ~= flash_attn speed, slightly faster.
|
|
12
|
+
- decode (q seq == 1, GPT autoregressive step): cuDNN is *rejected* by
|
|
13
|
+
PyTorch's sdp_utils guard ("cudnn SDPA does not support sequence length 1"
|
|
14
|
+
— conservative perf guard in sdp_utils.cpp:458, not a library limitation).
|
|
15
|
+
Use ``FLASH_ATTENTION`` on Linux, ``EFFICIENT_ATTENTION`` on Windows
|
|
16
|
+
(mem_eff is the Windows-safe fallback; official flash backend needs a
|
|
17
|
+
wheel match).
|
|
18
|
+
|
|
19
|
+
The kernel choice is made per-call (cheap) so a single module can mix prefill
|
|
20
|
+
and decode without two code paths. All public helpers accept the standard SDPA
|
|
21
|
+
tensors and forward ``is_causal`` / ``scale``.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
|
|
27
|
+
import torch
|
|
28
|
+
import torch.nn.functional as F
|
|
29
|
+
from torch.nn.attention import SDPBackend, sdpa_kernel
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Platform detection — Windows gets mem_eff for decode; Linux gets flash.
|
|
33
|
+
# cuDNN is always preferred for prefill where available.
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
_IS_WINDOWS = os.name == "nt" or os.environ.get("WINDEXTTS_FORCE_WIN") == "1"
|
|
37
|
+
# Allow override for benchmarking / debugging.
|
|
38
|
+
_FORCE_PREFILL = os.environ.get("WINDEXTTS_ATTN_PREFILL", "") # cudnn|flash|eff|math
|
|
39
|
+
_FORCE_DECODE = os.environ.get("WINDEXTTS_ATTN_DECODE", "")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _backend_for_prefill() -> SDPBackend:
|
|
43
|
+
if _FORCE_PREFILL == "flash":
|
|
44
|
+
return SDPBackend.FLASH_ATTENTION
|
|
45
|
+
if _FORCE_PREFILL == "eff":
|
|
46
|
+
return SDPBackend.EFFICIENT_ATTENTION
|
|
47
|
+
if _FORCE_PREFILL == "math":
|
|
48
|
+
return SDPBackend.MATH
|
|
49
|
+
# Default: cuDNN (fastest, bundled in torch CUDA wheel).
|
|
50
|
+
return SDPBackend.CUDNN_ATTENTION
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _backend_for_decode() -> SDPBackend:
|
|
54
|
+
if _FORCE_DECODE == "flash":
|
|
55
|
+
return SDPBackend.FLASH_ATTENTION
|
|
56
|
+
if _FORCE_DECODE == "eff":
|
|
57
|
+
return SDPBackend.EFFICIENT_ATTENTION
|
|
58
|
+
if _FORCE_DECODE == "math":
|
|
59
|
+
return SDPBackend.MATH
|
|
60
|
+
# cuDNN cannot run with query seq==1 (sdp_utils.cpp:458 guard). Pick the
|
|
61
|
+
# best available: flash on Linux, mem_eff on Windows (no wheel dependency).
|
|
62
|
+
return SDPBackend.EFFICIENT_ATTENTION if _IS_WINDOWS else SDPBackend.FLASH_ATTENTION
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_decode(query: torch.Tensor) -> bool:
|
|
66
|
+
"""A decode step has query sequence length == 1 (single new token)."""
|
|
67
|
+
return query.size(-2) == 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Public API
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def attn(
|
|
76
|
+
query: torch.Tensor,
|
|
77
|
+
key: torch.Tensor,
|
|
78
|
+
value: torch.Tensor,
|
|
79
|
+
*,
|
|
80
|
+
attn_mask: torch.Tensor | None = None,
|
|
81
|
+
dropout_p: float = 0.0,
|
|
82
|
+
is_causal: bool = False,
|
|
83
|
+
scale: float | None = None,
|
|
84
|
+
) -> torch.Tensor:
|
|
85
|
+
"""Adaptive-backend scaled dot-product attention.
|
|
86
|
+
|
|
87
|
+
Picks cuDNN for prefill (seq>1) and flash/mem_eff for decode (q seq==1),
|
|
88
|
+
matching the measured-optimal backend per shape.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
query/key/value: [..., seq, head_dim] tensors (leading dims broadcast).
|
|
92
|
+
attn_mask, dropout_p, is_causal, scale: forwarded to SDPA.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Attention output, same shape as ``query``.
|
|
96
|
+
"""
|
|
97
|
+
backend = _backend_for_decode() if _is_decode(query) else _backend_for_prefill()
|
|
98
|
+
with sdpa_kernel([backend]):
|
|
99
|
+
return F.scaled_dot_product_attention(
|
|
100
|
+
query,
|
|
101
|
+
key,
|
|
102
|
+
value,
|
|
103
|
+
attn_mask=attn_mask,
|
|
104
|
+
dropout_p=dropout_p,
|
|
105
|
+
is_causal=is_causal,
|
|
106
|
+
scale=scale,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def attn_prefill(
|
|
111
|
+
query: torch.Tensor,
|
|
112
|
+
key: torch.Tensor,
|
|
113
|
+
value: torch.Tensor,
|
|
114
|
+
**kwargs,
|
|
115
|
+
) -> torch.Tensor:
|
|
116
|
+
"""Prefill-path attention — pinned to cuDNN (seq>1, e.g. DiT / GPT cond)."""
|
|
117
|
+
with sdpa_kernel([_backend_for_prefill()]):
|
|
118
|
+
return F.scaled_dot_product_attention(query, key, value, **kwargs)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def attn_decode(
|
|
122
|
+
query: torch.Tensor,
|
|
123
|
+
key: torch.Tensor,
|
|
124
|
+
value: torch.Tensor,
|
|
125
|
+
**kwargs,
|
|
126
|
+
) -> torch.Tensor:
|
|
127
|
+
"""Decode-path attention — flash(Linux)/mem_eff(Win), for q seq==1."""
|
|
128
|
+
with sdpa_kernel([_backend_for_decode()]):
|
|
129
|
+
return F.scaled_dot_product_attention(query, key, value, **kwargs)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
# Smoke test: verify backend selection and that decode falls back correctly.
|
|
134
|
+
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
|
135
|
+
dt = torch.bfloat16 if dev == "cuda" else torch.float32
|
|
136
|
+
print(f"platform: {'windows' if _IS_WINDOWS else 'linux'}, torch {torch.__version__}")
|
|
137
|
+
print(
|
|
138
|
+
f"prefill backend: {_backend_for_prefill().name} | "
|
|
139
|
+
f"decode backend: {_backend_for_decode().name}"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# prefill
|
|
143
|
+
q = k = v = torch.randn(1, 16, 512, 64, device=dev, dtype=dt)
|
|
144
|
+
o1 = attn(q, k, v, is_causal=True)
|
|
145
|
+
assert o1.shape == q.shape
|
|
146
|
+
print(f"prefill ok, out {tuple(o1.shape)}")
|
|
147
|
+
|
|
148
|
+
# decode (q seq=1)
|
|
149
|
+
qd = torch.randn(1, 16, 1, 64, device=dev, dtype=dt)
|
|
150
|
+
kd = vd = torch.randn(1, 16, 512, 64, device=dev, dtype=dt)
|
|
151
|
+
o2 = attn(qd, kd, vd)
|
|
152
|
+
assert o2.shape == qd.shape
|
|
153
|
+
print(f"decode ok, out {tuple(o2.shape)}")
|
|
154
|
+
print("SMOKE OK")
|
windextts/cli.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""WIndexTTS CLI — synthesize speech from the command line.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
# install model weights first (~7.5GB, one time, resumable)
|
|
5
|
+
windextts --install-model --model-dir /path/to/IndexTTS-2.5
|
|
6
|
+
|
|
7
|
+
# basic (fp16, GPU)
|
|
8
|
+
windextts --ref ref.wav --text "你好世界" -o out.wav
|
|
9
|
+
|
|
10
|
+
# W4A16 quantized GPT (fastest)
|
|
11
|
+
windextts --ref ref.wav --text "你好世界" -o out.wav --w4a16
|
|
12
|
+
|
|
13
|
+
# low VRAM (3-4GB GPUs; keeps beam3, ~2.9GB steady)
|
|
14
|
+
windextts --ref ref.wav --text "你好世界" -o out.wav --w4a16 --low-vram
|
|
15
|
+
|
|
16
|
+
# from a text file, with emotion vector + duration control
|
|
17
|
+
windextts --ref ref.wav --text-file story.txt -o out.wav \
|
|
18
|
+
--emo-vector 0.8,0,0,0,0,0,0.2,0 --duration 1.1
|
|
19
|
+
|
|
20
|
+
# fp32 reference precision, verbose timing
|
|
21
|
+
windextts --ref ref.wav --text "hello" -o out.wav --fp32 --verbose
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
33
|
+
p = argparse.ArgumentParser(
|
|
34
|
+
prog="windextts",
|
|
35
|
+
description="WIndexTTS: pure-torch accelerated IndexTTS-2.5 inference "
|
|
36
|
+
"(zero JIT compile, Windows friendly).",
|
|
37
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
38
|
+
epilog=__doc__.split("Usage:")[-1] if __doc__ else None,
|
|
39
|
+
)
|
|
40
|
+
# required inputs
|
|
41
|
+
p.add_argument("--ref", required=True, help="reference audio path (5-15s clean speech)")
|
|
42
|
+
p.add_argument("--text", help="text to synthesize (or use --text-file)")
|
|
43
|
+
p.add_argument("--text-file", help="read text from a file (UTF-8)")
|
|
44
|
+
p.add_argument("-o", "--output", default="output.wav", help="output wav path")
|
|
45
|
+
|
|
46
|
+
# engine mode
|
|
47
|
+
mode = p.add_mutually_exclusive_group()
|
|
48
|
+
mode.add_argument("--fp32", action="store_true", help="fp32 weights (highest precision, ~7.6GB)")
|
|
49
|
+
mode.add_argument("--w4a16", action="store_true",
|
|
50
|
+
help="W4A16 INT4 GPT quantization (fastest; needs torchao)")
|
|
51
|
+
p.add_argument("--low-vram", action="store_true",
|
|
52
|
+
help="low-VRAM mode for 3-4GB GPUs (w2v streamed to CPU, "
|
|
53
|
+
"beam3 kept, ~2.9GB steady)")
|
|
54
|
+
p.add_argument("--model-dir", default=None, help="weights directory (gpt.pth, config.yaml, ...)")
|
|
55
|
+
|
|
56
|
+
# language / prosody
|
|
57
|
+
p.add_argument("--lang", default="ZH",
|
|
58
|
+
help="language token (ZH/EN/JA/KO/YUE/..., default ZH)")
|
|
59
|
+
p.add_argument("--duration", type=float, default=1.0,
|
|
60
|
+
help="duration factor (official 1.72 scale × factor, default 1.0)")
|
|
61
|
+
|
|
62
|
+
# emotion (mutually exclusive sources)
|
|
63
|
+
emo = p.add_mutually_exclusive_group()
|
|
64
|
+
emo.add_argument("--emo-vector",
|
|
65
|
+
help="8-dim emotion weights happy,angry,sad,afraid,disgusted,"
|
|
66
|
+
"melancholic,surprised,calm — e.g. 0.8,0,0,0,0,0,0.2,0")
|
|
67
|
+
emo.add_argument("--emo-text", help="free-text emotion description (QwenEmotion)")
|
|
68
|
+
emo.add_argument("--emo-ref", help="emotion reference audio path (conformer path)")
|
|
69
|
+
|
|
70
|
+
# sampling
|
|
71
|
+
p.add_argument("--greedy", action="store_true", help="greedy decode (beam=1, deterministic)")
|
|
72
|
+
p.add_argument("--top-p", type=float, default=0.8)
|
|
73
|
+
p.add_argument("--top-k", type=int, default=30)
|
|
74
|
+
p.add_argument("--temperature", type=float, default=0.8)
|
|
75
|
+
|
|
76
|
+
# perf knobs
|
|
77
|
+
p.add_argument("--cfm-steps", type=int, default=12,
|
|
78
|
+
help="S2Mel CFM Euler steps (official 25, default 12)")
|
|
79
|
+
p.add_argument("--teacache", type=float, default=0.25,
|
|
80
|
+
help="TeaCache threshold (0=off, default 0.25)")
|
|
81
|
+
p.add_argument("--no-normalize", action="store_true",
|
|
82
|
+
help="skip text normalization (G2P digits/punctuation)")
|
|
83
|
+
p.add_argument("--segment-tokens", type=int, default=120,
|
|
84
|
+
help="max text tokens per segment (long texts auto-split)")
|
|
85
|
+
|
|
86
|
+
p.add_argument("--verbose", action="store_true", help="print per-run timing + VRAM")
|
|
87
|
+
|
|
88
|
+
# model install (download weights without synthesizing) — ModelScope direct
|
|
89
|
+
# links, no SDK dependency, resumable
|
|
90
|
+
p.add_argument("--install-model", action="store_true",
|
|
91
|
+
help="download model weights to --model-dir (or WINDEXTTS_WEIGHTS_DIR) "
|
|
92
|
+
"and exit; ~7.5GB from ModelScope direct links. Resumable. "
|
|
93
|
+
"Combine with --skip-qwen.")
|
|
94
|
+
p.add_argument("--skip-qwen", action="store_true",
|
|
95
|
+
help="with --install-model: skip qwen0.6bemo4-merge (1.2GB, only "
|
|
96
|
+
"needed for emo_text)")
|
|
97
|
+
return p
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main(argv: list[str] | None = None) -> int:
|
|
101
|
+
args = build_parser().parse_args(argv)
|
|
102
|
+
|
|
103
|
+
# --- model install mode ---
|
|
104
|
+
if args.install_model:
|
|
105
|
+
from windextts.download import download_model
|
|
106
|
+
target = args.model_dir or os.environ.get("WINDEXTTS_WEIGHTS_DIR")
|
|
107
|
+
if not target:
|
|
108
|
+
print("error: --install-model needs --model-dir or WINDEXTTS_WEIGHTS_DIR",
|
|
109
|
+
file=sys.stderr)
|
|
110
|
+
return 2
|
|
111
|
+
download_model(target, include_qwen=not args.skip_qwen)
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
# --- text source ---
|
|
115
|
+
if args.text_file:
|
|
116
|
+
with open(args.text_file, encoding="utf-8") as f:
|
|
117
|
+
text = f.read()
|
|
118
|
+
elif args.text:
|
|
119
|
+
text = args.text
|
|
120
|
+
else:
|
|
121
|
+
print("error: either --text or --text-file is required", file=sys.stderr)
|
|
122
|
+
return 2
|
|
123
|
+
text = text.strip()
|
|
124
|
+
if not text:
|
|
125
|
+
print("error: empty text", file=sys.stderr)
|
|
126
|
+
return 2
|
|
127
|
+
|
|
128
|
+
# --- emotion vector parsing ---
|
|
129
|
+
emo_vector = None
|
|
130
|
+
if args.emo_vector:
|
|
131
|
+
try:
|
|
132
|
+
emo_vector = [float(x) for x in args.emo_vector.split(",")]
|
|
133
|
+
except ValueError:
|
|
134
|
+
print("error: --emo-vector must be 8 comma-separated floats", file=sys.stderr)
|
|
135
|
+
return 2
|
|
136
|
+
if len(emo_vector) != 8:
|
|
137
|
+
print("error: --emo-vector needs exactly 8 values "
|
|
138
|
+
"(happy,angry,sad,afraid,disgusted,melancholic,surprised,calm)", file=sys.stderr)
|
|
139
|
+
return 2
|
|
140
|
+
|
|
141
|
+
# --- engine ---
|
|
142
|
+
import torch
|
|
143
|
+
from windextts.inference import WIndexTTS
|
|
144
|
+
|
|
145
|
+
dtype = torch.float32 if args.fp32 else torch.float16
|
|
146
|
+
t0 = time.perf_counter()
|
|
147
|
+
tts = WIndexTTS(
|
|
148
|
+
weights_dir=args.model_dir, device="cuda", dtype=dtype,
|
|
149
|
+
enable_w4a16=args.w4a16, low_vram=args.low_vram,
|
|
150
|
+
)
|
|
151
|
+
tts.warmup()
|
|
152
|
+
if args.verbose:
|
|
153
|
+
print(f"[load+warmup] {time.perf_counter() - t0:.1f}s "
|
|
154
|
+
f"VRAM alloc {torch.cuda.memory_allocated() / 1e9:.2f}GB", file=sys.stderr)
|
|
155
|
+
|
|
156
|
+
# --- synthesize ---
|
|
157
|
+
t0 = time.perf_counter()
|
|
158
|
+
sr, audio = tts.infer(
|
|
159
|
+
spk_audio_prompt=args.ref,
|
|
160
|
+
text=text,
|
|
161
|
+
lang=args.lang,
|
|
162
|
+
emo_vector=emo_vector,
|
|
163
|
+
emo_text=args.emo_text,
|
|
164
|
+
emo_ref_path=args.emo_ref,
|
|
165
|
+
duration_factor=args.duration,
|
|
166
|
+
do_sample=not args.greedy,
|
|
167
|
+
top_p=args.top_p,
|
|
168
|
+
top_k=args.top_k,
|
|
169
|
+
temperature=args.temperature,
|
|
170
|
+
cfm_steps=args.cfm_steps,
|
|
171
|
+
teacache_thresh=args.teacache,
|
|
172
|
+
text_normalization=not args.no_normalize,
|
|
173
|
+
max_text_tokens_per_segment=args.segment_tokens,
|
|
174
|
+
num_beams=1 if args.greedy else 3,
|
|
175
|
+
)
|
|
176
|
+
dt = time.perf_counter() - t0
|
|
177
|
+
dur = audio.numel() / sr
|
|
178
|
+
|
|
179
|
+
import soundfile as sf
|
|
180
|
+
sf.write(args.output, audio.float().cpu().numpy().squeeze(), sr)
|
|
181
|
+
print(f"{args.output} ({dur:.2f}s audio in {dt * 1000:.0f}ms, RTF={dt / dur:.3f}, {sr}Hz)")
|
|
182
|
+
if args.verbose:
|
|
183
|
+
print(f"[peak VRAM] {torch.cuda.max_memory_allocated() / 1e9:.2f}GB", file=sys.stderr)
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
sys.exit(main())
|
windextts/config.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Typed access to the IndexTTS-2.5 config.yaml.
|
|
2
|
+
|
|
3
|
+
This is the single source of truth for hyperparameters. The ``Config`` object
|
|
4
|
+
is constructed once from ``/root/IndexTTS-2.5/config.yaml`` (or a copied file)
|
|
5
|
+
and passed to every module. No module should hardcode magic numbers that exist
|
|
6
|
+
in config.yaml — read them here.
|
|
7
|
+
|
|
8
|
+
Magic numbers that live in source code (infer_v2_5.py), not config.yaml — e.g.
|
|
9
|
+
``w2v-bert hidden_states[17]``, ``1.72`` duration scale, ``diffusion_steps=25`` —
|
|
10
|
+
are collected in :class:`RuntimeConstants` so they are also centralized.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Runtime constants — these come from infer_v2_5.py source, NOT config.yaml.
|
|
23
|
+
# Every value must cite its source line. Do NOT invent or tweak.
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class RuntimeConstants:
|
|
29
|
+
"""Constants hardcoded in indextts/infer_v2_5.py (not in config.yaml).
|
|
30
|
+
|
|
31
|
+
Each field cites the source line in ``infer_v2_5.py``. These are the
|
|
32
|
+
"seam magic numbers" — changing any of them produces wrong output.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# w2v-bert: which hidden_states layer to use as ref audio feature.
|
|
36
|
+
# infer_v2_5.py:288 hidden_states[17]
|
|
37
|
+
w2v_layer: int = 17
|
|
38
|
+
|
|
39
|
+
# w2v-bert feature normalization: (feat - mean) / sqrt(var)
|
|
40
|
+
# stats loaded from wav2vec2bert_stats.pt {mean:[1024], var:[1024]}
|
|
41
|
+
# infer_v2_5.py:177-179, 289
|
|
42
|
+
# (handled in weights/frontend, kept here only as documentation anchor)
|
|
43
|
+
|
|
44
|
+
# S2Mel duration scaling. infer_v2_5.py:855 1.72 * duration_factor
|
|
45
|
+
s2mel_duration_scale: float = 1.72
|
|
46
|
+
|
|
47
|
+
# S2Mel length_regulator n_quantizers. infer_v2_5.py:655,859
|
|
48
|
+
s2mel_n_quantizers: int = 3
|
|
49
|
+
|
|
50
|
+
# S2Mel CFM. infer_v2_5.py:849-850 diffusion_steps=25, inference_cfg_rate=0.7
|
|
51
|
+
cfm_diffusion_steps: int = 25
|
|
52
|
+
cfm_inference_cfg_rate: float = 0.7
|
|
53
|
+
|
|
54
|
+
# Output audio. infer_v2_5.py:514 22050 Hz mono
|
|
55
|
+
output_sr: int = 22050
|
|
56
|
+
|
|
57
|
+
# Reference audio resample targets. infer_v2_5.py:628-629
|
|
58
|
+
ref_sr_mel: int = 22000 # for mel / mel-condition path
|
|
59
|
+
ref_sr_w2v: int = 16000 # for w2v-bert & campplus
|
|
60
|
+
|
|
61
|
+
# Max reference audio length taken (seconds). infer_v2_5.py path
|
|
62
|
+
ref_max_seconds: float = 15.0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# config.yaml sections — mirrors /root/IndexTTS-2.5/config.yaml structure.
|
|
67
|
+
# Kept loosely typed (nested dataclasses of plain values) to avoid coupling to
|
|
68
|
+
# any external library's config schema.
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class MelConfig:
|
|
74
|
+
sample_rate: int = 24000
|
|
75
|
+
n_fft: int = 1024
|
|
76
|
+
hop_length: int = 256
|
|
77
|
+
win_length: int = 1024
|
|
78
|
+
n_mels: int = 100
|
|
79
|
+
mel_fmin: float = 0.0
|
|
80
|
+
normalize: bool = False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class GPTConfig:
|
|
85
|
+
model_dim: int = 1280
|
|
86
|
+
max_mel_tokens: int = 1815
|
|
87
|
+
max_text_tokens: int = 600
|
|
88
|
+
heads: int = 20
|
|
89
|
+
use_mel_codes_as_input: bool = True
|
|
90
|
+
mel_length_compression: int = 1024
|
|
91
|
+
layers: int = 24
|
|
92
|
+
number_text_tokens: int = 60509
|
|
93
|
+
number_mel_codes: int = 8194
|
|
94
|
+
start_mel_token: int = 8192
|
|
95
|
+
stop_mel_token: int = 8193
|
|
96
|
+
start_text_token: int = 0
|
|
97
|
+
stop_text_token: int = 1
|
|
98
|
+
train_solo_embeddings: bool = False
|
|
99
|
+
condition_type: str = "conformer_perceiver"
|
|
100
|
+
# condition_module / emo_condition_module are nested dicts; kept as-is.
|
|
101
|
+
condition_module: dict = field(default_factory=dict)
|
|
102
|
+
emo_condition_module: dict = field(default_factory=dict)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class SemanticCodecConfig:
|
|
107
|
+
codebook_size: int = 8192
|
|
108
|
+
hidden_size: int = 1024
|
|
109
|
+
codebook_dim: int = 8
|
|
110
|
+
vocos_dim: int = 384
|
|
111
|
+
vocos_intermediate_dim: int = 2048
|
|
112
|
+
vocos_num_layers: int = 12
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class S2MelDiTConfig:
|
|
117
|
+
hidden_dim: int = 512
|
|
118
|
+
num_heads: int = 8
|
|
119
|
+
depth: int = 13
|
|
120
|
+
class_dropout_prob: float = 0.1
|
|
121
|
+
block_size: int = 8192
|
|
122
|
+
in_channels: int = 80
|
|
123
|
+
style_condition: bool = True
|
|
124
|
+
final_layer_type: str = "wavenet"
|
|
125
|
+
target: str = "mel"
|
|
126
|
+
content_dim: int = 512
|
|
127
|
+
content_codebook_size: int = 1024
|
|
128
|
+
content_type: str = "discrete"
|
|
129
|
+
f0_condition: bool = False
|
|
130
|
+
n_f0_bins: int = 512
|
|
131
|
+
content_codebooks: int = 1
|
|
132
|
+
is_causal: bool = False
|
|
133
|
+
long_skip_connection: bool = True
|
|
134
|
+
zero_prompt_speech_token: bool = False
|
|
135
|
+
time_as_token: bool = False
|
|
136
|
+
style_as_token: bool = False
|
|
137
|
+
uvit_skip_connection: bool = True
|
|
138
|
+
add_resblock_in_transformer: bool = False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class S2MelLengthRegConfig:
|
|
143
|
+
channels: int = 512
|
|
144
|
+
is_discrete: bool = False
|
|
145
|
+
in_channels: int = 1024
|
|
146
|
+
content_codebook_size: int = 2048
|
|
147
|
+
sampling_ratios: tuple = (1, 1, 1, 1)
|
|
148
|
+
vector_quantize: bool = False
|
|
149
|
+
n_codebooks: int = 1
|
|
150
|
+
quantizer_dropout: float = 0.0
|
|
151
|
+
f0_condition: bool = False
|
|
152
|
+
n_f0_bins: int = 512
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass
|
|
156
|
+
class S2MelConfig:
|
|
157
|
+
# preprocess_params.spect_params
|
|
158
|
+
sr: int = 22050
|
|
159
|
+
n_fft: int = 1024
|
|
160
|
+
win_length: int = 1024
|
|
161
|
+
hop_length: int = 256
|
|
162
|
+
n_mels: int = 80
|
|
163
|
+
fmin: float = 0.0
|
|
164
|
+
fmax: float | None = None
|
|
165
|
+
dit_type: str = "DiT"
|
|
166
|
+
reg_loss_type: str = "l1"
|
|
167
|
+
style_encoder_dim: int = 192
|
|
168
|
+
length_reg: S2MelLengthRegConfig = field(default_factory=S2MelLengthRegConfig)
|
|
169
|
+
dit: S2MelDiTConfig = field(default_factory=S2MelDiTConfig)
|
|
170
|
+
wavenet_hidden_dim: int = 512
|
|
171
|
+
wavenet_num_layers: int = 8
|
|
172
|
+
wavenet_kernel_size: int = 5
|
|
173
|
+
wavenet_dilation_rate: int = 1
|
|
174
|
+
wavenet_p_dropout: float = 0.2
|
|
175
|
+
wavenet_style_condition: bool = True
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class Config:
|
|
180
|
+
"""Top-level config mirroring IndexTTS-2.5 config.yaml."""
|
|
181
|
+
|
|
182
|
+
# dataset.mel
|
|
183
|
+
mel: MelConfig = field(default_factory=MelConfig)
|
|
184
|
+
gpt: GPTConfig = field(default_factory=GPTConfig)
|
|
185
|
+
semantic_codec: SemanticCodecConfig = field(default_factory=SemanticCodecConfig)
|
|
186
|
+
s2mel: S2MelConfig = field(default_factory=S2MelConfig)
|
|
187
|
+
# vocoder
|
|
188
|
+
vocoder_type: str = "bigvgan"
|
|
189
|
+
vocoder_name: str = "bigvgan_generator.pt"
|
|
190
|
+
version: str = "2.5"
|
|
191
|
+
# raw dict fallback for anything not yet dataclassed
|
|
192
|
+
_raw: dict = field(default_factory=dict, repr=False)
|
|
193
|
+
# runtime constants from infer_v2_5.py
|
|
194
|
+
rt: RuntimeConstants = field(default_factory=RuntimeConstants)
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def from_yaml(cls, path: str | Path) -> "Config":
|
|
198
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
199
|
+
raw = yaml.safe_load(f)
|
|
200
|
+
|
|
201
|
+
ds = raw.get("dataset", {}) or {}
|
|
202
|
+
mel_raw = ds.get("mel", {}) or {}
|
|
203
|
+
mel = MelConfig(
|
|
204
|
+
sample_rate=mel_raw.get("sample_rate", 24000),
|
|
205
|
+
n_fft=mel_raw.get("n_fft", 1024),
|
|
206
|
+
hop_length=mel_raw.get("hop_length", 256),
|
|
207
|
+
win_length=mel_raw.get("win_length", 1024),
|
|
208
|
+
n_mels=mel_raw.get("n_mels", 100),
|
|
209
|
+
mel_fmin=mel_raw.get("mel_fmin", 0.0),
|
|
210
|
+
normalize=mel_raw.get("normalize", False),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
g = raw.get("gpt", {}) or {}
|
|
214
|
+
gpt = GPTConfig(
|
|
215
|
+
model_dim=g.get("model_dim", 1280),
|
|
216
|
+
max_mel_tokens=g.get("max_mel_tokens", 1815),
|
|
217
|
+
max_text_tokens=g.get("max_text_tokens", 600),
|
|
218
|
+
heads=g.get("heads", 20),
|
|
219
|
+
use_mel_codes_as_input=g.get("use_mel_codes_as_input", True),
|
|
220
|
+
mel_length_compression=g.get("mel_length_compression", 1024),
|
|
221
|
+
layers=g.get("layers", 24),
|
|
222
|
+
number_text_tokens=g.get("number_text_tokens", 60509),
|
|
223
|
+
number_mel_codes=g.get("number_mel_codes", 8194),
|
|
224
|
+
start_mel_token=g.get("start_mel_token", 8192),
|
|
225
|
+
stop_mel_token=g.get("stop_mel_token", 8193),
|
|
226
|
+
start_text_token=g.get("start_text_token", 0),
|
|
227
|
+
stop_text_token=g.get("stop_text_token", 1),
|
|
228
|
+
train_solo_embeddings=g.get("train_solo_embeddings", False),
|
|
229
|
+
condition_type=g.get("condition_type", "conformer_perceiver"),
|
|
230
|
+
condition_module=g.get("condition_module", {}) or {},
|
|
231
|
+
emo_condition_module=g.get("emo_condition_module", {}) or {},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
sc = raw.get("semantic_codec", {}) or {}
|
|
235
|
+
semantic_codec = SemanticCodecConfig(
|
|
236
|
+
codebook_size=sc.get("codebook_size", 8192),
|
|
237
|
+
hidden_size=sc.get("hidden_size", 1024),
|
|
238
|
+
codebook_dim=sc.get("codebook_dim", 8),
|
|
239
|
+
vocos_dim=sc.get("vocos_dim", 384),
|
|
240
|
+
vocos_intermediate_dim=sc.get("vocos_intermediate_dim", 2048),
|
|
241
|
+
vocos_num_layers=sc.get("vocos_num_layers", 12),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
s2 = raw.get("s2mel", {}) or {}
|
|
245
|
+
pre = (s2.get("preprocess_params", {}) or {}).get("spect_params", {}) or {}
|
|
246
|
+
fmax_raw = pre.get("fmax", None)
|
|
247
|
+
fmax = None if (fmax_raw is None or fmax_raw == "None") else float(fmax_raw)
|
|
248
|
+
lr_raw = s2.get("length_regulator", {}) or {}
|
|
249
|
+
length_reg = S2MelLengthRegConfig(
|
|
250
|
+
channels=lr_raw.get("channels", 512),
|
|
251
|
+
is_discrete=lr_raw.get("is_discrete", False),
|
|
252
|
+
in_channels=lr_raw.get("in_channels", 1024),
|
|
253
|
+
content_codebook_size=lr_raw.get("content_codebook_size", 2048),
|
|
254
|
+
sampling_ratios=tuple(lr_raw.get("sampling_ratios", [1, 1, 1, 1])),
|
|
255
|
+
vector_quantize=lr_raw.get("vector_quantize", False),
|
|
256
|
+
n_codebooks=lr_raw.get("n_codebooks", 1),
|
|
257
|
+
quantizer_dropout=lr_raw.get("quantizer_dropout", 0.0),
|
|
258
|
+
f0_condition=lr_raw.get("f0_condition", False),
|
|
259
|
+
n_f0_bins=lr_raw.get("n_f0_bins", 512),
|
|
260
|
+
)
|
|
261
|
+
dit_raw = s2.get("DiT", {}) or {}
|
|
262
|
+
dit = S2MelDiTConfig(
|
|
263
|
+
hidden_dim=dit_raw.get("hidden_dim", 512),
|
|
264
|
+
num_heads=dit_raw.get("num_heads", 8),
|
|
265
|
+
depth=dit_raw.get("depth", 13),
|
|
266
|
+
class_dropout_prob=dit_raw.get("class_dropout_prob", 0.1),
|
|
267
|
+
block_size=dit_raw.get("block_size", 8192),
|
|
268
|
+
in_channels=dit_raw.get("in_channels", 80),
|
|
269
|
+
style_condition=dit_raw.get("style_condition", True),
|
|
270
|
+
final_layer_type=dit_raw.get("final_layer_type", "wavenet"),
|
|
271
|
+
target=dit_raw.get("target", "mel"),
|
|
272
|
+
content_dim=dit_raw.get("content_dim", 512),
|
|
273
|
+
content_codebook_size=dit_raw.get("content_codebook_size", 1024),
|
|
274
|
+
content_type=dit_raw.get("content_type", "discrete"),
|
|
275
|
+
f0_condition=dit_raw.get("f0_condition", False),
|
|
276
|
+
n_f0_bins=dit_raw.get("n_f0_bins", 512),
|
|
277
|
+
content_codebooks=dit_raw.get("content_codebooks", 1),
|
|
278
|
+
is_causal=dit_raw.get("is_causal", False),
|
|
279
|
+
long_skip_connection=dit_raw.get("long_skip_connection", True),
|
|
280
|
+
zero_prompt_speech_token=dit_raw.get("zero_prompt_speech_token", False),
|
|
281
|
+
time_as_token=dit_raw.get("time_as_token", False),
|
|
282
|
+
style_as_token=dit_raw.get("style_as_token", False),
|
|
283
|
+
uvit_skip_connection=dit_raw.get("uvit_skip_connection", True),
|
|
284
|
+
add_resblock_in_transformer=dit_raw.get("add_resblock_in_transformer", False),
|
|
285
|
+
)
|
|
286
|
+
wn = s2.get("wavenet", {}) or {}
|
|
287
|
+
s2mel = S2MelConfig(
|
|
288
|
+
sr=pre.get("sr", 22050),
|
|
289
|
+
n_fft=pre.get("n_fft", 1024),
|
|
290
|
+
win_length=pre.get("win_length", 1024),
|
|
291
|
+
hop_length=pre.get("hop_length", 256),
|
|
292
|
+
n_mels=pre.get("n_mels", 80),
|
|
293
|
+
fmin=pre.get("fmin", 0.0),
|
|
294
|
+
fmax=fmax,
|
|
295
|
+
dit_type=s2.get("dit_type", "DiT"),
|
|
296
|
+
reg_loss_type=s2.get("reg_loss_type", "l1"),
|
|
297
|
+
style_encoder_dim=(s2.get("style_encoder", {}) or {}).get("dim", 192),
|
|
298
|
+
length_reg=length_reg,
|
|
299
|
+
dit=dit,
|
|
300
|
+
wavenet_hidden_dim=wn.get("hidden_dim", 512),
|
|
301
|
+
wavenet_num_layers=wn.get("num_layers", 8),
|
|
302
|
+
wavenet_kernel_size=wn.get("kernel_size", 5),
|
|
303
|
+
wavenet_dilation_rate=wn.get("dilation_rate", 1),
|
|
304
|
+
wavenet_p_dropout=wn.get("p_dropout", 0.2),
|
|
305
|
+
wavenet_style_condition=wn.get("style_condition", True),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
voc = raw.get("vocoder", {}) or {}
|
|
309
|
+
return cls(
|
|
310
|
+
mel=mel,
|
|
311
|
+
gpt=gpt,
|
|
312
|
+
semantic_codec=semantic_codec,
|
|
313
|
+
s2mel=s2mel,
|
|
314
|
+
vocoder_type=voc.get("type", "bigvgan"),
|
|
315
|
+
vocoder_name=voc.get("name", "bigvgan_generator.pt"),
|
|
316
|
+
version=raw.get("version", "2.5"),
|
|
317
|
+
_raw=raw,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
DEFAULT_CONFIG_PATH = os.environ.get("WINDEXTTS_WEIGHTS_DIR", "/root/IndexTTS-2.5") + "/config.yaml"
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def load_default_config() -> Config:
|
|
325
|
+
"""Load config from the canonical IndexTTS-2.5 weights dir."""
|
|
326
|
+
return Config.from_yaml(DEFAULT_CONFIG_PATH)
|
|
Binary file
|
|
Binary file
|