import os import json import traceback import logging import gradio as gr import numpy as np import librosa import torch import asyncio import edge_tts import re import shutil import time from datetime import datetime 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 # ============================= # LOAD ENVIRONMENT VARIABLES # ============================= from dotenv import load_dotenv load_dotenv() HF_TOKEN = os.getenv("HF_TOKEN") if HF_TOKEN: print("šŸ”‘ Hugging Face token detected") os.environ["HUGGINGFACE_TOKEN"] = HF_TOKEN else: print("āš ļø No HF_TOKEN found") # ============================= # AUTO-DOWNLOAD DARI HUGGING FACE - UNTUK DATE-A-LIVE # ============================= def download_required_weights(): """Fungsi untuk download model Date-A-Live dari Hugging Face""" print("=" * 50) print("šŸš€ DATE A LIVE VOICE CONVERSION v2.0") print("=" * 50) target_dir = "weights" # Cek jika model sudah ada date_a_live_dir = os.path.join(target_dir, "Date-A-Live") if os.path.exists(date_a_live_dir): print(f"šŸ“ Checking existing models in: {date_a_live_dir}") model_files = [] for root, dirs, files in os.walk(date_a_live_dir): for file in files: if file.endswith(".pth"): model_files.append(os.path.join(root, file)) if len(model_files) >= 15: # Sesuai jumlah model di model_info.json print(f"āœ… Models already exist: {len(model_files)} .pth files found") return True else: print(f"āš ļø Incomplete models: {len(model_files)}/15 .pth files found") try: from huggingface_hub import snapshot_download repo_id = "Plana-Archive/Anime-RCV" print(f"šŸ“„ Downloading from: {repo_id}") print("šŸ“ Looking for: Date-A-Live-RCV/weights") # Download dengan pattern yang spesifik untuk Date-A-Live downloaded_path = snapshot_download( repo_id=repo_id, allow_patterns=[ "Date-A-Live-RCV/weights/**", ], local_dir=".", local_dir_use_symlinks=False, token=HF_TOKEN, max_workers=2 ) print("āœ… Download completed") # Pindahkan file source_dir = "Date-A-Live-RCV/weights" if os.path.exists(source_dir): os.makedirs(target_dir, exist_ok=True) # Pindahkan semua konten for item in os.listdir(source_dir): s = os.path.join(source_dir, item) d = os.path.join(target_dir, item) if os.path.isdir(s): if os.path.exists(d): shutil.rmtree(d) shutil.move(s, d) else: shutil.move(s, d) print(f"šŸ“‚ Moved models to: {target_dir}") # Buat folder_info.json jika tidak ada folder_info_path = os.path.join(target_dir, "folder_info.json") if not os.path.exists(folder_info_path): folder_info = { "DateALive": { "title": "Date A Live - RCV Collection", "folder_path": "Date-A-Live", "description": "Official RVC Weights for Date A Live characters by Plana-Archive", "enable": True } } with open(folder_info_path, "w", encoding="utf-8") as f: json.dump(folder_info, f, indent=2, ensure_ascii=False) print(f"šŸ“„ Created folder_info.json") # Buat model_info.json yang sesuai dengan file yang sebenarnya create_model_info_from_files(target_dir) return True else: print("āŒ Source directory not found after download!") return False except Exception as e: print(f"āš ļø Download failed: {str(e)}") traceback.print_exc() print("\nšŸ“ Manual setup:") print("1. Create folder: weights/") print("2. Download from: https://huggingface.co/Library-Anime/Anime-RCV/tree/main/Date-A-Live-RCV/weights") print("3. Put Date-A-Live folder in weights/") return False def create_model_info_from_files(base_path): """Buat model_info.json berdasarkan file yang sebenarnya ada""" date_a_live_dir = os.path.join(base_path, "Date-A-Live") if not os.path.exists(date_a_live_dir): return model_info_path = os.path.join(date_a_live_dir, "model_info.json") # Mapping karakter dengan nama file yang benar character_mapping = { "Kaguya": { "title": "Date A Live - Kaguya Yamai", "cover": "cover.png" }, "Kotori": { "title": "Date A Live - Kotori Itsuka", "cover": "cover.png" }, "Kurumi": { "title": "Date A Live - Kurumi Tokisaki", "cover": "cover.png" }, "Maria": { "title": "Date A Live - Maria Arusu", "cover": "cover.png" }, "Maria_v2": { "title": "Date A Live - Maria Arusu v2", "cover": "cover.png" }, "Marina": { "title": "Date A Live - Marina Arusu", "cover": "cover.png" }, "Marina_v2": { "title": "Date A Live - Marina Arusu v2", "cover": "cover.png" }, "Miku": { "title": "Date A Live - Miku Izayoi", "cover": "cover.png" }, "Origami": { "title": "Date A Live - Origami Tobiichi", "cover": "cover.png" }, "Rinne": { "title": "Date A Live - Rinne Sonogami", "cover": "cover.png" }, "Rinne_v2": { "title": "Date A Live - Rinne Sonogami v2", "cover": "cover.png" }, "Rio": { "title": "Date A Live - Rio Sonogami", "cover": "cover.png" }, "Rio_v2": { "title": "Date A Live - Rio Sonogami v2", "cover": "cover.png" }, "Tohka": { "title": "Date A Live - Tohka Yatogami", "cover": "cover.png" }, "Yoshino": { "title": "Date A Live - Yoshino Himesaki", "cover": "cover.png" }, "Yuzuru": { "title": "Date A Live - Yuzuru Yamai", "cover": "cover.png" } } # Cari semua file yang ada all_files = [] for root, dirs, files in os.walk(date_a_live_dir): for file in files: if file.endswith(('.pth', '.index', '.png', '.jpg', '.jpeg')): all_files.append(os.path.join(root, file)) # Kelompokkan file berdasarkan karakter character_files = {} for char_name in character_mapping.keys(): char_files = [] for file_path in all_files: file_name = os.path.basename(file_path) # Cari file yang mengandung nama karakter if char_name.lower() in file_name.lower(): char_files.append(file_path) if char_files: character_files[char_name] = char_files # Buat model_info.json model_info = {} for char_name, files in character_files.items(): # Cari file .pth pth_files = [f for f in files if f.endswith('.pth')] index_files = [f for f in files if f.endswith('.index')] image_files = [f for f in files if f.endswith(('.png', '.jpg', '.jpeg'))] if pth_files: model_info[char_name] = { "enable": True, "model_path": os.path.basename(pth_files[0]), "title": character_mapping[char_name]["title"], "cover": os.path.basename(image_files[0]) if image_files else "cover.png", "feature_retrieval_library": os.path.basename(index_files[0]) if index_files else "", "author": "Plana-Archive" } with open(model_info_path, "w", encoding="utf-8") as f: json.dump(model_info, f, indent=2, ensure_ascii=False) print(f"āœ… Created model_info.json with {len(model_info)} characters") return model_info # Jalankan download download_required_weights() # Inisialisasi konfigurasi config = Config() logging.getLogger("numba").setLevel(logging.WARNING) logging.getLogger("fairseq").setLevel(logging.WARNING) # Cache untuk model model_cache = {} hubert_loaded = False hubert_model = None # Mode audio spaces = True if spaces: audio_mode = ["Upload audio", "TTS Audio"] else: audio_mode = ["Input path", "Upload audio", "TTS Audio"] # Metode F0 extraction f0method_mode = ["pm", "harvest"] if os.path.isfile("rmvpe.pt"): f0method_mode.insert(2, "rmvpe") def clean_title(title): """Membersihkan judul model""" title = re.sub(r'^Blue Archive\s*-\s*', '', title, flags=re.IGNORECASE) title = re.sub(r'^Bocchi the Rock!\s*-\s*', '', title, flags=re.IGNORECASE) title = re.sub(r'^Date A Live\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, spaces_limit=20): """Memuat audio dari berbagai sumber""" temp_file = None try: 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("Please upload an audio file!") 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, res_type='kaiser_fast') return audio.astype(np.float32), 16000, None elif vc_audio_mode == "TTS Audio": if not tts_text or tts_text.strip() == "": raise ValueError("Please enter text for TTS!") temp_file = f"tts_temp_{int(time.time())}.wav" async def tts_task(): return await edge_tts.Communicate(tts_text, "ja-JP-NanamiNeural").save(temp_file) try: asyncio.run(asyncio.wait_for(tts_task(), timeout=15)) except asyncio.TimeoutError: raise ValueError("TTS timeout!") audio, sr = librosa.load(temp_file, sr=16000, mono=True) return audio.astype(np.float32), 16000, temp_file except Exception as e: if temp_file and os.path.exists(temp_file): os.remove(temp_file) raise e raise ValueError("Invalid audio mode") def adjust_audio_speed(audio, speed): """Menyesuaikan kecepatan audio""" if speed == 1.0: return audio return librosa.effects.time_stretch(audio.astype(np.float32), rate=speed) def preprocess_audio(audio): """Preprocessing audio""" if np.max(np.abs(audio)) > 1.0: audio = audio / np.max(np.abs(audio)) * 0.9 return audio.astype(np.float32) def create_vc_fn(model_key, tgt_sr, net_g, vc, if_f0, version, file_index): """Membuat fungsi konversi voice""" def vc_fn( vc_audio_mode, vc_input, vc_upload, tts_text, f0_up_key, f0_method, index_rate, filter_radius, resample_sr, rms_mix_rate, protect, speed, ): temp_audio_file = None try: if torch.cuda.is_available(): torch.cuda.empty_cache() net_g.to(config.device) yield "Status: šŸš€ Processing audio...", None audio, sr, temp_audio_file = _load_audio_input(vc_audio_mode, vc_input, vc_upload, tts_text) audio = preprocess_audio(audio) audio_tensor = torch.FloatTensor(audio).to(config.device) times = [0, 0, 0] max_chunk_size = 16000 * 30 if len(audio) > max_chunk_size: chunks = [] for i in range(0, len(audio), max_chunk_size): chunk = audio[i:i + max_chunk_size] chunk_tensor = torch.FloatTensor(chunk).to(config.device) chunk_opt = vc.pipeline( hubert_model, net_g, 0, chunk_tensor, "chunk" if vc_input else "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, ) chunks.append(chunk_opt) audio_opt = np.concatenate(chunks) else: audio_opt = vc.pipeline( hubert_model, net_g, 0, audio_tensor, vc_input if vc_input else "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, ) audio_opt = audio_opt.astype(np.float32) if speed != 1.0: audio_opt = adjust_audio_speed(audio_opt, speed) if np.max(np.abs(audio_opt)) > 0: audio_opt = (audio_opt / np.max(np.abs(audio_opt)) * 0.9).astype(np.float32) yield "Status: āœ… Conversion completed!", (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) if torch.cuda.is_available(): torch.cuda.empty_cache() if model_key not in model_cache: net_g.to('cpu') return vc_fn def load_model(): """Memuat semua model""" print("\n" + "=" * 50) print("šŸŽµ LOADING VOICE MODELS") print("=" * 50) categories = [] base_path = "weights" if not os.path.exists(base_path): print(f"āŒ Folder '{base_path}' not found!") return categories # Baca folder_info.json atau buat default folder_info_path = f"{base_path}/folder_info.json" if not os.path.isfile(folder_info_path): print(f"šŸ“„ Creating default folder_info.json...") folder_info = { "DateALive": { "title": "Date A Live - RCV Collection", "folder_path": "Date-A-Live", "description": "Official RVC Weights for Date A Live characters by Plana-Archive", "enable": True } } with open(folder_info_path, "w", encoding="utf-8") as f: json.dump(folder_info, f, indent=2, ensure_ascii=False) with open(folder_info_path, "r", encoding="utf-8") as f: folder_info = json.load(f) print(f"šŸ“ Found {len(folder_info)} category(ies) in folder_info.json") for category_name, category_info in folder_info.items(): if not category_info.get('enable', True): continue category_title = category_info['title'] category_folder = category_info['folder_path'] description = category_info['description'] models = [] model_info_path = f"{base_path}/{category_folder}/model_info.json" print(f"\nšŸ“‚ Loading category: {category_title}") print(f" Path: {model_info_path}") # Jika model_info.json tidak ada, buat dari file yang ada if not os.path.exists(model_info_path): print(f" āš ļø model_info.json not found, creating from files...") model_info = create_model_info_from_files(base_path) if not model_info: continue with open(model_info_path, "r", encoding="utf-8") as f: models_info = json.load(f) print(f" Found {len(models_info)} character(s) in model_info.json") for character_name, info in models_info.items(): if not info.get('enable', True): continue model_title = info['title'] model_name = info['model_path'] model_author = info.get("author", "Plana-Archive") cache_key = f"{category_folder}_{character_name}" # Path ke folder karakter char_dir = f"{base_path}/{category_folder}/{character_name}" model_path = f"{char_dir}/{model_name}" cover_path = f"{char_dir}/{info['cover']}" index_path = f"{char_dir}/{info['feature_retrieval_library']}" print(f"\n šŸ‘¤ Character: {character_name}") print(f" Expected model: {model_name}") print(f" Expected cover: {info['cover']}") print(f" Expected index: {info['feature_retrieval_library']}") print(f" Character dir: {char_dir}") # Cek apakah folder karakter ada if not os.path.exists(char_dir): print(f" āš ļø Character folder not found: {char_dir}") # Coba cari di root folder char_dir = f"{base_path}/{category_folder}" model_path = f"{char_dir}/{model_name}" cover_path = f"{char_dir}/{info['cover']}" index_path = f"{char_dir}/{info['feature_retrieval_library']}" print(f" Trying root folder: {char_dir}") # Cek file yang diperlukan required_files = [model_path, cover_path, index_path] missing_files = [f for f in required_files if not os.path.exists(f)] if missing_files: print(f" āš ļø Missing files:") for f in missing_files: print(f" - {os.path.basename(f)}") # Coba cari file alternatif if os.path.exists(char_dir): actual_files = os.listdir(char_dir) print(f" šŸ“ Actual files in directory:") for f in actual_files: print(f" - {f}") # Cari file .pth (cari yang mengandung nama karakter) pth_files = [f for f in actual_files if f.endswith('.pth')] if pth_files and not os.path.exists(model_path): # Cari model yang cocok dengan nama karakter matching_models = [f for f in pth_files if character_name.lower() in f.lower()] if matching_models: print(f" šŸ”„ Found alternative model: {matching_models[0]}") model_path = f"{char_dir}/{matching_models[0]}" else: # Ambil model pertama print(f" šŸ”„ Using first available model: {pth_files[0]}") model_path = f"{char_dir}/{pth_files[0]}" # Cari file index (cari yang mengandung nama karakter atau IVF pattern) index_files = [f for f in actual_files if f.endswith('.index')] if index_files and not os.path.exists(index_path): # Cari index yang cocok dengan nama karakter matching_indices = [f for f in index_files if character_name.lower() in f.lower()] if not matching_indices: # Cari berdasarkan pattern IVF for f in index_files: if 'IVF' in f: matching_indices = [f] break if matching_indices: print(f" šŸ”„ Found alternative index: {matching_indices[0]}") index_path = f"{char_dir}/{matching_indices[0]}" else: # Ambil index pertama print(f" šŸ”„ Using first available index: {index_files[0]}") index_path = f"{char_dir}/{index_files[0]}" # Cari cover image_files = [f for f in actual_files if f.lower().endswith(('.png', '.jpg', '.jpeg'))] if image_files and not os.path.exists(cover_path): # Cari cover yang mengandung nama karakter matching_images = [f for f in image_files if character_name.lower() in f.lower()] if not matching_images: # Cari file bernama cover.png cover_files = [f for f in image_files if 'cover' in f.lower()] if cover_files: matching_images = [cover_files[0]] if matching_images: print(f" šŸ”„ Found alternative cover: {matching_images[0]}") cover_path = f"{char_dir}/{matching_images[0]}" else: # Gunakan cover pertama yang ditemukan print(f" šŸ”„ Using first available cover: {image_files[0]}") cover_path = f"{char_dir}/{image_files[0]}" # Cek ulang setelah mencari alternatif required_files = [model_path, cover_path, index_path] missing_files = [f for f in required_files if not os.path.exists(f)] if missing_files: print(f" āŒ Skipping {character_name} - still missing files") continue # Gunakan cache jika tersedia if cache_key in model_cache: tgt_sr, net_g, vc, if_f0, version, model_index = model_cache[cache_key] print(f" āœ… Loaded from cache") else: try: print(f" ā³ Loading model weights...") cpt = torch.load(model_path, map_location="cpu") tgt_sr = cpt["config"][-1] cpt["config"][-3] = cpt["weight"]["emb_g.weight"].shape[0] if_f0 = cpt.get("f0", 1) version = cpt.get("version", "v1") if version == "v1": if if_f0 == 1: net_g = SynthesizerTrnMs256NSFsid(*cpt["config"], is_half=config.is_half) else: net_g = SynthesizerTrnMs256NSFsid_nono(*cpt["config"]) else: if if_f0 == 1: net_g = SynthesizerTrnMs768NSFsid(*cpt["config"], is_half=config.is_half) else: net_g = SynthesizerTrnMs768NSFsid_nono(*cpt["config"]) if hasattr(net_g, "enc_q"): del net_g.enc_q net_g.load_state_dict(cpt["weight"], strict=False) net_g.eval().to('cpu') vc = VC(tgt_sr, config) model_cache[cache_key] = (tgt_sr, net_g, vc, if_f0, version, index_path) print(f" āœ… Model loaded successfully (v{version}, SR: {tgt_sr})") except Exception as e: print(f" āŒ Error loading model: {str(e)}") traceback.print_exc() continue models.append(( character_name, model_title, model_author, cover_path, version, create_vc_fn(cache_key, tgt_sr, net_g, vc, if_f0, version, index_path) )) if models: categories.append([category_title, category_folder, description, models]) print(f"\n šŸ“Š Category '{category_title}' loaded with {len(models)} model(s)") else: print(f"\n āš ļø No models loaded for category '{category_title}'") total_models = sum(len(models) for _, _, _, models in categories) print(f"\nšŸŽÆ Total categories loaded: {len(categories)}") print(f"šŸ‘„ Total models loaded: {total_models}") print("=" * 50) return categories def load_hubert(): """Memuat model HuBERT""" global hubert_model, hubert_loaded if hubert_loaded: return print("šŸ”§ Loading HuBERT model...") torch.serialization.add_safe_globals([Dictionary]) models, _, _ = checkpoint_utils.load_model_ensemble_and_task( ["hubert_base.pt"], suffix="", ) hubert_model = models[0].to(config.device) hubert_model = hubert_model.half() if config.is_half else hubert_model.float() hubert_model.eval() hubert_loaded = True print("āœ… HuBERT model loaded successfully") 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") # CSS dengan tema PINK 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; } """ if __name__ == '__main__': # Preload HuBERT load_hubert() # Load models categories = load_model() total_models = sum(len(models) for _, _, _, models in categories) # UI dengan Gradio with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="pink")) as app: gr.HTML('
') # Status card if total_models > 0: gr.HTML(f'''
Voice Conversion System Online
šŸ‘„ {total_models} Spirits Ready for Conversion
šŸ“Š Total Models Database: {total_models}
''') else: gr.HTML(f'''

