Opera8 commited on
Commit
2457c1e
·
verified ·
1 Parent(s): 339799c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -125
app.py CHANGED
@@ -13,8 +13,11 @@ import re
13
  import spaces
14
  import uuid
15
  import soundfile as sf
 
 
 
16
 
17
- # --- تنظیمات اولیه و نصب پکیج‌ها ---
18
  downloaded_resources = {
19
  "configs": False,
20
  "tokenizer_vq8192": False,
@@ -34,25 +37,15 @@ def install_espeak():
34
 
35
  install_espeak()
36
 
37
- # پچ کردن مشکل LangSegment
38
  def patch_langsegment_init():
39
  try:
40
  spec = importlib.util.find_spec("LangSegment")
41
  if spec is None or spec.origin is None: return
42
  init_path = os.path.join(os.path.dirname(spec.origin), '__init__.py')
43
- if not os.path.exists(init_path):
44
- for site_pkg_path in site.getsitepackages():
45
- potential_path = os.path.join(site_pkg_path, 'LangSegment', '__init__.py')
46
- if os.path.exists(potential_path):
47
- init_path = potential_path
48
- break
49
- else: return
50
-
51
  with open(init_path, 'r') as f: lines = f.readlines()
52
  modified = False
53
  new_lines = []
54
  target_line_prefix = "from .LangSegment import"
55
-
56
  for line in lines:
57
  if line.strip().startswith(target_line_prefix) and ('setLangfilters' in line or 'getLangfilters' in line):
58
  mod_line = line.replace(',setLangfilters', '').replace(',getLangfilters', '')
@@ -61,7 +54,6 @@ def patch_langsegment_init():
61
  modified = True
62
  else:
63
  new_lines.append(line)
64
-
65
  if modified:
66
  with open(init_path, 'w') as f: f.writelines(new_lines)
67
  try:
@@ -72,14 +64,9 @@ def patch_langsegment_init():
72
 
73
  patch_langsegment_init()
74
 
75
- # دریافت ریپازیتوری Amphion
76
  if not os.path.exists("Amphion"):
77
  subprocess.run(["git", "clone", "https://github.com/open-mmlab/Amphion.git"])
78
  os.chdir("Amphion")
79
- else:
80
- if not os.getcwd().endswith("Amphion"):
81
- os.chdir("Amphion")
82
-
83
  if os.path.dirname(os.path.abspath("Amphion")) not in sys.path:
84
  sys.path.append(os.path.dirname(os.path.abspath("Amphion")))
85
 
@@ -88,24 +75,31 @@ os.makedirs("ckpts/Vevo", exist_ok=True)
88
 
89
  from models.vc.vevo.vevo_utils import VevoInferencePipeline
90
 
 
 
 
 
 
 
 
 
 
 
 
91
  def save_audio_pcm16(waveform, output_path, sample_rate=24000):
 
92
  try:
93
  if isinstance(waveform, torch.Tensor):
94
- waveform = waveform.detach().cpu()
95
- if waveform.dim() == 2 and waveform.shape[0] == 1:
96
- waveform = waveform.squeeze(0)
97
- waveform = waveform.numpy()
98
  sf.write(output_path, waveform, sample_rate, subtype='PCM_16')
99
  except Exception as e:
100
  print(f"Save error: {e}")
101
- raise e
102
 
103
  def setup_configs():
104
  if downloaded_resources["configs"]: return
105
  config_path = "models/vc/vevo/config"
106
  os.makedirs(config_path, exist_ok=True)
107
  config_files = ["Vq8192ToMels.json", "Vocoder.json"]
108
-
109
  for file in config_files:
110
  file_path = f"{config_path}/{file}"
111
  if not os.path.exists(file_path):
@@ -116,9 +110,7 @@ def setup_configs():
116
  downloaded_resources["configs"] = True
117
 
118
  setup_configs()
119
-
120
  device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
121
- print(f"Using device: {device}")
122
 
123
  inference_pipelines = {}
124
 
@@ -128,31 +120,23 @@ def preload_all_resources():
128
  global downloaded_content_style_tokenizer_path, downloaded_fmt_path, downloaded_vocoder_path
129
 
130
  if not downloaded_resources["tokenizer_vq8192"]:
131
- local_dir = snapshot_download(repo_id="amphion/Vevo", repo_type="model", cache_dir="./ckpts/Vevo", allow_patterns=["tokenizer/vq8192/*"])
132
- downloaded_content_style_tokenizer_path = local_dir
133
  downloaded_resources["tokenizer_vq8192"] = True
134
-
135
  if not downloaded_resources["fmt_Vq8192ToMels"]:
136
- local_dir = snapshot_download(repo_id="amphion/Vevo", repo_type="model", cache_dir="./ckpts/Vevo", allow_patterns=["acoustic_modeling/Vq8192ToMels/*"])
137
- downloaded_fmt_path = local_dir
138
  downloaded_resources["fmt_Vq8192ToMels"] = True
139
-
140
  if not downloaded_resources["vocoder"]:
141
- local_dir = snapshot_download(repo_id="amphion/Vevo", repo_type="model", cache_dir="./ckpts/Vevo", allow_patterns=["acoustic_modeling/Vocoder/*"])
142
- downloaded_vocoder_path = local_dir
143
  downloaded_resources["vocoder"] = True
144
  print("Resources ready.")
145
 
146
  downloaded_content_style_tokenizer_path = None
147
  downloaded_fmt_path = None
148
  downloaded_vocoder_path = None
149
-
150
  preload_all_resources()
151
 
152
  def get_pipeline():
153
- if "timbre" in inference_pipelines:
154
- return inference_pipelines["timbre"]
155
-
156
  pipeline = VevoInferencePipeline(
157
  content_style_tokenizer_ckpt_path=os.path.join(downloaded_content_style_tokenizer_path, "tokenizer/vq8192"),
158
  fmt_cfg_path="./models/vc/vevo/config/Vq8192ToMels.json",
@@ -161,7 +145,6 @@ def get_pipeline():
161
  vocoder_ckpt_path=os.path.join(downloaded_vocoder_path, "acoustic_modeling/Vocoder"),
162
  device=device,
163
  )
164
-
165
  inference_pipelines["timbre"] = pipeline
166
  return pipeline
167
 
@@ -176,88 +159,60 @@ def vevo_timbre(content_wav, reference_wav):
176
  raise ValueError("Please upload audio files")
177
 
178
  try:
179
- # --- 1. آماده‌سازی فایل ورودی (Content) ---
180
  if isinstance(content_wav, tuple):
181
  content_sr, content_data = content_wav if isinstance(content_wav[0], int) else (content_wav[1], content_wav[0])
182
  else:
183
  content_sr, content_data = content_wav
184
-
185
- if len(content_data.shape) > 1 and content_data.shape[1] > 1:
186
- content_data = np.mean(content_data, axis=1)
187
 
188
  content_tensor = torch.FloatTensor(content_data).unsqueeze(0)
189
  if content_sr != 24000:
190
  content_tensor = torchaudio.functional.resample(content_tensor, content_sr, 24000)
191
- content_sr = 24000
192
-
193
- # نرمال‌سازی
194
  content_tensor = content_tensor / (torch.max(torch.abs(content_tensor)) + 1e-6) * 0.95
195
  content_full_np = content_tensor.squeeze().numpy()
196
 
197
- # --- 2. آماده‌سازی فایل رفرنس (Reference) ---
198
  if isinstance(reference_wav, tuple):
199
  ref_sr, ref_data = reference_wav if isinstance(reference_wav[0], int) else (reference_wav[1], reference_wav[0])
200
  else:
201
  ref_sr, ref_data = reference_wav
202
-
203
- if len(ref_data.shape) > 1 and ref_data.shape[1] > 1:
204
- ref_data = np.mean(ref_data, axis=1)
205
-
206
  ref_tensor = torch.FloatTensor(ref_data).unsqueeze(0)
207
  if ref_sr != 24000:
208
  ref_tensor = torchaudio.functional.resample(ref_tensor, ref_sr, 24000)
209
- ref_sr = 24000
210
-
211
- ref_max = torch.max(torch.abs(ref_tensor)) + 1e-6
212
- ref_tensor = ref_tensor / ref_max * 0.95
213
-
214
- if ref_tensor.shape[1] > 24000 * 20:
215
- ref_tensor = ref_tensor[:, :24000 * 20]
216
-
217
- save_audio_pcm16(ref_tensor, temp_reference_path, ref_sr)
218
 
219
- # --- 3. منطق جدید: Pre-roll Context (بدون هم‌پوشانی طولانی) ---
220
  pipeline = get_pipeline()
221
  SR = 24000
222
 
223
- # استراتژی:
224
- # ما می‌خواهیم ۱۰ ثانیه جدید بسازیم.
225
- # اما برای اینکه لحن نپرد، ۳ ثانیه از صدای قبلی را هم به ورودی اضافه می‌کنیم.
226
- # بعد از تولید، آن ۳ ثانیه اول را دور می‌ریزیم.
227
-
228
- NEW_CHUNK_SEC = 10.0 # مقدار صدای جدید در هر مرحله
229
- CONTEXT_SEC = 3.0 # مقدار صدای قدیمی برای حفظ لحن
230
 
231
  new_chunk_samples = int(NEW_CHUNK_SEC * SR)
232
  context_samples = int(CONTEXT_SEC * SR)
233
  total_samples = len(content_full_np)
234
 
235
- final_output = []
 
236
 
237
- # نشانگر: تا کجای فایل را "نهایی" و تولید کرده‌ایم
238
  current_cursor = 0
239
-
240
- print(f"[{session_id}] Starting processing with Context-Discard strategy...")
241
 
242
  while current_cursor < total_samples:
243
- # تعیین محدوده برش از فایل اصلی
244
- # شروع: کمی عقب‌تر از جایی که هستیم (برای کانتکست)
245
  start_slice = max(0, current_cursor - context_samples)
246
- # پایان: ۱۰ ثانیه جلوتر از جایی که هستیم
247
  end_slice = min(total_samples, current_cursor + new_chunk_samples)
248
 
249
- # اگر چیزی برای پردازش نمانده
250
- if start_slice >= end_slice:
251
- break
252
 
253
- # استخراج تکه از فایل اصلی
254
  chunk_np = content_full_np[start_slice:end_slice]
255
-
256
- # ذخیره موقت برای مدل
257
  save_audio_pcm16(torch.FloatTensor(chunk_np).unsqueeze(0), temp_content_path, SR)
258
 
259
  try:
260
- # تولید صدا توسط مدل
261
  gen = pipeline.inference_fm(
262
  src_wav_path=temp_content_path,
263
  timbre_ref_wav_path=temp_reference_path,
@@ -265,65 +220,51 @@ def vevo_timbre(content_wav, reference_wav):
265
  )
266
 
267
  if torch.isnan(gen).any(): gen = torch.nan_to_num(gen, nan=0.0)
268
- if gen.dim() == 1: gen = gen.unsqueeze(0)
269
- gen_np = gen.cpu().squeeze(0).numpy()
270
 
271
- # --- برش هوشمند (Trimming) ---
272
- # ما (current_cursor - start_slice) مقدار سمپل را قبلا تولید کرده بودیم (کانتکست)
273
- # پس باید این مقدار را از ابتدای خروجی جدید حذف کنیم تا صدای دو نفره نشود.
274
- trim_amount = current_cursor - start_slice
275
 
276
- if len(gen_np) > trim_amount:
277
- valid_audio = gen_np[trim_amount:]
278
 
279
- # --- اتصال نرم (Micro Cross-fade) ---
280
- # فقط ۱۰ میلی ثانیه فید می‌کنیم تا صدای "تیک" ندهد.
281
- # این مقدار آنقدر کم است که گوش انسان صدای دو نفره نمی‌شنود.
282
- if len(final_output) > 0:
283
- # 10ms fade
284
- fade_len = int(0.01 * SR)
285
- if len(final_output[-1]) > fade_len and len(valid_audio) > fade_len:
286
- fade_out_curve = np.linspace(1, 0, fade_len)
287
- fade_in_curve = np.linspace(0, 1, fade_len)
288
-
289
- # میکس فقط روی ۱۰ میلی ثانیه مرز
290
- prev_tail = final_output[-1][-fade_len:]
291
- curr_head = valid_audio[:fade_len]
292
-
293
- blended = (prev_tail * fade_out_curve) + (curr_head * fade_in_curve)
294
-
295
- # جایگزینی
296
- final_output[-1][-fade_len:] = blended
297
- valid_audio = valid_audio[fade_len:]
298
 
299
- final_output.append(valid_audio)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
- # مکان‌نما را جلو می‌بریم
302
  current_cursor = end_slice
303
 
304
  except Exception as e:
305
- print(f"Error in chunk: {e}")
306
- # در صورت خطا، سکوت اضافه می‌کنیم تا زمان‌بندی به هم نریزد
307
- missing_len = end_slice - current_cursor
308
- final_output.append(np.zeros(missing_len))
309
- current_cursor = end_slice
310
-
311
- # چسباندن نهایی
312
- if len(final_output) > 0:
313
- full_audio = np.concatenate(final_output)
314
- else:
315
- full_audio = np.zeros(SR)
316
 
317
- save_audio_pcm16(full_audio, output_path, SR)
 
318
  return output_path
319
 
320
  finally:
321
  if os.path.exists(temp_content_path): os.remove(temp_content_path)
322
  if os.path.exists(temp_reference_path): os.remove(temp_reference_path)
323
 
324
- with gr.Blocks(title="Vevo-Timbre (Clean Stitch)") as demo:
325
- gr.Markdown("## Vevo-Timbre: Zero-Shot Voice Conversion")
326
- gr.Markdown("No Ghosting Version: Uses Context-Discard buffering to ensure single voice playback.")
327
 
328
  with gr.Row():
329
  with gr.Column():
 
13
  import spaces
14
  import uuid
15
  import soundfile as sf
16
+ # اضافه شدن کتابخانه PyDub
17
+ from pydub import AudioSegment
18
+ import io
19
 
20
+ # --- نصب و پچ کردن ---
21
  downloaded_resources = {
22
  "configs": False,
23
  "tokenizer_vq8192": False,
 
37
 
38
  install_espeak()
39
 
 
40
  def patch_langsegment_init():
41
  try:
42
  spec = importlib.util.find_spec("LangSegment")
43
  if spec is None or spec.origin is None: return
44
  init_path = os.path.join(os.path.dirname(spec.origin), '__init__.py')
 
 
 
 
 
 
 
 
45
  with open(init_path, 'r') as f: lines = f.readlines()
46
  modified = False
47
  new_lines = []
48
  target_line_prefix = "from .LangSegment import"
 
49
  for line in lines:
50
  if line.strip().startswith(target_line_prefix) and ('setLangfilters' in line or 'getLangfilters' in line):
51
  mod_line = line.replace(',setLangfilters', '').replace(',getLangfilters', '')
 
54
  modified = True
55
  else:
56
  new_lines.append(line)
 
57
  if modified:
58
  with open(init_path, 'w') as f: f.writelines(new_lines)
59
  try:
 
64
 
65
  patch_langsegment_init()
66
 
 
67
  if not os.path.exists("Amphion"):
68
  subprocess.run(["git", "clone", "https://github.com/open-mmlab/Amphion.git"])
69
  os.chdir("Amphion")
 
 
 
 
70
  if os.path.dirname(os.path.abspath("Amphion")) not in sys.path:
71
  sys.path.append(os.path.dirname(os.path.abspath("Amphion")))
72
 
 
75
 
76
  from models.vc.vevo.vevo_utils import VevoInferencePipeline
77
 
78
+ # --- توابع کمکی جدید برای PyDub ---
79
+ def numpy_to_audiosegment(audio_arr, sample_rate=24000):
80
+ """تبدیل آرایه نامپای (Float32) به آبجکت AudioSegment"""
81
+ # تبدیل به PCM 16-bit
82
+ audio_int16 = (audio_arr * 32767).astype(np.int16)
83
+ # ایجاد فایل در حافظه
84
+ byte_io = io.BytesIO()
85
+ sf.write(byte_io, audio_int16, sample_rate, format='WAV', subtype='PCM_16')
86
+ byte_io.seek(0)
87
+ return AudioSegment.from_wav(byte_io)
88
+
89
  def save_audio_pcm16(waveform, output_path, sample_rate=24000):
90
+ # این تابع فقط برای ذخیره فایل‌های موقت ورودی مدل است
91
  try:
92
  if isinstance(waveform, torch.Tensor):
93
+ waveform = waveform.detach().cpu().squeeze().numpy()
 
 
 
94
  sf.write(output_path, waveform, sample_rate, subtype='PCM_16')
95
  except Exception as e:
96
  print(f"Save error: {e}")
 
97
 
98
  def setup_configs():
99
  if downloaded_resources["configs"]: return
100
  config_path = "models/vc/vevo/config"
101
  os.makedirs(config_path, exist_ok=True)
102
  config_files = ["Vq8192ToMels.json", "Vocoder.json"]
 
103
  for file in config_files:
104
  file_path = f"{config_path}/{file}"
105
  if not os.path.exists(file_path):
 
110
  downloaded_resources["configs"] = True
111
 
112
  setup_configs()
 
113
  device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
 
114
 
115
  inference_pipelines = {}
116
 
 
120
  global downloaded_content_style_tokenizer_path, downloaded_fmt_path, downloaded_vocoder_path
121
 
122
  if not downloaded_resources["tokenizer_vq8192"]:
123
+ downloaded_content_style_tokenizer_path = snapshot_download(repo_id="amphion/Vevo", repo_type="model", cache_dir="./ckpts/Vevo", allow_patterns=["tokenizer/vq8192/*"])
 
124
  downloaded_resources["tokenizer_vq8192"] = True
 
125
  if not downloaded_resources["fmt_Vq8192ToMels"]:
126
+ downloaded_fmt_path = snapshot_download(repo_id="amphion/Vevo", repo_type="model", cache_dir="./ckpts/Vevo", allow_patterns=["acoustic_modeling/Vq8192ToMels/*"])
 
127
  downloaded_resources["fmt_Vq8192ToMels"] = True
 
128
  if not downloaded_resources["vocoder"]:
129
+ downloaded_vocoder_path = snapshot_download(repo_id="amphion/Vevo", repo_type="model", cache_dir="./ckpts/Vevo", allow_patterns=["acoustic_modeling/Vocoder/*"])
 
130
  downloaded_resources["vocoder"] = True
131
  print("Resources ready.")
132
 
133
  downloaded_content_style_tokenizer_path = None
134
  downloaded_fmt_path = None
135
  downloaded_vocoder_path = None
 
136
  preload_all_resources()
137
 
138
  def get_pipeline():
139
+ if "timbre" in inference_pipelines: return inference_pipelines["timbre"]
 
 
140
  pipeline = VevoInferencePipeline(
141
  content_style_tokenizer_ckpt_path=os.path.join(downloaded_content_style_tokenizer_path, "tokenizer/vq8192"),
142
  fmt_cfg_path="./models/vc/vevo/config/Vq8192ToMels.json",
 
145
  vocoder_ckpt_path=os.path.join(downloaded_vocoder_path, "acoustic_modeling/Vocoder"),
146
  device=device,
147
  )
 
148
  inference_pipelines["timbre"] = pipeline
149
  return pipeline
150
 
 
159
  raise ValueError("Please upload audio files")
160
 
161
  try:
162
+ # --- 1. پردازش ورودی ---
163
  if isinstance(content_wav, tuple):
164
  content_sr, content_data = content_wav if isinstance(content_wav[0], int) else (content_wav[1], content_wav[0])
165
  else:
166
  content_sr, content_data = content_wav
167
+ if len(content_data.shape) > 1: content_data = np.mean(content_data, axis=1)
 
 
168
 
169
  content_tensor = torch.FloatTensor(content_data).unsqueeze(0)
170
  if content_sr != 24000:
171
  content_tensor = torchaudio.functional.resample(content_tensor, content_sr, 24000)
 
 
 
172
  content_tensor = content_tensor / (torch.max(torch.abs(content_tensor)) + 1e-6) * 0.95
173
  content_full_np = content_tensor.squeeze().numpy()
174
 
175
+ # --- 2. پردازش رفرنس ---
176
  if isinstance(reference_wav, tuple):
177
  ref_sr, ref_data = reference_wav if isinstance(reference_wav[0], int) else (reference_wav[1], reference_wav[0])
178
  else:
179
  ref_sr, ref_data = reference_wav
180
+ if len(ref_data.shape) > 1: ref_data = np.mean(ref_data, axis=1)
181
+
 
 
182
  ref_tensor = torch.FloatTensor(ref_data).unsqueeze(0)
183
  if ref_sr != 24000:
184
  ref_tensor = torchaudio.functional.resample(ref_tensor, ref_sr, 24000)
185
+ ref_tensor = ref_tensor / (torch.max(torch.abs(ref_tensor)) + 1e-6) * 0.95
186
+ if ref_tensor.shape[1] > 24000 * 20: ref_tensor = ref_tensor[:, :24000 * 20]
187
+ save_audio_pcm16(ref_tensor, temp_reference_path, 24000)
 
 
 
 
 
 
188
 
189
+ # --- 3. منطق پردازش با استفاده از PyDub ---
190
  pipeline = get_pipeline()
191
  SR = 24000
192
 
193
+ NEW_CHUNK_SEC = 10.0
194
+ CONTEXT_SEC = 3.0
 
 
 
 
 
195
 
196
  new_chunk_samples = int(NEW_CHUNK_SEC * SR)
197
  context_samples = int(CONTEXT_SEC * SR)
198
  total_samples = len(content_full_np)
199
 
200
+ # ایجاد یک AudioSegment خالی برای جمع‌آوری خروجی نهایی
201
+ final_audio_segment = AudioSegment.empty()
202
 
 
203
  current_cursor = 0
204
+ print(f"[{session_id}] Processing with PyDub stitching...")
 
205
 
206
  while current_cursor < total_samples:
 
 
207
  start_slice = max(0, current_cursor - context_samples)
 
208
  end_slice = min(total_samples, current_cursor + new_chunk_samples)
209
 
210
+ if start_slice >= end_slice: break
 
 
211
 
 
212
  chunk_np = content_full_np[start_slice:end_slice]
 
 
213
  save_audio_pcm16(torch.FloatTensor(chunk_np).unsqueeze(0), temp_content_path, SR)
214
 
215
  try:
 
216
  gen = pipeline.inference_fm(
217
  src_wav_path=temp_content_path,
218
  timbre_ref_wav_path=temp_reference_path,
 
220
  )
221
 
222
  if torch.isnan(gen).any(): gen = torch.nan_to_num(gen, nan=0.0)
223
+ gen_np = gen.detach().cpu().squeeze().numpy()
 
224
 
225
+ # محاسبه مقدار برشی (حذف کانتکست تکراری)
226
+ trim_samples = current_cursor - start_slice
 
 
227
 
228
+ if len(gen_np) > trim_samples:
229
+ valid_part_np = gen_np[trim_samples:]
230
 
231
+ # تبدیل به فرمت PyDub
232
+ new_segment = numpy_to_audiosegment(valid_part_np, SR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
+ # اتصال:
235
+ # اگر اولین تکه نیست، یک فید (Crossfade) بسیار کوتاه (5 میلی ثانیه)
236
+ # اعمال می‌کنیم تا صدای "تیک" حذف شود.
237
+ if len(final_audio_segment) > 0:
238
+ # تکنیک: یک فید بسیار ریز (Crossfade 5ms)
239
+ # نکته: PyDub برای کراس‌فید نیاز به همپوشانی دارد، اما چون ما کانتکست را دقیق بریدیم،
240
+ # اینجا از append ساده استفاده می‌کنیم و فقط لبه‌ها را نرم می‌کنیم.
241
+
242
+ # نرم کردن ابتدای تکه جدید (Fade In 5ms)
243
+ new_segment = new_segment.fade_in(5)
244
+ # نرم کردن انتهای تکه قبلی (Fade Out 5ms) - (قبلاً انجام شده یا الان انجام میدیم)
245
+ # در اینجا فقط چسباندن (Append) با فید این کافیست.
246
+
247
+ final_audio_segment += new_segment
248
+ else:
249
+ final_audio_segment += new_segment
250
 
 
251
  current_cursor = end_slice
252
 
253
  except Exception as e:
254
+ print(f"Error: {e}")
255
+ current_cursor = end_slice # Skip on error
 
 
 
 
 
 
 
 
 
256
 
257
+ # ذخیره خروجی نهایی با PyDub
258
+ final_audio_segment.export(output_path, format="wav")
259
  return output_path
260
 
261
  finally:
262
  if os.path.exists(temp_content_path): os.remove(temp_content_path)
263
  if os.path.exists(temp_reference_path): os.remove(temp_reference_path)
264
 
265
+ with gr.Blocks(title="Vevo-Timbre (PyDub)") as demo:
266
+ gr.Markdown("## Vevo-Timbre: Voice Conversion")
267
+ gr.Markdown("Seamless stitching powered by PyDub library.")
268
 
269
  with gr.Row():
270
  with gr.Column():