vox-dia 0.1.0__tar.gz
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.
- vox_dia-0.1.0/.gitignore +17 -0
- vox_dia-0.1.0/PKG-INFO +11 -0
- vox_dia-0.1.0/pyproject.toml +23 -0
- vox_dia-0.1.0/src/vox_dia/__init__.py +3 -0
- vox_dia-0.1.0/src/vox_dia/adapter.py +236 -0
vox_dia-0.1.0/.gitignore
ADDED
vox_dia-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vox-dia
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dia TTS adapter for Vox
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: numpy<2.4,>=1.26.0
|
|
7
|
+
Requires-Dist: sentencepiece>=0.2.0
|
|
8
|
+
Requires-Dist: soundfile>=0.13.1
|
|
9
|
+
Requires-Dist: torch>=2.1.0
|
|
10
|
+
Requires-Dist: transformers>=4.52.1
|
|
11
|
+
Requires-Dist: vox>=0.1.0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vox-dia"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Dia TTS adapter for Vox"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"vox>=0.1.0",
|
|
8
|
+
"transformers>=4.52.1",
|
|
9
|
+
"torch>=2.1.0",
|
|
10
|
+
"numpy>=1.26.0,<2.4",
|
|
11
|
+
"soundfile>=0.13.1",
|
|
12
|
+
"sentencepiece>=0.2.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.entry-points."vox.adapters"]
|
|
16
|
+
dia = "vox_dia.adapter:DiaAdapter"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/vox_dia"]
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import importlib.util
|
|
5
|
+
import logging
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import AsyncIterator
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import torch
|
|
16
|
+
from numpy.typing import NDArray
|
|
17
|
+
|
|
18
|
+
from vox.core.adapter import TTSAdapter
|
|
19
|
+
from vox.core.types import AdapterInfo, ModelFormat, ModelType, SynthesizeChunk, VoiceInfo
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
DIA_SAMPLE_RATE = 44_100
|
|
24
|
+
_DEFAULT_MAX_NEW_TOKENS = 3_072
|
|
25
|
+
_DEFAULT_GUIDANCE_SCALE = 3.0
|
|
26
|
+
_DEFAULT_TEMPERATURE = 1.8
|
|
27
|
+
_DEFAULT_TOP_P = 0.90
|
|
28
|
+
_DEFAULT_TOP_K = 45
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _select_device(device: str) -> str:
|
|
32
|
+
if device == "cpu":
|
|
33
|
+
return "cpu"
|
|
34
|
+
if device in ("cuda", "auto") and torch.cuda.is_available():
|
|
35
|
+
return "cuda"
|
|
36
|
+
return "cpu"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_transformers_runtime() -> tuple[Any, Any]:
|
|
40
|
+
try:
|
|
41
|
+
from transformers import AutoProcessor, DiaForConditionalGeneration
|
|
42
|
+
except ImportError:
|
|
43
|
+
_install_transformers_runtime()
|
|
44
|
+
_clear_transformers_modules()
|
|
45
|
+
try:
|
|
46
|
+
from transformers import AutoProcessor, DiaForConditionalGeneration
|
|
47
|
+
except ImportError as retry_exc:
|
|
48
|
+
raise RuntimeError(
|
|
49
|
+
"Dia requires Hugging Face Transformers with DiaForConditionalGeneration support. "
|
|
50
|
+
"Install the main branch of transformers: "
|
|
51
|
+
"`pip install git+https://github.com/huggingface/transformers.git`."
|
|
52
|
+
) from retry_exc
|
|
53
|
+
return AutoProcessor, DiaForConditionalGeneration
|
|
54
|
+
|
|
55
|
+
return AutoProcessor, DiaForConditionalGeneration
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _clear_transformers_modules() -> None:
|
|
59
|
+
for name in list(sys.modules):
|
|
60
|
+
if name == "transformers" or name.startswith("transformers."):
|
|
61
|
+
sys.modules.pop(name, None)
|
|
62
|
+
importlib.invalidate_caches()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _ensure_pip_available() -> None:
|
|
66
|
+
if importlib.util.find_spec("pip") is not None:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
result = subprocess.run(
|
|
70
|
+
[sys.executable, "-m", "ensurepip", "--default-pip"],
|
|
71
|
+
capture_output=True,
|
|
72
|
+
text=True,
|
|
73
|
+
timeout=300,
|
|
74
|
+
)
|
|
75
|
+
if result.returncode != 0:
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
"Failed to bootstrap pip for Dia's runtime install. "
|
|
78
|
+
f"stderr: {result.stderr.strip()}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _install_transformers_runtime() -> None:
|
|
83
|
+
_ensure_pip_available()
|
|
84
|
+
result = subprocess.run(
|
|
85
|
+
[
|
|
86
|
+
sys.executable,
|
|
87
|
+
"-m",
|
|
88
|
+
"pip",
|
|
89
|
+
"install",
|
|
90
|
+
"git+https://github.com/huggingface/transformers.git@main",
|
|
91
|
+
"sentencepiece>=0.2.0",
|
|
92
|
+
"tiktoken>=0.9.0",
|
|
93
|
+
],
|
|
94
|
+
capture_output=True,
|
|
95
|
+
text=True,
|
|
96
|
+
timeout=900,
|
|
97
|
+
)
|
|
98
|
+
if result.returncode != 0:
|
|
99
|
+
raise RuntimeError(
|
|
100
|
+
"Failed to install Dia runtime from Hugging Face Transformers main branch. "
|
|
101
|
+
f"stderr: {result.stderr.strip()}"
|
|
102
|
+
) from None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DiaAdapter(TTSAdapter):
|
|
106
|
+
def __init__(self) -> None:
|
|
107
|
+
self._model: Any = None
|
|
108
|
+
self._processor: Any = None
|
|
109
|
+
self._loaded = False
|
|
110
|
+
self._model_id: str = ""
|
|
111
|
+
self._device: str = "cpu"
|
|
112
|
+
|
|
113
|
+
def info(self) -> AdapterInfo:
|
|
114
|
+
return AdapterInfo(
|
|
115
|
+
name="dia",
|
|
116
|
+
type=ModelType.TTS,
|
|
117
|
+
architectures=("dia",),
|
|
118
|
+
default_sample_rate=DIA_SAMPLE_RATE,
|
|
119
|
+
supported_formats=(ModelFormat.PYTORCH,),
|
|
120
|
+
supports_streaming=False,
|
|
121
|
+
supports_voice_cloning=False,
|
|
122
|
+
supported_languages=("en",),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def load(self, model_path: str, device: str, **kwargs: Any) -> None:
|
|
126
|
+
if self._loaded:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
source = kwargs.pop("_source", None)
|
|
130
|
+
self._model_id = source if source else model_path
|
|
131
|
+
self._device = _select_device(device)
|
|
132
|
+
if self._device != "cuda":
|
|
133
|
+
raise RuntimeError(
|
|
134
|
+
"Dia requires a CUDA-capable GPU. CPU execution is not supported by the official runtime."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
AutoProcessor, DiaForConditionalGeneration = _load_transformers_runtime()
|
|
138
|
+
|
|
139
|
+
logger.info("Loading Dia model: %s (device=%s)", self._model_id, self._device)
|
|
140
|
+
start = time.perf_counter()
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
self._processor = AutoProcessor.from_pretrained(self._model_id)
|
|
144
|
+
except ValueError as exc:
|
|
145
|
+
message = str(exc)
|
|
146
|
+
if "sentencepiece" not in message and "tiktoken" not in message:
|
|
147
|
+
raise
|
|
148
|
+
self._processor = AutoProcessor.from_pretrained(self._model_id, use_fast=False)
|
|
149
|
+
self._model = DiaForConditionalGeneration.from_pretrained(self._model_id).to(self._device)
|
|
150
|
+
self._model.eval()
|
|
151
|
+
|
|
152
|
+
elapsed = time.perf_counter() - start
|
|
153
|
+
logger.info("Dia model loaded in %.2fs", elapsed)
|
|
154
|
+
self._loaded = True
|
|
155
|
+
|
|
156
|
+
def unload(self) -> None:
|
|
157
|
+
self._model = None
|
|
158
|
+
self._processor = None
|
|
159
|
+
self._loaded = False
|
|
160
|
+
if torch.cuda.is_available():
|
|
161
|
+
torch.cuda.empty_cache()
|
|
162
|
+
logger.info("Dia adapter unloaded")
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def is_loaded(self) -> bool:
|
|
166
|
+
return self._loaded
|
|
167
|
+
|
|
168
|
+
async def synthesize(
|
|
169
|
+
self,
|
|
170
|
+
text: str,
|
|
171
|
+
*,
|
|
172
|
+
voice: str | None = None,
|
|
173
|
+
speed: float = 1.0,
|
|
174
|
+
language: str | None = None,
|
|
175
|
+
reference_audio: NDArray[np.float32] | None = None,
|
|
176
|
+
reference_text: str | None = None,
|
|
177
|
+
) -> AsyncIterator[SynthesizeChunk]:
|
|
178
|
+
if not self._loaded or self._model is None or self._processor is None:
|
|
179
|
+
raise RuntimeError("Dia model is not loaded — call load() first")
|
|
180
|
+
|
|
181
|
+
if reference_audio is not None or reference_text is not None:
|
|
182
|
+
raise NotImplementedError(
|
|
183
|
+
"Dia transformers backend does not yet wire the audio-prompt voice cloning path. "
|
|
184
|
+
"Use the official nari-labs/dia runtime if you need reference-audio cloning."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not text or not text.strip():
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
inputs = self._processor(text=[text], padding=True, return_tensors="pt")
|
|
191
|
+
inputs = inputs.to(self._device) if hasattr(inputs, "to") else inputs
|
|
192
|
+
|
|
193
|
+
with torch.inference_mode():
|
|
194
|
+
output = self._model.generate(
|
|
195
|
+
**inputs,
|
|
196
|
+
max_new_tokens=_DEFAULT_MAX_NEW_TOKENS,
|
|
197
|
+
guidance_scale=_DEFAULT_GUIDANCE_SCALE,
|
|
198
|
+
temperature=_DEFAULT_TEMPERATURE,
|
|
199
|
+
top_p=_DEFAULT_TOP_P,
|
|
200
|
+
top_k=_DEFAULT_TOP_K,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
decoded = self._processor.batch_decode(output)
|
|
204
|
+
|
|
205
|
+
temp_path: Path | None = None
|
|
206
|
+
try:
|
|
207
|
+
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
|
|
208
|
+
temp_path = Path(tmp.name)
|
|
209
|
+
self._processor.save_audio(decoded, str(temp_path))
|
|
210
|
+
|
|
211
|
+
import soundfile as sf
|
|
212
|
+
|
|
213
|
+
audio, sample_rate = sf.read(str(temp_path), dtype="float32")
|
|
214
|
+
audio = np.asarray(audio, dtype=np.float32)
|
|
215
|
+
if audio.ndim > 1:
|
|
216
|
+
audio = audio.mean(axis=1)
|
|
217
|
+
|
|
218
|
+
chunk_size = sample_rate * 2
|
|
219
|
+
for i in range(0, len(audio), chunk_size):
|
|
220
|
+
chunk = audio[i : i + chunk_size]
|
|
221
|
+
yield SynthesizeChunk(
|
|
222
|
+
audio=chunk.tobytes(),
|
|
223
|
+
sample_rate=sample_rate,
|
|
224
|
+
is_final=False,
|
|
225
|
+
)
|
|
226
|
+
finally:
|
|
227
|
+
if temp_path is not None:
|
|
228
|
+
temp_path.unlink(missing_ok=True)
|
|
229
|
+
|
|
230
|
+
yield SynthesizeChunk(audio=b"", sample_rate=DIA_SAMPLE_RATE, is_final=True)
|
|
231
|
+
|
|
232
|
+
def list_voices(self) -> list[VoiceInfo]:
|
|
233
|
+
return []
|
|
234
|
+
|
|
235
|
+
def estimate_vram_bytes(self, **kwargs: Any) -> int:
|
|
236
|
+
return 10_000_000_000
|