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.
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .venv/
10
+ venv/
11
+ .env
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ .mypy_cache/
15
+ *.so
16
+ *.dylib
17
+ .coverage
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,3 @@
1
+ from vox_dia.adapter import DiaAdapter
2
+
3
+ __all__ = ["DiaAdapter"]
@@ -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