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 sys import io import wave import re import shutil import time from datetime import datetime from huggingface_hub import snapshot_download from fairseq import checkpoint_utils from fairseq.data.dictionary import Dictionary 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) logging.getLogger("fairseq").setLevel(logging.WARNING) # === SISTEM AUTO-DOWNLOAD === def download_required_weights(): repo_id = "Plana-Archive/Anime-RCV" # First download folder_info.json print("π¦ Downloading folder_info.json...") try: snapshot_download( repo_id=repo_id, allow_patterns=["Waifu-Anime-RCV/weights/folder_info.json"], local_dir=".", local_dir_use_symlinks=False ) print("β folder_info.json downloaded!") except Exception as e: print(f"β οΈ Failed to download folder_info.json: {e}") return # Load folder_info.json folder_info_path = "Waifu-Anime-RCV/weights/folder_info.json" if not os.path.exists(folder_info_path): print("β folder_info.json not found!") return with open(folder_info_path, "r", encoding="utf-8") as f: folder_info = json.load(f) # Download each enabled category for cat_key, cat_info in folder_info.items(): if cat_info.get("enable", False): folder_path = cat_info["folder_path"] print(f"π¦ Downloading models from {folder_path}...") try: snapshot_download( repo_id=repo_id, allow_patterns=[f"{folder_path}/*"], local_dir=".", local_dir_use_symlinks=False ) print(f"β {folder_path} downloaded!") except Exception as e: print(f"β οΈ Failed to download {folder_path}: {e}") download_required_weights() model_cache = {} hubert_loaded = False hubert_model = None spaces = True if spaces: audio_mode = ["Upload audio", "TTS Audio"] else: audio_mode = ["Input path", "Upload audio", "TTS Audio"] f0method_mode = ["pm", "harvest"] if os.path.isfile("rmvpe.pt"): f0method_mode.insert(2, "rmvpe") def clean_title(title): title = re.sub(r'^Blue Archive\s*-\s*', '', title, flags=re.IGNORECASE) title = re.sub(r'^Waifu Lovers\s*-\s*', '', title, flags=re.IGNORECASE) return re.sub(r'\s*-\s*\d+\s*epochs', '', title, flags=re.IGNORECASE) def _load_audio_input(vc_audio_mode, vc_input, vc_upload, tts_text, use_mic, mic_input): temp_file = None try: if use_mic and mic_input is not None: sampling_rate, audio = mic_input if audio.dtype != np.float32: audio = audio.astype(np.float32) / np.iinfo(audio.dtype).max if len(audio.shape) > 1: audio = np.mean(audio, axis=0) if sampling_rate != 16000: audio = librosa.resample(audio, orig_sr=sampling_rate, target_sr=16000) return audio.astype(np.float32), 16000, None if vc_audio_mode == "Input path" and vc_input: audio, sr = librosa.load(vc_input, sr=16000, mono=True) return audio.astype(np.float32), 16000, None elif vc_audio_mode == "Upload audio": if vc_upload is None: raise ValueError("Upload audio!") sampling_rate, audio = vc_upload if audio.dtype != np.float32: audio = audio.astype(np.float32) / np.iinfo(audio.dtype).max if len(audio.shape) > 1: audio = np.mean(audio, axis=0) if sampling_rate != 16000: audio = librosa.resample(audio, orig_sr=sampling_rate, target_sr=16000) return audio.astype(np.float32), 16000, None elif vc_audio_mode == "TTS Audio": if not tts_text: raise ValueError("Isi teks!") temp_file = "tts_temp.wav" async def tts_task(): return await edge_tts.Communicate(tts_text, "ja-JP-NanamiNeural").save(temp_file) asyncio.run(tts_task()) audio, sr = librosa.load(temp_file, sr=16000, mono=True) return audio.astype(np.float32), 16000, temp_file except Exception as e: raise e def create_vc_fn(model_key, tgt_sr, net_g, vc, if_f0, version, file_index): def vc_fn(vc_audio_mode, vc_input, vc_upload, tts_text, use_mic, mic_input, f0_up_key, f0_method, index_rate, filter_radius, resample_sr, rms_mix_rate, protect, speed): temp_audio_file = None try: yield "Status: π Memproses...", None audio, sr, temp_audio_file = _load_audio_input(vc_audio_mode, vc_input, vc_upload, tts_text, use_mic, mic_input) audio_tensor = torch.FloatTensor(audio).to(config.device) times = [0, 0, 0] audio_opt = vc.pipeline(hubert_model, net_g, 0, audio_tensor, "temp", times, int(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) if speed != 1.0: audio_opt = librosa.effects.time_stretch(audio_opt.astype(np.float32), rate=speed) yield "Status: β Selesai!", (tgt_sr, audio_opt) except Exception as e: yield f"β Error: {str(e)}", None finally: if temp_audio_file and os.path.exists(temp_audio_file): os.remove(temp_audio_file) return vc_fn def load_model(): categories = [] # Load folder_info.json folder_info_path = "Waifu-Anime-RCV/weights/folder_info.json" if not os.path.exists(folder_info_path): print("β folder_info.json not found!") return categories with open(folder_info_path, "r", encoding="utf-8") as f: folder_info = json.load(f) # Mapping untuk nama pendek kategori category_short_names = { "WaifuLovers": "π©·Waifu Lovers", "BanGDream": "π©·BanG Dream!", "Bocchi-the-Rock": "π©·Bocchi the Rock!", "Oshi-no-Ko": "π©·Oshi no Ko", "honkai-impact-3": "π©·Honkai Impact 3rd" } # Process each enabled category for cat_key, cat_info in folder_info.items(): if not cat_info.get("enable", False): continue folder_path = cat_info["folder_path"] category_title = cat_info["title"] category_description = cat_info.get("description", "") # Gunakan nama pendek jika ada, jika tidak gunakan yang asli display_title = category_short_names.get(cat_key, category_title) # Extract folder name from path base_path = os.path.dirname(folder_path) target_folder = os.path.basename(folder_path) models = [] # Check if category folder exists full_category_path = os.path.join(base_path, target_folder) if not os.path.exists(full_category_path): print(f"β οΈ Category folder not found: {full_category_path}") continue # Scan for character folders for char_name in os.listdir(full_category_path): char_dir = os.path.join(full_category_path, char_name) if not os.path.isdir(char_dir): continue model_info_path = os.path.join(char_dir, "model_info.json") if not os.path.exists(model_info_path): # Check for any .pth file as model pth_files = glob.glob(os.path.join(char_dir, "*.pth")) if pth_files: # Create a basic model_info entry model_path = pth_files[0] index_files = glob.glob(os.path.join(char_dir, "*.index")) index_path = index_files[0] if index_files else "" cover_files = glob.glob(os.path.join(char_dir, "*.png")) + glob.glob(os.path.join(char_dir, "*.jpg")) cover_path = cover_files[0] if cover_files else "" info = { 'title': char_name, 'model_path': os.path.basename(model_path), 'feature_retrieval_library': os.path.basename(index_path) if index_path else "", 'cover': os.path.basename(cover_path) if cover_path else "", 'author': 'Plana-Archive' } else: continue else: with open(model_info_path, "r", encoding="utf-8") as f: info = json.load(f) model_path = os.path.join(char_dir, info.get('model_path', '')) index_path = os.path.join(char_dir, info.get('feature_retrieval_library', '')) if os.path.exists(model_path): try: cpt = torch.load(model_path, map_location="cpu") tgt_sr, if_f0, version = cpt["config"][-1], cpt.get("f0", 1), cpt.get("version", "v1") if version == "v1": net_g = SynthesizerTrnMs256NSFsid(*cpt["config"], is_half=config.is_half) if if_f0==1 else SynthesizerTrnMs256NSFsid_nono(*cpt["config"]) else: net_g = SynthesizerTrnMs768NSFsid(*cpt["config"], is_half=config.is_half) if if_f0==1 else SynthesizerTrnMs768NSFsid_nono(*cpt["config"]) net_g.load_state_dict(cpt["weight"], strict=False) net_g.eval().to(config.device) vc = VC(tgt_sr, config) cover_path = os.path.join(char_dir, info.get('cover', '')) models.append(( char_name, info.get('title', char_name), info.get('author', 'Plana-Archive'), cover_path if os.path.exists(cover_path) else "", version, create_vc_fn(char_name, tgt_sr, net_g, vc, if_f0, version, index_path if os.path.exists(index_path) else None) )) print(f"β Loaded model: {char_name} from {display_title}") except Exception as e: print(f"β οΈ Failed to load model {char_name}: {e}") if models: categories.append([display_title, target_folder, category_description, models]) print(f"β Loaded category: {display_title} with {len(models)} models") return categories def load_hubert(): global hubert_model torch.serialization.add_safe_globals([Dictionary]) models, _, _ = checkpoint_utils.load_model_ensemble_and_task(["hubert_base.pt"]) hubert_model = models[0].to(config.device) hubert_model = hubert_model.half() if config.is_half else hubert_model.float() hubert_model.eval() # ============================= # FUNGSI TAMBAHAN UNTUK UI # ============================= def change_audio_mode(vc_audio_mode): """Mengubah tampilan input audio""" is_input_path = vc_audio_mode == "Input path" is_upload = vc_audio_mode == "Upload audio" is_tts = vc_audio_mode == "TTS Audio" return ( gr.Textbox.update(visible=is_input_path), gr.Checkbox.update(visible=is_upload), gr.Audio.update(visible=is_upload), gr.Textbox.update(visible=is_tts, lines=4 if is_tts else 2) ) def use_microphone(microphone): """Toggle microphone/upload source""" return gr.Audio.update(source="microphone" if microphone else "upload") # ============================= # TAMPILAN MY WIFE π©· # ============================= css = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Quicksand:wght@400;600;700&display=swap'); body, .gradio-container { background-color: #ffffff !important; font-family: 'Inter', sans-serif !important; } footer { display: none !important; } .arona-loading-container { display: flex; align-items: center; justify-content: center; gap: 15px; margin-top: 15px; padding: 10px; } .loading-text-pink { font-family: 'Quicksand', sans-serif; font-size: 20px; font-weight: 700; color: #ff69b4; letter-spacing: 1px; } .loading-gif-small { width: 100px; height: auto; border-radius: 8px; } .header-img-container { text-align: center; padding: 10px 0; background: #ffffff !important; } .header-img { width: 100%; max-width: 500px; border-radius: 15px; margin: 0 auto; display: block; } .status-card { background: #ffffff; border: 1px solid #ffe4ec; border-radius: 14px; padding: 15px 10px; margin: 0 auto 15px auto; max-width: 400px; display: flex; flex-direction: column; align-items: center; } .status-online-box { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; } .status-details-container { display: flex; width: 100%; justify-content: center; align-items: center; border-top: 1px solid #fff0f7; padding-top: 10px; } .status-detail-item { flex: 1; display: flex; flex-direction: column; align-items: center; text-align: center; } .status-detail-item:first-child { border-right: 1px solid #ffe4ec; } .status-text-main { font-size: 13px !important; font-weight: 600; color: #7b4d5a; } .status-text-sub { font-size: 11px !important; color: #b07d8b; } .dot-online { height: 8px; width: 8px; background-color: #ff69b4; border-radius: 50%; display: inline-block; animation: blink-pink 1.5s infinite; } @keyframes blink-pink { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } } .gr-form .gr-block label span, .gr-box label span, .gr-panel label span { background: linear-gradient(135deg, #ff69b4 0%, #ff1493 100%) !important; color: white !important; padding: 4px 12px !important; border-radius: 8px !important; font-weight: 600 !important; box-shadow: 0 0 15px rgba(255, 105, 180, 0.4) !important; } input[type="range"] { accent-color: #ff69b4 !important; } .char-scroll-box { display: grid !important; grid-template-columns: repeat(2, 1fr) !important; gap: 12px !important; max-height: 280px; overflow-y: auto; padding: 15px; background: #ffffff; border: 2px solid #ffeef4; border-radius: 14px; } .char-card { background: white; padding: 12px; border-radius: 12px; cursor: pointer; border: 1px solid #ffe4ec; border-left: 5px solid #ff69b4; transition: all 0.2s ease; display: flex; flex-direction: column; height: 65px; } .char-card:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(255, 105, 180, 0.2); border-left-color: #ff1493; } .char-name-jp { font-weight: 700; font-size: 11px !important; color: #7b4d5a; } .char-name-en { font-size: 8.5px !important; color: #b07d8b; text-transform: uppercase; } .speed-section { margin-top: 20px; padding: 18px; border-radius: 20px; background: linear-gradient(135deg, #fff0f7 0%, #ffffff 100%); border: 2px solid #ffe4ec; } .speed-title { font-family: 'Quicksand', sans-serif; font-weight: 700; color: #ff69b4; text-align: center; margin-bottom: 12px; font-size: 14px; } .generate-btn { font-family: 'Quicksand', sans-serif; font-weight: 700 !important; background: linear-gradient(135deg, #ff69b4 0%, #ff1493 100%) !important; color: white !important; border-radius: 12px !important; padding: 12px 24px !important; transition: all 0.3s ease !important; } .generate-btn:hover { transform: scale(1.05); box-shadow: 0 5px 20px rgba(255, 20, 147, 0.3) !important; } .footer-text { text-align: center; padding: 20px; border-top: 1px solid #f8f0f4; color: #b07d8b; font-size: 11px; } .speed-notes-box { font-family: 'Arial'; border: 1px solid #ffd1dc; border-radius: 8px; padding: 12px; background: #fff5f8; border-left: 4px solid #ff69b4; margin-top: 10px; } .speed-notes-title { color: #ff1493; font-size: 12px; margin: 0 0 5px 0; font-weight: bold; } .speed-notes-content { color: #d81b60; font-size: 11px; margin: 0; } .model-tab { background: linear-gradient(135deg, #fff8fb 0%, #ffffff 100%) !important; border-radius: 15px !important; padding: 15px !important; } .advanced-settings { background: #f9f9f9 !important; border-radius: 10px !important; padding: 15px !important; border: 1px solid #e0e0e0 !important; } .error-box { background: #ffebee; border: 1px solid #ffcdd2; border-radius=8px; padding: 15px; margin: 10px 0; color: #c62828; } .info-box { background: #fce4ec; border: 1px solid #f8bbd9; border-radius=8px; padding: 15px; margin: 10px 0; color: #ad1457; } .peringatan-box { font-family: 'Arial'; border: 1px solid #ffd6e7; border-radius: 8px; padding: 15px; background: #fff0f7; border-left: 4px solid #ff69b4; margin-top: 20px; } .peringatan-title { color: #ff1493; font-size: 14px; margin: 0 0 8px 0; font-weight: bold; text-align: center; } .peringatan-content { color: #d81b60; font-size: 12px; margin: 0; text-align: center; font-weight: 600; } /* CSS untuk tab kategori yang lebih kompak */ .tabs { font-size: 14px !important; } .tab-nav { display: flex !important; flex-wrap: wrap !important; gap: 5px !important; margin-bottom: 10px !important; } .tab-nav button { font-size: 12px !important; padding: 6px 10px !important; margin: 2px !important; border-radius: 8px !important; background: linear-gradient(135deg, #ffe4ec 0%, #ffffff 100%) !important; border: 1px solid #ffb6c1 !important; color: #ff1493 !important; font-weight: 600 !important; transition: all 0.3s ease !important; min-width: 100px !important; text-align: center !important; } .tab-nav button:hover { background: linear-gradient(135deg, #ff69b4 0%, #ff1493 100%) !important; color: white !important; transform: translateY(-2px) !important; box-shadow: 0 3px 10px rgba(255, 105, 180, 0.3) !important; } .tab-nav button.selected { background: linear-gradient(135deg, #ff69b4 0%, #ff1493 100%) !important; color: white !important; border: 1px solid #ff1493 !important; } .category-tab-title { font-size: 12px !important; font-weight: 700 !important; text-align: center !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } /* CSS khusus untuk tabs model di dalam kategori */ .model-inner-tabs .tab-nav { display: flex !important; flex-wrap: wrap !important; gap: 3px !important; margin-bottom: 15px !important; } .model-inner-tabs .tab-nav button { font-size: 11px !important; padding: 5px 8px !important; margin: 1px !important; border-radius: 6px !important; background: linear-gradient(135deg, #f0f8ff 0%, #ffffff 100%) !important; border: 1px solid #d1e7ff !important; color: #4169e1 !important; font-weight: 600 !important; transition: all 0.3s ease !important; min-width: 80px !important; } .model-inner-tabs .tab-nav button:hover { background: linear-gradient(135deg, #4169e1 0%, #6495ed 100%) !important; color: white !important; transform: translateY(-1px) !important; } .model-inner-tabs .tab-nav button.selected { background: linear-gradient(135deg, #4169e1 0%, #6495ed 100%) !important; color: white !important; border: 1px solid #4169e1 !important; } /* CSS untuk video */ .video-demo-container { text-align: center; margin: 30px auto; padding: 20px; background: linear-gradient(135deg, #fff8fb 0%, #ffffff 100%); border-radius: 20px; border: 2px solid #ffe4ec; max-width: 600px; width: 100%; } .video-demo-title { font-family: 'Quicksand', sans-serif; font-weight: 700; color: #ff1493; font-size: 18px; margin-bottom: 15px; text-align: center; } .video-demo-player { border-radius: 15px; border: 2px solid #ffb6c1; width: 100%; max-width: 560px; height: auto; display: block; margin: 0 auto; } """ if __name__ == '__main__': load_hubert() categories = load_model() total_models = sum(len(m) for _, _, _, m in categories) with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="pink")) as app: # HEADER DENGAN GAMBAR gr.HTML('
Please check console logs for details.
Pitch: Adjust voice pitch
Algorithm: F0 extraction method
Retrieval: Voice similarity (0-1)
Filter: Noise reduction
Volume: Volume stability
Protect: Protect voice
Pitch: Untuk cewek ubah jadi (+12)
Pitch: Untuk Cowok ubah jadi (0)
Algorithm: RMVPE
Retrieval: 0.75
Filter: 7
Volume: 0.76
Protect: 0.33
'
'