Spaces:
Running
feat(dub): thêm giọng "My Review"; giọng clone chạy bằng trọng số mã hoá sẵn
Browse filesThêm my_review vào danh sách giọng có sẵn tiếng Việt (file mẫu my_review.wav).
Clone trong VieNeu = chạy codec-encoder trên wav mẫu để ra mảng ref_codes.
Bản cũ truyền ref_audio= nên việc này LẶP LẠI Ở MỖI DÒNG THOẠI. Đo thực tế:
my_review ~8,5s lần đầu (nạp session codec) rồi ~1,5s/dòng; adam ~27s lần đầu
— trong khi mảng codes chỉ 10-28 KB và không bao giờ đổi.
Nay mã hoá một lần rồi tái sử dụng, 3 tầng:
- Nhớ trong RAM theo (đường dẫn, mtime, cỡ file), có khoá vì synth_line chạy
song song nhiều luồng.
- File <wav>.v3turbo.codes.npy cạnh wav, sống qua cả lần khởi động lại. Hai
giọng preset đã có sẵn file này trong repo -> lần chạy đầu trên máy chủ
cũng không phải chờ mã hoá.
- Không có thì mới mã hoá, xong ghi lại cho lần sau (thư mục chỉ-đọc thì bỏ
qua, vẫn còn cache RAM).
Sidecar để git thường chứ không qua LFS: file chỉ 10-28 KB mà code đọc lúc
khởi động, LFS lỗi smudge sẽ thành file con trỏ. Mọi nhánh đều có đường lùi
về ref_audio nên hỏng sidecar/đổi engine cũng không gãy.
Đã kiểm: encode_reference gọi 0 lần khi dựng lại giọng, output tương đương
đường cũ. Bộ test 92/92 PASS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@@ -46,3 +46,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 46 |
*.webm filter=lfs diff=lfs merge=lfs -text
|
| 47 |
*.otf filter=lfs diff=lfs merge=lfs -text
|
| 48 |
*.ttf filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
*.webm filter=lfs diff=lfs merge=lfs -text
|
| 47 |
*.otf filter=lfs diff=lfs merge=lfs -text
|
| 48 |
*.ttf filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
|
| 50 |
+
# Trọng số giọng clone (10-30 KB) — để git thường, KHÔNG qua LFS: code đọc
|
| 51 |
+
# lúc khởi động, LFS lỗi smudge sẽ thành file con trỏ và phải clone lại từ đầu.
|
| 52 |
+
*.codes.npy -filter -diff -merge binary
|
|
@@ -5,12 +5,29 @@ của phần Dịch/Đổi giọng (ngôn ngữ Việt). Model VieNeu sẽ **clo
|
|
| 5 |
|
| 6 |
## Đang khai báo sẵn
|
| 7 |
|
| 8 |
-
| Tên hiện trên UI | id
|
| 9 |
-
|-----------------------------------------|--------|-----------------------------|
|
| 10 |
-
| Adam — nam, giọng trầm ấm | `adam`
|
|
|
|
| 11 |
|
| 12 |
> Mục chỉ hiện khi file tương ứng đã tồn tại. Thiếu file → không hiện (tránh lỗi).
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
## Yêu cầu file mẫu (để clone đạt chất lượng)
|
| 15 |
|
| 16 |
- Định dạng: `.wav` (khuyến nghị) — 16–48 kHz, mono hoặc stereo đều được.
|
|
@@ -24,6 +41,9 @@ của phần Dịch/Đổi giọng (ngôn ngữ Việt). Model VieNeu sẽ **clo
|
|
| 24 |
```python
|
| 25 |
"ten_id": ("Nhãn hiện trên UI", "ten_file.wav"),
|
| 26 |
```
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
## Lưu ý về giọng "Adam"
|
| 29 |
|
|
|
|
| 5 |
|
| 6 |
## Đang khai báo sẵn
|
| 7 |
|
| 8 |
+
| Tên hiện trên UI | id | File cần thả (đặt đúng tên) |
|
| 9 |
+
|-----------------------------------------|-------------|-----------------------------|
|
| 10 |
+
| Adam — nam, giọng trầm ấm | `adam` | `adam.wav` |
|
| 11 |
+
| My Review — nữ, giọng review | `my_review` | `my_review.wav` |
|
| 12 |
|
| 13 |
> Mục chỉ hiện khi file tương ứng đã tồn tại. Thiếu file → không hiện (tránh lỗi).
|
| 14 |
|
| 15 |
+
## File `*.v3turbo.codes.npy` — trọng số giọng đã mã hoá sẵn
|
| 16 |
+
|
| 17 |
+
Cạnh mỗi `.wav` có một file `<tên>.wav.v3turbo.codes.npy`. Đó là kết quả bước
|
| 18 |
+
"clone" (codec-encoder chạy trên file mẫu) đã tính trước và commit vào repo, chỉ
|
| 19 |
+
10–30 KB. Nhờ vậy TTS **không phải clone lại ở mỗi dòng thoại** — đo thực tế trên
|
| 20 |
+
`my_review.wav`: ~8,5s lần đầu rồi ~1,5s mỗi dòng, `adam.wav` tới ~27s lần đầu.
|
| 21 |
+
|
| 22 |
+
Thay file `.wav` thì **xoá luôn file `.npy`** tương ứng (hoặc để nguyên cũng
|
| 23 |
+
được: code bỏ qua sidecar cũ hơn wav và tự mã hoá lại, xong ghi đè). Sinh lại bằng:
|
| 24 |
+
|
| 25 |
+
```python
|
| 26 |
+
import numpy as np, vieneu
|
| 27 |
+
m = vieneu.Vieneu(mode="v3turbo")
|
| 28 |
+
np.save("ten.wav.v3turbo.codes.npy", np.asarray(m.encode_reference("ten.wav"), dtype="int64"))
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
## Yêu cầu file mẫu (để clone đạt chất lượng)
|
| 32 |
|
| 33 |
- Định dạng: `.wav` (khuyến nghị) — 16–48 kHz, mono hoặc stereo đều được.
|
|
|
|
| 41 |
```python
|
| 42 |
"ten_id": ("Nhãn hiện trên UI", "ten_file.wav"),
|
| 43 |
```
|
| 44 |
+
3. Nên sinh luôn file `.v3turbo.codes.npy` (xem mục trên) rồi commit cùng, để
|
| 45 |
+
lần chạy đầu tiên trên máy chủ không phải chờ mã hoá. Bỏ qua cũng chạy được
|
| 46 |
+
— code tự mã hoá lần đầu rồi ghi file cho những lần sau.
|
| 47 |
|
| 48 |
## Lưu ý về giọng "Adam"
|
| 49 |
|
|
Binary file (29.1 kB). View file
|
|
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ff5e6d04cff39e9a971a7601fc419b1e168497c04f33cfd50e48391dba29e582
|
| 3 |
+
size 304910
|
|
Binary file (10.4 kB). View file
|
|
|
|
@@ -33,9 +33,15 @@ _ASSET_VOICE_DIR = os.path.normpath(
|
|
| 33 |
_CLONE_PRESETS: dict[str, dict[str, tuple[str, str]]] = {
|
| 34 |
"vi": {
|
| 35 |
"adam": ("Adam — nam, giọng trầm ấm (clone)", "adam.wav"),
|
|
|
|
| 36 |
},
|
| 37 |
}
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
def _clone_preset_path(lang: str, preset_id: str) -> str | None:
|
| 41 |
"""Trả đường dẫn file mẫu nếu preset_id là giọng clone-preset và file TỒN TẠI."""
|
|
@@ -200,10 +206,73 @@ class TTSEngine:
|
|
| 200 |
# --------------------------------------------------------------- vi: VieNeu ---
|
| 201 |
|
| 202 |
class VieNeuEngine(TTSEngine): # pragma: no cover - cần model local
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
def __init__(self):
|
| 204 |
import vieneu # type: ignore
|
|
|
|
| 205 |
# v3turbo: 48 kHz, CPU chạy ONNX (nhẹ, real-time), không bắt buộc torch.
|
| 206 |
self.model = vieneu.Vieneu(mode="v3turbo")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
def synth(self, text, out_wav, cfg):
|
| 209 |
kw = {}
|
|
@@ -216,7 +285,11 @@ class VieNeuEngine(TTSEngine): # pragma: no cover - cần model local
|
|
| 216 |
# Ưu tiên mẫu upload/video; nếu preset là giọng clone-preset (vd Adam) -> clone file mẫu.
|
| 217 |
ref = cfg.speaker_wav or _clone_preset_path(cfg.target_lang, cfg.preset)
|
| 218 |
if ref: # clone từ mẫu
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
else: # giọng preset built-in (voice nhận tên str)
|
| 221 |
audio = self.model.infer(text, voice=cfg.preset or None, **kw)
|
| 222 |
self.model.save(audio, out_wav) # ghi wav ở sample_rate của model
|
|
|
|
| 33 |
_CLONE_PRESETS: dict[str, dict[str, tuple[str, str]]] = {
|
| 34 |
"vi": {
|
| 35 |
"adam": ("Adam — nam, giọng trầm ấm (clone)", "adam.wav"),
|
| 36 |
+
"my_review": ("My Review — nữ, giọng review (clone)", "my_review.wav"),
|
| 37 |
},
|
| 38 |
}
|
| 39 |
|
| 40 |
+
# Đuôi file "trọng số giọng" đã mã hoá sẵn, nằm cạnh file wav mẫu.
|
| 41 |
+
# Có gắn tên engine vì mã phụ thuộc codec của từng model — đổi engine thì
|
| 42 |
+
# tên sidecar cũng khác, không ăn nhầm mã cũ.
|
| 43 |
+
_REF_CODES_SUFFIX = ".v3turbo.codes.npy"
|
| 44 |
+
|
| 45 |
|
| 46 |
def _clone_preset_path(lang: str, preset_id: str) -> str | None:
|
| 47 |
"""Trả đường dẫn file mẫu nếu preset_id là giọng clone-preset và file TỒN TẠI."""
|
|
|
|
| 206 |
# --------------------------------------------------------------- vi: VieNeu ---
|
| 207 |
|
| 208 |
class VieNeuEngine(TTSEngine): # pragma: no cover - cần model local
|
| 209 |
+
"""VieNeu v3 Turbo. Giọng clone chạy bằng "trọng số" mã hoá sẵn, không clone lại.
|
| 210 |
+
|
| 211 |
+
Clone trong VieNeu = chạy codec-encoder trên file wav mẫu để ra mảng
|
| 212 |
+
`ref_codes` (T, 16). Bản cũ truyền thẳng `ref_audio=` nên VIỆC NÀY LẶP LẠI
|
| 213 |
+
Ở MỖI DÒNG THOẠI — đo trên my_review.wav: ~8.5s lần đầu (nạp session codec)
|
| 214 |
+
rồi ~1.5s/dòng, trong khi mảng codes chỉ 10 KB và không đổi.
|
| 215 |
+
|
| 216 |
+
Nay mã hoá 1 lần rồi tái sử dụng, 3 tầng từ rẻ đến đắt:
|
| 217 |
+
1. Nhớ trong RAM theo (đường dẫn, mtime, cỡ file) — hết trong 1 job.
|
| 218 |
+
2. File `<wav>.v3turbo.codes.npy` cạnh wav — sống qua cả lần khởi động lại.
|
| 219 |
+
Giọng preset (adam, my_review) có sẵn file này trong repo -> chạy phát
|
| 220 |
+
đầu tiên đã nhanh, không phải chờ mã hoá lần nào.
|
| 221 |
+
3. Không có thì mới mã hoá, xong ghi lại cho lần sau.
|
| 222 |
+
"""
|
| 223 |
+
|
| 224 |
def __init__(self):
|
| 225 |
import vieneu # type: ignore
|
| 226 |
+
import threading
|
| 227 |
# v3turbo: 48 kHz, CPU chạy ONNX (nhẹ, real-time), không bắt buộc torch.
|
| 228 |
self.model = vieneu.Vieneu(mode="v3turbo")
|
| 229 |
+
self._codes: dict[tuple, object] = {}
|
| 230 |
+
# synth_line chạy song song nhiều luồng (xem pipeline) -> khoá lại để
|
| 231 |
+
# cùng một mẫu không bị mã hoá mấy lần cùng lúc.
|
| 232 |
+
self._codes_lock = threading.Lock()
|
| 233 |
+
# Model không có encode_reference (mode khác) -> lùi về truyền ref_audio.
|
| 234 |
+
self._can_cache = hasattr(self.model, "encode_reference")
|
| 235 |
+
|
| 236 |
+
def _ref_codes(self, ref: str):
|
| 237 |
+
"""Trọng số giọng của file mẫu `ref`. Không lấy được -> None (lùi ref_audio)."""
|
| 238 |
+
if not self._can_cache:
|
| 239 |
+
return None
|
| 240 |
+
try:
|
| 241 |
+
st = os.stat(ref)
|
| 242 |
+
key = (os.path.abspath(ref), st.st_mtime_ns, st.st_size)
|
| 243 |
+
except OSError:
|
| 244 |
+
return None
|
| 245 |
+
|
| 246 |
+
with self._codes_lock:
|
| 247 |
+
if key in self._codes:
|
| 248 |
+
return self._codes[key]
|
| 249 |
+
|
| 250 |
+
import numpy as np # type: ignore
|
| 251 |
+
sidecar = ref + _REF_CODES_SUFFIX
|
| 252 |
+
codes = None
|
| 253 |
+
try:
|
| 254 |
+
# Chỉ nhận sidecar KHÔNG cũ hơn wav; wav thay mới thì mã hoá lại.
|
| 255 |
+
if os.path.exists(sidecar) and os.path.getmtime(sidecar) >= st.st_mtime:
|
| 256 |
+
arr = np.load(sidecar)
|
| 257 |
+
if arr.ndim == 2 and arr.size:
|
| 258 |
+
codes = arr.astype("int64", copy=False)
|
| 259 |
+
except Exception as ex:
|
| 260 |
+
print(f"[tts] bỏ qua trọng số giọng hỏng {sidecar}: {ex}")
|
| 261 |
+
|
| 262 |
+
if codes is None:
|
| 263 |
+
try:
|
| 264 |
+
codes = np.asarray(self.model.encode_reference(ref), dtype="int64")
|
| 265 |
+
except Exception as ex:
|
| 266 |
+
print(f"[tts] mã hoá mẫu giọng lỗi ({ref}): {ex}")
|
| 267 |
+
self._codes[key] = None
|
| 268 |
+
return None
|
| 269 |
+
try:
|
| 270 |
+
np.save(sidecar, codes)
|
| 271 |
+
except Exception:
|
| 272 |
+
pass # thư mục chỉ-đọc -> vẫn còn cache RAM, bỏ qua
|
| 273 |
+
|
| 274 |
+
self._codes[key] = codes
|
| 275 |
+
return codes
|
| 276 |
|
| 277 |
def synth(self, text, out_wav, cfg):
|
| 278 |
kw = {}
|
|
|
|
| 285 |
# Ưu tiên mẫu upload/video; nếu preset là giọng clone-preset (vd Adam) -> clone file mẫu.
|
| 286 |
ref = cfg.speaker_wav or _clone_preset_path(cfg.target_lang, cfg.preset)
|
| 287 |
if ref: # clone từ mẫu
|
| 288 |
+
codes = self._ref_codes(ref)
|
| 289 |
+
if codes is not None: # trọng số sẵn -> khỏi clone lại
|
| 290 |
+
audio = self.model.infer(text, ref_codes=codes, **kw)
|
| 291 |
+
else:
|
| 292 |
+
audio = self.model.infer(text, ref_audio=ref, **kw)
|
| 293 |
else: # giọng preset built-in (voice nhận tên str)
|
| 294 |
audio = self.model.infer(text, voice=cfg.preset or None, **kw)
|
| 295 |
self.model.save(audio, out_wav) # ghi wav ở sample_rate của model
|