tonyshark commited on
Commit
7014f5a
·
verified ·
1 Parent(s): 1f495a1

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +54 -153
  2. requirements.txt +0 -1
app.py CHANGED
@@ -1,13 +1,11 @@
1
  import re
2
- import io
3
- import numpy as np
4
  import torch
5
  import soundfile as sf
6
- import librosa
7
  import gradio as gr
 
8
  from styletts2 import tts
9
 
10
- SR_OUT = 24000 # sample rate output cho toàn bộ hệ
11
 
12
  # ---------------------------
13
  # Load StyleTTS2
@@ -15,197 +13,100 @@ SR_OUT = 24000 # sample rate output cho toàn bộ hệ
15
  model = tts.StyleTTS2()
16
 
17
  # ---------------------------
18
- # Audio utils
19
- # ---------------------------
20
- def load_wav_any(file_or_path, target_sr=None, mono=True):
21
- """Load wav (from path hoặc Gradio file object), optional resample."""
22
- if file_or_path is None:
23
- return None, None
24
- if hasattr(file_or_path, "name"): # Uploaded file (tempfile)
25
- path = file_or_path.name
26
- else:
27
- path = file_or_path
28
- wav, sr = sf.read(path, always_2d=False)
29
- if wav.ndim > 1 and mono:
30
- wav = wav.mean(axis=1)
31
- if target_sr and sr != target_sr:
32
- wav = librosa.resample(wav.astype(np.float32), orig_sr=sr, target_sr=target_sr)
33
- sr = target_sr
34
- return wav.astype(np.float32), sr
35
-
36
- def to_tensor_batch1(wav_np):
37
- return torch.tensor(wav_np).float().unsqueeze(0)
38
-
39
- def fade(wav, fade_ms=10, sr=SR_OUT):
40
- """Fade in/out để tránh click khi nối."""
41
- if wav is None or len(wav) == 0:
42
- return wav
43
- n = len(wav)
44
- fade_len = max(1, int(sr * fade_ms / 1000.0))
45
- env = np.ones(n, dtype=np.float32)
46
- ramp = np.linspace(0.0, 1.0, fade_len, dtype=np.float32)
47
- env[:fade_len] *= ramp
48
- env[-fade_len:] *= ramp[::-1]
49
- return wav * env
50
-
51
- def match_gain(wav, gain_db):
52
- """Áp gain dB lên clip."""
53
- g = 10 ** (gain_db / 20.0)
54
- return (wav * g).astype(np.float32)
55
-
56
- # ---------------------------
57
- # Style extraction
58
  # ---------------------------
59
- def get_style_embedding(file):
60
  if file is None:
61
  return None
62
- wav, sr = load_wav_any(file, target_sr=SR_OUT)
63
- if wav is None:
64
- return None
65
- wav_t = to_tensor_batch1(wav)
66
- return model.get_style_embedding(wav_t, SR_OUT) # (1, D)
67
 
68
  # ---------------------------
69
  # Core synthesis
70
  # ---------------------------
71
- TAG_PATTERN = r"(\[(?:laugh|whisper|giggle)\])"
72
-
73
- def synthesize(
74
- text,
75
- neutral_ref, whisper_ref, giggle_ref,
76
- laugh_sfx, # <-- audio tiếng cười để chèn
77
- embedding_scale=1.0,
78
- laugh_gain_db=0.0, # chỉnh âm lượng sfx
79
- laugh_stretch=1.0, # time-stretch sfx (1.0 = nguyên gốc)
80
- ):
81
- # 1) Chuẩn bị style embeddings
82
- style_neutral = get_style_embedding(neutral_ref)
83
- style_whisper = get_style_embedding(whisper_ref)
84
- style_giggle = get_style_embedding(giggle_ref)
85
 
 
 
86
  if style_neutral is None:
87
  return None
88
 
89
- # 2) Load sfx cười (resample, fade & gain)
90
- laugh_np, _ = load_wav_any(laugh_sfx, target_sr=SR_OUT)
91
- if laugh_np is not None:
92
- if laugh_stretch and abs(laugh_stretch - 1.0) > 1e-3:
93
- laugh_np = librosa.effects.time_stretch(laugh_np, rate=1.0/float(laugh_stretch))
94
- laugh_np = fade(laugh_np, fade_ms=12, sr=SR_OUT)
95
- if laugh_gain_db != 0.0:
96
- laugh_np = match_gain(laugh_np, laugh_gain_db)
97
 
98
- # 3) Parse text theo tag
99
  tokens = re.split(TAG_PATTERN, text)
100
 
101
- pieces = []
 
 
 
102
  for tok in tokens:
103
- if tok is None:
104
- continue
105
- t = tok.strip()
106
- if not t:
107
  continue