āš ļø No Models Loaded

Please check console logs for details.

Download from: https://huggingface.co/Plana-Archive/Anime-RCV

''') # Tabs untuk setiap kategori if categories: for cat_idx, (folder_title, folder, description, models) in enumerate(categories): with gr.TabItem(folder_title, elem_classes="model-tab"): with gr.Accordion("šŸ“‘ Character Information šŸ“‘", open=True): char_html = "".join([ f'
' f'{clean_title(title)}' f'{name}' f'
' for name, title, author, cover, version, vc_fn in models ]) gr.HTML(f'
{char_html}
') # Tabs untuk setiap model with gr.Tabs(): for model_idx, (name, title, author, cover, model_version, vc_fn) in enumerate(models): with gr.TabItem(name, id=f"model_{cat_idx}_{model_idx}"): with gr.Row(): # Kolom kiri: Model info with gr.Column(scale=1): gr.HTML(f'''
{clean_title(title)}
{model_version} • {author}
''') # Kolom tengah: Input dan settings with gr.Column(scale=2): # Input group with gr.Group(): vc_audio_mode = gr.Dropdown( label="Input Mode", choices=audio_mode, value="TTS Audio" ) vc_input = gr.Textbox(visible=False) vc_microphone_mode = gr.Checkbox( label="Use Microphone", value=False ) vc_upload = gr.Audio( label="Upload Audio Source", source="upload", visible=False, type="numpy" ) tts_text = gr.Textbox( label="TTS Text", visible=True, placeholder="Type your message here...", lines=4 ) # Basic settings with gr.Row(): with gr.Column(): vc_transform0 = gr.Slider( minimum=-12, maximum=12, label="Pitch", value=12, step=1 ) f0method0 = gr.Radio( label="Conversion Algorithm", choices=f0method_mode, value="rmvpe" if "rmvpe" in f0method_mode else "pm" ) with gr.Column(): with gr.Accordion("āš™ļø Advanced Settings āš™ļø", open=True, elem_classes="advanced-settings"): index_rate1 = gr.Slider( 0, 1, label="Index Rate", value=0.75 ) filter_radius0 = gr.Slider( 0, 7, label="Filter Radius", value=7, step=1 ) resample_sr0 = gr.Slider( 0, 48000, label="Resample SR", value=0 ) rms_mix_rate0 = gr.Slider( 0, 1, label="Volume Mix", value=0.76 ) protect0 = gr.Slider( 0, 0.5, label="Voice Protect", value=0.33 ) # Notes with gr.Row(): with gr.Column(): gr.HTML("""

šŸ“ Notes & Guide

Pitch: Adjust voice pitch

Algorithm: F0 extraction method

Retrieval: Voice similarity (0-1)

Filter: Noise reduction

Volume: Volume stability

Protect: Protect voice

""") with gr.Column(): gr.HTML("""

šŸ“‘ RECOMMENDED

Pitch: +12

Algorithm: RMVPE

Retrieval: 0.75

Filter: 7

Volume: 0.76

Protect: 0.33

""") # Speed section with gr.Column(elem_classes="speed-section"): gr.HTML('
⚔ VOICE SPEED CONTROL ⚔
') speed_slider = gr.Slider( 0.5, 2.0, value=1.0, step=0.1, label="Speed" ) gr.HTML("""
āšœļø Speed Voice āšœļø
• Left (0.5): Slow down voice
• Center (1.0): Normal speed
• Right (2.0): Speed up voice
""") # Loading indicator gr.HTML( '
' '
Ready to Generate!
' '' '
' ) # Kolom kanan: Output with gr.Column(scale=1): vc_log = gr.Textbox( label="Process Logs", interactive=False, lines=4 ) vc_output = gr.Audio( label="Result Audio", interactive=False, type="numpy" ) vc_convert = gr.Button( "🩷 GENERATE VOICE 🩷", variant="primary", elem_classes="generate-btn", size="lg" ) # Connect button click vc_convert.click( fn=vc_fn, inputs=[ vc_audio_mode, vc_input, vc_upload, tts_text, vc_transform0, f0method0, index_rate1, filter_radius0, resample_sr0, rms_mix_rate0, protect0, speed_slider ], outputs=[vc_log, vc_output] ) # Connect audio mode change vc_audio_mode.change( fn=change_audio_mode, inputs=[vc_audio_mode], outputs=[vc_input, vc_microphone_mode, vc_upload, tts_text] ) # Connect microphone toggle vc_microphone_mode.change( fn=use_microphone, inputs=vc_microphone_mode, outputs=vc_upload ) # Footer gr.HTML( '' ) # JavaScript untuk model selection app.load( None, None, None, js=""" () => { window.selectModel = (cat, mod) => { const tabs = document.querySelectorAll('.tabs .tab-nav button'); for (let t of tabs) { if (t.textContent.trim() === cat) { t.click(); setTimeout(() => { const mTabs = document.querySelectorAll('.tabs .tab-nav button'); for (let mt of mTabs) { if (mt.textContent.trim() === mod) { mt.click(); window.scrollTo({top: 0, behavior: 'smooth'}); } } }, 100); break; } } } } """ ) # Launch app print("\n" + "=" * 50) print("🌐 STARTING WEB INTERFACE") print("=" * 50) app.queue(max_size=3).launch( share=False, server_name="0.0.0.0" if os.getenv('SPACE_ID') else "127.0.0.1", server_port=7860, quiet=False, show_error=True )