import os import glob import json import traceback import logging import gradio as gr import numpy as np import librosa import torch import asyncio import edge_tts import yt_dlp import ffmpeg import subprocess import sys import io import wave import gc from datetime import datetime from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor from functools import lru_cache import psutil from fairseq import checkpoint_utils from lib.infer_pack.models import ( SynthesizerTrnMs256NSFsid, SynthesizerTrnMs256NSFsid_nono, SynthesizerTrnMs768NSFsid, SynthesizerTrnMs768NSFsid_nono, ) from vc_infer_pipeline import VC from config import Config config = Config() logging.getLogger("numba").setLevel(logging.WARNING) limitation = os.getenv("SYSTEM") == "spaces" # ============================== # OPTIMIZATION SETTINGS # ============================== MAX_WORKERS = 1 # Karena hanya 1 core CHUNK_SIZE = 10 * 16000 # 10 detik pada 16kHz (optimal untuk single core) OVERLAP_SIZE = int(0.5 * 16000) # 0.5 detik overlap untuk menghindari artifacts MEMORY_THRESHOLD = 85 # Clear cache jika memory usage > 85% USE_HALF_PRECISION = True # Gunakan float16 jika memungkinkan CACHE_MODELS = True # Cache model yang sudah dimuat # ============================== # MEMORY MANAGEMENT # ============================== class MemoryManager: @staticmethod def check_memory(): """Cek penggunaan memori""" memory = psutil.virtual_memory() return memory.percent @staticmethod def clear_cache(): """Bersihkan cache memori""" if torch.cuda.is_available(): torch.cuda.empty_cache() gc.collect() @staticmethod def auto_clear(): """Auto-clear jika memori tinggi""" if MemoryManager.check_memory() > MEMORY_THRESHOLD: print(f"Memory high ({MemoryManager.check_memory()}%), clearing cache...") MemoryManager.clear_cache() # ============================== # CHUNK PROCESSING SYSTEM # ============================== class ChunkProcessor: def __init__(self, vc_pipeline, chunk_size=CHUNK_SIZE, overlap=OVERLAP_SIZE): self.vc_pipeline = vc_pipeline self.chunk_size = chunk_size self.overlap = overlap def split_into_chunks(self, audio): """Split audio menjadi chunks dengan overlap""" audio_len = len(audio) chunks = [] if audio_len <= self.chunk_size: return [audio], [0] start = 0 while start < audio_len: end = min(start + self.chunk_size, audio_len) chunk = audio[start:end] chunks.append(chunk) start += self.chunk_size - self.overlap return chunks, list(range(0, audio_len, self.chunk_size - self.overlap)) def process_chunk(self, chunk_data): """Process single chunk""" chunk, index, params = chunk_data try: # Ekstrak parameters (hubert_model, net_g, sid, vc_input, times, f0_up_key, f0_method, file_index, index_rate, if_f0, filter_radius, tgt_sr, resample_sr, rms_mix_rate, version, protect) = params # Process chunk chunk_audio = self.vc_pipeline( hubert_model, net_g, sid, chunk, vc_input, times, f0_up_key, f0_method, file_index, index_rate, if_f0, filter_radius, tgt_sr, resample_sr, rms_mix_rate, version, protect, f0_file=None, ) return index, chunk_audio except Exception as e: print(f"Error processing chunk {index}: {e}") return index, None def process_parallel(self, audio, params, max_workers=MAX_WORKERS): """Process chunks dengan parallel execution""" chunks, indices = self.split_into_chunks(audio) if len(chunks) == 1: # Direct processing untuk audio pendek return self.vc_pipeline(params[0], params[1], params[2], audio, *params[3:]) # Prepare chunk data chunk_data = [(chunk, idx, params) for chunk, idx in zip(chunks, indices)] # Sequential processing untuk 1 core (lebih stabil) results = [] for data in chunk_data: idx, result = self.process_chunk(data) if result is not None: results.append((idx, result)) # Sort by original index results.sort(key=lambda x: x[0]) # Merge chunks with crossfade return self.merge_chunks([r[1] for r in results]) def merge_chunks(self, chunks): """Merge chunks dengan crossfade untuk menghindari artifacts""" if not chunks: return np.array([], dtype=np.float32) if len(chunks) == 1: return chunks[0] merged = chunks[0] for i in range(1, len(chunks)): current_chunk = chunks[i] if len(merged) < self.overlap or len(current_chunk) < self.overlap: # Jika chunk terlalu pendek, langsung concatenate merged = np.concatenate([merged, current_chunk]) else: # Crossfade overlap region fade_out = merged[-self.overlap:] fade_in = current_chunk[:self.overlap] # Linear crossfade t = np.linspace(0, 1, self.overlap) faded = fade_out * (1 - t) + fade_in * t # Merge merged = np.concatenate([ merged[:-self.overlap], faded, current_chunk[self.overlap:] ]) return merged # ============================== # MODEL CACHE SYSTEM # ============================== class ModelCache: _instance = None _models = {} _hubert = None def __new__(cls): if cls._instance is None: cls._instance = super(ModelCache, cls).__new__(cls) return cls._instance @classmethod def get_hubert(cls): if cls._hubert is None: cls._hubert = cls.load_hubert() return cls._hubert @classmethod def load_hubert(cls): """Load hubert model dengan optimasi""" print("Loading HuBERT model...") models, _, _ = checkpoint_utils.load_model_ensemble_and_task( ["hubert_base.pt"], suffix="", ) hubert_model = models[0] hubert_model = hubert_model.to(config.device) if USE_HALF_PRECISION and config.is_half: hubert_model = hubert_model.half() else: hubert_model = hubert_model.float() hubert_model.eval() return hubert_model @classmethod def get_model(cls, model_path): """Get model from cache atau load baru""" if model_path in cls._models and CACHE_MODELS: print(f"Using cached model: {model_path}") return cls._models[model_path] return None @classmethod def cache_model(cls, model_path, model_data): """Cache model""" if CACHE_MODELS: cls._models[model_path] = model_data @classmethod def clear_model_cache(cls): """Clear model cache""" cls._models.clear() if torch.cuda.is_available(): torch.cuda.empty_cache() gc.collect() # ============================== # OPTIMIZED VC FUNCTION # ============================== def create_vc_fn(model_title, tgt_sr, net_g, vc, if_f0, version, file_index): def vc_fn( vc_audio_mode, vc_input, vc_upload, tts_text, tts_voice, f0_up_key, f0_method, index_rate, filter_radius, resample_sr, rms_mix_rate, protect, ): try: # Auto clear memory sebelum proses MemoryManager.auto_clear() # Load audio if vc_audio_mode == "Input path" or "Youtube" and vc_input != "": # Gunakan librosa dengan optimasi audio, sr = librosa.load(vc_input, sr=16000, mono=True) elif vc_audio_mode == "Upload audio": if vc_upload is None: return "You need to upload an audio", None sampling_rate, audio = vc_upload # Batasi durasi jika di space if limitation: duration = audio.shape[0] / sampling_rate if duration > 360: return "Please upload an audio file that is less than 1 minute.", None # Konversi audio audio = (audio / np.iinfo(audio.dtype).max).astype(np.float32) if len(audio.shape) > 1: audio = librosa.to_mono(audio.transpose(1, 0)) if sampling_rate != 16000: audio = librosa.resample(audio, orig_sr=sampling_rate, target_sr=16000) elif vc_audio_mode == "TTS Audio": if limitation and len(tts_text) > 600: return "Text is too long", None if tts_text is None or tts_voice is None: return "You need to enter text and select a voice", None # Generate TTS asyncio.run(edge_tts.Communicate(tts_text, "-".join(tts_voice.split('-')[:-1])).save("tts.mp3")) audio, sr = librosa.load("tts.mp3", sr=16000, mono=True) vc_input = "tts.mp3" else: return "Invalid audio mode", None # Persiapkan parameters hubert_model = ModelCache.get_hubert() times = [0, 0, 0] f0_up_key = int(f0_up_key) # Parameters untuk chunk processing params = ( hubert_model, net_g, 0, vc_input, times, f0_up_key, f0_method, file_index, index_rate, if_f0, filter_radius, tgt_sr, resample_sr, rms_mix_rate, version, protect ) # Gunakan chunk processor untuk audio panjang if len(audio) > CHUNK_SIZE * 2: # Hanya chunk jika > 20 detik print(f"Processing {len(audio)/16000:.2f}s audio in chunks...") processor = ChunkProcessor(vc.pipeline) audio_opt = processor.process_parallel(audio, params, max_workers=MAX_WORKERS) else: # Direct processing untuk audio pendek audio_opt = vc.pipeline( hubert_model, net_g, 0, audio, vc_input, times, f0_up_key, f0_method, file_index, index_rate, if_f0, filter_radius, tgt_sr, resample_sr, rms_mix_rate, version, protect, f0_file=None, ) info = f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}]: npy: {times[0]:.2f}s, f0: {times[1]:.2f}s, infer: {times[2]:.2f}s" print(f"{model_title} | {info}") # Clear memory setelah proses MemoryManager.auto_clear() return info, (tgt_sr, audio_opt) except Exception as e: info = f"Error: {str(e)}\n{traceback.format_exc()}" print(info) MemoryManager.auto_clear() return info, (None, None) return vc_fn # ============================== # OPTIMIZED MODEL LOADING # ============================== def load_model(): categories = [] with open("weights/folder_info.json", "r", encoding="utf-8") as f: folder_info = json.load(f) for category_name, category_info in folder_info.items(): if not category_info['enable']: continue category_title = category_info['title'] category_folder = category_info['folder_path'] models = [] with open(f"weights/{category_folder}/model_info.json", "r", encoding="utf-8") as f: models_info = json.load(f) for character_name, info in models_info.items(): if not info['enable']: continue model_title = info['title'] model_name = info['model_path'] model_author = info.get("author", None) model_cover = f"weights/{category_folder}/{character_name}/{info['cover']}" model_index = f"weights/{category_folder}/{character_name}/{info['feature_retrieval_library']}" # Cek cache dulu model_path = f"weights/{category_folder}/{character_name}/{model_name}" cached_model = ModelCache.get_model(model_path) if cached_model: cpt, tgt_sr, if_f0, version, net_g = cached_model else: # Load model dengan memory optimasi cpt = torch.load(model_path, map_location="cpu", weights_only=False) tgt_sr = cpt["config"][-1] cpt["config"][-3] = cpt["weight"]["emb_g.weight"].shape[0] # n_spk if_f0 = cpt.get("f0", 1) version = cpt.get("version", "v1") # Load model architecture if version == "v1": if if_f0 == 1: net_g = SynthesizerTrnMs256NSFsid(*cpt["config"], is_half=config.is_half) else: net_g = SynthesizerTrnMs256NSFsid_nono(*cpt["config"]) elif version == "v2": if if_f0 == 1: net_g = SynthesizerTrnMs768NSFsid(*cpt["config"], is_half=config.is_half) else: net_g = SynthesizerTrnMs768NSFsid_nono(*cpt["config"]) # Load weights del net_g.enc_q net_g.load_state_dict(cpt["weight"], strict=False) net_g.eval().to(config.device) if USE_HALF_PRECISION and config.is_half: net_g = net_g.half() else: net_g = net_g.float() # Cache model ModelCache.cache_model(model_path, (cpt, tgt_sr, if_f0, version, net_g)) # Create VC vc = VC(tgt_sr, config) models.append((character_name, model_title, model_author, model_cover, version.upper(), create_vc_fn(model_title, tgt_sr, net_g, vc, if_f0, version, model_index))) print(f"Model loaded: {character_name} | {version.upper()}") categories.append([category_title, category_folder, models]) return categories # ============================== # OPTIMIZED FUNCTIONS # ============================== def cut_vocal_and_inst(url, audio_provider, split_model): """Optimized audio splitting dengan progress feedback""" if not url: raise gr.Error("URL Required!") if not os.path.exists("dl_audio"): os.mkdir("dl_audio") try: if audio_provider == "Youtube": ydl_opts = { 'format': 'bestaudio/best', 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'wav', }], "outtmpl": 'dl_audio/youtube_audio', 'quiet': True, 'no_warnings': True, } with yt_dlp.YoutubeDL(ydl_opts) as ydl: ydl.download([url]) audio_path = "dl_audio/youtube_audio.wav" # Optimize demucs command model_name = "htdemucs" if split_model == "htdemucs" else "mdx_extra_q" output_dir = f"output/{model_name}" # Gunakan subprocess dengan Popen untuk better control cmd = ["demucs", "--two-stems=vocals", "-n", model_name, audio_path, "-o", "output", "--quiet"] process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) # Monitor progress for line in process.stdout: if "Progress" in line: print(line.strip()) process.wait() vocal_path = f"{output_dir}/youtube_audio/vocals.wav" inst_path = f"{output_dir}/youtube_audio/no_vocals.wav" return vocal_path, inst_path, audio_path, vocal_path except Exception as e: raise gr.Error(f"Error processing audio: {str(e)}") def combine_vocal_and_inst(audio_data, audio_volume, split_model): """Optimized audio combining""" if not os.path.exists("output/result"): os.makedirs("output/result") vocal_path = "output/result/output.wav" output_path = "output/result/combine.mp3" model_name = "htdemucs" if split_model == "htdemucs" else "mdx_extra_q" inst_path = f"output/{model_name}/youtube_audio/no_vocals.wav" # Write vocal file with wave.open(vocal_path, "w") as wave_file: wave_file.setnchannels(1) wave_file.setsampwidth(2) wave_file.setframerate(audio_data[0]) wave_file.writeframes(audio_data[1].tobytes()) # Optimize ffmpeg command cmd = [ 'ffmpeg', '-y', '-i', inst_path, '-i', vocal_path, '-filter_complex', f'[1:a]volume={audio_volume}dB[v];[0:a][v]amix=inputs=2:duration=longest', '-b:a', '320k', '-c:a', 'libmp3lame', '-threads', '1', # Gunakan single thread '-loglevel', 'error', output_path ] subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return output_path # ============================== # MAIN APPLICATION # ============================== if __name__ == '__main__': # Setup audio mode audio_mode = [] f0method_mode = [] f0method_info = "" if limitation is True: audio_mode = ["Upload audio", "TTS Audio"] f0method_mode = ["pm", "crepe", "harvest"] f0method_info = "PM is fast, rmvpe is middle, Crepe or harvest is good but it was extremely slow (Default: PM)" else: audio_mode = ["Upload audio", "Youtube", "TTS Audio"] f0method_mode = ["pm", "crepe", "harvest"] f0method_info = "PM is fast, rmvpe is middle. Crepe or harvest is good but it was extremely slow (Default: PM)" if os.path.isfile("rmvpe.pt"): f0method_mode.insert(2, "rmvpe") # Pre-load hubert model print("Initializing HuBERT model...") ModelCache.get_hubert() # Load models print("Loading RVC models...") categories = load_model() # Get TTS voices tts_voice_list = asyncio.get_event_loop().run_until_complete(edge_tts.list_voices()) voices = [f"{v['ShortName']}-{v['Gender']}" for v in tts_voice_list] # Create Gradio app with gr.Blocks(theme=gr.themes.Base(), css=""" .gradio-container {max-width: 1200px !important;} .tab-nav {scroll-behavior: smooth;} """) as app: gr.Markdown(""" # ▶️ RVC Youtuber Indonesia 👳🏿♂️ [⚡] **Optimized Version** - Chunk Processing Enabled """) for (folder_title, folder, models) in categories: with gr.TabItem(folder_title): if not models: gr.Markdown("#