nukopy commited on
Commit
f05333a
·
1 Parent(s): 454083d

feat: implement cached audio cloning functionality

Browse files

- Added a new module `cheched_vallex.py` for cached audio cloning, allowing users to save and infer from audio prompts.
- Integrated the cached functionality into the main application, providing a new tab for zero-shot audio cloning with cached prompts.
- Enhanced the `infer_from_audio` function to include timing metrics for better performance tracking.

apps/audio_cloning/cheched_vallex.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import re
4
+ import shutil
5
+ import time
6
+ from typing import List, Optional, Tuple
7
+
8
+ import gradio as gr
9
+ import numpy as np
10
+ import torch
11
+
12
+ from .vallex import main as vallex
13
+ from .vallex.descriptions import infer_from_audio_ja_md, top_ja_md
14
+ from .vallex.examples import infer_from_audio_examples
15
+ from .vallex.macros import code2lang, lang2token, langdropdown2token, token2lang
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ PROMPTS_DIR = "./models/prompts"
20
+ PROMPT_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]+$")
21
+
22
+
23
+ def _ensure_prompt_dir() -> str:
24
+ os.makedirs(PROMPTS_DIR, exist_ok=True)
25
+ return PROMPTS_DIR
26
+
27
+
28
+ def _list_saved_prompts() -> List[str]:
29
+ directory = _ensure_prompt_dir()
30
+ files = [f for f in os.listdir(directory) if f.endswith(".npz")]
31
+ return sorted(files)
32
+
33
+
34
+ def _format_prompt_list() -> str:
35
+ prompts = _list_saved_prompts()
36
+ return "\n".join(prompts) if prompts else "保存済みプロンプトはありません。"
37
+
38
+
39
+ def save_prompt_to_cache(
40
+ prompt_id: str,
41
+ upload_audio_prompt: Optional[Tuple[int, np.ndarray]],
42
+ record_audio_prompt: Optional[Tuple[int, np.ndarray]],
43
+ transcript_content: str,
44
+ ):
45
+ prompt_id = prompt_id.strip()
46
+ if prompt_id.lower().endswith(".npz"):
47
+ prompt_id = prompt_id[:-4]
48
+
49
+ if not prompt_id:
50
+ return (
51
+ "プロンプト ID を入力してください。",
52
+ None,
53
+ gr.update(choices=_list_saved_prompts(), value=None),
54
+ gr.update(value=_format_prompt_list()),
55
+ )
56
+
57
+ if not PROMPT_ID_PATTERN.match(prompt_id):
58
+ return (
59
+ "プロンプト ID には英数字・ハイフン・アンダースコアのみ使用できます。",
60
+ None,
61
+ gr.update(choices=_list_saved_prompts(), value=None),
62
+ gr.update(value=_format_prompt_list()),
63
+ )
64
+
65
+ audio_prompt = (
66
+ upload_audio_prompt if upload_audio_prompt is not None else record_audio_prompt
67
+ )
68
+ if audio_prompt is None:
69
+ return (
70
+ "音声をアップロードするか録音してください。",
71
+ None,
72
+ gr.update(choices=_list_saved_prompts(), value=None),
73
+ gr.update(value=_format_prompt_list()),
74
+ )
75
+
76
+ try:
77
+ message, temp_path = vallex.make_npz_prompt(
78
+ prompt_id,
79
+ upload_audio_prompt,
80
+ record_audio_prompt,
81
+ transcript_content,
82
+ )
83
+ except Exception as err: # pylint: disable=broad-except
84
+ logger.exception("Failed to create prompt", exc_info=err)
85
+ return (
86
+ f"プロンプト作成に失敗しました: {err}",
87
+ None,
88
+ gr.update(choices=_list_saved_prompts(), value=None),
89
+ gr.update(value=_format_prompt_list()),
90
+ )
91
+
92
+ _ensure_prompt_dir()
93
+ cached_filename = f"{prompt_id}.npz"
94
+ cached_path = os.path.join(PROMPTS_DIR, cached_filename)
95
+
96
+ try:
97
+ shutil.copy(temp_path, cached_path)
98
+ except OSError as err:
99
+ logger.exception("Failed to copy prompt to cache", exc_info=err)
100
+ return (
101
+ f"プロンプトの保存に失敗しました: {err}",
102
+ None,
103
+ gr.update(choices=_list_saved_prompts(), value=None),
104
+ gr.update(value=_format_prompt_list()),
105
+ )
106
+ finally:
107
+ try:
108
+ os.remove(temp_path)
109
+ except OSError:
110
+ pass
111
+
112
+ choices = _list_saved_prompts()
113
+ message = (
114
+ f"{message}\nSaved cached prompt to {cached_path}"
115
+ if message
116
+ else f"Saved cached prompt to {cached_path}"
117
+ )
118
+ return (
119
+ message,
120
+ cached_path,
121
+ gr.update(choices=choices, value=cached_filename),
122
+ gr.update(value=_format_prompt_list()),
123
+ )
124
+
125
+
126
+ def refresh_prompt_choices():
127
+ choices = _list_saved_prompts()
128
+ value = choices[0] if choices else None
129
+ return (
130
+ gr.update(choices=choices, value=value),
131
+ gr.update(value=_format_prompt_list()),
132
+ )
133
+
134
+
135
+ def infer_from_cached_prompt(
136
+ text: str,
137
+ language: str,
138
+ accent: str,
139
+ prompt_filename: Optional[str],
140
+ ):
141
+ if not text:
142
+ return "テキストを入力してください。", None
143
+
144
+ if not prompt_filename:
145
+ return "プロンプトを選択してください。", None
146
+
147
+ prompt_path = os.path.join(_ensure_prompt_dir(), prompt_filename)
148
+ if not os.path.exists(prompt_path):
149
+ return f"プロンプトが見つかりません: {prompt_path}", None
150
+
151
+ timings: List[Tuple[str, float]] = []
152
+ start_time = time.perf_counter()
153
+ try:
154
+ logger.info("Loading cached prompt from: %s", prompt_path)
155
+ prompt_data = np.load(prompt_path)
156
+ audio_tokens = torch.from_numpy(prompt_data["audio_tokens"]).to(
157
+ dtype=torch.long
158
+ )
159
+ text_prompts = torch.from_numpy(prompt_data["text_tokens"]).to(dtype=torch.long)
160
+ lang_code = (
161
+ int(prompt_data["lang_code"])
162
+ if prompt_data["lang_code"].shape == ()
163
+ else int(prompt_data["lang_code"][0])
164
+ )
165
+ except Exception as err: # pylint: disable=broad-except
166
+ logger.exception("Failed to load cached prompt", exc_info=err)
167
+ return (f"プロンプトの読み込みに失敗しました: {err}", None)
168
+ timings.append(("プロンプト読込", time.perf_counter() - start_time))
169
+
170
+ lang_pr = code2lang.get(lang_code, "en")
171
+
172
+ start_time = time.perf_counter()
173
+ if language == "auto-detect":
174
+ detected_lang = vallex.langid.classify(text)[0]
175
+ lang_token = lang2token.get(detected_lang, "[EN]")
176
+ else:
177
+ lang_token = langdropdown2token[language]
178
+
179
+ conditioned_text = f"{lang_token}{text}{lang_token}"
180
+
181
+ phone_tokens, langs = vallex.text_tokenizer.tokenize(
182
+ text=f"_{conditioned_text}".strip()
183
+ )
184
+ text_tokens, text_tokens_lens = vallex.text_collater([phone_tokens])
185
+
186
+ enroll_x_lens = torch.IntTensor([text_prompts.shape[-1]])
187
+ text_tokens = torch.cat([text_prompts, text_tokens], dim=-1)
188
+ text_tokens_lens += enroll_x_lens
189
+ timings.append(("テキスト準備", time.perf_counter() - start_time))
190
+
191
+ vallex.model.to(vallex.device)
192
+
193
+ audio_prompts = audio_tokens.to(vallex.device)
194
+ if audio_prompts.dim() == 2:
195
+ audio_prompts = audio_prompts.unsqueeze(0)
196
+
197
+ start_time = time.perf_counter()
198
+ logger.info("Start inferring from cached prompt: %s", prompt_path)
199
+ encoded_frames = vallex.model.inference(
200
+ text_tokens.to(vallex.device),
201
+ text_tokens_lens.to(vallex.device),
202
+ audio_prompts,
203
+ enroll_x_lens=enroll_x_lens.to(vallex.device),
204
+ top_k=-100,
205
+ temperature=1,
206
+ prompt_language=lang_pr,
207
+ text_language=langs
208
+ if accent == "no-accent"
209
+ else token2lang[langdropdown2token[accent]],
210
+ best_of=5,
211
+ )
212
+ timings.append(("モデル推論", time.perf_counter() - start_time))
213
+ logger.info("Inference completed")
214
+
215
+ start_time = time.perf_counter()
216
+ logger.info("Decoding with Vocos...")
217
+ frames = encoded_frames.permute(2, 0, 1)
218
+ features = vallex.vocos.codes_to_features(frames)
219
+ samples = vallex.vocos.decode(
220
+ features, bandwidth_id=torch.tensor([2], device=vallex.device)
221
+ )
222
+ timings.append(("ボコーダ復号", time.perf_counter() - start_time))
223
+ logger.info("Decoding completed")
224
+
225
+ message = (
226
+ f"Loaded cached prompt: {prompt_filename}\n"
227
+ f"Prompt language: {lang_pr}\n"
228
+ f"Synthesized text: {conditioned_text}"
229
+ )
230
+
231
+ timing_report = "\n↓\n".join(
232
+ f"{step}:{duration:.4f} sec" for step, duration in timings
233
+ )
234
+ logger.info("推論ステップ計測結果\n%s", timing_report)
235
+
236
+ return message, (24000, samples.squeeze(0).cpu().numpy())
237
+
238
+
239
+ def main():
240
+ prompt_choices = _list_saved_prompts()
241
+
242
+ gr.Markdown(top_ja_md)
243
+ gr.Markdown(infer_from_audio_ja_md)
244
+ gr.Markdown("[Cached] Zero-shot 音声クローニング")
245
+ with gr.Row():
246
+ with gr.Column():
247
+ textbox = gr.TextArea(
248
+ label="音声合成で喋らせたいテキスト",
249
+ placeholder="ここに音声合成で喋らせたいテキストを入力してください。",
250
+ value="Welcome back, Master. What can I do for you today?",
251
+ elem_id="tts-input-cached",
252
+ )
253
+ language_dropdown = gr.Dropdown(
254
+ choices=["auto-detect", "English", "中文", "日本語"],
255
+ value="auto-detect",
256
+ label="language",
257
+ )
258
+ accent_dropdown = gr.Dropdown(
259
+ choices=["no-accent", "English", "中文", "日本語"],
260
+ value="no-accent",
261
+ label="accent",
262
+ )
263
+ textbox_transcript = gr.TextArea(
264
+ label="Transcript",
265
+ placeholder="アップロードした音声、または録音した音声のテキストを入力してください。(whisper を使用する場合は空のままにしてください。)",
266
+ value="",
267
+ )
268
+ upload_audio_prompt = gr.Audio(
269
+ label="音声アップロード",
270
+ sources=["upload"],
271
+ interactive=True,
272
+ )
273
+ record_audio_prompt = gr.Audio(
274
+ label="音声を録音する",
275
+ sources=["microphone"],
276
+ interactive=True,
277
+ )
278
+ prompt_id_box = gr.Textbox(
279
+ label="Prompt ID",
280
+ placeholder="例: my_speaker01",
281
+ value="",
282
+ )
283
+ cached_prompt_dropdown = gr.Dropdown(
284
+ label="Cached prompts",
285
+ choices=prompt_choices,
286
+ value=prompt_choices[0] if prompt_choices else None,
287
+ interactive=True,
288
+ )
289
+ prompt_list_box = gr.Textbox(
290
+ label="保存済みプロンプト一���",
291
+ value=_format_prompt_list(),
292
+ interactive=False,
293
+ lines=6,
294
+ )
295
+ refresh_btn = gr.Button("キャッシュ一覧を更新")
296
+
297
+ with gr.Column():
298
+ text_output = gr.Textbox(label="Message")
299
+ audio_output = gr.Audio(label="Output Audio", elem_id="tts-audio-cached")
300
+ btn_infer = gr.Button("音声合成を開始する")
301
+ btn_infer.click(
302
+ vallex.infer_from_audio,
303
+ inputs=[
304
+ textbox,
305
+ language_dropdown,
306
+ accent_dropdown,
307
+ upload_audio_prompt,
308
+ record_audio_prompt,
309
+ textbox_transcript,
310
+ ],
311
+ outputs=[text_output, audio_output],
312
+ )
313
+
314
+ prompt_output = gr.File(label="Generated prompt", interactive=False)
315
+ btn_save = gr.Button("./models/prompts に保存")
316
+ btn_save.click(
317
+ save_prompt_to_cache,
318
+ inputs=[
319
+ prompt_id_box,
320
+ upload_audio_prompt,
321
+ record_audio_prompt,
322
+ textbox_transcript,
323
+ ],
324
+ outputs=[
325
+ text_output,
326
+ prompt_output,
327
+ cached_prompt_dropdown,
328
+ prompt_list_box,
329
+ ],
330
+ )
331
+
332
+ btn_cached_infer = gr.Button("キャッシュしたプロンプトで合成")
333
+ btn_cached_infer.click(
334
+ infer_from_cached_prompt,
335
+ inputs=[
336
+ textbox,
337
+ language_dropdown,
338
+ accent_dropdown,
339
+ cached_prompt_dropdown,
340
+ ],
341
+ outputs=[text_output, audio_output],
342
+ )
343
+
344
+ refresh_btn.click(
345
+ refresh_prompt_choices,
346
+ inputs=None,
347
+ outputs=[cached_prompt_dropdown, prompt_list_box],
348
+ )
349
+
350
+ gr.Examples(
351
+ examples=infer_from_audio_examples,
352
+ inputs=[
353
+ textbox,
354
+ language_dropdown,
355
+ accent_dropdown,
356
+ upload_audio_prompt,
357
+ record_audio_prompt,
358
+ textbox_transcript,
359
+ ],
360
+ outputs=[text_output, audio_output],
361
+ fn=vallex.infer_from_audio,
362
+ cache_examples=False,
363
+ )
apps/audio_cloning/main.py CHANGED
@@ -4,6 +4,7 @@ import gradio as gr
4
 
