import os import sys import importlib.util import torch import torchaudio import numpy as np from huggingface_hub import snapshot_download, hf_hub_download import subprocess import uuid import soundfile as sf import spaces import librosa import shutil import re import gradio as gr # --- 1. نصب و تنظیمات اولیه (بدون تغییر) --- def install_espeak(): try: result = subprocess.run(["which", "espeak-ng"], capture_output=True, text=True) if result.returncode != 0: print("Installing espeak-ng...") subprocess.run(["apt-get", "update"], check=True) subprocess.run(["apt-get", "install", "-y", "espeak-ng", "espeak-ng-data"], check=True) except Exception as e: print(f"Error installing espeak-ng: {e}") install_espeak() def patch_langsegment_init(): try: spec = importlib.util.find_spec("LangSegment") if spec is None or spec.origin is None: return init_path = os.path.join(os.path.dirname(spec.origin), '__init__.py') with open(init_path, 'r') as f: lines = f.readlines() modified = False new_lines =[] target_line_prefix = "from .LangSegment import" for line in lines: if line.strip().startswith(target_line_prefix) and ('setLangfilters' in line or 'getLangfilters' in line): mod_line = line.replace(',setLangfilters', '').replace(',getLangfilters', '') mod_line = mod_line.replace('setLangfilters,', '').replace('getLangfilters,', '').rstrip(',') new_lines.append(mod_line + '\n') modified = True else: new_lines.append(line) if modified: with open(init_path, 'w') as f: f.writelines(new_lines) try: import LangSegment importlib.reload(LangSegment) except: pass except: pass patch_langsegment_init() if not os.path.exists("Amphion"): print("Cloning Amphion repository...") subprocess.run(["git", "clone", "https://github.com/open-mmlab/Amphion.git"]) amphion_path = os.path.abspath("Amphion") if amphion_path not in sys.path: sys.path.append(amphion_path) # --- پچ کردن تمام کدهای Amphion برای سازگاری با نسخه جدید transformers --- def patch_amphion_llama_config(): try: for root, dirs, files in os.walk(amphion_path): for file in files: if file.endswith(".py"): file_path = os.path.join(root, file) try: with open(file_path, "r", encoding="utf-8") as f: content = f.read() new_content = re.sub( r'LlamaConfig\(\s*0\s*,\s*256\s*,\s*1024\s*,\s*1\s*,\s*1\s*\)', r'LlamaConfig(vocab_size=0, hidden_size=256, intermediate_size=1024, num_hidden_layers=1, num_attention_heads=1)', content ) if new_content != content: with open(file_path, "w", encoding="utf-8") as f: f.write(new_content) print(f"Patched {file} successfully.") except Exception: pass except Exception as e: print(f"Failed to patch Amphion: {e}") patch_amphion_llama_config() # --- رفع مشکل rope_theta با تزریق (Monkey Patch) در LlamaConfig --- import transformers from transformers.models.llama.configuration_llama import LlamaConfig _original_llama_init = LlamaConfig.__init__ def _patched_llama_init(self, *args, **kwargs): kwargs.setdefault('rope_theta', 10000.0) kwargs.setdefault('attention_bias', False) kwargs.setdefault('rope_scaling', None) kwargs.setdefault('max_position_embeddings', 4096) _original_llama_init(self, *args, **kwargs) if not hasattr(self, 'rope_theta'): self.rope_theta = 10000.0 if not hasattr(self, 'attention_bias'): self.attention_bias = False LlamaConfig.__init__ = _patched_llama_init os.makedirs("wav", exist_ok=True) os.makedirs("ckpts/Vevo", exist_ok=True) try: from models.vc.vevo.vevo_utils import VevoInferencePipeline except ImportError: sys.path.append(os.path.join(amphion_path)) from models.vc.vevo.vevo_utils import VevoInferencePipeline # --- توابع ذخیره و تنظیمات مدل --- def save_audio_pcm16(waveform, output_path, sample_rate=24000): try: if isinstance(waveform, torch.Tensor): waveform = waveform.detach().cpu() if waveform.dim() == 2 and waveform.shape[0] == 1: waveform = waveform.squeeze(0) waveform = waveform.numpy() sf.write(output_path, waveform, sample_rate, subtype='PCM_16') except Exception as e: print(f"Save error: {e}") downloaded_resources = { "configs": False, "tokenizer_vq8192": False, "fmt_Vq8192ToMels": False, "vocoder": False } def setup_configs(): if downloaded_resources["configs"]: return target_config_path = "models/vc/vevo/config" os.makedirs(target_config_path, exist_ok=True) config_files =["Vq8192ToMels.json", "Vocoder.json", "hubert_large_l18_mean_std.npz"] source_config_path = os.path.join(amphion_path, "models/vc/vevo/config") for file in config_files: target_file_path = f"{target_config_path}/{file}" source_file_path = os.path.join(source_config_path, file) if not os.path.exists(target_file_path): if os.path.exists(source_file_path): shutil.copy(source_file_path, target_file_path) else: try: file_data = hf_hub_download(repo_id="amphion/Vevo", filename=f"config/{file}", repo_type="model") if os.path.exists(target_file_path): os.remove(target_file_path) subprocess.run(["cp", file_data, target_file_path]) except Exception as e: print(f"Error downloading config {file}: {e}") downloaded_resources["configs"] = True setup_configs() device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") inference_pipelines = {} downloaded_content_style_tokenizer_path = None downloaded_fmt_path = None downloaded_vocoder_path = None def preload_all_resources(): setup_configs() global downloaded_content_style_tokenizer_path, downloaded_fmt_path, downloaded_vocoder_path if not downloaded_resources["tokenizer_vq8192"]: downloaded_content_style_tokenizer_path = snapshot_download(repo_id="amphion/Vevo", repo_type="model", cache_dir="./ckpts/Vevo", allow_patterns=["tokenizer/vq8192/"]) downloaded_resources["tokenizer_vq8192"] = True if not downloaded_resources["fmt_Vq8192ToMels"]: downloaded_fmt_path = snapshot_download(repo_id="amphion/Vevo", repo_type="model", cache_dir="./ckpts/Vevo", allow_patterns=["acoustic_modeling/Vq8192ToMels/"]) downloaded_resources["fmt_Vq8192ToMels"] = True if not downloaded_resources["vocoder"]: downloaded_vocoder_path = snapshot_download(repo_id="amphion/Vevo", repo_type="model", cache_dir="./ckpts/Vevo", allow_patterns=["acoustic_modeling/Vocoder/*"]) downloaded_resources["vocoder"] = True preload_all_resources() def get_pipeline(): if "timbre" in inference_pipelines: return inference_pipelines["timbre"] tokenizer_path = os.path.join(downloaded_content_style_tokenizer_path, "tokenizer/vq8192") fmt_ckpt = os.path.join(downloaded_fmt_path, "acoustic_modeling/Vq8192ToMels") vocoder_ckpt = os.path.join(downloaded_vocoder_path, "acoustic_modeling/Vocoder") pipeline = VevoInferencePipeline( content_style_tokenizer_ckpt_path=tokenizer_path, fmt_cfg_path="./models/vc/vevo/config/Vq8192ToMels.json", fmt_ckpt_path=fmt_ckpt, vocoder_cfg_path="./models/vc/vevo/config/Vocoder.json", vocoder_ckpt_path=vocoder_ckpt, device=device, ) inference_pipelines["timbre"] = pipeline return pipeline # --- تابع جدید برش هوشمند با قابلیت افزایش داینامیک بازه جستجو --- def smart_split_audio_vevo(y, sr, min_segment=4): total_samples = len(y) current_sample = 0 chunks = [] # اگر طول کل فایل کمتر از حد مجاز اولیه (8 ثانیه) باشد، نیازی به برش نیست if total_samples <= 8 * sr: return [y] while current_sample < total_samples: # اگر حجم نمونه‌های باقی‌مانده کمتر از حداقل زمان قطعه (4 ثانیه) باشد، بقیه فایل را یکجا بردار if total_samples - current_sample <= min_segment * sr: chunks.append(y[current_sample:]) break # بازه‌های گام‌به‌گام برای جستجوی سکوت: ابتدا ۸، سپس ۱۲، ۱۵ و نهایتاً ۲۰ ثانیه steps = [8, 12, 15, 20] silence_threshold = 0.015 # آستانه شدت انرژی سیگنال برای پذیرش به عنوان سکوت واقعی best_split_point = None best_rms_val = float('inf') for max_sec in steps: start_search = current_sample + int(min_segment * sr) end_search = current_sample + int(max_sec * sr) if start_search >= total_samples: best_split_point = total_samples break if end_search > total_samples: end_search = total_samples search_region = y[start_search:end_search] if len(search_region) == 0: best_split_point = total_samples break # محاسبه میزان انرژی (RMS) قطعه rms = librosa.feature.rms(y=search_region, frame_length=2048, hop_length=512)[0] if len(rms) == 0: continue min_rms_idx = np.argmin(rms) min_rms_val = rms[min_rms_idx] candidate_point = start_search + (min_rms_idx * 512) # ذخیره بهترین نقطه کم‌صدا (در صورتی که سکوت مطلق پیدا نشود، کم‌صدا‌ترین نقطه انتخاب می‌شود) if min_rms_val < best_rms_val: best_rms_val = min_rms_val best_split_point = candidate_point # اگر شدت صدای نقطه پیدا شده پایین‌تر از آستانه سکوت بود، فرآیند جستجو را متوقف کرده و برش بزن if min_rms_val < silence_threshold: break if best_split_point is None: best_split_point = min(current_sample + int(8 * sr), total_samples) chunk_audio = y[current_sample:best_split_point] chunks.append(chunk_audio) current_sample = best_split_point # بررسی قطعه آخر: اگر بیش از حد کوتاه باشد، آن را با بخش قبلی ادغام می‌کنیم if len(chunks) > 1 and len(chunks[-1]) < int(1.5 * sr): last_chunk = chunks.pop() combined_len = len(chunks[-1]) + len(last_chunk) # تا سقف حداکثر ۲۰ ثانیه ادغام قطعه آخر مجاز است if combined_len <= int(20 * sr): chunks[-1] = np.concatenate([chunks[-1], last_chunk]) else: chunks.append(last_chunk) return chunks # ========================================================================= # سیستم استنتاج مستقیم بر روی Zero-GPU با رابط کاربری Gradio # ========================================================================= @spaces.GPU(duration=120) def predict_voice_conversion(source_audio_path, ref_audio_path): if source_audio_path is None or ref_audio_path is None: raise gr.Error("لطفاً هر دو فایل صوتی (اصلی و مرجع) را وارد کنید.") task_id = str(uuid.uuid4()) out_path = f"wav/{task_id}_out.wav" SR = 24000 try: # پردازش صوت اصلی content_data, _ = librosa.load(source_audio_path, sr=SR) # پردازش صوت مرجع ref_data, _ = librosa.load(ref_audio_path, sr=SR) ref_tensor = torch.FloatTensor(ref_data).unsqueeze(0) ref_tensor = ref_tensor / (torch.max(torch.abs(ref_tensor)) + 1e-6) * 0.95 temp_r = f"wav/{task_id}_temp_r.wav" save_audio_pcm16(ref_tensor, temp_r, SR) # اعمال تقسیم‌کننده هوشمند چندمرحله‌ای chunks = smart_split_audio_vevo(content_data, SR, min_segment=4) generated_chunks = [] pipeline = get_pipeline() # شبیه‌سازی قطعه به قطعه for idx, chunk in enumerate(chunks): chunk_tensor = torch.FloatTensor(chunk).unsqueeze(0) chunk_tensor = chunk_tensor / (torch.max(torch.abs(chunk_tensor)) + 1e-6) * 0.95 temp_c_chunk = f"wav/{task_id}_temp_c_{idx}.wav" save_audio_pcm16(chunk_tensor, temp_c_chunk, SR) gen = pipeline.inference_fm( src_wav_path=temp_c_chunk, timbre_ref_wav_path=temp_r, flow_matching_steps=32, ) if torch.isnan(gen).any(): gen = torch.nan_to_num(gen, nan=0.0) gen_np = gen.detach().cpu().squeeze().numpy() generated_chunks.append(gen_np) if os.path.exists(temp_c_chunk): os.remove(temp_c_chunk) # ادغام قطعات خروجی final_gen = np.concatenate(generated_chunks, axis=0) # ذخیره نهایی save_audio_pcm16(final_gen, out_path, SR) if os.path.exists(temp_r): os.remove(temp_r) return out_path except Exception as e: raise gr.Error(f"خطایی در حین پردازش رخ داد: {str(e)}") # طراحی ساختار وب با استفاده از Gradio with gr.Blocks() as demo: gr.Markdown("# سامانه تبدیل صدا (Vevo Voice Conversion) - نسخه Zero-GPU PRO") with gr.Row(): with gr.Column(): source_input = gr.Audio(type="filepath", label="فایل صوتی اصلی (Source Audio)") ref_input = gr.Audio(type="filepath", label="فایل صوتی مرجع/هدف (Reference Timbre)") submit_btn = gr.Button("شروع فرآیند تبدیل صدا", variant="primary") with gr.Column(): audio_output = gr.Audio(label="خروجی صدای شبیه‌سازی شده", type="filepath") submit_btn.click( fn=predict_voice_conversion, inputs=[source_input, ref_input], outputs=audio_output ) if __name__ == "__main__": demo.queue(max_size=20).launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft())