titusgiap Claude Opus 5 commited on
Commit
9e4ef47
·
1 Parent(s): 9dbe21e

feat(dub): thêm giọng "My Review"; giọng clone chạy bằng trọng số mã hoá sẵn

Browse files

Thê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>

.gitattributes CHANGED
@@ -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
backend/app/assets/voices/vi/README.md CHANGED
@@ -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 | File cần thả (đặt đúng tên) |
9
- |-----------------------------------------|--------|-----------------------------|
10
- | Adam — nam, giọng trầm ấm | `adam` | `adam.wav` |
 
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
 
backend/app/assets/voices/vi/adam.wav.v3turbo.codes.npy ADDED
Binary file (29.1 kB). View file
 
backend/app/assets/voices/vi/my_review.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ff5e6d04cff39e9a971a7601fc419b1e168497c04f33cfd50e48391dba29e582
3
+ size 304910
backend/app/assets/voices/vi/my_review.wav.v3turbo.codes.npy ADDED
Binary file (10.4 kB). View file
 
backend/app/services/dubber/tts.py CHANGED
@@ -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
- audio = self.model.infer(text, ref_audio=ref, **kw)
 
 
 
 
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