5
  from logger import setup_logger
6
 
 
7
  from .vallex.main import main as vallex
8
 
9
  logger = logging.getLogger(__name__)
@@ -18,6 +19,9 @@ def main():
18
  gr.Markdown("# Charamix Audio Cloning Prototype")
19
 
20
  # zero-shot audio cloning
 
 
 
21
  with gr.Tab("Zero-shot Audio Cloning with VALL-E-X"):
22
  vallex()
23
 
 
4
 
5
  from logger import setup_logger
6
 
7
+ from .cheched_vallex import main as cached_vallex
8
  from .vallex.main import main as vallex
9
 
10
  logger = logging.getLogger(__name__)
 
19
  gr.Markdown("# Charamix Audio Cloning Prototype")
20
 
21
  # zero-shot audio cloning
22
+ with gr.Tab("[Cached] Zero-shot Audio Cloning"):
23
+ cached_vallex()
24
+
25
  with gr.Tab("Zero-shot Audio Cloning with VALL-E-X"):
26
  vallex()
27
 
apps/audio_cloning/vallex/main.py CHANGED
@@ -359,6 +359,9 @@ def infer_from_audio(
359
  text, language, accent, audio_prompt, record_audio_prompt, transcript_content
360
  ):
361
  global model, text_collater, text_tokenizer, audio_tokenizer
 
 
 
