Spaces:
Sleeping
Sleeping
File size: 5,463 Bytes
a4a797b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | import os, re, tempfile
import numpy as np
import gradio as gr
import torch
import librosa
import soundfile as sf
from transformers import AutoTokenizer, AutoModelForCausalLM
from neucodec import NeuCodec
from phonemizer.backend import EspeakBackend
from vinorm import TTSnorm
MODEL_ID = "dinhthuan/neutts-air-vi"
CODEC_ID = "neuphonic/neucodec"
# Optional default reference (put your own files here)
DEFAULT_REF_WAV = "assets/reference.wav"
DEFAULT_REF_TXT = "assets/reference.txt"
SPEECH_START = "<|SPEECH_GENERATION_START|>"
SPEECH_END = "<|SPEECH_GENERATION_END|>"
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.bfloat16 if device == "cuda" else torch.float32
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
torch_dtype=dtype,
trust_remote_code=True,
).to(device)
model.eval()
codec = NeuCodec.from_pretrained(CODEC_ID).to(device)
codec.eval()
phonemizer = EspeakBackend(language="vi", preserve_punctuation=True, with_stress=True)
def _phones_vi(text: str) -> str:
# same normalization style as model card example
t = TTSnorm(text, punc=False, unknown=True, lower=False, rule=False)
return phonemizer.phonemize([t])[0]
def _encode_ref_16k(ref_wav_path: str) -> torch.Tensor:
# model card encodes reference audio at 16kHz mono
wav, _ = librosa.load(ref_wav_path, sr=16000, mono=True)
wav = torch.from_numpy(wav).float().unsqueeze(0).unsqueeze(0).to(device) # (1,1,T)
with torch.no_grad():
codes = codec.encode_code(audio_or_path=wav).squeeze(0).squeeze(0).detach().cpu()
return codes
def _extract_codes(text: str) -> list[int]:
# take codes between generation tags if present
if SPEECH_START in text and SPEECH_END in text:
text = text.split(SPEECH_START, 1)[1].split(SPEECH_END, 1)[0]
return [int(x) for x in re.findall(r"<\|speech_(\d+)\|>", text)]
@torch.inference_mode()
def tts(text: str, ref_audio_path: str | None, ref_text: str | None, max_new_tokens: int):
text = (text or "").strip()
if not text:
raise gr.Error("Hãy nhập văn bản tiếng Việt.")
# choose reference
if ref_audio_path:
wav_path = ref_audio_path
rt = (ref_text or "").strip()
if not rt:
raise gr.Error("Bạn đã upload reference audio thì cần nhập reference text (đúng nội dung audio).")
else:
# fallback to default assets
if not (os.path.exists(DEFAULT_REF_WAV) and os.path.exists(DEFAULT_REF_TXT)):
raise gr.Error(
"Chưa có reference. Hãy upload ref audio + ref text, "
"hoặc thêm assets/reference.wav và assets/reference.txt vào repo."
)
wav_path = DEFAULT_REF_WAV
rt = open(DEFAULT_REF_TXT, "r", encoding="utf-8").read().strip()
# phonemize
phones = _phones_vi(text)
ref_phones = _phones_vi(rt)
# encode reference audio to speech codes
ref_codes = _encode_ref_16k(wav_path)
codes_str = "".join([f"<|speech_{i}|>" for i in ref_codes.tolist()])
combined_phones = ref_phones + " " + phones
# prompt format follows model card
chat = (
"user: Convert the text to speech:"
f"<|TEXT_PROMPT_START|>{combined_phones}<|TEXT_PROMPT_END|>\n"
f"assistant:{SPEECH_START}{codes_str}"
)
input_ids = tokenizer.encode(chat, return_tensors="pt").to(device)
speech_end_id = tokenizer.convert_tokens_to_ids(SPEECH_END)
out = model.generate(
input_ids,
max_new_tokens=int(max_new_tokens),
temperature=1.0,
top_k=50,
eos_token_id=speech_end_id,
pad_token_id=tokenizer.eos_token_id,
)
out_text = tokenizer.decode(out[0], skip_special_tokens=False)
all_codes = _extract_codes(out_text)
# remove the prefix ref_codes if present
gen_codes = all_codes[len(ref_codes):] if len(all_codes) > len(ref_codes) else all_codes
if len(gen_codes) < 10:
raise gr.Error("Không trích xuất được speech codes. Hãy thử ref audio rõ hơn hoặc text ngắn hơn.")
codes_tensor = torch.tensor(gen_codes, dtype=torch.long).view(1, 1, -1).to(device)
audio = codec.decode_code(codes_tensor).detach().cpu().numpy()[0, 0, :]
audio = np.clip(audio, -1.0, 1.0)
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
# model card indicates output sample rate 24kHz
sf.write(tmp.name, audio, 24000)
return tmp.name
with gr.Blocks(title="Vietnamese TTS (NeuTTS-Air finetune)") as demo:
gr.Markdown("## Vietnamese TTS – dinhthuan/neutts-air-vi\nNhập tiếng Việt → Xuất âm thanh (WAV 24kHz).")
text_in = gr.Textbox(label="Văn bản tiếng Việt", lines=4, value="Xin chào, đây là mô hình TTS tiếng Việt.")
with gr.Row():
ref_audio = gr.Audio(label="Reference audio (3–10s, WAV)", type="filepath")
ref_text = gr.Textbox(label="Reference text (đúng nội dung của ref audio)", lines=2)
max_tok = gr.Slider(256, 3072, value=1536, step=128, label="max_new_tokens")
btn = gr.Button("Tạo giọng nói")
out_audio = gr.Audio(label="Kết quả", type="filepath")
btn.click(tts, inputs=[text_in, ref_audio, ref_text, max_tok], outputs=out_audio)
demo.queue().launch()
|