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

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +45 -38
  2. tts_engine.py +31 -42
app.py CHANGED
@@ -2,12 +2,11 @@ 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 = {
@@ -23,62 +22,70 @@ VOICE_SAMPLES = {
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ệ sĩ")
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)
 
2
  os.environ['SPACES_ZERO_GPU'] = '1'
3
  from tts_engine import VoiceEngine
4
 
5
+ # --- SETUP ---
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:
 
10
  tts = None
11
 
12
  VOICE_SAMPLES = {
 
22
  "Dung (nữ miền Nam)": {"audio": "./sample/Dung (nữ miền Nam).wav", "text": "./sample/Dung (nữ miền Nam).txt"}
23
  }
24
 
 
 
 
 
 
 
25
  @spaces.GPU(duration=120)
26
+ def run_tts(text, voice, c_audio, c_text, mode, pause, speed):
27
+ if not tts: return None, "❌ Lỗi hệ thống"
28
  try:
 
29
  start = time.time()
30
+ 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())
31
  codes = tts.encode_reference(ref_path)
32
  wav = tts.infer(text[:400], codes, ref_txt)
33
  if speed != 1.0: wav = librosa.effects.time_stretch(wav, rate=float(speed))
34
  with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
35
  sf.write(tmp.name, wav, 24000)
36
+ return tmp.name, f"✅ Thành công ({time.time()-start:.2f}s)"
37
  except Exception as e: return None, f"❌ Lỗi: {str(e)}"
38
 
39
+ # --- CẤU HÌNH GIAO DIỆN (TONE XANH ĐẬM STUDIO) ---
40
+ theme = gr.themes.Soft(
41
+ primary_hue="blue",
42
+ neutral_hue="slate",
43
+ font=[gr.themes.GoogleFont('Roboto'), 'sans-serif']
44
+ ).set(
45
+ body_background_fill="#0f172a",
46
+ block_background_fill="#1e293b",
47
+ input_background_fill="#334155",
48
+ button_primary_background_fill="linear-gradient(135deg, #2563eb 0%, #0891b2 100%)", # Chuyển sang xanh biển/cyan
49
+ button_primary_text_color="white",
50
+ block_label_text_size="0.85rem",
51
+ block_title_text_weight="600",
52
  )
53
 
54
+ css = """
55
+ .main-container { max-width: 1200px !important; margin: auto !important; padding: 20px !important; }
56
+ .card-box { border-radius: 12px !important; border: 1px solid rgba(255,255,255,0.05) !important; padding: 20px; background: #1e293b !important; }
57
+ textarea, input, span, label { line-height: 1.6 !important; } /* Sửa lỗi font đè chữ */
58
+ .footer-text { text-align: center; margin-top: 40px; color: #64748b; font-size: 0.8rem; letter-spacing: 1px; }
59
+ """
60
 
61
+ with gr.Blocks(theme=theme, css=css, title="AI Studio") as demo:
62
+ with gr.Column(elem_classes="main-container"):
63
+ with gr.Row(equal_height=True):
64
  with gr.Column(scale=1):
65
+ with gr.Group(elem_classes="card-box"):
66
+ t_in = gr.Textbox(label="VĂN BẢN ĐẦU VÀO", lines=20, placeholder="Nhập văn bản cần chuyển giọng...")
67
+ t_cnt = gr.HTML("<div style='text-align: right; color: #38bdf8; font-weight: 700; padding: 5px;'>0 / 250</div>")
68
  with gr.Column(scale=1):
69
+ with gr.Tabs() as tabs:
70
  with gr.TabItem("👤 Giọng Nghệ Sĩ", id="preset"):
71
  v_sel = gr.Dropdown(choices=list(VOICE_SAMPLES.keys()), value="Tuyên (nam miền Bắc)", label="Chọn nghệ sĩ")
72
+ with gr.TabItem("🎙️ Nhân Bản Giọng", id="custom"):
 
73
  ca = gr.Audio(label="Audio gốc", type="filepath")
74
+ ct = gr.Textbox(label="Lời thoại audio gốc", lines=4)
75
  with gr.Row():
76
  pl = gr.Radio(choices=["Mặc định", "Trung bình", "Dài"], value="Mặc định", label="Ngắt nghỉ")
77
  sv = gr.Dropdown(choices=[0.8, 0.9, 1.0, 1.1, 1.2, 1.5], value=1.0, label="Tốc độ")
78
+ m_state = gr.State("preset")
79
+ btn = gr.Button("BẮT ĐẦU TỔNG HỢP", variant="primary", size="lg")
80
+ with gr.Group(elem_classes="card-box"):
81
+ ao = gr.Audio(label="KẾT QUẢ ÂM THANH", interactive=False, autoplay=True)
82
+ st = gr.Markdown("<p style='text-align: center; color: #38bdf8;'>Hệ thống sẵn sàng</p>")
83
+ gr.HTML("<div class='footer-text'>POWERED BY KTVOICE STUDIO • 2025</div>")
84
 