108
-
109
- if t.startswith("[") and t.endswith("]"):
110
- tag = t[1:-1].lower()
111
- if tag == "laugh":
112
- # chèn trực tiếp sfx tiếng cười
113
- if laugh_np is not None:
114
- pieces.append(laugh_np)
115
- # nếu chưa upload sfx, bỏ qua hoặc có thể synthesize "hahaha" bằng style_giggle
116
- else:
117
- # fallback: synthesize một âm tiết ngắn với giggle style nếu có
118
- style_use = style_giggle if style_giggle is not None else style_neutral
119
- audio = model.inference(
120
- "ha ha", style_embedding=style_use * embedding_scale, output_sample_rate=SR_OUT
121
- )
122
- pieces.append(audio.astype(np.float32))
123
- elif tag == "whisper":
124
- # tạo một đoạn ngắn im lặng mang "breath" hoặc synth 1 khoảng ngắn trống
125
- # ở đây ta không synth text vì tag đơn lẻ, chỉ chuyển style kế tiếp
126
- # => chèn đoạn im lặng rất ngắn để tách
127
- pieces.append(np.zeros(int(0.05*SR_OUT), dtype=np.float32))
128
- # Đặt "current style" cho phần text tiếp theo
129
- # Cách đơn giản: lưu "style kế tiếp" trong biến
130
- pieces.append(("__STYLE__", "whisper"))
131
- elif tag == "giggle":
132
- pieces.append(np.zeros(int(0.05*SR_OUT), dtype=np.float32))
133
- pieces.append(("__STYLE__", "giggle"))
134
- else:
135
- # default: bỏ qua
136
- pass
137
  else:
138
- # text bình thường => synth với style hiện thời (nếu có)
139
- # tìm xem có cờ "__STYLE__" trước đó không
140
- curr_style = style_neutral
141
- # duyệt từ cuối pieces để tìm chỉ thị style gần nhất (nếu có)
142
- for it in reversed(pieces):
143
- if isinstance(it, tuple) and it[0] == "__STYLE__":
144
- mode = it[1]
145
- if mode == "whisper" and style_whisper is not None:
146
- curr_style = style_whisper
147
- elif mode == "giggle" and style_giggle is not None:
148
- curr_style = style_giggle
149
- break
150
-
151
  audio = model.inference(
152
- t, style_embedding=curr_style * embedding_scale, output_sample_rate=SR_OUT
 
 
153
  )
154
- pieces.append(audio.astype(np.float32))
155
 
156
- # 4) Gộp các đoạn
157
- # Lọc bỏ các marker style "__STYLE__"
158
- merged = []
159
- for it in pieces:
160
- if isinstance(it, tuple):
161
- continue
162
- if it is None:
163
- continue
164
- merged.append(it)
165
-
166
- if not merged:
167
  return None
168
 
169
- out = np.concatenate(merged, axis=0)
170
- out = fade(out, fade_ms=8, sr=SR_OUT)
171
- return (SR_OUT, out)
172
 
173
  # ---------------------------
174
  # Gradio UI
175
  # ---------------------------
176
  with gr.Blocks() as demo:
177
- gr.Markdown("# 🎙️ StyleTTS2 Tags + Laugh SFX (Hugging Face Radio App)")
178
  gr.Markdown(
179
- "Nhập text tag: `[whisper]`, `[giggle]`, **`[laugh]`**.\n\n"
180
- "- Với `[laugh]`: app **chèn trực tiếp audio tiếng cười** bạn upload.\n"
181
- "- Với `[whisper]` / `[giggle]`: app dùng **style embedding** từ file tham chiếu.\n"
182
- "- Upload *ít nhất* 1 file neutral để lấy giọng cơ bản."
183
  )
184
 
185
  with gr.Row():
186
  with gr.Column():
187
  text_in = gr.Textbox(
188
- value="Xin chào mọi người [laugh] bây giờ tôi sẽ nói nhỏ [whisper] rồi khúc khích [giggle] và lại bình thường.",
189
- label="Text tags",
190
  lines=4
191
  )
192
  neutral_in = gr.File(label="Neutral reference (.wav)", file_types=[".wav"])
193
- whisper_in = gr.File(label="Whisper reference (.wav)", file_types=[".wav"])
194
- giggle_in = gr.File(label="Giggle reference (.wav)", file_types=[".wav"])
195
-
196
- gr.Markdown("### 🎧 Laugh SFX (chèn trực tiếp khi gặp [laugh])")
197
- laugh_in = gr.File(label="Laugh SFX (.wav)", file_types=[".wav"])
198
- laugh_gain = gr.Slider(-12, 12, value=0.0, step=0.5, label="Laugh gain (dB)")
199
- laugh_stch = gr.Slider(0.5, 2.0, value=1.0, step=0.05, label="Laugh time-stretch (x)")
200
-
201
- emb_scale = gr.Slider(0.5, 2.0, value=1.0, step=0.1, label="Embedding scale (StyleTTS2)")
202
  btn = gr.Button("Generate")
203
  with gr.Column():
204
  audio_out = gr.Audio(label="Kết quả", type="numpy")
205
 
206
  btn.click(
207
  fn=synthesize,
208
- inputs=[text_in, neutral_in, whisper_in, giggle_in, laugh_in, emb_scale, laugh_gain, laugh_stch],
209
  outputs=audio_out
210
  )
211
 
 
1
  import re
 
 
2
  import torch
