Update app.py
Browse files
app.py
CHANGED
|
@@ -11,9 +11,10 @@ from huggingface_hub import snapshot_download, hf_hub_download
|
|
| 11 |
import subprocess
|
| 12 |
import uuid
|
| 13 |
import soundfile as sf
|
| 14 |
-
import spaces
|
|
|
|
| 15 |
|
| 16 |
-
# ---
|
| 17 |
downloaded_resources = {
|
| 18 |
"configs": False,
|
| 19 |
"tokenizer_vq8192": False,
|
|
@@ -132,6 +133,93 @@ def get_pipeline():
|
|
| 132 |
inference_pipelines["timbre"] = pipeline
|
| 133 |
return pipeline
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
@spaces.GPU()
|
| 136 |
def vevo_timbre(content_wav, reference_wav):
|
| 137 |
session_id = str(uuid.uuid4())[:8]
|
|
@@ -145,7 +233,7 @@ def vevo_timbre(content_wav, reference_wav):
|
|
| 145 |
try:
|
| 146 |
SR = 24000
|
| 147 |
|
| 148 |
-
# ---
|
| 149 |
if isinstance(content_wav, tuple):
|
| 150 |
content_sr, content_data = content_wav if isinstance(content_wav[0], int) else (content_wav[1], content_wav[0])
|
| 151 |
else:
|
|
@@ -158,7 +246,7 @@ def vevo_timbre(content_wav, reference_wav):
|
|
| 158 |
content_tensor = content_tensor / (torch.max(torch.abs(content_tensor)) + 1e-6) * 0.95
|
| 159 |
content_full_np = content_tensor.squeeze().numpy()
|
| 160 |
|
| 161 |
-
# ---
|
| 162 |
if isinstance(reference_wav, tuple):
|
| 163 |
ref_sr, ref_data = reference_wav if isinstance(reference_wav[0], int) else (reference_wav[1], reference_wav[0])
|
| 164 |
else:
|
|
@@ -172,40 +260,27 @@ def vevo_timbre(content_wav, reference_wav):
|
|
| 172 |
if ref_tensor.shape[1] > SR * 20: ref_tensor = ref_tensor[:, :SR * 20]
|
| 173 |
save_audio_pcm16(ref_tensor, temp_reference_path, SR)
|
| 174 |
|
| 175 |
-
# --- استراتژی Center-Only Processing (حذف لرزش) ---
|
| 176 |
pipeline = get_pipeline()
|
| 177 |
|
| 178 |
-
#
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
core_samples = int(CORE_CHUNK_SEC * SR)
|
| 183 |
-
padding_samples = int(PADDING_SEC * SR)
|
| 184 |
-
total_samples = len(content_full_np)
|
| 185 |
|
| 186 |
final_output = []
|
|
|
|
| 187 |
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
while cursor < total_samples:
|
| 192 |
-
# ۱. خواندن بازه وسیعتر (شامل پدینگ)
|
| 193 |
-
read_start = max(0, cursor - padding_samples)
|
| 194 |
-
read_end = min(total_samples, cursor + core_samples + padding_samples)
|
| 195 |
|
| 196 |
-
|
| 197 |
-
|
| 198 |
|
| 199 |
chunk_input = content_full_np[read_start:read_end]
|
| 200 |
-
|
| 201 |
-
# اگر تکه انتهایی خیلی کوچک است، بیخیال شو
|
| 202 |
-
if len(chunk_input) < SR * 0.5:
|
| 203 |
-
break
|
| 204 |
-
|
| 205 |
save_audio_pcm16(torch.FloatTensor(chunk_input).unsqueeze(0), temp_content_path, SR)
|
| 206 |
|
| 207 |
try:
|
| 208 |
-
# ۲. تولید صدا
|
| 209 |
gen = pipeline.inference_fm(
|
| 210 |
src_wav_path=temp_content_path,
|
| 211 |
timbre_ref_wav_path=temp_reference_path,
|
|
@@ -214,47 +289,35 @@ def vevo_timbre(content_wav, reference_wav):
|
|
| 214 |
if torch.isnan(gen).any(): gen = torch.nan_to_num(gen, nan=0.0)
|
| 215 |
gen_np = gen.detach().cpu().squeeze().numpy()
|
| 216 |
|
| 217 |
-
|
| 218 |
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
trim_front = 0 # در اولین تکه، پدینگ جلو نداریم
|
| 222 |
-
else:
|
| 223 |
-
trim_front = padding_samples # در بقیه، به اندازه پدینگ جلو میبریم
|
| 224 |
-
|
| 225 |
-
# محاسبه طول مفید
|
| 226 |
-
valid_length = min(core_samples, total_samples - cursor)
|
| 227 |
-
|
| 228 |
-
if len(gen_np) > trim_front:
|
| 229 |
-
# استخراج فقط هسته مرکزی (بدون لرزش)
|
| 230 |
-
core_audio = gen_np[trim_front : trim_front + valid_length]
|
| 231 |
-
|
| 232 |
-
# ۴. اتصال میکروسکوپی (۵۰ میلی ثانیه) فقط برای حذف کلیک
|
| 233 |
-
fade_len = int(0.05 * SR)
|
| 234 |
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
| 238 |
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
| 243 |
mixed = (prev_tail * fade_out) + (curr_head * fade_in)
|
| 244 |
final_output[-1][-fade_len:] = mixed
|
| 245 |
-
|
| 246 |
|
| 247 |
-
final_output.append(
|
| 248 |
-
|
| 249 |
except Exception as e:
|
| 250 |
-
print(f"Error
|
| 251 |
-
|
| 252 |
-
final_output.append(np.zeros(
|
| 253 |
-
|
| 254 |
-
# ۵. جلو رفتن دقیق به اندازه ۱۰ ثانیه
|
| 255 |
-
cursor += core_samples
|
| 256 |
|
| 257 |
-
# چسباندن نهایی
|
| 258 |
if len(final_output) > 0:
|
| 259 |
full_audio = np.concatenate(final_output)
|
| 260 |
else:
|
|
@@ -267,9 +330,9 @@ def vevo_timbre(content_wav, reference_wav):
|
|
| 267 |
if os.path.exists(temp_content_path): os.remove(temp_content_path)
|
| 268 |
if os.path.exists(temp_reference_path): os.remove(temp_reference_path)
|
| 269 |
|
| 270 |
-
with gr.Blocks(title="Vevo-Timbre (
|
| 271 |
gr.Markdown("## Vevo-Timbre: Zero-Shot Voice Conversion")
|
| 272 |
-
gr.Markdown("
|
| 273 |
|
| 274 |
with gr.Row():
|
| 275 |
with gr.Column():
|
|
|
|
| 11 |
import subprocess
|
| 12 |
import uuid
|
| 13 |
import soundfile as sf
|
| 14 |
+
import spaces
|
| 15 |
+
import librosa
|
| 16 |
|
| 17 |
+
# --- 1. نصب و راهاندازی ---
|
| 18 |
downloaded_resources = {
|
| 19 |
"configs": False,
|
| 20 |
"tokenizer_vq8192": False,
|
|
|
|
| 133 |
inference_pipelines["timbre"] = pipeline
|
| 134 |
return pipeline
|
| 135 |
|
| 136 |
+
# --- 2. الگوریتم برش فوق هوشمند ---
|
| 137 |
+
def find_advanced_split_points(audio_np, sr):
|
| 138 |
+
"""
|
| 139 |
+
پیدا کردن نقاط برش با استراتژی فالبک (Fallback Strategy):
|
| 140 |
+
۱. تلاش برای پیدا کردن سکوت در بازه ۸ تا ۱۲ ثانیه.
|
| 141 |
+
۲. اگر نشد، تلاش در بازه وسیعتر ۶ تا ۱۴ ثانیه.
|
| 142 |
+
۳. انتخاب نقطه با کمترین انرژی (حتی اگر سکوت نباشد).
|
| 143 |
+
۴. تنظیم دقیق روی نزدیکترین Zero-Crossing.
|
| 144 |
+
"""
|
| 145 |
+
total_samples = len(audio_np)
|
| 146 |
+
|
| 147 |
+
# تنظیمات بازه جستجو
|
| 148 |
+
MIN_PREFERRED = 8.0
|
| 149 |
+
MAX_PREFERRED = 12.0
|
| 150 |
+
MIN_HARD = 6.0
|
| 151 |
+
MAX_HARD = 15.0
|
| 152 |
+
|
| 153 |
+
split_points = [0]
|
| 154 |
+
current_pos = 0
|
| 155 |
+
|
| 156 |
+
hop_length = 512
|
| 157 |
+
frame_length = 1024
|
| 158 |
+
|
| 159 |
+
while current_pos < total_samples:
|
| 160 |
+
# استراتژی ۱: بازه ایدهآل
|
| 161 |
+
start_search = current_pos + int(MIN_PREFERRED * sr)
|
| 162 |
+
end_search = current_pos + int(MAX_PREFERRED * sr)
|
| 163 |
+
|
| 164 |
+
# اگر به انتهای فایل نزدیکیم
|
| 165 |
+
if start_search >= total_samples:
|
| 166 |
+
split_points.append(total_samples)
|
| 167 |
+
break
|
| 168 |
+
|
| 169 |
+
end_search = min(end_search, total_samples)
|
| 170 |
+
|
| 171 |
+
# استراتژی ۲: اگر بازه ایدهآل خیلی کوتاه است (ته فایل)، گسترش بده
|
| 172 |
+
if end_search - start_search < sr:
|
| 173 |
+
# استفاده از بازه سخت (وسیع)
|
| 174 |
+
start_search = current_pos + int(MIN_HARD * sr)
|
| 175 |
+
end_search = current_pos + int(MAX_HARD * sr)
|
| 176 |
+
start_search = min(start_search, total_samples)
|
| 177 |
+
end_search = min(end_search, total_samples)
|
| 178 |
+
|
| 179 |
+
# برش منطقه جستجو
|
| 180 |
+
region = audio_np[start_search:end_search]
|
| 181 |
+
|
| 182 |
+
if len(region) == 0:
|
| 183 |
+
split_points.append(total_samples)
|
| 184 |
+
break
|
| 185 |
+
|
| 186 |
+
# محاسبه انرژی
|
| 187 |
+
rms = librosa.feature.rms(y=region, frame_length=frame_length, hop_length=hop_length)[0]
|
| 188 |
+
|
| 189 |
+
# پیدا کردن کمانرژیترین نقطه (Local Minimum)
|
| 190 |
+
min_idx = np.argmin(rms)
|
| 191 |
+
local_cut_sample = min_idx * hop_length
|
| 192 |
+
|
| 193 |
+
# --- تکنیک Zero Crossing ---
|
| 194 |
+
# نقطه برش تقریبی را پیدا کردیم. حالا باید دقیقاً روی محور صفر برش دهیم
|
| 195 |
+
# تا صدای "کلیک" ایجاد نشود.
|
| 196 |
+
|
| 197 |
+
cut_absolute_approx = start_search + local_cut_sample
|
| 198 |
+
|
| 199 |
+
# جستجو در اطراف نقطه تقریبی (±500 نمونه) برای پیدا کردن صفر
|
| 200 |
+
search_radius = 500
|
| 201 |
+
zc_start = max(0, cut_absolute_approx - search_radius)
|
| 202 |
+
zc_end = min(total_samples, cut_absolute_approx + search_radius)
|
| 203 |
+
|
| 204 |
+
zc_region = audio_np[zc_start:zc_end]
|
| 205 |
+
|
| 206 |
+
# پیدا کردن نزدیکترین عبور از صفر
|
| 207 |
+
# (جایی که علامت عدد تغییر میکند)
|
| 208 |
+
zero_crossings = np.where(np.diff(np.signbit(zc_region)))[0]
|
| 209 |
+
|
| 210 |
+
if len(zero_crossings) > 0:
|
| 211 |
+
# نزدیکترین صفر به وسط بازه جستجو
|
| 212 |
+
closest_zc = zero_crossings[np.argmin(np.abs(zero_crossings - search_radius))]
|
| 213 |
+
best_cut_absolute = zc_start + closest_zc
|
| 214 |
+
else:
|
| 215 |
+
# اگر صفر پیدا نشد (خیلی بعید)، همان نقطه کمانرژی را بگیر
|
| 216 |
+
best_cut_absolute = cut_absolute_approx
|
| 217 |
+
|
| 218 |
+
split_points.append(best_cut_absolute)
|
| 219 |
+
current_pos = best_cut_absolute
|
| 220 |
+
|
| 221 |
+
return split_points
|
| 222 |
+
|
| 223 |
@spaces.GPU()
|
| 224 |
def vevo_timbre(content_wav, reference_wav):
|
| 225 |
session_id = str(uuid.uuid4())[:8]
|
|
|
|
| 233 |
try:
|
| 234 |
SR = 24000
|
| 235 |
|
| 236 |
+
# --- ورودی ---
|
| 237 |
if isinstance(content_wav, tuple):
|
| 238 |
content_sr, content_data = content_wav if isinstance(content_wav[0], int) else (content_wav[1], content_wav[0])
|
| 239 |
else:
|
|
|
|
| 246 |
content_tensor = content_tensor / (torch.max(torch.abs(content_tensor)) + 1e-6) * 0.95
|
| 247 |
content_full_np = content_tensor.squeeze().numpy()
|
| 248 |
|
| 249 |
+
# --- رفرنس ---
|
| 250 |
if isinstance(reference_wav, tuple):
|
| 251 |
ref_sr, ref_data = reference_wav if isinstance(reference_wav[0], int) else (reference_wav[1], reference_wav[0])
|
| 252 |
else:
|
|
|
|
| 260 |
if ref_tensor.shape[1] > SR * 20: ref_tensor = ref_tensor[:, :SR * 20]
|
| 261 |
save_audio_pcm16(ref_tensor, temp_reference_path, SR)
|
| 262 |
|
|
|
|
| 263 |
pipeline = get_pipeline()
|
| 264 |
|
| 265 |
+
# --- تقسیمبندی پیشرفته ---
|
| 266 |
+
print(f"[{session_id}] Finding best energy split points (Zero-Crossing)...")
|
| 267 |
+
split_points = find_advanced_split_points(content_full_np, SR)
|
| 268 |
+
print(f"[{session_id}] Split into {len(split_points)-1} chunks.")
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
final_output = []
|
| 271 |
+
PADDING_SAMPLES = int(2.5 * SR) # کمی پدینگ بیشتر برای اطمینان
|
| 272 |
|
| 273 |
+
for i in range(len(split_points) - 1):
|
| 274 |
+
start = split_points[i]
|
| 275 |
+
end = split_points[i+1]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
+
read_start = max(0, start - PADDING_SAMPLES)
|
| 278 |
+
read_end = end
|
| 279 |
|
| 280 |
chunk_input = content_full_np[read_start:read_end]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
save_audio_pcm16(torch.FloatTensor(chunk_input).unsqueeze(0), temp_content_path, SR)
|
| 282 |
|
| 283 |
try:
|
|
|
|
| 284 |
gen = pipeline.inference_fm(
|
| 285 |
src_wav_path=temp_content_path,
|
| 286 |
timbre_ref_wav_path=temp_reference_path,
|
|
|
|
| 289 |
if torch.isnan(gen).any(): gen = torch.nan_to_num(gen, nan=0.0)
|
| 290 |
gen_np = gen.detach().cpu().squeeze().numpy()
|
| 291 |
|
| 292 |
+
trim_amount = start - read_start
|
| 293 |
|
| 294 |
+
if len(gen_np) > trim_amount:
|
| 295 |
+
valid_audio = gen_np[trim_amount:]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
+
# اتصال
|
| 298 |
+
if len(final_output) > 0:
|
| 299 |
+
# اگر برش روی سکوت نبوده (اجباری)، باید کمی بیشتر کراسفید کنیم
|
| 300 |
+
# تا تغییر ناگهانی لحن مخفی شود.
|
| 301 |
+
fade_len = int(0.03 * SR) # 30ms standard
|
| 302 |
|
| 303 |
+
if len(final_output[-1]) > fade_len and len(valid_audio) > fade_len:
|
| 304 |
+
fade_out = np.linspace(1, 0, fade_len)
|
| 305 |
+
fade_in = np.linspace(0, 1, fade_len)
|
| 306 |
+
|
| 307 |
+
prev_tail = final_output[-1][-fade_len:]
|
| 308 |
+
curr_head = valid_audio[:fade_len]
|
| 309 |
+
|
| 310 |
mixed = (prev_tail * fade_out) + (curr_head * fade_in)
|
| 311 |
final_output[-1][-fade_len:] = mixed
|
| 312 |
+
valid_audio = valid_audio[fade_len:]
|
| 313 |
|
| 314 |
+
final_output.append(valid_audio)
|
| 315 |
+
|
| 316 |
except Exception as e:
|
| 317 |
+
print(f"Error segment {i}: {e}")
|
| 318 |
+
# پر کردن جای خالی با سکوت برای به هم نریختن تایم
|
| 319 |
+
final_output.append(np.zeros(end - start))
|
|
|
|
|
|
|
|
|
|
| 320 |
|
|
|
|
| 321 |
if len(final_output) > 0:
|
| 322 |
full_audio = np.concatenate(final_output)
|
| 323 |
else:
|
|
|
|
| 330 |
if os.path.exists(temp_content_path): os.remove(temp_content_path)
|
| 331 |
if os.path.exists(temp_reference_path): os.remove(temp_reference_path)
|
| 332 |
|
| 333 |
+
with gr.Blocks(title="Vevo-Timbre (Pro Logic)") as demo:
|
| 334 |
gr.Markdown("## Vevo-Timbre: Zero-Shot Voice Conversion")
|
| 335 |
+
gr.Markdown("Robust Splitting: Uses Minimum Energy + Zero Crossing detection to handle fast speech without glitches.")
|
| 336 |
|
| 337 |
with gr.Row():
|
| 338 |
with gr.Column():
|