362
  audio_prompt = audio_prompt if audio_prompt is not None else record_audio_prompt
363
  sr, wav_pr = audio_prompt
364
  if not isinstance(wav_pr, torch.FloatTensor):
@@ -370,28 +373,36 @@ def infer_from_audio(
370
  if wav_pr.ndim == 1:
371
  wav_pr = wav_pr.unsqueeze(0)
372
  assert wav_pr.ndim and wav_pr.size(0) == 1
 
373
 
 
374
  if transcript_content == "":
375
  text_pr, lang_pr = make_prompt("dummy", wav_pr, sr, save=False)
376
  else:
377
  lang_pr = langid.classify(str(transcript_content))[0]
378
  lang_token = lang2token[lang_pr]
379
  text_pr = f"{lang_token}{str(transcript_content)}{lang_token}"
 
380
 
 
381
  if language == "auto-detect":
382
  lang_token = lang2token[langid.classify(text)[0]]
383
  else:
384
  lang_token = langdropdown2token[language]
385
  lang = token2lang[lang_token]
386
  text = lang_token + text + lang_token
 
387
 
388
  # onload model
389
  model.to(device)
390
 
 
391
  # tokenize audio
392
  encoded_frames = tokenize_audio(audio_tokenizer, (wav_pr, sr))
393
  audio_prompts = encoded_frames[0][0].transpose(2, 1).to(device)
 
394
 
 
395
  # tokenize text
396
  logging.info(f"synthesize text: {text}")
397
  phone_tokens, langs = text_tokenizer.tokenize(text=f"_{text}".strip())
@@ -404,6 +415,9 @@ def infer_from_audio(
404
  text_tokens = torch.cat([text_prompts, text_tokens], dim=-1)
405
  text_tokens_lens += enroll_x_lens
406
  lang = lang if accent == "no-accent" else token2lang[langdropdown2token[accent]]
 
 
 
407
  encoded_frames = model.inference(
408
  text_tokens.to(device),
409
  text_tokens_lens.to(device),
@@ -415,14 +429,18 @@ def infer_from_audio(
415
  text_language=langs if accent == "no-accent" else lang,
416
  best_of=5,
417
  )
 
418
  # Decode with Vocos
 
419
  frames = encoded_frames.permute(2, 0, 1)
420
  features = vocos.codes_to_features(frames)
421
  samples = vocos.decode(features, bandwidth_id=torch.tensor([2], device=device))
 
422
 
423
- # offload model
424
- model.to("cpu")
425
- torch.cuda.empty_cache()
 
426
 
427
  message = f"text prompt: {text_pr}\nsythesized text: {text}"
428
  return message, (24000, samples.squeeze(0).cpu().numpy())
 
359
  text, language, accent, audio_prompt, record_audio_prompt, transcript_content
360
  ):
361
  global model, text_collater, text_tokenizer, audio_tokenizer
362
+ timings = []
363
+
364
+ start_time = time.perf_counter()
365
  audio_prompt = audio_prompt if audio_prompt is not None else record_audio_prompt
366
  sr, wav_pr = audio_prompt
367
  if not isinstance(wav_pr, torch.FloatTensor):
 
373
  if wav_pr.ndim == 1:
374
  wav_pr = wav_pr.unsqueeze(0)
375
  assert wav_pr.ndim and wav_pr.size(0) == 1
376
+ timings.append(("音声前処理", time.perf_counter() - start_time))
377
 
378
+ start_time = time.perf_counter()
379
  if transcript_content == "":
380
  text_pr, lang_pr = make_prompt("dummy", wav_pr, sr, save=False)
381
  else:
382
  lang_pr = langid.classify(str(transcript_content))[0]
383
  lang_token = lang2token[lang_pr]
384
  text_pr = f"{lang_token}{str(transcript_content)}{lang_token}"
385
+ timings.append(("プロンプト生成", time.perf_counter() - start_time))
386
 
387
+ start_time = time.perf_counter()
388
  if language == "auto-detect":
389
  lang_token = lang2token[langid.classify(text)[0]]
390
  else:
391
  lang_token = langdropdown2token[language]
392
  lang = token2lang[lang_token]
393
  text = lang_token + text + lang_token
394
+ timings.append(("言語設定", time.perf_counter() - start_time))
395
 
396
  # onload model
397
  model.to(device)
398
 
399
+ start_time = time.perf_counter()
400
  # tokenize audio
401
  encoded_frames = tokenize_audio(audio_tokenizer, (wav_pr, sr))
402
  audio_prompts = encoded_frames[0][0].transpose(2, 1).to(device)
403
+ timings.append(("音声トークナイズ", time.perf_counter() - start_time))
404
 
405
+ start_time = time.perf_counter()
406
  # tokenize text
407
  logging.info(f"synthesize text: {text}")
408
  phone_tokens, langs = text_tokenizer.tokenize(text=f"_{text}".strip())
 
415
  text_tokens = torch.cat([text_prompts, text_tokens], dim=-1)
416
  text_tokens_lens += enroll_x_lens
417
  lang = lang if accent == "no-accent" else token2lang[langdropdown2token[accent]]
418
+ timings.append(("テキストトークナイズ", time.perf_counter() - start_time))
419
+
420
+ start_time = time.perf_counter()
421
  encoded_frames = model.inference(
422
  text_tokens.to(device),
423
  text_tokens_lens.to(device),
 
429
  text_language=langs if accent == "no-accent" else lang,
430
  best_of=5,
431
  )
432
+ timings.append(("モデル推論", time.perf_counter() - start_time))
433
  # Decode with Vocos
434
+ start_time = time.perf_counter()
435
  frames = encoded_frames.permute(2, 0, 1)
436
  features = vocos.codes_to_features(frames)
437
  samples = vocos.decode(features, bandwidth_id=torch.tensor([2], device=device))
438
+ timings.append(("ボコーダ復号", time.perf_counter() - start_time))
439
 
440
+ timing_report = "\n↓\n".join(
441
+ f"{step}:{duration:.4f} sec" for step, duration in timings
442
+ )
443
+ logger.info("推論ステップ計測結果\n%s", timing_report)
444
 
445
  message = f"text prompt: {text_pr}\nsythesized text: {text}"
446
  return message, (24000, samples.squeeze(0).cpu().numpy())