ktvoice commited on
Commit
e58b011
·
verified ·
1 Parent(s): 3503795

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +38 -90
  2. tts_engine.py +87 -145
app.py CHANGED
@@ -1,39 +1,15 @@
1
- import spaces
2
- import os
3
  os.environ['SPACES_ZERO_GPU'] = '1'
4
-
5
- import gradio as gr
6
- import soundfile as sf
7
- import tempfile
8
- import torch
9
- import librosa
10
- import time
11
- import traceback
12
-
13
  from tts_engine import VoiceEngine
14
 
15
- # --- 1. SETUP MODEL ---
16
  device = "cuda" if torch.cuda.is_available() else "cpu"
17
- MY_BACKBONE = "ktvoice/Backbone"
18
- MY_CODEC = "ktvoice/Codec"
19
-
20
  try:
21
- tts = VoiceEngine(
22
- backbone_repo=MY_BACKBONE,
23
- backbone_device=device,
24
- codec_repo=MY_CODEC,
25
- codec_device=device
26
- )
27
  except Exception as e:
28
- print(f" LỖI KHỞI TẠO: {traceback.format_exc()}")
29
- class MockTTS:
30
- def encode_reference(self, path): return None
31
- def infer(self, text, ref, ref_text):
32
- time.sleep(1); import numpy as np
33
- return np.random.uniform(-0.1, 0.1, 24000*2)
34
- tts = MockTTS()
35
 
