Opera8 commited on
Commit
ef837dd
·
verified ·
1 Parent(s): 3a081bb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +108 -72
app.py CHANGED
@@ -14,7 +14,7 @@ import spaces
14
  import uuid
15
  import soundfile as sf
16
 
17
- # منابع ضروری
18
  downloaded_resources = {
19
  "configs": False,
20
  "tokenizer_vq8192": False,
@@ -174,23 +174,26 @@ def vevo_timbre(content_wav, reference_wav):
174
  raise ValueError("Please upload audio files")
175
 
176
  try:
177
- # --- پردازش ورودی ---
178
  if isinstance(content_wav, tuple):
179
  content_sr, content_data = content_wav if isinstance(content_wav[0], int) else (content_wav[1], content_wav[0])
180
  else:
181
  content_sr, content_data = content_wav
182
 
 
183
  if len(content_data.shape) > 1 and content_data.shape[1] > 1:
184
  content_data = np.mean(content_data, axis=1)
185
 
 
186
  content_tensor = torch.FloatTensor(content_data).unsqueeze(0)
187
  if content_sr != 24000:
188
  content_tensor = torchaudio.functional.resample(content_tensor, content_sr, 24000)
189
  content_sr = 24000
190
 
191
  content_tensor = content_tensor / (torch.max(torch.abs(content_tensor)) + 1e-6) * 0.95
 
192
 
193
- # --- پردازش رفرنس ---
194
  if isinstance(reference_wav, tuple):
195
  ref_sr, ref_data = reference_wav if isinstance(reference_wav[0], int) else (reference_wav[1], reference_wav[0])
196
  else:
@@ -207,109 +210,142 @@ def vevo_timbre(content_wav, reference_wav):
207
  ref_max = torch.max(torch.abs(ref_tensor)) + 1e-6
208
  ref_tensor = ref_tensor / ref_max * 0.95
209
 
 
210
  if ref_tensor.shape[1] > 24000 * 20:
211
  ref_tensor = ref_tensor[:, :24000 * 20]
212
 
213
  save_audio_pcm16(ref_tensor, temp_reference_path, ref_sr)
214
 
215
- # --- منطق Pre-roll Chunking (حفظ لحن پیوسته) ---
216
  pipeline = get_pipeline()
217
-
218
  SR = 24000
219
- TARGET_CHUNK_LEN = 10 * SR # طول هدف برای هر تکه جدید (۱۰ ثانیه)
220
- CONTEXT_LEN = int(2.5 * SR) # مقدار پیش‌خوانی (۲.۵ ثانیه)
221
 
222
- total_samples = content_tensor.shape[1]
223
- print(f"[{session_id}] Processing with Pre-roll context...")
 
 
 
 
 
 
 
224
 
225
- final_output = []
 
226
 
227
- # مکان‌نما: نشان می‌دهد تا کجای فایل را "نهایی" کرده‌ایم
228
- current_cursor = 0
229
 
230
- while current_cursor < total_samples:
231
- # تعیین بازه ورودی به مدل
232
- # شروع: از ۲.۵ ثانیه قبل (اگر وجود داشته باشد)
233
- start_input = max(0, current_cursor - CONTEXT_LEN)
234
- # پایان: ۱۰ ثانیه جلوتر از مکان‌نما
235
- end_input = min(current_cursor + TARGET_CHUNK_LEN, total_samples)
236
 
237
- # اگر به ته فایل رسیدیم و چیزی نمانده
238
- if start_input >= end_input:
239
- break
240
 
241
- # استخراج تکه (شامل کانتکست + دیتای جدید)
242
- chunk_tensor = content_tensor[:, start_input:end_input]
243
- save_audio_pcm16(chunk_tensor, temp_content_path, SR)
 
 
 
244
 
245
  try:
246
- # تولید صدا
247
  gen = pipeline.inference_fm(
248
  src_wav_path=temp_content_path,
249
  timbre_ref_wav_path=temp_reference_path,
250
  flow_matching_steps=64,
251
  )
252
 
 
253
  if torch.isnan(gen).any(): gen = torch.nan_to_num(gen, nan=0.0)
254
  if gen.dim() == 1: gen = gen.unsqueeze(0)
255
- gen = gen.cpu().squeeze(0).numpy()
256
-
257
- # --- برش هوشمند ---
258
- # ما به مدل (start_input تا end_input) را دادیم.
259
- # اما فقط قسمت (current_cursor تا end_input) را می‌خواهیم.
260
- # پس باید قسمت اول (که مربوط به کانتکست است) را ببریم.
261
-
262
- cut_amount = current_cursor - start_input
263
 
264
- if len(gen) > cut_amount:
265
- valid_part = gen[cut_amount:]
 
 
 
 
 
 
 
 
266
 
267
- # اعمال یک میکرو-فید خیلی کوتاه (10 میلی ثانیه) فقط برای رفع نویز اتصال PCM
268
- # این Cross-fade نیست، فقط De-click است
269
- if len(final_output) > 0:
270
- fade_samples = int(0.01 * SR) # 10ms
271
- if len(valid_part) > fade_samples and len(final_output[-1]) > fade_samples:
272
- # نرم کردن اتصال
273
- fade_in = np.linspace(0, 1, fade_samples)
274
- fade_out = np.linspace(1, 0, fade_samples)
275
-
276
- # میکس ۱۰ میلی ثانیه آخرِ قبلی با ۱۰ میلی ثانیه اولِ جدید
277
- prev_tail = final_output[-1][-fade_samples:]
278
- curr_head = valid_part[:fade_samples]
279
-
280
- # جایگزینی دم قبلی (میکس شده)
281
- final_output[-1][-fade_samples:] = (prev_tail * fade_out) + (curr_head * fade_in)
282
- # حذف سرِ فعلی (چون میکس شد)
283
- valid_part = valid_part[fade_samples:]
284
-
285
- final_output.append(valid_part)
286
-
287
- # آپدیت مکان‌نما
288
- current_cursor = end_input
289
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  except Exception as e:
291
- print(f"Error: {e}")
292
- # مدیریت خطا
293
- missing = end_input - current_cursor
294
- final_output.append(np.zeros(missing))
295
- current_cursor = end_input
296
-
297
- # چسباندن
298
- if len(final_output) > 0:
299
- full_audio = np.concatenate(final_output)
300
- else:
301
- full_audio = np.zeros(24000)
302
 
303
- save_audio_pcm16(full_audio, output_path, SR)
 
304
  return output_path
305
 
306
  finally:
307
  if os.path.exists(temp_content_path): os.remove(temp_content_path)
308
  if os.path.exists(temp_reference_path): os.remove(temp_reference_path)
309
 
310
- with gr.Blocks(title="Vevo-Timbre (Seamless)") as demo:
311
  gr.Markdown("## Vevo-Timbre: Zero-Shot Voice Conversion")
312
- gr.Markdown("نسخه نهایی: اتصال کاملاً پیوسته و بدون شوک (Pre-roll Context).")
313
 
314
  with gr.Row():
315
  with gr.Column():
 
14
  import uuid
15
  import soundfile as sf
16
 
17
+ # --- بخش نصب و راه‌اندازی اولیه ---
18
  downloaded_resources = {
19
  "configs": False,
20
  "tokenizer_vq8192": False,
 
174
  raise ValueError("Please upload audio files")
175
 
176
  try:
177
+ # --- 1. پردازش ورودی (Content) ---
178
  if isinstance(content_wav, tuple):
179
  content_sr, content_data = content_wav if isinstance(content_wav[0], int) else (content_wav[1], content_wav[0])
180
  else:
181
  content_sr, content_data = content_wav
182
 
183
+ # تبدیل استریو به مونو
184
  if len(content_data.shape) > 1 and content_data.shape[1] > 1:
185
  content_data = np.mean(content_data, axis=1)
186
 
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
  content_tensor = content_tensor / (torch.max(torch.abs(content_tensor)) + 1e-6) * 0.95
194
+ content_audio_np = content_tensor.squeeze().numpy()
195
 
196
+ # --- 2. پردازش رفرنس (Reference) ---
197
  if isinstance(reference_wav, tuple):
198
  ref_sr, ref_data = reference_wav if isinstance(reference_wav[0], int) else (reference_wav[1], reference_wav[0])
199
  else:
 
210
  ref_max = torch.max(torch.abs(ref_tensor)) + 1e-6
211
  ref_tensor = ref_tensor / ref_max * 0.95
212
 
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. منطق Overlap-Add برای اتصال بدون لرزش ---
220
  pipeline = get_pipeline()
 
221
  SR = 24000
 
 
222
 
223
+ # تنظیمات تکه‌بندی
224
+ PROCESS_CHUNK_SEC = 12 # طول هر تکه برای پردازش (۱۲ ثانیه)
225
+ OVERLAP_SEC = 2 # میزان هم‌پوشانی برای میکس (۲ ثانیه)
226
+ # میزان پیشروی موثر در هر گام (۱۰ ثانیه)
227
+ HOP_SEC = PROCESS_CHUNK_SEC - OVERLAP_SEC
228
+
229
+ hop_samples = HOP_SEC * SR
230
+ chunk_samples = PROCESS_CHUNK_SEC * SR
231
+ overlap_samples = OVERLAP_SEC * SR
232
 
233
+ total_length = len(content_audio_np)
234
+ final_output_audio = np.array([], dtype=np.float32)
235
 
236
+ print(f"[{session_id}] Starting seamless processing. Total length: {total_length/SR:.2f}s")
 
237
 
238
+ # حلقه روی فایل با گام ۱۰ ثانیه
239
+ for start_idx in range(0, total_length, hop_samples):
240
+ end_idx = min(start_idx + chunk_samples, total_length)
 
 
 
241
 
242
+ # استخراج تکه فعلی
243
+ current_chunk_np = content_audio_np[start_idx:end_idx]
 
244
 
245
+ # اگر تکه خیلی کوتاه باشد (کمتر از نیم ثانیه)، صرف نظر کن
246
+ if len(current_chunk_np) < SR * 0.5:
247
+ continue
248
+
249
+ # ذخیره موقت برای مدل
250
+ save_audio_pcm16(torch.FloatTensor(current_chunk_np).unsqueeze(0), temp_content_path, SR)
251
 
252
  try:
253
+ # اجرای مدل
254
  gen = pipeline.inference_fm(
255
  src_wav_path=temp_content_path,
256
  timbre_ref_wav_path=temp_reference_path,
257
  flow_matching_steps=64,
258
  )
259
 
260
+ # تبدیل خروجی به numpy
261
  if torch.isnan(gen).any(): gen = torch.nan_to_num(gen, nan=0.0)
262
  if gen.dim() == 1: gen = gen.unsqueeze(0)
263
+ gen_np = gen.cpu().squeeze(0).numpy()
 
 
 
 
 
 
 
264
 
265
+ # --- میکس (Cross-Fade) ---
266
+ if len(final_output_audio) == 0:
267
+ # اولین تکه: مستقیماً اضافه کن
268
+ final_output_audio = gen_np
269
+ else:
270
+ # تکه‌های بعدی
271
+ # ۱. محاسبه طول هم‌پوشانی واقعی
272
+ # انتظار داریم خروجی مدل تقریباً هم‌اندازه ورودی باشد
273
+ # انتهای صدای فعلی در final_output_audio کجاست؟
274
+ # ما در هر دور hop_samples جلو می‌رویم.
275
 
276
+ # روش ایمن:
277
+ # دو ثانیه آخر صدای قبلی را با دو ثانیه اول صدای جدید ترکیب می‌کنیم
278
+
279
+ prev_audio_len = len(final_output_audio)
280
+ new_audio_len = len(gen_np)
281
+
282
+ # اگر خروجی مدل خیلی کوتاه بود، فقط بچسبان
283
+ if new_audio_len < overlap_samples:
284
+ final_output_audio = np.concatenate([final_output_audio, gen_np])
285
+ continue
286
+
287
+ # برش بخش هم‌پوشانی
288
+ # ما قبلاً در دور قبل، تمام خروجی (۱۲ ثانیه) را ذخیره کردیم.
289
+ # اما الان ۱۰ ثانیه جلو آمدیم. پس ۲ ثانیه آخر دور قبلی، با ۲ ثانیه اول دور جدید هم‌پوشانی دارد.
290
+ # (توجه: این منطق فرض می‌کند خروجی مدل دقیقا هم‌طول ورودی است که در VC تقریبا درست است)
291
+
292
+ # حذف ۲ ثانیه آخر از بافر نهایی (برای آماده‌سازی میکس)
293
+ # اما صبر کن، ما در دور قبل کامل اضافه کردیم. پس الان بافر نهایی شامل Overlap هم هست.
294
+ # بخش غیر اورلپ قبلی: تا سرِ overlap
295
+ # بخش اورلپ قبلی: از سرِ overlap تا آخر
296
+
297
+ # منطق ساده‌تر Overlap-Add:
298
+ # همیشه فقط بخش "جدید" (۱۰ ثانیه) را نگه نداریم، بلکه همیشه میکس کنیم.
299
+
300
+ # پیاده‌سازی دقیق Cross-Fade:
301
+ fade_out = np.linspace(1, 0, overlap_samples)
302
+ fade_in = np.linspace(0, 1, overlap_samples)
303
+
304
+ # ۲ ثانیه آخر بافر فعلی
305
+ prev_tail = final_output_audio[-overlap_samples:]
306
+ # ۲ ثانیه اول صدای جدید
307
+ curr_head = gen_np[:overlap_samples]
308
+
309
+ # اطمینان از هم‌اندازه بودن (ممکن است مدل خروجی کمی متفاوت بدهد)
310
+ min_len = min(len(prev_tail), len(curr_head))
311
+ if min_len < overlap_samples:
312
+ # اگر به هر دلیلی سایزها نخواند (نادر)، برش بزن
313
+ prev_tail = prev_tail[:min_len]
314
+ curr_head = curr_head[:min_len]
315
+ fade_out = fade_out[:min_len]
316
+ fade_in = fade_in[:min_len]
317
+ # اصلاح بافر اصلی
318
+ final_output_audio = final_output_audio[:len(final_output_audio)-(overlap_samples-min_len)]
319
+
320
+ # انجام میکس
321
+ blended_overlap = (prev_tail * fade_out) + (curr_head * fade_in)
322
+
323
+ # بروزرسانی بافر نهایی:
324
+ # ۱. حذف بخش Overlap خام قبلی
325
+ final_output_audio = final_output_audio[:-len(blended_overlap)]
326
+ # ۲. اضافه کردن بخش میکس شده
327
+ final_output_audio = np.concatenate([final_output_audio, blended_overlap])
328
+ # ۳. اضافه کردن باقی‌مانده صدای جدید (بعد از بخش Overlap)
329
+ remaining_new = gen_np[len(blended_overlap):]
330
+ final_output_audio = np.concatenate([final_output_audio, remaining_new])
331
+
332
  except Exception as e:
333
+ print(f"Error generating chunk at {start_idx}: {e}")
334
+ # در صورت خطا، سکوت اضافه کن تا تایمینگ به هم نریزد
335
+ silence_len = chunk_samples if start_idx + chunk_samples < total_length else total_length - start_idx
336
+ final_output_audio = np.concatenate([final_output_audio, np.zeros(silence_len)])
 
 
 
 
 
 
 
337
 
338
+ # ذخیره نهایی
339
+ save_audio_pcm16(final_output_audio, output_path, SR)
340
  return output_path
341
 
342
  finally:
343
  if os.path.exists(temp_content_path): os.remove(temp_content_path)
344
  if os.path.exists(temp_reference_path): os.remove(temp_reference_path)
345
 
346
+ with gr.Blocks(title="Vevo-Timbre (Seamless Overlap)") as demo:
347
  gr.Markdown("## Vevo-Timbre: Zero-Shot Voice Conversion")
348
+ gr.Markdown("Fixed Seamless Version: Uses 2s Cross-Fade Overlap to eliminate glitches.")
349
 
350
  with gr.Row():
351
  with gr.Column():