Hoanglinhn01 commited on
Commit
dfddb5d
·
verified ·
1 Parent(s): fe9455a

Upload 7 files

Browse files
Files changed (7) hide show
  1. README.md +282 -11
  2. config.yaml +81 -0
  3. gradio_app.py +965 -0
  4. main.py +80 -0
  5. pyproject.toml +70 -0
  6. requirements.txt +162 -0
  7. uv.lock +0 -0
README.md CHANGED
@@ -1,14 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Tts
3
- emoji: 😻
4
- colorFrom: pink
5
- colorTo: gray
6
- sdk: gradio
7
- sdk_version: 6.3.0
8
- app_file: app.py
9
- pinned: false
10
- license: apache-2.0
11
- short_description: vieneu_tts/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # VieNeu-TTS
2
+
3
+ [![GitHub](https://img.shields.io/badge/GitHub-Repository-blue)](https://github.com/pnnbao97/VieNeu-TTS)
4
+ [![Hugging Face](https://img.shields.io/badge/Hugging%20Face-0.5B-yellow)](https://huggingface.co/pnnbao-ump/VieNeu-TTS)
5
+ [![Hugging Face](https://img.shields.io/badge/Hugging%20Face-0.3B-orange)](https://huggingface.co/pnnbao-ump/VieNeu-TTS-0.3B)
6
+ [![Hugging Face](https://img.shields.io/badge/Hugging%20Face-0.3B--GGUF-green)](https://huggingface.co/pnnbao-ump/VieNeu-TTS-0.3B-q8-gguf)
7
+ [![Discord](https://img.shields.io/badge/Discord-Join%20Us-5865F2?logo=discord&logoColor=white)](https://discord.gg/mQWr4cp3)
8
+ [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1V1DjG-KdmurCAhvXrxxTLsa9tteDxSVO?usp=sharing)
9
+
10
+ <img width="899" height="615" alt="Untitled" src="https://github.com/user-attachments/assets/7eb9b816-6ab7-4049-866f-f85e36cb9c6f" />
11
+
12
+ **VieNeu-TTS** is an advanced on-device Vietnamese Text-to-Speech (TTS) model with **instant voice cloning**.
13
+
14
+ > [!TIP]
15
+ > **Voice Cloning:** All model variants (including GGUF) support instant voice cloning with just **3-5 seconds** of reference audio.
16
+
17
+ This project features two core architectures trained on the [VieNeu-TTS-1000h](https://huggingface.co/datasets/pnnbao-ump/VieNeu-TTS-1000h) dataset:
18
+ - **VieNeu-TTS (0.5B):** An enhanced model fine-tuned from the NeuTTS Air architecture for maximum stability.
19
+ - **VieNeu-TTS-0.3B:** A specialized model **trained from scratch**, delivering 2x faster inference and ultra-low latency.
20
+
21
+ These represent a significant upgrade from the previous VieNeu-TTS-140h with the following improvements:
22
+
23
+ - **Enhanced pronunciation**: More accurate and stable Vietnamese pronunciation
24
+ - **Code-switching support**: Seamless transitions between Vietnamese and English
25
+ - **Better voice cloning**: Higher fidelity and speaker consistency
26
+ - **Real-time synthesis**: 24 kHz waveform generation on CPU or GPU
27
+ - **Multiple model formats**: Support for PyTorch, GGUF Q4/Q8 (CPU optimized), and ONNX codec
28
+
29
+ VieNeu-TTS delivers production-ready speech synthesis fully offline.
30
+
31
+ **Author:** Phạm Nguyễn Ngọc Bảo
32
+
33
+ ---
34
+
35
+ [<img width="600" height="595" alt="VieNeu-TTS" src="https://github.com/user-attachments/assets/6b32df9d-7e2e-474f-94c8-43d6fa586d15" />](https://github.com/user-attachments/assets/6b32df9d-7e2e-474f-94c8-43d6fa586d15)
36
+
37
+ ---
38
+
39
+ ## 🔬 Model Overview
40
+
41
+ - **Backbone:**
42
+ - **VieNeu-TTS (0.5B):** Qwen-0.5B fine-tuned from [NeuTTS Air](https://huggingface.co/neuphonic/neutts-air).
43
+ - **VieNeu-TTS-0.3B:** Custom 0.3B model **trained from scratch**, optimized for extreme speed (2x faster).
44
+ - **Audio codec:** NeuCodec (torch implementation; ONNX & quantized variants supported)
45
+ - **Context window:** 2,048 tokens shared by prompt text and speech tokens
46
+ - **Output watermark:** Enabled by default
47
+ - **Training data:** [VieNeu-TTS-1000h](https://huggingface.co/datasets/pnnbao-ump/VieNeu-TTS-1000h) — 443,641 curated Vietnamese samples (Used for both versions).
48
+
49
+ ### Model Variants
50
+
51
+ | Model | Format | Device | Quality | Speed |
52
+ | ----------------------- | ------- | ------- | ---------- | ----------------------- |
53
+ | VieNeu-TTS | PyTorch | GPU/CPU | ⭐⭐⭐⭐⭐ | Very Fast with lmdeploy |
54
+ | VieNeu-TTS-0.3B | PyTorch | GPU/CPU | ⭐⭐⭐⭐ | **Ultra Fast (2x)** |
55
+ | VieNeu-TTS-q8-gguf | GGUF Q8 | CPU/GPU | ⭐⭐⭐⭐ | Fast |
56
+ | VieNeu-TTS-q4-gguf | GGUF Q4 | CPU/GPU | ⭐⭐⭐ | Very Fast |
57
+ | VieNeu-TTS-0.3B-q8-gguf | GGUF Q8 | CPU/GPU | ⭐⭐⭐⭐ | **Ultra Fast (1.5x)** |
58
+ | VieNeu-TTS-0.3B-q4-gguf | GGUF Q4 | CPU/GPU | ⭐⭐⭐ | **Extreme Speed (2x)** |
59
+
60
+ **Recommendations:**
61
+
62
+ - **GPU users**: Use `VieNeu-TTS` (PyTorch) for best quality
63
+ - **CPU users**: Use `VieNeu-TTS-0.3B-q4-gguf` for fastest inference or `VieNeu-TTS-0.3B-q8-gguf` for best CPU quality.
64
+ - **Streaming**: Only GGUF models support streaming inference (Requires `llama-cpp-python >= 0.3.16`)
65
+
66
+ ---
67
+
68
+ ## ✅ Todo & Status
69
+
70
+ - [x] Publish safetensor artifacts
71
+ - [x] Release GGUF Q4 / Q8 models
72
+ - [x] Release datasets (1000h and 140h)
73
+ - [x] Enable streaming on GPU
74
+ - [x] Provide Dockerized setup
75
+ - [ ] Release fine-tuning code
76
+
77
+ ---
78
+
79
+ ## 🏁 Getting Started
80
+
81
+ ### 1. Clone the repository
82
+ ```bash
83
+ git clone https://github.com/pnnbao97/VieNeu-TTS.git
84
+ cd VieNeu-TTS
85
+ ```
86
+
87
+ ### 2. Install eSpeak NG (Required)
88
+ Phonemizer requires eSpeak NG to function.
89
+
90
+ - **Windows:** Download installer from [eSpeak NG Releases](https://github.com/espeak-ng/espeak-ng/releases) (Recommended: `.msi`).
91
+ - **macOS:** `brew install espeak`
92
+ - **Ubuntu/Debian:** `sudo apt install espeak-ng`
93
+ - **Arch Linux:** `paru -S aur/espeak-ng`
94
+
95
+ ---
96
+
97
+ ### 3. Environment Setup (Choose ONE method)
98
+
99
+ #### Method 1: Standard with `uv` (Recommended)
100
+ This is the fastest and most reliable way to manage dependencies.
101
+
102
+ **A. Install `uv`** (If you haven't already):
103
+ - **Windows:** `powershell -c "irm https://astral.sh/uv/install.ps1 | iex"`
104
+ - **Linux/macOS:** `curl -LsSf https://astral.sh/uv/install.sh | sh`
105
+
106
+ **B. Choose your hardware:**
107
+
108
+ **Option A: For GPU Users (NVIDIA 30xx/40xx/50xx)**
109
+
110
+ > [!IMPORTANT]
111
+ > **Update your NVIDIA Drivers & Install CUDA Toolkit!**
112
+ > This project uses **CUDA 12.8**. Please ensure your NVIDIA driver is up-to-date (support CUDA 12.8 or newer) to avoid compatibility issues, especially on RTX 30 series.
113
+ >
114
+ > To use `lmdeploy`, you **MUST** install the **NVIDIA GPU Computing Toolkit**: [https://developer.nvidia.com/cuda-downloads](https://developer.nvidia.com/cuda-downloads).
115
+
116
+ ```bash
117
+ uv sync
118
+ ```
119
+
120
+ **Option B: For CPU-only Users**
121
+
122
+ 1. Switch to CPU configuration:
123
+ ```bash
124
+ # Windows:
125
+ ren pyproject.toml pyproject.toml.bak
126
+ copy pyproject.toml.cpu pyproject.toml
127
+
128
+ # Linux/macOS:
129
+ mv pyproject.toml pyproject.toml.bak
130
+ cp pyproject.toml.cpu pyproject.toml
131
+ ```
132
+ 2. Install dependencies:
133
+ ```bash
134
+ uv sync
135
+ ```
136
+
137
+ **C. Run the Application:**
138
+ ```bash
139
+ uv run gradio_app.py
140
+ ```
141
+
142
+ Then access the Web UI at `http://127.0.0.1:7860`.
143
+
144
+ ---
145
+
146
+ #### Method 2: Automatic with Makefile (Alternative)
147
+ Best if you have `make` installed (standard on Linux/macOS, or via Git Bash on Windows). It handles configuration swaps automatically.
148
+
149
+ - **Setup GPU:** `make setup-gpu`
150
+ - **Setup CPU:** `make setup-cpu`
151
+ - **Run Demo:** `make demo`
152
+
153
+
154
+ Then access the Web UI at `http://127.0.0.1:7860`.
155
+
156
+ ---
157
+
158
+ ---
159
+
160
+ ## 🐋 Docker Deployment
161
+
162
+ For a quick start or production deployment without manually installing dependencies, use Docker.
163
+
164
+ ### Quick Start
165
+
166
+ Copy .env.example to .env
167
+
168
+ ```
169
+ cp .env.example .env
170
+ ```
171
+
172
+ Build and start container
173
+
174
+ ```bash
175
+ # Run with CPU
176
+ docker compose --profile cpu up
177
+
178
+ # Run with GPU (requires NVIDIA Container Toolkit)
179
+ docker compose --profile gpu up
180
+ ```
181
+
182
+ Access the Web UI at `http://localhost:7860`.
183
+
184
+ For detailed deployment instructions, including production setup, see [docs/Deploy.md](docs/Deploy.md).
185
+
186
+ ---
187
+
188
+ ## 📦 Project Structure
189
+
190
+ ```
191
+ VieNeu-TTS/
192
+ ├── examples/
193
+ │ ├── infer_long_text.py # CLI for long-form synthesis (chunked)
194
+ │ └── sample_long_text.txt # Example paragraph for testing
195
+ ├── gradio_app.py # Local Gradio web demo with LMDeploy support
196
+ ├── main.py # Basic batch inference script
197
+ ├── config.yaml # Configuration for models, codecs, and voices
198
+ ├── output_audio/ # Generated audio (created when running scripts)
199
+ ├── sample/ # Reference voices (audio + transcript + codes)
200
+ │ ├── Bình (nam miền Bắc).wav/txt/pt
201
+ │ ├── Đoan (nữ miền Nam).wav/txt/pt
202
+ │ ├── Dung (nữ miền Nam).wav/txt/pt
203
+ │ ├── Hương (nữ miền Bắc).wav/txt/pt
204
+ │ ├── Ly (nữ miền Bắc).wav/txt/pt
205
+ │ ├── Ngọc (nữ miền Bắc).wav/txt/pt
206
+ │ ├── Nguyên (nam miền Nam).wav/txt/pt
207
+ │ ├── Sơn (nam miền Nam).wav/txt/pt
208
+ │ ├── Tuyên (nam miền Bắc).wav/txt/pt
209
+ │ └── Vĩnh (nam miền Nam).wav/txt/pt
210
+ ├── utils/
211
+ │ ├── __init__.py
212
+ │ ├── core_utils.py # Text chunking utilities
213
+ │ ├── normalize_text.py # Vietnamese text normalization pipeline
214
+ │ ├── phonemize_text.py # Text to phoneme conversion
215
+ │ └── phoneme_dict.json # Phoneme dictionary
216
+ ├── vieneu_tts/
217
+ │ ├── __init__.py # Exports VieNeuTTS and FastVieNeuTTS
218
+ │ └── vieneu_tts.py # Core VieNeuTTS implementation (VieNeuTTS & FastVieNeuTTS)
219
+ ├── README.md
220
+ ├── requirements.txt # Basic dependencies (legacy)
221
+ ├── pyproject.toml # Project configuration with full dependencies (UV)
222
+ └── uv.lock # UV lock file for dependency management
223
+ ```
224
+
225
  ---
226
+
227
+ ## 📚 References
228
+
229
+ - [GitHub Repository](https://github.com/pnnbao97/VieNeu-TTS)
230
+ - [Hugging Face Model (0.5B)](https://huggingface.co/pnnbao-ump/VieNeu-TTS)
231
+ - [Hugging Face Model (0.3B)](https://huggingface.co/pnnbao-ump/VieNeu-TTS-0.3B)
232
+ - [VieNeuTTS Fine-tuning Guide](https://github.com/pnnbao-ump/VieNeuTTS/blob/main/finetune.ipynb)
233
+ - [VieNeuCodec dataset](https://huggingface.co/datasets/pnnbao-ump/VieNeuCodec-dataset)
234
+
235
+ ---
236
+
237
+ ## 📄 License
238
+
239
+ - **VieNeu-TTS (0.5B):** Original terms (Apache 2.0).
240
+ - **VieNeu-TTS-0.3B:** Released under **CC BY-NC 4.0** (Non-Commercial).
241
+ - This version is currently **experimental**.
242
+ - **Commercial use is prohibited** without authorization. Please contact the author for commercial licensing.
243
+
244
+ ---
245
+
246
+ ## 📑 Citation
247
+
248
+ ```bibtex
249
+ @misc{vieneutts2026,
250
+ title = {VieNeu-TTS: Vietnamese Text-to-Speech with Instant Voice Cloning},
251
+ author = {Pham Nguyen Ngoc Bao},
252
+ year = {2026},
253
+ publisher = {Hugging Face},
254
+ howpublished = {\url{https://huggingface.co/pnnbao-ump/VieNeu-TTS}}
255
+ }
256
+ ```
257
+
258
+ ## 🤝 Contributing
259
+
260
+ Contributions are welcome!
261
+
262
+ 1. Fork the repository
263
+ 2. Create a feature branch: `git checkout -b feature/amazing-feature`
264
+ 3. Commit your changes: `git commit -m "Add amazing feature"`
265
+ 4. Push the branch: `git push origin feature/amazing-feature`
266
+ 5. Open a pull request
267
+
268
+ ---
269
+
270
+ ## 📞 Support
271
+
272
+ - GitHub Issues: [github.com/pnnbao97/VieNeu-TTS/issues](https://github.com/pnnbao97/VieNeu-TTS/issues)
273
+ - Hugging Face: [huggingface.co/pnnbao-ump](https://huggingface.co/pnnbao-ump)
274
+ - Discord: [Join with us](https://discord.gg/mQWr4cp3)
275
+ - Facebook: [Phạm Nguyễn Ngọc Bảo](https://www.facebook.com/bao.phamnguyenngoc.5)
276
+
277
+ ---
278
+
279
+ ## 🙏 Acknowledgements
280
+
281
+ This project builds upon [NeuTTS Air](https://huggingface.co/neuphonic/neutts-air) for the original 0.5B model. The 0.3B version is a custom architecture trained from scratch using the [VieNeu-TTS-1000h](https://huggingface.co/datasets/pnnbao-ump/VieNeu-TTS-1000h) dataset.
282
+
283
  ---
284
 
285
+ **Made with ❤️ for the Vietnamese TTS community**
config.yaml ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ text_settings:
2
+ max_chars_per_chunk: 256
3
+ max_total_chars_streaming: 3000
4
+
5
+ backbone_configs:
6
+ "VieNeu-TTS (GPU)":
7
+ repo: pnnbao-ump/VieNeu-TTS
8
+ supports_streaming: false
9
+ description: Chất lượng cao nhất, yêu cầu GPU
10
+ "VieNeu-TTS-0.3B (GPU)":
11
+ repo: pnnbao-ump/VieNeu-TTS-0.3B
12
+ supports_streaming: false
13
+ description: Phiên bản nhẹ cho GPU, tốc độ nhanh x2 so với phiên bản gốc
14
+ "VieNeu-TTS-q8-gguf":
15
+ repo: pnnbao-ump/VieNeu-TTS-q8-gguf
16
+ supports_streaming: true
17
+ description: Phiên bản GGUF có chất lượng cao nhất
18
+ "VieNeu-TTS-q4-gguf":
19
+ repo: pnnbao-ump/VieNeu-TTS-q4-gguf
20
+ supports_streaming: true
21
+ description: Cân bằng giữa chất lượng và tốc độ
22
+ "VieNeu-TTS-0.3B-q4-gguf":
23
+ repo: pnnbao-ump/VieNeu-TTS-0.3B-q4-gguf
24
+ supports_streaming: true
25
+ description: Phiên bản cực nhẹ, chạy mượt trên CPU
26
+ "VieNeu-TTS-0.3B-q8-gguf":
27
+ repo: pnnbao-ump/VieNeu-TTS-0.3B-q8-gguf
28
+ supports_streaming: true
29
+ description: Phiên bản 0.3B nén Q8, chất lượng cao và tốc độ nhanh trên CPU
30
+
31
+ codec_configs:
32
+ "NeuCodec (Standard)":
33
+ repo: neuphonic/neucodec
34
+ description: Codec chuẩn, tốc độ trung bình
35
+ use_preencoded: false
36
+ "NeuCodec (Distill)":
37
+ repo: neuphonic/distill-neucodec
38
+ description: Codec tối ưu, tốc độ cao
39
+ use_preencoded: false
40
+ "NeuCodec ONNX (Fast CPU)":
41
+ repo: neuphonic/neucodec-onnx-decoder-int8
42
+ description: Tối ưu cho CPU, cần pre-encoded codes
43
+ use_preencoded: true
44
+
45
+ voice_samples:
46
+ "Tuyên (nam miền Bắc)":
47
+ audio: ./sample/Tuyên (nam miền Bắc).wav
48
+ text: ./sample/Tuyên (nam miền Bắc).txt
49
+ codes: ./sample/Tuyên (nam miền Bắc).pt
50
+ "Vĩnh (nam miền Nam)":
51
+ audio: ./sample/Vĩnh (nam miền Nam).wav
52
+ text: ./sample/Vĩnh (nam miền Nam).txt
53
+ codes: ./sample/Vĩnh (nam miền Nam).pt
54
+ "Bình (nam miền Bắc)":
55
+ audio: ./sample/Bình (nam miền Bắc).wav
56
+ text: ./sample/Bình (nam miền Bắc).txt
57
+ codes: ./sample/Bình (nam miền Bắc).pt
58
+ "Nguyên (nam miền Nam)":
59
+ audio: ./sample/Nguyên (nam miền Nam).wav
60
+ text: ./sample/Nguyên (nam miền Nam).txt
61
+ codes: ./sample/Nguyên (nam miền Nam).pt
62
+ "Sơn (nam miền Nam)":
63
+ audio: ./sample/Sơn (nam miền Nam).wav
64
+ text: ./sample/Sơn (nam miền Nam).txt
65
+ codes: ./sample/Sơn (nam miền Nam).pt
66
+ "Đoan (nữ miền Nam)":
67
+ audio: ./sample/Đoan (nữ miền Nam).wav
68
+ text: ./sample/Đoan (nữ miền Nam).txt
69
+ codes: ./sample/Đoan (nữ miền Nam).pt
70
+ "Ngọc (nữ miền Bắc)":
71
+ audio: ./sample/Ngọc (nữ miền Bắc).wav
72
+ text: ./sample/Ngọc (nữ miền Bắc).txt
73
+ codes: ./sample/Ngọc (nữ miền Bắc).pt
74
+ "Ly (nữ miền Bắc)":
75
+ audio: ./sample/Ly (nữ miền Bắc).wav
76
+ text: ./sample/Ly (nữ miền Bắc).txt
77
+ codes: ./sample/Ly (nữ miền Bắc).pt
78
+ "Dung (nữ miền Nam)":
79
+ audio: ./sample/Dung (nữ miền Nam).wav
80
+ text: ./sample/Dung (nữ miền Nam).txt
81
+ codes: ./sample/Dung (nữ miền Nam).pt
gradio_app.py ADDED
@@ -0,0 +1,965 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ print("⏳ Đang khởi động... Vui lòng chờ...")
3
+ import soundfile as sf
4
+ import tempfile
5
+ import torch
6
+ from vieneu_tts import VieNeuTTS, FastVieNeuTTS
7
+ import os
8
+ import sys
9
+ import time
10
+ import numpy as np
11
+ from typing import Generator, Optional, Tuple
12
+ import queue
13
+ import threading
14
+ import yaml
15
+ from utils.core_utils import split_text_into_chunks, env_bool
16
+ from functools import lru_cache
17
+ import gc
18
+
19
+ print("⏳ Đang khởi động VieNeu-TTS...")
20
+
21
+ # --- CONSTANTS & CONFIG ---
22
+ CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.yaml")
23
+ try:
24
+ with open(CONFIG_PATH, "r", encoding="utf-8") as f:
25
+ _config = yaml.safe_load(f) or {}
26
+ except Exception as e:
27
+ raise RuntimeError(f"Không thể đọc config.yaml: {e}")
28
+
29
+ BACKBONE_CONFIGS = _config.get("backbone_configs", {})
30
+ CODEC_CONFIGS = _config.get("codec_configs", {})
31
+ VOICE_SAMPLES = _config.get("voice_samples", {})
32
+
33
+ _text_settings = _config.get("text_settings", {})
34
+ MAX_CHARS_PER_CHUNK = _text_settings.get("max_chars_per_chunk", 256)
35
+ MAX_TOTAL_CHARS_STREAMING = _text_settings.get("max_total_chars_streaming", 3000)
36
+
37
+ if not BACKBONE_CONFIGS or not CODEC_CONFIGS:
38
+ raise ValueError("config.yaml thiếu backbone_configs hoặc codec_configs")
39
+ if not VOICE_SAMPLES:
40
+ raise ValueError("config.yaml thiếu voice_samples")
41
+
42
+ # --- 1. MODEL CONFIGURATION ---
43
+ # Global model instance
44
+ tts = None
45
+ current_backbone = None
46
+ current_codec = None
47
+ model_loaded = False
48
+ using_lmdeploy = False
49
+
50
+ # Cache for reference texts
51
+ _ref_text_cache = {}
52
+
53
+ def get_available_devices() -> list[str]:
54
+ """Get list of available devices for current platform."""
55
+ devices = ["Auto", "CPU"]
56
+
57
+ if sys.platform == "darwin":
58
+ # macOS - check MPS
59
+ if torch.backends.mps.is_available():
60
+ devices.append("MPS")
61
+ else:
62
+ # Windows/Linux - check CUDA
63
+ if torch.cuda.is_available():
64
+ devices.append("CUDA")
65
+
66
+ return devices
67
+
68
+ def get_model_status_message() -> str:
69
+ """Reconstruct status message from global state"""
70
+ global model_loaded, tts, using_lmdeploy, current_backbone, current_codec
71
+ if not model_loaded or tts is None:
72
+ return "⏳ Chưa tải model."
73
+
74
+ backbone_config = BACKBONE_CONFIGS.get(current_backbone, {})
75
+ codec_config = CODEC_CONFIGS.get(current_codec, {})
76
+
77
+ backend_name = "🚀 LMDeploy (Optimized)" if using_lmdeploy else "📦 Standard"
78
+
79
+ # We don't track the exact device strings perfectly in global state, so we estimate
80
+ device_info = "GPU" if using_lmdeploy else "Auto"
81
+ codec_device = "CPU" if "ONNX" in (current_codec or "") else ("GPU/MPS" if torch.cuda.is_available() or torch.backends.mps.is_available() else "CPU")
82
+
83
+ preencoded_note = "\n⚠️ Codec ONNX không hỗ trợ chức năng clone giọng nói." if codec_config.get('use_preencoded') else ""
84
+
85
+ opt_info = ""
86
+ if using_lmdeploy and hasattr(tts, 'get_optimization_stats'):
87
+ stats = tts.get_optimization_stats()
88
+ opt_info = (
89
+ f"\n\n🔧 Tối ưu hóa:"
90
+ f"\n • Triton: {'✅' if stats['triton_enabled'] else '❌'}"
91
+ f"\n • Max Batch Size (Default): {stats.get('max_batch_size', 'N/A')}"
92
+ f"\n • Reference Cache: {stats['cached_references']} voices"
93
+ f"\n • Prefix Caching: ✅"
94
+ )
95
+
96
+ return (
97
+ f"✅ Model đã tải thành công!\n\n"
98
+ f"🔧 Backend: {backend_name}\n"
99
+ f"🦜 Backbone: {current_backbone}\n"
100
+ f"🎵 Codec: {current_codec}{preencoded_note}{opt_info}"
101
+ )
102
+
103
+ def restore_ui_state():
104
+ """Update UI components based on persistence"""
105
+ global model_loaded
106
+ msg = get_model_status_message()
107
+ return (
108
+ msg,
109
+ gr.update(interactive=model_loaded), # btn_generate
110
+ gr.update(interactive=False) # btn_stop
111
+ )
112
+
113
+ def should_use_lmdeploy(backbone_choice: str, device_choice: str) -> bool:
114
+ """Determine if we should use LMDeploy backend."""
115
+ # LMDeploy not supported on macOS
116
+ if sys.platform == "darwin":
117
+ return False
118
+
119
+ if "gguf" in backbone_choice.lower():
120
+ return False
121
+
122
+ if device_choice == "Auto":
123
+ has_gpu = torch.cuda.is_available()
124
+ elif device_choice == "CUDA":
125
+ has_gpu = torch.cuda.is_available()
126
+ else:
127
+ has_gpu = False
128
+
129
+ return has_gpu
130
+
131
+ @lru_cache(maxsize=32)
132
+ def get_ref_text_cached(text_path: str) -> str:
133
+ """Cache reference text loading"""
134
+ with open(text_path, "r", encoding="utf-8") as f:
135
+ return f.read()
136
+
137
+ def cleanup_gpu_memory():
138
+ """Aggressively cleanup GPU memory"""
139
+ if torch.cuda.is_available():
140
+ torch.cuda.empty_cache()
141
+ torch.cuda.synchronize()
142
+ elif torch.backends.mps.is_available():
143
+ torch.mps.empty_cache()
144
+ gc.collect()
145
+
146
+ def load_model(backbone_choice: str, codec_choice: str, device_choice: str,
147
+ force_lmdeploy: bool):
148
+ """Load model with optimizations and max batch size control"""
149
+ global tts, current_backbone, current_codec, model_loaded, using_lmdeploy
150
+ lmdeploy_error_reason = None
151
+
152
+ yield (
153
+ "⏳ Đang tải model với tối ưu hóa... Lưu ý: Quá trình này sẽ tốn thời gian. Vui lòng kiên nhẫn.",
154
+ gr.update(interactive=False),
155
+ gr.update(interactive=False),
156
+ gr.update(interactive=False)
157
+ )
158
+
159
+ try:
160
+ # Cleanup before loading new model
161
+ if model_loaded and tts is not None:
162
+ del tts
163
+ cleanup_gpu_memory()
164
+
165
+ backbone_config = BACKBONE_CONFIGS[backbone_choice]
166
+ codec_config = CODEC_CONFIGS[codec_choice]
167
+
168
+ use_lmdeploy = force_lmdeploy and should_use_lmdeploy(backbone_choice, device_choice)
169
+
170
+ if use_lmdeploy:
171
+ lmdeploy_error_reason = None
172
+ print(f"🚀 Using LMDeploy backend with optimizations")
173
+
174
+ backbone_device = "cuda"
175
+
176
+ if "ONNX" in codec_choice:
177
+ codec_device = "cpu"
178
+ else:
179
+ codec_device = "cuda" if torch.cuda.is_available() else "cpu"
180
+
181
+ print(f"📦 Loading optimized model...")
182
+ print(f" Backbone: {backbone_config['repo']} on {backbone_device}")
183
+ print(f" Codec: {codec_config['repo']} on {codec_device}")
184
+ print(f" Triton: Enabled")
185
+
186
+ try:
187
+ tts = FastVieNeuTTS(
188
+ backbone_repo=backbone_config["repo"],
189
+ backbone_device=backbone_device,
190
+ codec_repo=codec_config["repo"],
191
+ codec_device=codec_device,
192
+ memory_util=0.3,
193
+ tp=1,
194
+ enable_prefix_caching=True,
195
+ enable_triton=True,
196
+ )
197
+ using_lmdeploy = True
198
+
199
+ # Pre-cache voice references
200
+ print("📝 Pre-caching voice references...")
201
+ for voice_name, voice_info in VOICE_SAMPLES.items():
202
+ audio_path = voice_info["audio"]
203
+ text_path = voice_info["text"]
204
+ if os.path.exists(audio_path) and os.path.exists(text_path):
205
+ ref_text = get_ref_text_cached(text_path)
206
+ tts.get_cached_reference(voice_name, audio_path, ref_text)
207
+ print(f" ✅ Cached {len(VOICE_SAMPLES)} voices")
208
+
209
+ except Exception as e:
210
+ import traceback
211
+ traceback.print_exc()
212
+
213
+ error_str = str(e)
214
+ if "$env:CUDA_PATH" in error_str:
215
+ lmdeploy_error_reason = "Không tìm thấy biến môi trường CUDA_PATH. Vui lòng cài đặt NVIDIA GPU Computing Toolkit."
216
+ else:
217
+ lmdeploy_error_reason = f"{error_str}"
218
+
219
+ yield (
220
+ f"⚠️ LMDeploy Init Error: {lmdeploy_error_reason}. Đang loading model với backend mặc định - tốc độ chậm hơn so với lmdeploy...",
221
+ gr.update(interactive=False),
222
+ gr.update(interactive=False)
223
+ )
224
+ time.sleep(1)
225
+ use_lmdeploy = False
226
+ using_lmdeploy = False
227
+
228
+ if not use_lmdeploy:
229
+ print(f"📦 Using original backend")
230
+
231
+ if device_choice == "Auto":
232
+ if "gguf" in backbone_choice.lower():
233
+ # GGUF: uses Metal on Mac, CUDA on Windows/Linux
234
+ if sys.platform == "darwin":
235
+ backbone_device = "gpu" # llama-cpp-python uses Metal
236
+ else:
237
+ backbone_device = "gpu" if torch.cuda.is_available() else "cpu"
238
+ else:
239
+ # PyTorch model
240
+ if sys.platform == "darwin":
241
+ backbone_device = "mps" if torch.backends.mps.is_available() else "cpu"
242
+ else:
243
+ backbone_device = "cuda" if torch.cuda.is_available() else "cpu"
244
+
245
+ # Codec device
246
+ if "ONNX" in codec_choice:
247
+ codec_device = "cpu"
248
+ elif sys.platform == "darwin":
249
+ codec_device = "mps" if torch.backends.mps.is_available() else "cpu"
250
+ else:
251
+ codec_device = "cuda" if torch.cuda.is_available() else "cpu"
252
+
253
+ elif device_choice == "MPS":
254
+ backbone_device = "mps"
255
+ codec_device = "mps" if "ONNX" not in codec_choice else "cpu"
256
+
257
+ else:
258
+ backbone_device = device_choice.lower()
259
+ codec_device = device_choice.lower()
260
+
261
+ if "ONNX" in codec_choice:
262
+ codec_device = "cpu"
263
+
264
+ if "gguf" in backbone_choice.lower() and backbone_device == "cuda":
265
+ backbone_device = "gpu"
266
+
267
+ print(f"📦 Loading model...")
268
+ print(f" Backbone: {backbone_config['repo']} on {backbone_device}")
269
+ print(f" Codec: {codec_config['repo']} on {codec_device}")
270
+
271
+ tts = VieNeuTTS(
272
+ backbone_repo=backbone_config["repo"],
273
+ backbone_device=backbone_device,
274
+ codec_repo=codec_config["repo"],
275
+ codec_device=codec_device
276
+ )
277
+ using_lmdeploy = False
278
+
279
+ current_backbone = backbone_choice
280
+ current_codec = codec_choice
281
+ model_loaded = True
282
+
283
+ # Success message with optimization info
284
+ backend_name = "🚀 LMDeploy (Optimized)" if using_lmdeploy else "📦 Standard"
285
+ device_info = "cuda" if use_lmdeploy else (backbone_device if not use_lmdeploy else "N/A")
286
+
287
+ streaming_support = "✅ Có" if backbone_config['supports_streaming'] else "❌ Không"
288
+ preencoded_note = "\n⚠️ Codec này cần sử dụng pre-encoded codes (.pt files)" if codec_config['use_preencoded'] else ""
289
+
290
+ opt_info = ""
291
+ if using_lmdeploy and hasattr(tts, 'get_optimization_stats'):
292
+ stats = tts.get_optimization_stats()
293
+ opt_info = (
294
+ f"\n\n🔧 Tối ưu hóa:"
295
+ f"\n • Triton: {'✅' if stats['triton_enabled'] else '❌'}"
296
+ f"\n • Max Batch Size (Default): {stats.get('max_batch_size', 'N/A')}"
297
+ f"\n • Reference Cache: {stats['cached_references']} voices"
298
+ f"\n • Prefix Caching: ✅"
299
+ )
300
+
301
+ warning_msg = ""
302
+ if lmdeploy_error_reason:
303
+ warning_msg = (
304
+ f"\n\n⚠️ **Cảnh báo:** Không thể kích hoạt LMDeploy (Optimized Backend) do lỗi sau:\n"
305
+ f"👉 {lmdeploy_error_reason}\n"
306
+ f"💡 Hệ thống đã tự động chuyển về chế độ Standard (chậm hơn)."
307
+ )
308
+
309
+ success_msg = get_model_status_message()
310
+ if warning_msg:
311
+ success_msg += warning_msg
312
+
313
+ yield (
314
+ success_msg,
315
+ gr.update(interactive=True), # btn_generate
316
+ gr.update(interactive=True), # btn_load
317
+ gr.update(interactive=False) # btn_stop
318
+ )
319
+
320
+ except Exception as e:
321
+ import traceback
322
+ traceback.print_exc()
323
+ model_loaded = False
324
+ using_lmdeploy = False
325
+
326
+ if "$env:CUDA_PATH" in str(e):
327
+ yield (
328
+ "❌ Lỗi khi tải model: Không tìm thấy biến môi trường CUDA_PATH. Vui lòng cài đặt NVIDIA GPU Computing Toolkit (https://developer.nvidia.com/cuda/toolkit)",
329
+ gr.update(interactive=False),
330
+ gr.update(interactive=True),
331
+ gr.update(interactive=False)
332
+ )
333
+ else:
334
+ yield (
335
+ f"❌ Lỗi khi tải model: {str(e)}",
336
+ gr.update(interactive=False),
337
+ gr.update(interactive=True),
338
+ gr.update(interactive=False)
339
+ )
340
+
341
+
342
+ # --- 2. DATA & HELPERS ---
343
+ GGUF_ALLOWED_VOICES = [
344
+ "Vĩnh (nam miền Nam)",
345
+ "Bình (nam miền Bắc)",
346
+ "Ngọc (nữ miền Bắc)",
347
+ "Dung (nữ miền Nam)",
348
+ ]
349
+
350
+ def get_voice_options(backbone_choice: str):
351
+ """Filter voice options: GGUF only shows the 4 allowed voices."""
352
+ if "gguf" in backbone_choice.lower():
353
+ return [v for v in GGUF_ALLOWED_VOICES if v in VOICE_SAMPLES]
354
+ return list(VOICE_SAMPLES.keys())
355
+
356
+ def update_voice_dropdown(backbone_choice: str, current_voice: str):
357
+ options = get_voice_options(backbone_choice)
358
+ new_value = current_voice if current_voice in options else (options[0] if options else None)
359
+ return gr.update(choices=options, value=new_value)
360
+
361
+ # --- 3. CORE LOGIC FUNCTIONS ---
362
+ def load_reference_info(voice_choice: str) -> Tuple[Optional[str], str]:
363
+ """Load reference audio and text with caching"""
364
+ if voice_choice in VOICE_SAMPLES:
365
+ audio_path = VOICE_SAMPLES[voice_choice]["audio"]
366
+ text_path = VOICE_SAMPLES[voice_choice]["text"]
367
+ try:
368
+ if os.path.exists(text_path):
369
+ ref_text = get_ref_text_cached(text_path)
370
+ return audio_path, ref_text
371
+ else:
372
+ return audio_path, "⚠️ Không tìm thấy file text mẫu."
373
+ except Exception as e:
374
+ return None, f"❌ Lỗi: {str(e)}"
375
+ return None, ""
376
+
377
+ def synthesize_speech(text: str, voice_choice: str, custom_audio, custom_text: str,
378
+ mode_tab: str, generation_mode: str, use_batch: bool, max_batch_size_run: int):
379
+ """Synthesis with optimization support and max batch size control"""
380
+ global tts, current_backbone, current_codec, model_loaded, using_lmdeploy
381
+
382
+ if not model_loaded or tts is None:
383
+ yield None, "⚠️ Vui lòng tải model trước!"
384
+ return
385
+
386
+ if not text or text.strip() == "":
387
+ yield None, "⚠️ Vui lòng nhập văn bản!"
388
+ return
389
+
390
+ raw_text = text.strip()
391
+
392
+ codec_config = CODEC_CONFIGS[current_codec]
393
+ use_preencoded = codec_config['use_preencoded']
394
+
395
+ # Setup Reference
396
+ if mode_tab == "custom_mode":
397
+ if custom_audio is None or not custom_text:
398
+ yield None, "⚠️ Thiếu Audio hoặc Text mẫu custom."
399
+ return
400
+ ref_audio_path = custom_audio
401
+ ref_text_raw = custom_text
402
+ ref_codes_path = None
403
+ else:
404
+ if voice_choice not in VOICE_SAMPLES:
405
+ yield None, "⚠️ Vui lòng chọn giọng mẫu."
406
+ return
407
+ ref_audio_path = VOICE_SAMPLES[voice_choice]["audio"]
408
+ text_path = VOICE_SAMPLES[voice_choice]["text"]
409
+ ref_codes_path = VOICE_SAMPLES[voice_choice]["codes"]
410
+
411
+ if not os.path.exists(ref_audio_path):
412
+ yield None, "❌ Không tìm thấy file audio mẫu."
413
+ return
414
+
415
+ ref_text_raw = get_ref_text_cached(text_path)
416
+
417
+ yield None, "📄 Đang xử lý Reference..."
418
+
419
+ # Encode or get cached reference
420
+ try:
421
+ if use_preencoded and ref_codes_path and os.path.exists(ref_codes_path):
422
+ ref_codes = torch.load(ref_codes_path, map_location="cpu", weights_only=True)
423
+ else:
424
+ # Use cached reference if available (LMDeploy only)
425
+ if using_lmdeploy and hasattr(tts, 'get_cached_reference') and mode_tab == "preset_mode":
426
+ ref_codes = tts.get_cached_reference(voice_choice, ref_audio_path, ref_text_raw)
427
+ else:
428
+ ref_codes = tts.encode_reference(ref_audio_path)
429
+
430
+ if isinstance(ref_codes, torch.Tensor):
431
+ ref_codes = ref_codes.cpu().numpy()
432
+ except Exception as e:
433
+ yield None, f"❌ Lỗi xử lý reference: {e}"
434
+ return
435
+
436
+ text_chunks = split_text_into_chunks(raw_text, max_chars=MAX_CHARS_PER_CHUNK)
437
+ total_chunks = len(text_chunks)
438
+
439
+ # === STANDARD MODE ===
440
+ if generation_mode == "Standard (Một lần)":
441
+ backend_name = "LMDeploy" if using_lmdeploy else "Standard"
442
+ batch_info = " (Batch Mode)" if use_batch and using_lmdeploy and total_chunks > 1 else ""
443
+
444
+ # Show batch size info
445
+ batch_size_info = ""
446
+ if use_batch and using_lmdeploy and hasattr(tts, 'max_batch_size'):
447
+ batch_size_info = f" [Max batch: {tts.max_batch_size}]"
448
+
449
+ yield None, f"🚀 Bắt đầu tổng hợp {backend_name}{batch_info}{batch_size_info} ({total_chunks} đoạn)..."
450
+
451
+ all_audio_segments = []
452
+ sr = 24000
453
+ silence_pad = np.zeros(int(sr * 0.15), dtype=np.float32)
454
+
455
+ start_time = time.time()
456
+
457
+ try:
458
+ # Use batch processing if enabled and using LMDeploy
459
+ if use_batch and using_lmdeploy and hasattr(tts, 'infer_batch') and total_chunks > 1:
460
+ # Show how many mini-batches will be processed
461
+ num_batches = (total_chunks + max_batch_size_run - 1) // max_batch_size_run
462
+
463
+ yield None, f"⚡ Xử lý {num_batches} mini-batch(es) (max {max_batch_size_run} đoạn/batch)..."
464
+
465
+ chunk_wavs = tts.infer_batch(text_chunks, ref_codes, ref_text_raw, max_batch_size=max_batch_size_run)
466
+
467
+ for i, chunk_wav in enumerate(chunk_wavs):
468
+ if chunk_wav is not None and len(chunk_wav) > 0:
469
+ all_audio_segments.append(chunk_wav)
470
+ if i < total_chunks - 1:
471
+ all_audio_segments.append(silence_pad)
472
+ else:
473
+ # Sequential processing
474
+ for i, chunk in enumerate(text_chunks):
475
+ yield None, f"⏳ Đang xử lý đoạn {i+1}/{total_chunks}..."
476
+
477
+ chunk_wav = tts.infer(chunk, ref_codes, ref_text_raw)
478
+
479
+ if chunk_wav is not None and len(chunk_wav) > 0:
480
+ all_audio_segments.append(chunk_wav)
481
+ if i < total_chunks - 1:
482
+ all_audio_segments.append(silence_pad)
483
+
484
+ if not all_audio_segments:
485
+ yield None, "❌ Không sinh được audio nào."
486
+ return
487
+
488
+ yield None, "💾 Đang ghép file và lưu..."
489
+
490
+ final_wav = np.concatenate(all_audio_segments)
491
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
492
+ sf.write(tmp.name, final_wav, sr)
493
+ output_path = tmp.name
494
+
495
+ process_time = time.time() - start_time
496
+ backend_info = f" (Backend: {'LMDeploy 🚀' if using_lmdeploy else 'Standard 📦'})"
497
+ speed_info = f", Tốc độ: {len(final_wav)/sr/process_time:.2f}x realtime" if process_time > 0 else ""
498
+
499
+ yield output_path, f"✅ Hoàn tất! (Thời gian: {process_time:.2f}s{speed_info}){backend_info}"
500
+
501
+ # Cleanup memory
502
+ if using_lmdeploy and hasattr(tts, 'cleanup_memory'):
503
+ tts.cleanup_memory()
504
+ cleanup_gpu_memory()
505
+
506
+ except torch.cuda.OutOfMemoryError as e:
507
+ cleanup_gpu_memory()
508
+ yield None, (
509
+ f"❌ GPU hết VRAM! Hãy thử:\n"
510
+ f"• Giảm Max Batch Size (hiện tại: {tts.max_batch_size if hasattr(tts, 'max_batch_size') else 'N/A'})\n"
511
+ f"• Giảm độ dài văn bản\n\n"
512
+ f"Chi tiết: {str(e)}"
513
+ )
514
+ return
515
+
516
+ except Exception as e:
517
+ import traceback
518
+ traceback.print_exc()
519
+ cleanup_gpu_memory()
520
+ yield None, f"❌ Lỗi Standard Mode: {str(e)}"
521
+ return
522
+
523
+ # === STREAMING MODE ===
524
+ else:
525
+ sr = 24000
526
+ crossfade_samples = int(sr * 0.03)
527
+ audio_queue = queue.Queue(maxsize=100)
528
+ PRE_BUFFER_SIZE = 3
529
+
530
+ end_event = threading.Event()
531
+ error_event = threading.Event()
532
+ error_msg = ""
533
+
534
+ def producer_thread():
535
+ nonlocal error_msg
536
+ try:
537
+ previous_tail = None
538
+
539
+ for i, chunk_text in enumerate(text_chunks):
540
+ stream_gen = tts.infer_stream(chunk_text, ref_codes, ref_text_raw)
541
+
542
+ for part_idx, audio_part in enumerate(stream_gen):
543
+ if audio_part is None or len(audio_part) == 0:
544
+ continue
545
+
546
+ if previous_tail is not None and len(previous_tail) > 0:
547
+ overlap = min(len(previous_tail), len(audio_part), crossfade_samples)
548
+ if overlap > 0:
549
+ fade_out = np.linspace(1.0, 0.0, overlap, dtype=np.float32)
550
+ fade_in = np.linspace(0.0, 1.0, overlap, dtype=np.float32)
551
+
552
+ blended = (audio_part[:overlap] * fade_in +
553
+ previous_tail[-overlap:] * fade_out)
554
+
555
+ processed = np.concatenate([
556
+ previous_tail[:-overlap] if len(previous_tail) > overlap else np.array([]),
557
+ blended,
558
+ audio_part[overlap:]
559
+ ])
560
+ else:
561
+ processed = np.concatenate([previous_tail, audio_part])
562
+
563
+ tail_size = min(crossfade_samples, len(processed))
564
+ previous_tail = processed[-tail_size:].copy()
565
+ output_chunk = processed[:-tail_size] if len(processed) > tail_size else processed
566
+ else:
567
+ tail_size = min(crossfade_samples, len(audio_part))
568
+ previous_tail = audio_part[-tail_size:].copy()
569
+ output_chunk = audio_part[:-tail_size] if len(audio_part) > tail_size else audio_part
570
+
571
+ if len(output_chunk) > 0:
572
+ audio_queue.put((sr, output_chunk))
573
+
574
+ if previous_tail is not None and len(previous_tail) > 0:
575
+ audio_queue.put((sr, previous_tail))
576
+
577
+ except Exception as e:
578
+ import traceback
579
+ traceback.print_exc()
580
+ error_msg = str(e)
581
+ error_event.set()
582
+ finally:
583
+ end_event.set()
584
+ audio_queue.put(None)
585
+
586
+ threading.Thread(target=producer_thread, daemon=True).start()
587
+
588
+ yield (sr, np.zeros(int(sr * 0.05))), "📄 Đang buffering..."
589
+
590
+ pre_buffer = []
591
+ while len(pre_buffer) < PRE_BUFFER_SIZE:
592
+ try:
593
+ item = audio_queue.get(timeout=5.0)
594
+ if item is None:
595
+ break
596
+ pre_buffer.append(item)
597
+ except queue.Empty:
598
+ if error_event.is_set():
599
+ yield None, f"❌ Lỗi: {error_msg}"
600
+ return
601
+ break
602
+
603
+ full_audio_buffer = []
604
+ backend_info = "🚀 LMDeploy" if using_lmdeploy else "📦 Standard"
605
+ for sr, audio_data in pre_buffer:
606
+ full_audio_buffer.append(audio_data)
607
+ yield (sr, audio_data), f"🔊 Đang phát ({backend_info})..."
608
+
609
+ while True:
610
+ try:
611
+ item = audio_queue.get(timeout=0.05)
612
+ if item is None:
613
+ break
614
+ sr, audio_data = item
615
+ full_audio_buffer.append(audio_data)
616
+ yield (sr, audio_data), f"🔊 Đang phát ({backend_info})..."
617
+ except queue.Empty:
618
+ if error_event.is_set():
619
+ yield None, f"❌ Lỗi: {error_msg}"
620
+ break
621
+ if end_event.is_set() and audio_queue.empty():
622
+ break
623
+ continue
624
+
625
+ if full_audio_buffer:
626
+ final_wav = np.concatenate(full_audio_buffer)
627
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
628
+ sf.write(tmp.name, final_wav, sr)
629
+ yield tmp.name, f"✅ Hoàn tất Streaming! ({backend_info})"
630
+
631
+ # Cleanup memory
632
+ if using_lmdeploy and hasattr(tts, 'cleanup_memory'):
633
+ tts.cleanup_memory()
634
+ cleanup_gpu_memory()
635
+
636
+
637
+ # --- 4. UI SETUP ---
638
+ theme = gr.themes.Soft(
639
+ primary_hue="indigo",
640
+ secondary_hue="cyan",
641
+ neutral_hue="slate",
642
+ font=[gr.themes.GoogleFont('Inter'), 'ui-sans-serif', 'system-ui'],
643
+ ).set(
644
+ button_primary_background_fill="linear-gradient(90deg, #6366f1 0%, #0ea5e9 100%)",
645
+ button_primary_background_fill_hover="linear-gradient(90deg, #4f46e5 0%, #0284c7 100%)",
646
+ )
647
+
648
+ css = """
649
+ .container { max-width: 1400px; margin: auto; }
650
+ .header-box {
651
+ text-align: center;
652
+ margin-bottom: 25px;
653
+ padding: 25px;
654
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
655
+ border-radius: 12px;
656
+ color: white !important;
657
+ }
658
+ .header-title {
659
+ font-size: 2.5rem;
660
+ font-weight: 800;
661
+ color: white !important;
662
+ }
663
+ .gradient-text {
664
+ background: -webkit-linear-gradient(45deg, #60A5FA, #22D3EE);
665
+ -webkit-background-clip: text;
666
+ -webkit-text-fill-color: transparent;
667
+ }
668
+ .header-icon {
669
+ color: white;
670
+ }
671
+ .status-box {
672
+ font-weight: bold;
673
+ text-align: center;
674
+ border: none;
675
+ background: transparent;
676
+ }
677
+ .model-card-content {
678
+ display: flex;
679
+ flex-wrap: wrap;
680
+ justify-content: center;
681
+ align-items: center;
682
+ gap: 15px;
683
+ font-size: 0.9rem;
684
+ text-align: center;
685
+ color: white !important;
686
+ }
687
+ .model-card-item {
688
+ display: flex;
689
+ align-items: center;
690
+ justify-content: center;
691
+ gap: 6px;
692
+ color: white !important;
693
+ }
694
+ .model-card-item strong {
695
+ color: white !important;
696
+ }
697
+ .model-card-item span {
698
+ color: white !important;
699
+ }
700
+ .model-card-link {
701
+ color: #60A5FA;
702
+ text-decoration: none;
703
+ font-weight: 500;
704
+ transition: color 0.2s;
705
+ }
706
+ .model-card-link:hover {
707
+ color: #22D3EE;
708
+ text-decoration: underline;
709
+ }
710
+ .warning-banner {
711
+ background-color: #fffbeb;
712
+ border: 1px solid #fef3c7;
713
+ border-radius: 12px;
714
+ padding: 16px;
715
+ margin-bottom: 20px;
716
+ }
717
+ .warning-banner-title {
718
+ color: #92400e;
719
+ font-weight: 700;
720
+ font-size: 1.1rem;
721
+ display: flex;
722
+ align-items: center;
723
+ gap: 8px;
724
+ margin-bottom: 12px;
725
+ }
726
+ .warning-banner-grid {
727
+ display: flex;
728
+ gap: 15px;
729
+ flex-wrap: wrap;
730
+ }
731
+ .warning-banner-item {
732
+ flex: 1;
733
+ min-width: 240px;
734
+ background: #fef3c7;
735
+ padding: 12px;
736
+ border-radius: 8px;
737
+ border: 1px solid #fde68a;
738
+ }
739
+ .warning-banner-item strong {
740
+ color: #b45309;
741
+ display: block;
742
+ margin-bottom: 4px;
743
+ font-size: 0.95rem;
744
+ }
745
+ .warning-banner-content {
746
+ color: #78350f;
747
+ font-size: 0.9rem;
748
+ line-height: 1.5;
749
+ }
750
+ .warning-banner-content b {
751
+ color: #451a03;
752
+ background: rgba(251, 191, 36, 0.2);
753
+ padding: 1px 4px;
754
+ border-radius: 4px;
755
+ }
756
+ """
757
+
758
+ EXAMPLES_LIST = [
759
+ ["Về miền Tây không chỉ để ngắm nhìn sông nước hữu tình, mà còn để cảm nhận tấm chân tình của người dân nơi đây.", "Vĩnh (nam miền Nam)"],
760
+ ["Hà Nội những ngày vào thu mang một vẻ đẹp trầm mặc và cổ kính đến lạ thường.", "Bình (nam miền Bắc)"],
761
+ ]
762
+
763
+ with gr.Blocks(theme=theme, css=css, title="VieNeu-TTS") as demo:
764
+ with gr.Column(elem_classes="container"):
765
+ gr.HTML("""
766
+ <div class="header-box">
767
+ <h1 class="header-title">
768
+ <span class="header-icon">🦜</span>
769
+ <span class="gradient-text">VieNeu-TTS Studio</span>
770
+ </h1>
771
+ <div class="model-card-content">
772
+ <div class="model-card-item">
773
+ <strong>Models:</strong>
774
+ <a href="https://huggingface.co/pnnbao-ump/VieNeu-TTS" target="_blank" class="model-card-link">VieNeu-TTS</a>
775
+ <span>•</span>
776
+ <a href="https://huggingface.co/pnnbao-ump/VieNeu-TTS-0.3B" target="_blank" class="model-card-link">VieNeu-TTS-0.3B</a>
777
+ </div>
778
+ <div class="model-card-item">
779
+ <strong>Repository:</strong>
780
+ <a href="https://github.com/pnnbao97/VieNeu-TTS" target="_blank" class="model-card-link">GitHub</a>
781
+ </div>
782
+ <div class="model-card-item">
783
+ <strong>Tác giả:</strong>
784
+ <a href="https://www.facebook.com/bao.phamnguyenngoc.5" target="_blank" class="model-card-link">Phạm Nguyễn Ngọc Bảo</a>
785
+ </div>
786
+ </div>
787
+ </div>
788
+ """)
789
+
790
+ # --- CONFIGURATION ---
791
+ with gr.Group():
792
+ with gr.Row():
793
+ backbone_select = gr.Dropdown(
794
+ list(BACKBONE_CONFIGS.keys()),
795
+ value="VieNeu-TTS (GPU)",
796
+ label="🦜 Backbone"
797
+ )
798
+ codec_select = gr.Dropdown(list(CODEC_CONFIGS.keys()), value="NeuCodec (Distill)", label="🎵 Codec")
799
+ device_choice = gr.Radio(get_available_devices(), value="Auto", label="🖥️ Device")
800
+
801
+ with gr.Row():
802
+ use_lmdeploy_cb = gr.Checkbox(
803
+ value=True,
804
+ label="🚀 Optimize with LMDeploy (Khuyên dùng cho NVIDIA GPU)",
805
+ info="Tick nếu bạn dùng GPU để tăng tốc độ tổng hợp đáng kể."
806
+ )
807
+
808
+ gr.HTML("""
809
+ <div class="warning-banner">
810
+ <div class="warning-banner-title">
811
+ 🦜 Gợi ý tối ưu hiệu năng
812
+ </div>
813
+ <div class="warning-banner-grid">
814
+ <div class="warning-banner-item">
815
+ <strong>🐢 Hệ máy CPU</strong>
816
+ <div class="warning-banner-content">
817
+ Sử dụng <b>VieNeu-TTS-0.3B-q4-gguf</b> để đạt tốc độ xử lý nhanh nhất. Nếu ưu tiên độ chính xác thì dùng <b>VieNeu-TTS-0.3B-q8-gguf</b>.
818
+ </div>
819
+ </div>
820
+ <div class="warning-banner-item">
821
+ <strong>🐆 Hệ máy GPU</strong>
822
+ <div class="warning-banner-content">
823
+ Chọn <b>VieNeu-TTS-0.3B (GPU)</b> để x2 tốc độ (độ chính xác ~95% bản gốc).
824
+ </div>
825
+ </div>
826
+ </div>
827
+ </div>
828
+ """)
829
+
830
+ btn_load = gr.Button("🔄 Tải Model", variant="primary")
831
+ model_status = gr.Markdown("⏳ Chưa tải model.")
832
+
833
+ with gr.Row(elem_classes="container"):
834
+ # --- INPUT ---
835
+ with gr.Column(scale=3):
836
+ text_input = gr.Textbox(
837
+ label=f"Văn bản",
838
+ lines=4,
839
+ value="Hà Nội, trái tim của Việt Nam, là một thành phố ngàn năm văn hiến với bề dày lịch sử và văn hóa độc đáo. Bước chân trên những con phố cổ kính quanh Hồ Hoàn Kiếm, du khách như được du hành ngược thời gian, chiêm ngưỡng kiến trúc Pháp cổ điển hòa quyện với nét kiến trúc truyền thống Việt Nam. Mỗi con phố trong khu phố cổ mang một tên gọi đặc trưng, phản ánh nghề thủ công truyền thống từng thịnh hành nơi đây như phố Hàng Bạc, Hàng Đào, Hàng Mã. Ẩm thực Hà Nội cũng là một điểm nhấn đặc biệt, từ tô phở nóng hổi buổi sáng, bún chả thơm lừng trưa hè, đến chè Thái ngọt ngào chiều thu. Những món ăn dân dã này đã trở thành biểu tượng của văn hóa ẩm thực Việt, được cả thế giới yêu mến. Người Hà Nội nổi tiếng với tính cách hiền hòa, lịch thiệp nhưng cũng rất cầu toàn trong từng chi tiết nhỏ, từ cách pha trà sen cho đến cách chọn hoa sen tây để thưởng trà.",
840
+ )
841
+
842
+ with gr.Tabs() as tabs:
843
+ with gr.TabItem("👤 Preset", id="preset_mode") as tab_preset:
844
+ initial_voices = get_voice_options("VieNeu-TTS (GPU)")
845
+ default_voice = initial_voices[0] if initial_voices else None
846
+ voice_select = gr.Dropdown(initial_voices, value=default_voice, label="Giọng mẫu")
847
+
848
+ with gr.TabItem("🦜 Voice Cloning", id="custom_mode") as tab_custom:
849
+ custom_audio = gr.Audio(label="Audio giọng mẫu (3-5 giây) (.wav)", type="filepath")
850
+ custom_text = gr.Textbox(label="Nội dung audio mẫu - vui lòng gõ đúng nội dung của audio mẫu - kể cả dấu câu vì model rất nhạy cảm với dấu câu (.,?!)")
851
+ gr.Examples(
852
+ examples=[
853
+ [os.path.join("examples", "audio_ref", "example.wav"), "Ví dụ 2. Tính trung bình của dãy số."],
854
+ [os.path.join("examples", "audio_ref", "example_2.wav"), "Trên thực tế, các nghi ngờ đã bắt đầu xuất hiện."]
855
+ ],
856
+ inputs=[custom_audio, custom_text],
857
+ label="Ví dụ mẫu để thử nghiệm clone giọng"
858
+ )
859
+
860
+
861
+ generation_mode = gr.Radio(
862
+ ["Standard (Một lần)"],
863
+ value="Standard (Một lần)",
864
+ label="Chế độ sinh"
865
+ )
866
+ with gr.Row():
867
+ use_batch = gr.Checkbox(
868
+ value=True,
869
+ label="⚡ Batch Processing",
870
+ info="Xử lý nhiều đoạn cùng lúc (chỉ áp dụng khi sử dụng GPU và đã cài đặt LMDeploy)"
871
+ )
872
+ max_batch_size_run = gr.Slider(
873
+ minimum=1,
874
+ maximum=16,
875
+ value=4,
876
+ step=1,
877
+ label="📊 Batch Size (Generation)",
878
+ info="Số lượng đoạn văn bản xử lý cùng lúc. Giá trị cao = nhanh hơn nhưng tốn VRAM hơn. Giảm xuống nếu gặp lỗi Out of Memory."
879
+ )
880
+
881
+ # State to track current mode (replaces unreliable Textbox/Tabs input)
882
+ current_mode_state = gr.State("preset_mode")
883
+
884
+ with gr.Row():
885
+ btn_generate = gr.Button("🎵 Bắt đầu", variant="primary", scale=2, interactive=False)
886
+ btn_stop = gr.Button("⏹️ Dừng", variant="stop", scale=1, interactive=False)
887
+
888
+ # --- OUTPUT ---
889
+ with gr.Column(scale=2):
890
+ audio_output = gr.Audio(
891
+ label="Kết quả",
892
+ type="filepath",
893
+ autoplay=True
894
+ )
895
+ status_output = gr.Textbox(label="Trạng thái", elem_classes="status-box")
896
+ gr.Markdown("<div style='text-align: center; color: #64748b; font-size: 0.8rem;'>🔒 Audio được đóng dấu bản quyền ẩn (Watermarker) để bảo mật và định danh AI.</div>")
897
+
898
+ # # --- EVENT HANDLERS ---
899
+ # def update_info(backbone: str) -> str:
900
+ # return f"Streaming: {'✅' if BACKBONE_CONFIGS[backbone]['supports_streaming'] else '❌'}"
901
+
902
+ # backbone_select.change(update_info, backbone_select, model_status)
903
+ backbone_select.change(update_voice_dropdown, [backbone_select, voice_select], voice_select)
904
+
905
+ # Handler to show/hide Voice Cloning tab
906
+ def on_codec_change(codec: str):
907
+ is_onnx = "onnx" in codec.lower()
908
+ # If switching to ONNX and we are on custom mode, switch back to preset
909
+ return gr.update(visible=not is_onnx), gr.update(selected="preset_mode" if is_onnx else None)
910
+
911
+ codec_select.change(
912
+ on_codec_change,
913
+ inputs=[codec_select],
914
+ outputs=[tab_custom, tabs]
915
+ )
916
+
917
+ # Bind tab events to update state
918
+ tab_preset.select(lambda: "preset_mode", outputs=current_mode_state)
919
+ tab_custom.select(lambda: "custom_mode", outputs=current_mode_state)
920
+
921
+ btn_load.click(
922
+ fn=load_model,
923
+ inputs=[backbone_select, codec_select, device_choice, use_lmdeploy_cb],
924
+ outputs=[model_status, btn_generate, btn_load, btn_stop]
925
+ )
926
+
927
+ generate_event = btn_generate.click(
928
+ fn=synthesize_speech,
929
+ inputs=[text_input, voice_select, custom_audio, custom_text, current_mode_state, generation_mode, use_batch, max_batch_size_run],
930
+ outputs=[audio_output, status_output]
931
+ )
932
+
933
+ # When generation starts, enable stop button
934
+ btn_generate.click(lambda: gr.update(interactive=True), outputs=btn_stop)
935
+ # When generation ends/stops, disable stop button
936
+ generate_event.then(lambda: gr.update(interactive=False), outputs=btn_stop)
937
+
938
+ btn_stop.click(fn=None, cancels=[generate_event])
939
+ btn_stop.click(lambda: (None, "⏹️ Đã dừng tạo giọng nói."), outputs=[audio_output, status_output])
940
+ btn_stop.click(lambda: gr.update(interactive=False), outputs=btn_stop)
941
+
942
+ # Persistence: Restore UI state on load
943
+ demo.load(
944
+ fn=restore_ui_state,
945
+ outputs=[model_status, btn_generate, btn_stop]
946
+ )
947
+
948
+ if __name__ == "__main__":
949
+ # Cho phép override từ biến môi trường (hữu ích cho Docker)
950
+ server_name = os.getenv("GRADIO_SERVER_NAME", "127.0.0.1")
951
+ server_port = int(os.getenv("GRADIO_SERVER_PORT", "7860"))
952
+
953
+ # Check running in Colab
954
+ is_on_colab = os.getenv("COLAB_RELEASE_TAG") is not None
955
+
956
+ # Default:
957
+ # - Colab: share=True (convenient)
958
+ # - Docker/local: share=False (safe)
959
+ share = env_bool("GRADIO_SHARE", default=is_on_colab)
960
+ #
961
+ # If server_name is "0.0.0.0" and GRADIO_SHARE is not set, disable sharing
962
+ if server_name == "0.0.0.0" and os.getenv("GRADIO_SHARE") is None:
963
+ share = False
964
+
965
+ demo.queue().launch(server_name=server_name, server_port=server_port, share=share)
main.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from vieneu_tts import VieNeuTTS
2
+ import soundfile as sf
3
+ import torch
4
+ import os
5
+
6
+ device = "cpu"
7
+
8
+ input_texts = [
9
+ "Các khóa học trực tuyến đang giúp học sinh tiếp cận kiến thức mọi lúc mọi nơi. Giáo viên sử dụng video, bài tập tương tác và thảo luận trực tuyến để nâng cao hiệu quả học tập.",
10
+
11
+ "Các nghiên cứu về bệnh Alzheimer cho thấy tác dụng tích cực của các bài tập trí não và chế độ dinh dưỡng lành mạnh, giúp giảm tốc độ suy giảm trí nhớ ở người cao tuổi.",
12
+
13
+ "Một tiểu thuyết trinh thám hiện đại dẫn dắt độc giả qua những tình tiết phức tạp, bí ẩn, kết hợp yếu tố tâm lý sâu sắc khiến người đọc luôn hồi hộp theo dõi diễn biến câu chuyện.",
14
+
15
+ "Các nhà khoa học nghiên cứu gen người phát hiện những đột biến mới liên quan đến bệnh di truyền. Điều này giúp nâng cao khả năng chẩn đoán và điều trị.",
16
+ ]
17
+
18
+ output_dir = "./output_audio"
19
+ os.makedirs(output_dir, exist_ok=True)
20
+
21
+ def main(backbone="pnnbao-ump/VieNeu-TTS-q4-gguf", codec="neuphonic/neucodec-onnx-decoder"):
22
+ """
23
+ In the sample directory, there are wav files and txt files with matching names.
24
+ These are pre-prepared reference files for testing with Vietnamese names:
25
+ - Bình (nam miền Bắc) - Male, North accent
26
+ - Tuyên (nam miền Bắc) - Male, North accent
27
+ - Nguyên (nam miền Nam) - Male, South accent
28
+ - Sơn (nam miền Nam) - Male, South accent
29
+ - Vĩnh (nam miền Nam) - Male, South accent
30
+ - Hương (nữ miền Bắc) - Female, North accent
31
+ - Ly (nữ miền Bắc) - Female, North accent
32
+ - Ngọc (nữ miền Bắc) - Female, North accent
33
+ - Đoan (nữ miền Nam) - Female, South accent
34
+ - Dung (nữ miền Nam) - Female, South accent
35
+
36
+ Note: The model can clone any voice you provide (with corresponding text).
37
+ However, quality may not match the sample files. For best results, finetune
38
+ the model on your target voice. See finetune guide at:
39
+ https://github.com/pnnbao-ump/VieNeuTTS/blob/main/finetune.ipynb
40
+ """
41
+ # Male voice (South accent)
42
+ ref_audio_path = "./sample/Vĩnh (nam miền Nam).wav"
43
+ ref_text_path = "./sample/Vĩnh (nam miền Nam).txt"
44
+ ref_codes_path = "./sample/Vĩnh (nam miền Nam).pt"
45
+
46
+ # Female voice (South accent) - uncomment to use
47
+ # ref_audio_path = "./sample/Đoan (nữ miền Nam).wav"
48
+ # ref_text_path = "./sample/Đoan (nữ miền Nam).txt"
49
+
50
+ ref_text_raw = open(ref_text_path, "r", encoding="utf-8").read()
51
+
52
+ if not ref_audio_path or not ref_text_raw:
53
+ print("No reference audio or text provided.")
54
+ return None
55
+
56
+ # Initialize VieNeuTTS-1000h
57
+ tts = VieNeuTTS(
58
+ backbone_repo=backbone,
59
+ backbone_device=device,
60
+ codec_repo=codec,
61
+ codec_device=device
62
+ )
63
+
64
+ if codec == "neuphonic/neucodec-onnx-decoder":
65
+ print("Load reference codes...")
66
+ ref_codes = torch.load(ref_codes_path, map_location=device)
67
+ else:
68
+ print("Encoding reference audio...")
69
+ ref_codes = tts.encode_reference(ref_audio_path)
70
+
71
+ # Generate speech for all input texts
72
+ for i, text in enumerate(input_texts, 1):
73
+ print(f"Generating audio {i}/{len(input_texts)}: {text[:50]}...")
74
+ wav = tts.infer(text, ref_codes, ref_text_raw)
75
+ output_path = os.path.join(output_dir, f"output_{i}.wav")
76
+ sf.write(output_path, wav, 24000)
77
+ print(f"✓ Saved to {output_path}")
78
+
79
+ if __name__ == "__main__":
80
+ main()
pyproject.toml ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.uv]
2
+ index-strategy = "unsafe-best-match"
3
+ required-environments = [
4
+ "sys_platform == 'win32' and platform_machine == 'AMD64'",
5
+ "sys_platform == 'linux' and platform_machine == 'x86_64'",
6
+ "sys_platform == 'darwin' and platform_machine == 'arm64'",
7
+ ]
8
+ override-dependencies = [
9
+ "nvidia-nccl-cu12; sys_platform == 'linux'",
10
+ ]
11
+
12
+ [[tool.uv.index]]
13
+ name = "pytorch"
14
+ url = "https://download.pytorch.org/whl/cu128"
15
+ explicit = true
16
+
17
+ [[tool.uv.index]]
18
+ name = "pypi"
19
+ url = "https://pypi.org/simple"
20
+
21
+ [project]
22
+ name = "VieNeu-TTS"
23
+ version = "0.1.0"
24
+ description = "Advanced on-device Vietnamese TTS with instant voice cloning"
25
+ readme = "README.md"
26
+ requires-python = "==3.12.*"
27
+ dependencies = [
28
+ "phonemizer>=3.3.0",
29
+ "neucodec>=0.0.4",
30
+ "librosa>=0.11.0",
31
+ "gradio>=5.49.1",
32
+ "onnxruntime>=1.23.2",
33
+ "datasets>=3.2.0",
34
+ "lmdeploy; sys_platform != 'darwin'",
35
+ "triton-windows; sys_platform == 'win32'",
36
+ "triton; sys_platform == 'linux'",
37
+ "transformers; sys_platform == 'darwin'",
38
+ "accelerate; sys_platform == 'darwin'",
39
+ "torch",
40
+ "torchvision",
41
+ "torchaudio",
42
+ "perth>=0.2.0",
43
+ "llama-cpp-python==0.3.16",
44
+ ]
45
+
46
+ [tool.uv.sources]
47
+
48
+ torch = [
49
+ { index = "pytorch", marker = "sys_platform != 'darwin'" },
50
+ { index = "pypi", marker = "sys_platform == 'darwin'" }
51
+ ]
52
+ torchvision = [
53
+ { index = "pytorch", marker = "sys_platform != 'darwin'" },
54
+ { index = "pypi", marker = "sys_platform == 'darwin'" }
55
+ ]
56
+ torchaudio = [
57
+ { index = "pytorch", marker = "sys_platform != 'darwin'" },
58
+ { index = "pypi", marker = "sys_platform == 'darwin'" }
59
+ ]
60
+
61
+ lmdeploy = [
62
+ { url = "https://github.com/InternLM/lmdeploy/releases/download/v0.11.0/lmdeploy-0.11.0+cu128-cp312-cp312-win_amd64.whl", marker = "sys_platform == 'win32' and python_version == '3.12'" },
63
+ { url = "https://github.com/InternLM/lmdeploy/releases/download/v0.11.0/lmdeploy-0.11.0+cu128-cp312-cp312-manylinux2014_x86_64.whl", marker = "sys_platform == 'linux' and python_version == '3.12'" },
64
+ { index = "pypi", marker = "sys_platform == 'darwin'" }
65
+ ]
66
+
67
+ llama-cpp-python = [
68
+ { url = "https://github.com/pnnbao97/VieNeu-TTS/releases/download/wheels-v0.3.16/llama_cpp_python-0.3.16-cp312-cp312-win_amd64.whl", marker = "sys_platform == 'win32' and python_version == '3.12'" },
69
+ { index = "pypi", marker = "sys_platform != 'win32'" }
70
+ ]
requirements.txt ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ accelerate==1.12.0
2
+ addict==2.4.0
3
+ aiofiles==24.1.0
4
+ aiohappyeyeballs==2.6.1
5
+ aiohttp==3.13.1
6
+ aiosignal==1.4.0
7
+ annotated-doc==0.0.3
8
+ annotated-types==0.7.0
9
+ antlr4-python3-runtime==4.9.3
10
+ anyio==4.11.0
11
+ attrs==25.4.0
12
+ audioread==3.1.0
13
+ babel==2.17.0
14
+ blobfile==3.1.0
15
+ brotli==1.1.0
16
+ certifi==2025.10.5
17
+ cffi==2.0.0
18
+ charset-normalizer==3.4.4
19
+ click==8.3.0
20
+ cloudpickle==3.1.2
21
+ colorama==0.4.6
22
+ coloredlogs==15.0.1
23
+ csvw==3.7.0
24
+ datasets==4.3.0
25
+ decorator==5.2.1
26
+ dill==0.4.0
27
+ distro==1.9.0
28
+ dlinfo==2.0.0
29
+ einops==0.8.1
30
+ einx==0.3.0
31
+ fastapi==0.120.2
32
+ ffmpy==0.6.4
33
+ filelock==3.20.0
34
+ fire==0.7.1
35
+ flatbuffers==25.9.23
36
+ frozendict==2.4.6
37
+ frozenlist==1.8.0
38
+ fsspec==2025.9.0
39
+ gradio==5.49.1
40
+ gradio-client==1.13.3
41
+ groovy==0.1.2
42
+ h11==0.16.0
43
+ hf-transfer==0.1.9
44
+ httpcore==1.0.9
45
+ httpx==0.28.1
46
+ huggingface-hub==0.36.0
47
+ humanfriendly==10.0
48
+ hyper-connections==0.2.1
49
+ idna==3.11
50
+ inquirerpy==0.3.4
51
+ isodate==0.7.2
52
+ jinja2==3.1.6
53
+ jiter==0.12.0
54
+ joblib==1.5.2
55
+ jsonschema==4.25.1
56
+ jsonschema-specifications==2025.9.1
57
+ kagglehub==0.3.13
58
+ language-tags==1.2.0
59
+ lazy-loader==0.4
60
+ librosa==0.11.0
61
+ llvmlite==0.45.1
62
+ lmdeploy==0.11.0
63
+ local-attention==1.11.2
64
+ lxml==6.0.2
65
+ markdown-it-py==4.0.0
66
+ markupsafe==3.0.3
67
+ mdurl==0.1.2
68
+ mmengine-lite==0.10.7
69
+ mpmath==1.3.0
70
+ msgpack==1.1.2
71
+ multidict==6.7.0
72
+ multiprocess==0.70.16
73
+ networkx==3.5
74
+ neucodec==0.0.4
75
+ numba==0.62.1
76
+ numpy==2.3.4
77
+ omegaconf==2.3.0
78
+ onnxruntime==1.23.2
79
+ openai==2.12.0
80
+ openai-harmony==0.0.8
81
+ orjson==3.11.4
82
+ packaging==25.0
83
+ pandas==2.3.3
84
+ partial-json-parser==0.2.1.1.post7
85
+ peft==0.14.0
86
+ pfzy==0.3.4
87
+ phonemizer==3.3.0
88
+ pillow==11.3.0
89
+ platformdirs==4.5.0
90
+ pooch==1.8.2
91
+ prometheus-client==0.23.1
92
+ prompt-toolkit==3.0.52
93
+ propcache==0.4.1
94
+ protobuf==6.33.2
95
+ psutil==7.1.2
96
+ pyarrow==22.0.0
97
+ pycparser==2.23
98
+ pycryptodomex==3.23.0
99
+ pydantic==2.11.10
100
+ pydantic-core==2.33.2
101
+ pydub==0.25.1
102
+ pygments==2.19.2
103
+ pyparsing==3.2.5
104
+ pyreadline3==3.5.4
105
+ python-dateutil==2.9.0.post0
106
+ python-multipart==0.0.20
107
+ pytz==2025.2
108
+ pyyaml==6.0.3
109
+ pyzmq==27.1.0
110
+ ray==2.52.1
111
+ rdflib==7.3.0
112
+ referencing==0.37.0
113
+ regex==2025.10.23
114
+ requests==2.32.5
115
+ rfc3986==1.5.0
116
+ rich==14.2.0
117
+ rpds-py==0.28.0
118
+ ruff==0.14.2
119
+ safehttpx==0.1.7
120
+ safetensors==0.6.2
121
+ scikit-learn==1.7.2
122
+ scipy==1.16.2
123
+ segments==2.3.0
124
+ semantic-version==2.10.0
125
+ sentencepiece==0.2.1
126
+ setuptools==80.9.0
127
+ shellingham==1.5.4
128
+ shortuuid==1.0.13
129
+ six==1.17.0
130
+ sniffio==1.3.1
131
+ soundfile==0.13.1
132
+ soxr==1.0.0
133
+ starlette==0.49.1
134
+ sympy==1.14.0
135
+ termcolor==3.2.0
136
+ threadpoolctl==3.6.0
137
+ tiktoken==0.12.0
138
+ tokenizers==0.22.1
139
+ tomlkit==0.13.3
140
+ torch==2.7.1+cu118
141
+ torchao==0.14.1
142
+ torchaudio==2.7.1+cu118
143
+ torchdata==0.11.0
144
+ torchtune==0.6.1
145
+ torchvision==0.22.1+cu118
146
+ tqdm==4.67.1
147
+ transformers==4.57.1
148
+ triton-windows==3.5.1.post22
149
+ typer==0.20.0
150
+ typing-extensions==4.15.0
151
+ typing-inspection==0.4.2
152
+ tzdata==2025.2
153
+ uritemplate==4.2.0
154
+ urllib3==2.5.0
155
+ uvicorn==0.38.0
156
+ vector-quantize-pytorch==1.17.8
157
+ wcwidth==0.2.14
158
+ websockets==15.0.1
159
+ xgrammar==0.1.28
160
+ xxhash==3.6.0
161
+ yapf==0.43.0
162
+ yarl==1.22.0
uv.lock ADDED
The diff for this file is too large to render. See raw diff