85
+ t_in.change(lambda t: f"<div style='text-align: right; color: {'#38bdf8' if len(t)<=250 else '#f43f5e'}; font-weight: 700; padding: 5px;'>{len(t)} / 250</div>", t_in, t_cnt)
86
+ tabs.children[0].select(lambda: "preset", None, m_state)
87
+ tabs.children[1].select(lambda: "custom", None, m_state)
88
+ btn.click(run_tts, [t_in, v_sel, ca, ct, m_state, pl, sv], [ao, st])
89
 
90
  if __name__ == "__main__":
91
+ demo.queue().launch(server_name="0.0.0.0", server_port=7860)
tts_engine.py CHANGED
@@ -4,37 +4,37 @@ 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
@@ -61,11 +61,8 @@ 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
 
@@ -80,19 +77,16 @@ class VoiceEngine:
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:
@@ -106,13 +100,8 @@ class VoiceEngine:
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
 
 
4
  import torch
5
  import librosa
6
  import numpy as np
7
+ import json
8
  from pathlib import Path
9
  from typing import Generator
10
  from huggingface_hub import snapshot_download
11
 
12
+ # --- BẢN VÁ CAO CẤP (MONKEY PATCH) ĐỂ CHẠY REPO KTVOICE ---
13
  import neucodec.model
 
 
 
 
14
 
15
+ def _apply_robust_patch(target_cls):
16
+ """Vá lỗi AssertionError TypeError cho thư viện neucodec"""
17
+ orig_func = target_cls._from_pretrained
 
 
 
 
 
 
 
 
 
18
 
19
+ @classmethod
20
+ def _patched_func(cls, *args, **kwargs):
21
+ # Đảm bảo model_id luôn hợp lệ để thoả mãn lệnh assert của thư viện
22
+ official_id = "neuphonic/distill-neucodec" if "distill" in str(cls).lower() else "neuphonic/neucodec"
23
+
24
+ # Sửa lỗi: HubMixin truyền model_id ở vị trí đầu tiên
25
+ if args:
26
+ kwargs["model_id"] = official_id
27
+ return orig_func(*args[1:], **kwargs)
28
+ else:
29
+ kwargs["model_id"] = official_id
30
+ return orig_func(**kwargs)
31
+
32
+ target_cls._from_pretrained = _patched_func
33
 
34
+ # Áp dụng cho cả 2 lớp của neucodec
35
+ _apply_robust_patch(neucodec.model.NeuCodec)
36
+ _apply_robust_patch(neucodec.model.DistillNeuCodec)
37
+ # -------------------------------------------------------
38
 
39
  from neucodec import NeuCodec, DistillNeuCodec
40
  from transformers import AutoTokenizer, AutoModelForCausalLM
 
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._is_quantized_model = False
 
65
  self.tokenizer = None
 
66
  self._load_backbone(backbone_repo, backbone_device)
67
  self._load_codec(codec_repo, codec_device)
68
 
 
77
  self.backbone = AutoModelForCausalLM.from_pretrained(repo).to(torch.device(device))
78
 
79
  def _load_codec(self, repo, device):
80
+ print(f"Loading codec from your repo: {repo} ...")
81
+ # Tải trọng số từ repo ktvoice của bạn
82
  local_dir = snapshot_download(repo_id=repo)
83
 
84
+ # Tạo file cấu hình tạm thời để tránh lỗi "config.json not found"
85
+ # File này chỉ dùng để kích hoạt trình nạp của Hugging Face
86
+ tmp_config = os.path.join(local_dir, "config.json")
87
+ if not os.path.exists(tmp_config):
88
+ with open(tmp_config, "w") as f: json.dump({"model_type": "neucodec"}, f)
89
+
 
 
 
90
  if "distill" in repo.lower():
91
  self.codec = DistillNeuCodec.from_pretrained(local_dir)
92
  else:
 
100
  return self.codec.encode_code(audio_or_path=wav_tensor).squeeze(0).squeeze(0)
101
 
102
  def infer(self, text, ref_codes, ref_text):
 
 
 
 
103
  prompt_ids = self._apply_chat_template(ref_codes, ref_text, text)
104
  prompt_tensor = torch.tensor(prompt_ids).unsqueeze(0).to(self.backbone.device)
 
105
  with torch.no_grad():
106
  out = self.backbone.generate(prompt_tensor, max_length=self.max_context, do_sample=True, temperature=1)
107