36
- # --- 2. DATA ---
37
  VOICE_SAMPLES = {
38
  "Tuyên (nam miền Bắc)": {"audio": "./sample/Tuyên (nam miền Bắc).wav", "text": "./sample/Tuyên (nam miền Bắc).txt"},
39
  "Thiện Tâm": {"audio": "./sample/thientam.mp3", "text": "./sample/thientam.txt"},
@@ -47,90 +23,62 @@ VOICE_SAMPLES = {
47
  "Dung (nữ miền Nam)": {"audio": "./sample/Dung (nữ miền Nam).wav", "text": "./sample/Dung (nữ miền Nam).txt"}
48
  }
49
 
50
- def load_ref_info(choice):
51
  if choice in VOICE_SAMPLES:
52
- audio = VOICE_SAMPLES[choice]["audio"]
53
  with open(VOICE_SAMPLES[choice]["text"], "r", encoding="utf-8") as f:
54
- return audio, f.read()
55
  return None, ""
56
 
57
  @spaces.GPU(duration=120)
58
- def synthesize_speech(text, voice, c_audio, c_text, mode, pause, speed):
 
59
  try:
60
- if not text.strip(): return None, "⚠️ Vui lòng nhập nội dung!"
61
-
62
- p_text = text
63
- if pause == "Trung bình": p_text = p_text.replace(",", ", , ").replace(".", ". . ")
64
- elif pause == "Dài": p_text = p_text.replace(",", ", , , ").replace(".", ". . . . ")
65
-
66
- ref_path, ref_txt = (c_audio, c_text) if mode == "custom_mode" else (VOICE_SAMPLES[voice]["audio"], open(VOICE_SAMPLES[voice]["text"], "r", encoding="utf-8").read())
67
-
68
  start = time.time()
69
- ref_codes = tts.encode_reference(ref_path)
70
- wav = tts.infer(p_text[:400], ref_codes, ref_txt)
71
-
72
  if speed != 1.0: wav = librosa.effects.time_stretch(wav, rate=float(speed))
73
-
74
  with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
75
  sf.write(tmp.name, wav, 24000)
76
  return tmp.name, f"✅ Hoàn tất ({time.time()-start:.2f}s)"
77
  except Exception as e: return None, f"❌ Lỗi: {str(e)}"
78
 
79
- # --- 3. UI DESIGN (CHUYÊN NGHIỆP) ---
80
- theme = gr.themes.Default(
81
- primary_hue="indigo",
82
- neutral_hue="slate",
83
- font=[gr.themes.GoogleFont('Inter'), 'sans-serif']
84
- ).set(
85
- body_background_fill="#020617",
86
- block_background_fill="#0f172a",
87
- input_background_fill="#1e293b",
88
- button_primary_background_fill="linear-gradient(135deg, #6366f1 0%, #a855f7 100%)",
89
  )
90
 
91
- css = """
92
- .main-wrap { max-width: 1240px !important; margin: auto !important; padding: 20px !important; }
93
- .st-card { border-radius: 12px !important; border: 1px solid rgba(255,255,255,0.1) !important; padding: 20px !important; background: #0f172a !important; }
94
- .footer { text-align: center; margin-top: 50px; color: #475569; font-size: 0.8rem; }
95
- * { font-family: 'Inter', sans-serif !important; }
96
- """
97
 
98
- with gr.Blocks(title="AI Voice Studio") as demo:
99
  with gr.Column(elem_classes="main-wrap"):
100
- with gr.Row(equal_height=True):
101
  with gr.Column(scale=1):
102
  with gr.Group(elem_classes="st-card"):
103
- text_input = gr.Textbox(label="VĂN BẢN ĐẦU VÀO", lines=22, placeholder="Nhập văn bản cần đọc...")
104
- char_count = gr.HTML("<div style='text-align: right; color: #6366f1; font-weight: 600;'>0 / 250</div>")
105
-
106
  with gr.Column(scale=1):
107
- with gr.Tabs() as tabs:
108
- with gr.TabItem("👤 Giọng Nghệ Sĩ", id="preset_mode"):
109
- v_select = gr.Dropdown(choices=list(VOICE_SAMPLES.keys()), value="Tuyên (nam miền Bắc)", label="Chọn giọng đọc")
110
- with gr.Accordion("Nghe thử giọng mẫu", open=False):
111
- ref_p, ref_t = gr.Audio(interactive=False), gr.Markdown("...")
112
- with gr.TabItem("🎙️ Nhân Bản (Clone)", id="custom_mode"):
113
- c_audio = gr.Audio(label="Audio mẫu", type="filepath")
114
- c_text = gr.Textbox(label="Nội dung lời thoại", lines=5)
115
-
116
  with gr.Row():
117
- p_lvl = gr.Radio(choices=["Mặc định", "Trung bình", "Dài"], value="Mặc định", label="Ngắt nghỉ")
118
- s_val = gr.Dropdown(choices=[0.8, 0.9, 1.0, 1.1, 1.2, 1.5], value=1.0, label="Tốc độ")
119
-
120
- cur_mode = gr.State("preset_mode")
121
- btn = gr.Button("TỔNG HỢP GIỌNG NÓI", variant="primary", size="lg")
122
-
123
  with gr.Group(elem_classes="st-card"):
124
- a_out = gr.Audio(label="KẾT QUẢ", interactive=False, autoplay=True)
125
- s_out = gr.Markdown("<p style='text-align: center; color: #6366f1;'>✨ Sẵn sàng</p>")
126
- gr.HTML("<div class='footer'>ENGINE BY KTVOICE • 2025</div>")
127
 
128
- text_input.change(lambda t: f"<div style='text-align: right; color: {'#6366f1' if len(t)<=250 else '#ef4444'}; font-weight: 600;'>{len(t)} / 250</div>", text_input, char_count)
129
- v_select.change(load_ref_info, v_select, [ref_p, ref_t])
130
- tabs.children[0].select(lambda: "preset_mode", None, cur_mode)
131
- tabs.children[1].select(lambda: "custom_mode", None, cur_mode)
132
- btn.click(synthesize_speech, [text_input, v_select, c_audio, c_text, cur_mode, p_lvl, s_val], [a_out, s_out])
133
 
134
  if __name__ == "__main__":
135
- # Di chuyển theme và css vào launch() để đúng chuẩn Gradio 6.0
136
  demo.queue().launch(theme=theme, css=css, server_name="0.0.0.0", server_port=7860)
 
1
+ import spaces, os, gradio as gr, soundfile as sf, tempfile, torch, librosa, time
 
2
  os.environ['SPACES_ZERO_GPU'] = '1'
 
 
 
 
 
 
 
 
 
3
  from tts_engine import VoiceEngine
4
 
5
+ # --- 1. SETUP MODEL (GIỮ LOGIC TỰ ĐỘNG CỦA TÁC GIẢ) ---
6
  device = "cuda" if torch.cuda.is_available() else "cpu"
 
 
 
7
  try:
8
+ tts = VoiceEngine(backbone_repo="ktvoice/Backbone", backbone_device=device, codec_repo="ktvoice/Codec", codec_device=device)
 
 
 
 
 
9
  except Exception as e:
10
+ print(f"⚠️ Lỗi: {e}")
11
+ tts = None
 
 
 
 
 
12
 
 
13
  VOICE_SAMPLES = {
14
  "Tuyên (nam miền Bắc)": {"audio": "./sample/Tuyên (nam miền Bắc).wav", "text": "./sample/Tuyên (nam miền Bắc).txt"},
15
  "Thiện Tâm": {"audio": "./sample/thientam.mp3", "text": "./sample/thientam.txt"},
 
23
  "Dung (nữ miền Nam)": {"audio": "./sample/Dung (nữ miền Nam).wav", "text": "./sample/Dung (nữ miền Nam).txt"}
24
  }
25
 
26
+ def load_ref(choice):
27
  if choice in VOICE_SAMPLES:
 
28
  with open(VOICE_SAMPLES[choice]["text"], "r", encoding="utf-8") as f:
29
+ return VOICE_SAMPLES[choice]["audio"], f.read()
30
  return None, ""
31
 
32
  @spaces.GPU(duration=120)
33
+ def process_tts(text, voice, c_audio, c_text, mode, pause, speed):
34
+ if not tts: return None, "❌ Lỗi khởi tạo mô hình!"
35
  try:
36
+ ref_path, ref_txt = (c_audio, c_text) if mode == "custom" else (VOICE_SAMPLES[voice]["audio"], open(VOICE_SAMPLES[voice]["text"], "r", encoding="utf-8").read())
 
 
 
 
 
 
 
37
  start = time.time()
38
+ codes = tts.encode_reference(ref_path)
39
+ wav = tts.infer(text[:400], codes, ref_txt)
 
40
  if speed != 1.0: wav = librosa.effects.time_stretch(wav, rate=float(speed))
 
41
  with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
42
  sf.write(tmp.name, wav, 24000)
43
  return tmp.name, f"✅ Hoàn tất ({time.time()-start:.2f}s)"
44
  except Exception as e: return None, f"❌ Lỗi: {str(e)}"
45
 
46
+ # --- UI ---
47
+ theme = gr.themes.Default(primary_hue="indigo", neutral_hue="slate", font=[gr.themes.GoogleFont('Inter'), 'sans-serif']).set(
48
+ body_background_fill="#020617", block_background_fill="#0f172a",
49
+ input_background_fill="#1e293b", button_primary_background_fill="linear-gradient(135deg, #6366f1 0%, #a855f7 100%)",
 
 
 
 
 
 
50
  )
51
 
52
+ css = ".main-wrap { max-width: 1280px !important; margin: auto !important; padding: 20px !important; } .st-card { border-radius: 12px !important; border: 1px solid rgba(255,255,255,0.1) !important; padding: 25px !important; background: #0f172a !important; } * { font-family: 'Inter', sans-serif !important; } label span { font-weight: 700 !important; color: #818cf8 !important; text-transform: uppercase; font-size: 0.75rem !important; } .footer { text-align: center; margin-top: 50px; color: #475569; font-size: 0.8rem; }"
 
 
 
 
 
53
 
54
+ with gr.Blocks(title="AI Studio") as demo:
55
  with gr.Column(elem_classes="main-wrap"):
56
+ with gr.Row():
57
  with gr.Column(scale=1):
58
  with gr.Group(elem_classes="st-card"):
59
+ txt = gr.Textbox(label="VĂN BẢN ĐẦU VÀO", lines=20, placeholder="Nhập nội dung...")
60
+ gr.HTML("<div style='text-align: right; color: #6366f1; font-weight: 700;'>0 / 250</div>")
 
61
  with gr.Column(scale=1):
62
+ with gr.Tabs() as ts:
63
+ with gr.TabItem("👤 Giọng Nghệ Sĩ", id="preset"):
64
+ v_sel = gr.Dropdown(choices=list(VOICE_SAMPLES.keys()), value="Tuyên (nam miền Bắc)", label="Chọn nghệ ")
65
+ with gr.Accordion("Nghe thử", open=False): rp, rt = gr.Audio(interactive=False), gr.Markdown()
66
+ with gr.TabItem("🎙️ Tự Nhân Bản", id="custom"):
67
+ ca = gr.Audio(label="Audio gốc", type="filepath")
68
+ ct = gr.Textbox(label="Nội dung audio mẫu", lines=5)
 
 
69
  with gr.Row():
70
+ pl = gr.Radio(choices=["Mặc định", "Trung bình", "Dài"], value="Mặc định", label="Ngắt nghỉ")
71
+ sv = gr.Dropdown(choices=[0.8, 0.9, 1.0, 1.1, 1.2, 1.5], value=1.0, label="Tốc độ")
72
+ md = gr.State("preset")
73
+ btn = gr.Button("TẠO GIỌNG NÓI NGAY", variant="primary", size="lg")
 
 
74
  with gr.Group(elem_classes="st-card"):
75
+ ao, st = gr.Audio(label="KẾT QUẢ", interactive=False, autoplay=True), gr.Markdown("<p style='text-align: center; color: #6366f1;'>Sẵn sàng</p>")
76
+ gr.HTML("<div class='footer'>AI VOICE ENGINE PROFESSIONAL STUDIO 2025</div>")
 
77
 
78
+ v_sel.change(load_ref, v_sel, [rp, rt])
79
+ ts.children[0].select(lambda: "preset", None, md)
80
+ ts.children[1].select(lambda: "custom", None, md)
81
+ btn.click(process_tts, [txt, v_sel, ca, ct, md, pl, sv], [ao, st])
 
82
 
83
  if __name__ == "__main__":
 
84
  demo.queue().launch(theme=theme, css=css, server_name="0.0.0.0", server_port=7860)
tts_engine.py CHANGED
@@ -1,190 +1,132 @@
1
- from pathlib import Path
2
- from typing import Generator
 
 
3
  import librosa
4
  import numpy as np
5
- import torch
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  from neucodec import NeuCodec, DistillNeuCodec
7
  from transformers import AutoTokenizer, AutoModelForCausalLM
8
  from utils.phonemize_text import phonemize_text, phonemize_with_dict
9
- import re
10
 
11
  def _linear_overlap_add(frames: list[np.ndarray], stride: int) -> np.ndarray:
12
  assert len(frames)
13
  dtype = frames[0].dtype
14
  shape = frames[0].shape[:-1]
15
-
16
- total_size = 0
17
- for i, frame in enumerate(frames):
18
- frame_end = stride * i + frame.shape[-1]
19
- total_size = max(total_size, frame_end)
20
-
21
  sum_weight = np.zeros(total_size, dtype=dtype)
22
  out = np.zeros(*shape, total_size, dtype=dtype)
23
-
24
  offset: int = 0
25
  for frame in frames:
26
  frame_length = frame.shape[-1]
27
  t = np.linspace(0, 1, frame_length + 2, dtype=dtype)[1:-1]
28
  weight = np.abs(0.5 - (t - 0.5))
29
-
30
  out[..., offset : offset + frame_length] += weight * frame
31
  sum_weight[offset : offset + frame_length] += weight
32
  offset += stride
33
- assert sum_weight.min() > 0
34
  return out / sum_weight
35
 
36
  class VoiceEngine:
37
- def __init__(
38
- self,
39
- backbone_repo="ktvoice/Backbone", # Thiết lập mặc định về repo của bạn
40
- backbone_device="cpu",
41
- codec_repo="ktvoice/Codec", # Thiết lập mặc định về repo của bạn
42
- codec_device="cpu",
43
- ):
44
-
45
- # Constants
46
  self.sample_rate = 24_000
47
  self.max_context = 2048
48
  self.hop_length = 480
49
- self.streaming_overlap_frames = 1
50
- self.streaming_frames_per_chunk = 25
51
- self.streaming_lookforward = 5
52
- self.streaming_lookback = 50
53
- self.streaming_stride_samples = self.streaming_frames_per_chunk * self.hop_length
54
-
55
- # Flags
56
  self._is_quantized_model = False
57
  self._is_onnx_codec = False
58
-
59
  self.tokenizer = None
60
-
61
- # Khởi tạo mô hình
62
  self._load_backbone(backbone_repo, backbone_device)
63
  self._load_codec(codec_repo, codec_device)
64
 
65
- def _load_backbone(self, backbone_repo, backbone_device):
66
- print(f"Loading backbone from: {backbone_repo} on {backbone_device} ...")
67
-
68
- if backbone_repo.lower().endswith("gguf") or "gguf" in backbone_repo.lower():
69
- try:
70
- from llama_cpp import Llama
71
- except ImportError as e:
72
- raise ImportError("Vui lòng cài đặt llama-cpp-python để dùng model GGUF.") from e
73
- self.backbone = Llama.from_pretrained(
74
- repo_id=backbone_repo,
75
- filename="*.gguf",
76
- verbose=False,
77
- n_gpu_layers=-1 if backbone_device == "gpu" else 0,
78
- n_ctx=self.max_context,
79
- mlock=True,
80
- flash_attn=True if backbone_device == "gpu" else False,
81
- )
82
  self._is_quantized_model = True
83
  else:
84
- self.tokenizer = AutoTokenizer.from_pretrained(backbone_repo)
85
- self.backbone = AutoModelForCausalLM.from_pretrained(backbone_repo).to(
86
- torch.device(backbone_device)
87
- )
88
 
89
-
90
- def _load_codec(self, codec_repo, codec_device):
91
- print(f"Loading codec from: {codec_repo} on {codec_device} ...")
92
- from huggingface_hub import snapshot_download
93
-
94
- # Tải model về thư mục tạm để vượt qua lỗi check tên repo của thư viện neucodec
95
- local_codec_path = snapshot_download(repo_id=codec_repo)
96
-
97
- codec_repo_lower = codec_repo.lower()
98
 
99
- if "distill" in codec_repo_lower:
100
- self.codec = DistillNeuCodec.from_pretrained(local_codec_path)
101
- elif "onnx" in codec_repo_lower:
102
- try:
103
- from neucodec import NeuCodecOnnxDecoder
104
- except ImportError:
105
- raise ImportError("Vui lòng cài đặt onnxruntime và neucodec >= 0.0.4.")
106
- self.codec = NeuCodecOnnxDecoder.from_pretrained(local_codec_path)
107
- self._is_onnx_codec = True
 
 
108
  else:
109
- # Tải từ thư mục local đã download
110
- self.codec = NeuCodec.from_pretrained(local_codec_path)
111
-
112
- if not self._is_onnx_codec:
113
- self.codec.eval().to(codec_device)
114
-
115
-
116
 
117
- def infer(self, text: str, ref_codes: np.ndarray | torch.Tensor, ref_text: str) -> np.ndarray:
118
- if self._is_quantized_model:
119
- output_str = self._infer_ggml(ref_codes, ref_text, text)
120
- else:
121
- prompt_ids = self._apply_chat_template(ref_codes, ref_text, text)
122
- output_str = self._infer_torch(prompt_ids)
123
-
124
- wav = self._decode(output_str)
125
- return wav
126
-
127
- def encode_reference(self, ref_audio_path: str | Path):
128
- wav, _ = librosa.load(ref_audio_path, sr=16000, mono=True)
129
  wav_tensor = torch.from_numpy(wav).float().unsqueeze(0).unsqueeze(0)
130
  with torch.no_grad():
131
- ref_codes = self.codec.encode_code(audio_or_path=wav_tensor).squeeze(0).squeeze(0)
132
- return ref_codes
133
 
134
- def _decode(self, codes: str):
135
- speech_ids = [int(num) for num in re.findall(r"<\|speech_(\d+)\|>", codes)]
136
- if len(speech_ids) == 0:
137
- raise ValueError("Hệ thống không tạo được token speech hợp lệ.")
138
 
139
- if self._is_onnx_codec:
140
- codes_np = np.array(speech_ids, dtype=np.int32)[np.newaxis, np.newaxis, :]
141
- recon = self.codec.decode_code(codes_np)
142
- else:
143
- with torch.no_grad():
144
- codes_tensor = torch.tensor(speech_ids, dtype=torch.long)[None, None, :].to(self.codec.device)
145
- recon = self.codec.decode_code(codes_tensor).cpu().numpy()
146
 
147
- return recon[0, 0, :]
148
-
149
- def _apply_chat_template(self, ref_codes: list[int], ref_text: str, input_text: str) -> list[int]:
150
- input_text = phonemize_with_dict(ref_text) + " " + phonemize_with_dict(input_text)
151
-
152
- speech_replace = self.tokenizer.convert_tokens_to_ids("<|SPEECH_REPLACE|>")
153
- speech_gen_start = self.tokenizer.convert_tokens_to_ids("<|SPEECH_GENERATION_START|>")
154
- text_replace = self.tokenizer.convert_tokens_to_ids("<|TEXT_REPLACE|>")
155
- text_prompt_start = self.tokenizer.convert_tokens_to_ids("<|TEXT_PROMPT_START|>")
156
- text_prompt_end = self.tokenizer.convert_tokens_to_ids("<|TEXT_PROMPT_END|>")
157
-
158
- input_ids = self.tokenizer.encode(input_text, add_special_tokens=False)
159
- chat = "user: Convert the text to speech:<|TEXT_REPLACE|>\nassistant:<|SPEECH_REPLACE|>"
160
- ids = self.tokenizer.encode(chat)
161
-
162
- text_replace_idx = ids.index(text_replace)
163
- ids = ids[:text_replace_idx] + [text_prompt_start] + input_ids + [text_prompt_end] + ids[text_replace_idx + 1 :]
164
-
165
- speech_replace_idx = ids.index(speech_replace)
166
- codes_str = "".join([f"<|speech_{i}|>" for i in ref_codes])
167
- codes = self.tokenizer.encode(codes_str, add_special_tokens=False)
168
- ids = ids[:speech_replace_idx] + [speech_gen_start] + list(codes)
169
-
170
- return ids
171
 
172
- def _infer_torch(self, prompt_ids: list[int]) -> str:
173
- prompt_tensor = torch.tensor(prompt_ids).unsqueeze(0).to(self.backbone.device)
174
- speech_end_id = self.tokenizer.convert_tokens_to_ids("<|SPEECH_GENERATION_END|>")
175
  with torch.no_grad():
176
- output_tokens = self.backbone.generate(
177
- prompt_tensor,
178
- max_length=self.max_context,
179
- eos_token_id=speech_end_id,
180
- do_sample=True,
181
- temperature=1,
182
- top_k=50,
183
- use_cache=True,
184
- min_new_tokens=50,
185
- )
186
- input_length = prompt_tensor.shape[-1]
187
- output_str = self.tokenizer.decode(
188
- output_tokens[0, input_length:].cpu().numpy().tolist(), add_special_tokens=False
189
- )
190
- return output_str
 
1
+ import os
2
+ import re
3
+ import time
4
+ import torch
5
  import librosa
6
  import numpy as np
7
+ from pathlib import Path
8
+ from typing import Generator
9
+ from huggingface_hub import snapshot_download
10
+
11
+ # --- BẢN VÁ (PATCH) ĐỂ CHẠY ĐƯỢC VỚI REPO CÁ NHÂN KTVOICE ---
12
+ import neucodec.model
13
+ import json
14
+
15
+ # Lưu lại hàm gốc của thư viện neucodec
16
+ _orig_from_pretrained = neucodec.model.NeuCodec._from_pretrained
17
+
18
+ @classmethod
19
+ def _patched_from_pretrained(cls, model_id, *args, **kwargs):
20
+ """
21
+ Bản vá này giúp vượt qua lệnh assert model_id in [...] của thư viện neucodec.
22
+ Nó cho phép nạp mô hình từ bất kỳ repo nào (như ktvoice/Codec).
23
+ """
24
+ # Nếu model_id là một đường dẫn local hoặc repo cá nhân,
25
+ # chúng ta "đánh lừa" thư viện bằng cách dùng tên repo chính thức để qua cửa assert.
26
+ valid_ids = ["neuphonic/neucodec", "neuphonic/distill-neucodec"]
27
+ check_id = model_id
28
+ if model_id not in valid_ids:
29
+ check_id = "neuphonic/neucodec"
30
+
31
+ # Thực hiện nạp mô hình (Lệnh assert sẽ kiểm tra check_id thay vì model_id của bạn)
32
+ return _orig_from_pretrained(check_id, *args, **kwargs)
33
+
34
+ # Áp dụng bản vá vào cả hai lớp của thư viện neucodec
35
+ neucodec.model.NeuCodec._from_pretrained = _patched_from_pretrained
36
+ neucodec.model.DistillNeuCodec._from_pretrained = _patched_from_pretrained
37
+ # -----------------------------------------------------------
38
+
39
  from neucodec import NeuCodec, DistillNeuCodec
40
  from transformers import AutoTokenizer, AutoModelForCausalLM
41
  from utils.phonemize_text import phonemize_text, phonemize_with_dict
 
42
 
43
  def _linear_overlap_add(frames: list[np.ndarray], stride: int) -> np.ndarray:
44
  assert len(frames)
45
  dtype = frames[0].dtype
46
  shape = frames[0].shape[:-1]
47
+ total_size = max(stride * i + frame.shape[-1] for i, frame in enumerate(frames))
 
 
 
 
 
48
  sum_weight = np.zeros(total_size, dtype=dtype)
49
  out = np.zeros(*shape, total_size, dtype=dtype)
 
50
  offset: int = 0
51
  for frame in frames:
52
  frame_length = frame.shape[-1]
53
  t = np.linspace(0, 1, frame_length + 2, dtype=dtype)[1:-1]
54
  weight = np.abs(0.5 - (t - 0.5))
 
55
  out[..., offset : offset + frame_length] += weight * frame
56
  sum_weight[offset : offset + frame_length] += weight
57
  offset += stride
 
58
  return out / sum_weight
59
 
60
  class VoiceEngine:
61
+ def __init__(self, backbone_repo="ktvoice/Backbone", backbone_device="cpu", codec_repo="ktvoice/Codec", codec_device="cpu"):
 
 
 
 
 
 
 
 
62
  self.sample_rate = 24_000
63
  self.max_context = 2048
64
  self.hop_length = 480
 
 
 
 
 
 
 
65
  self._is_quantized_model = False
66
  self._is_onnx_codec = False
 
67
  self.tokenizer = None
68
+
 
69
  self._load_backbone(backbone_repo, backbone_device)
70
  self._load_codec(codec_repo, codec_device)
71
 
72
+ def _load_backbone(self, repo, device):
73
+ print(f"Loading backbone from: {repo} on {device} ...")
74
+ if "gguf" in repo.lower():
75
+ from llama_cpp import Llama
76
+ self.backbone = Llama.from_pretrained(repo_id=repo, filename="*.gguf", n_ctx=self.max_context)
 
 
 
 
 
 
 
 
 
 
 
 
77
  self._is_quantized_model = True
78
  else:
79
+ self.tokenizer = AutoTokenizer.from_pretrained(repo)
80
+ self.backbone = AutoModelForCausalLM.from_pretrained(repo).to(torch.device(device))
 
 
81
 
82
+ def _load_codec(self, repo, device):
83
+ print(f"Loading codec from: {repo} on {device} ...")
84
+ # Tải hình về thư mục tạm
85
+ local_dir = snapshot_download(repo_id=repo)
 
 
 
 
 
86
 
87
+ # GIẢI THÍCH: Tại sao cần tạo config.json giả?
88
+ # Thư viện neucodec mặc định tìm config.json khi repo name không phải là 'neuphonic/neucodec'.
89
+ # Chúng ta tạo một file config.json tối giản trong thư mục snapshot để đánh lừa nó.
90
+ config_path = os.path.join(local_dir, "config.json")
91
+ if not os.path.exists(config_path):
92
+ with open(config_path, "w") as f:
93
+ json.dump({"model_type": "neucodec"}, f)
94
+
95
+ # Nạp mô hình từ đường dẫn cục bộ
96
+ if "distill" in repo.lower():
97
+ self.codec = DistillNeuCodec.from_pretrained(local_dir)
98
  else:
99
+ self.codec = NeuCodec.from_pretrained(local_dir)
100
+ self.codec.eval().to(device)
 
 
 
 
 
101
 
102
+ def encode_reference(self, path):
103
+ wav, _ = librosa.load(path, sr=16000, mono=True)
 
 
 
 
 
 
 
 
 
 
104
  wav_tensor = torch.from_numpy(wav).float().unsqueeze(0).unsqueeze(0)
105
  with torch.no_grad():
106
+ return self.codec.encode_code(audio_or_path=wav_tensor).squeeze(0).squeeze(0)
 
107
 
108
+ def infer(self, text, ref_codes, ref_text):
109
+ if self._is_quantized_model:
110
+ # Placeholder cho logic GGUF nếu bạn cần
111
+ return np.zeros(48000)
112
 
113
+ prompt_ids = self._apply_chat_template(ref_codes, ref_text, text)
114
+ prompt_tensor = torch.tensor(prompt_ids).unsqueeze(0).to(self.backbone.device)
 
 
 
 
 
115
 
116
+ with torch.no_grad():
117
+ out = self.backbone.generate(prompt_tensor, max_length=self.max_context, do_sample=True, temperature=1)
118
+
119
+ tokens = self.tokenizer.decode(out[0, prompt_tensor.shape[-1]:], add_special_tokens=False)
120
+ return self._decode(tokens)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ def _decode(self, codes_str):
123
+ speech_ids = [int(n) for n in re.findall(r"<\|speech_(\d+)\|>", codes_str)]
 
124
  with torch.no_grad():
125
+ codes_tensor = torch.tensor(speech_ids, dtype=torch.long)[None, None, :].to(self.codec.device)
126
+ return self.codec.decode_code(codes_tensor).cpu().numpy()[0, 0, :]
127
+
128
+ def _apply_chat_template(self, ref_codes, ref_text, text):
129
+ input_text = phonemize_with_dict(ref_text) + " " + phonemize_with_dict(text)
130
+ chat = f"user: Convert the text to speech:<|TEXT_PROMPT_START|>{input_text}<|TEXT_PROMPT_END|>\nassistant:<|SPEECH_GENERATION_START|>"
131
+ c_str = "".join([f"<|speech_{i}|>" for i in ref_codes])
132
+ return self.tokenizer.encode(chat + c_str)