3
  import soundfile as sf
 
4
  import gradio as gr
5
+ import numpy as np
6
  from styletts2 import tts
7
 
8
+ SR_OUT = 24000
9
 
10
  # ---------------------------
11
  # Load StyleTTS2
 
13
  model = tts.StyleTTS2()
14
 
15
  # ---------------------------
16
+ # Helper: extract style embedding từ 1 file neutral
17
+ # (trong demo này ta chỉ có neutral, các style khác dùng "neutral" luôn,
18
+ # nhưng thể giả lập bằng cách áp embedding_scale hoặc fine-tune thêm)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  # ---------------------------
20
+ def extract_neutral(file):
21
  if file is None:
22
  return None
23
+ wav, sr = sf.read(file)
24
+ if wav.ndim > 1:
25
+ wav = wav.mean(axis=1) # mixdown mono
26
+ wav = torch.tensor(wav).float().unsqueeze(0)
27
+ return model.get_style_embedding(wav, sr)
28
 
29
  # ---------------------------
30
  # Core synthesis
31
  # ---------------------------
32
+ # Regex sẽ bắt các tag mở/đóng như [whisper] ... [/whisper]
33
+ TAG_PATTERN = r"(\[/?(?:whisper|giggle|laugh)\])"
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ def synthesize(text, neutral_ref, embedding_scale=1.0):
36
+ style_neutral = extract_neutral(neutral_ref)
37
  if style_neutral is None:
38
  return None
39
 
40
+ # Trong trường hợp bạn có checkpoint style riêng, thể thay thế ở đây.
41
+ # Ở demo này, tất cả style = neutral clone (bạn có thể mở rộng).
42
+ styles = {
43
+ "neutral": style_neutral,
44
+ "whisper": style_neutral,
45
+ "giggle": style_neutral,
46
+ "laugh": style_neutral,
47
+ }
48
 
49
+ # Parse text theo tag
50
  tokens = re.split(TAG_PATTERN, text)
51
 
52
+ current_style = styles["neutral"]
53
+ stack = []
54
+ final_audio = []
55
+
56
  for tok in tokens:
57
+ if not tok or tok.isspace():
 
 
 
58
  continue
59
+ if tok.startswith("[") and tok.endswith("]"):
60
+ tag = tok[1:-1].lower().strip("/")
61
+ if tok.startswith("[/"): # closing tag
62
+ if stack:
63
+ stack.pop()
64
+ current_style = styles["neutral"] if not stack else styles[stack[-1]]
65
+ else: # opening tag
66
+ stack.append(tag)
67
+ current_style = styles[tag]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  else:
69
+ # synth đoạn text với style hiện tại
 
 
 
 
 
 
 
 
 
 
 
 
70
  audio = model.inference(
71
+ tok,
72
+ style_embedding=current_style * embedding_scale,
73
+ output_sample_rate=SR_OUT
74
  )
75
+ final_audio.append(audio.astype(np.float32))
76
 
77
+ if not final_audio:
 
 
 
 
 
 
 
 
 
 
78
  return None
79
 
80
+ audio_out = np.concatenate(final_audio, axis=0)
81
+ return (SR_OUT, audio_out)
 
82
 
83
  # ---------------------------
84
  # Gradio UI
85
  # ---------------------------
86
  with gr.Blocks() as demo:
87
+ gr.Markdown("# 🎙️ StyleTTS2 với Tag mở/đóng ([whisper]...[/whisper], [giggle]...[/giggle], [laugh]...[/laugh])")
88
  gr.Markdown(
89
+ "- Upload **1 file neutral** để clone giọng.\n"
90
+ "- Trong text, bạn thể dùng tag mở/đóng để giữ style cho cả đoạn.\n"
91
+ "- dụ: `Xin chào [whisper] tôi sẽ thì thầm trong đoạn này [/whisper] giờ lại bình thường.`"
 
92
  )
93
 
94
  with gr.Row():
95
  with gr.Column():
96
  text_in = gr.Textbox(
97
+ value="Xin chào [laugh] đoạn này cười [/laugh] bây giờ [whisper] tôi sẽ thì thầm một lúc [/whisper] rồi lại bình thường.",
98
+ label="Text với tags",
99
  lines=4
100
  )
101
  neutral_in = gr.File(label="Neutral reference (.wav)", file_types=[".wav"])
102
+ emb_scale = gr.Slider(0.5, 2.0, value=1.0, step=0.1, label="Embedding Scale")
 
 
 
 
 
 
 
 
103
  btn = gr.Button("Generate")
104
  with gr.Column():
105
  audio_out = gr.Audio(label="Kết quả", type="numpy")
106
 
107
  btn.click(
108
  fn=synthesize,
109
+ inputs=[text_in, neutral_in, emb_scale],
110
  outputs=audio_out
111
  )
112
 
requirements.txt CHANGED
@@ -1,6 +1,5 @@
1
  styletts2
2
  torch
3
  soundfile
4
- librosa
5
  gradio
6
  numpy
 
1
  styletts2
2
  torch
3
  soundfile
 
4
  gradio
5
  numpy