Spaces:
Sleeping
Sleeping
| 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('<div class="header-img-container"><img src="https://huggingface.co/spaces/Library-Anime/DATE-A-LIVE/resolve/main/RIO.PNG" class="header-img"></div>') | |
| # Status card | |
| if total_models > 0: | |
| gr.HTML(f''' | |
| <div class="status-card"> | |
| <div class="status-online-box"> | |
| <span class="dot-online"></span> | |
| <b style="color: #ff69b4; font-size: 14px;">Voice Conversion System Online</b> | |
| </div> | |
| <div class="status-details-container"> | |
| <div class="status-detail-item"> | |
| <span class="status-text-main">π₯ {total_models} Spirits</span> | |
| <span class="status-text-sub">Ready for Conversion</span> | |
| </div> | |
| <div class="status-detail-item"> | |
| <span class="status-text-main">π Total Models</span> | |
| <span class="status-text-sub">Database: {total_models}</span> | |
| </div> | |
| </div> | |
| </div> | |
| ''') | |
| else: | |
| gr.HTML(f''' | |
| <div class="error-box"> | |
| <h3>β οΈ No Models Loaded</h3> | |
| <p>Please check console logs for details.</p> | |
| <p>Download from: <a href="https://huggingface.co/Library-Anime/Anime-RCV" target="_blank">https://huggingface.co/Plana-Archive/Anime-RCV</a></p> | |
| </div> | |
| ''') | |
| # 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'<div class="char-card" onclick="selectModel(\'{folder_title}\', \'{name}\')">' | |
| f'<span class="char-name-jp">{clean_title(title)}</span>' | |
| f'<span class="char-name-en">{name}</span>' | |
| f'</div>' | |
| for name, title, author, cover, version, vc_fn in models | |
| ]) | |
| gr.HTML(f'<div class="char-scroll-box">{char_html}</div>') | |
| # 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''' | |
| <div style="display:flex;flex-direction:column;align-items:center;padding:20px;background:white;border-radius:20px;border:1px solid #ffeef4;"> | |
| <img style="width:200px;height:260px;object-fit:cover;border-radius:15px;" src="file/{cover}"> | |
| <div style="font-family:'Quicksand',sans-serif;font-weight:700;font-size:18px;color:#ff1493;margin-top:15px;"> | |
| {clean_title(title)} | |
| </div> | |
| <div style="font-size:11px;color:#b07d8b;margin-top:5px;"> | |
| {model_version} β’ {author} | |
| </div> | |
| </div> | |
| ''') | |
| # 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(""" | |
| <div style="font-family: 'Arial'; border: 1px solid #ffd1e0; border-radius: 8px; padding: 12px; background: #fff5f9; border-left: 4px solid #ff69b4; margin-bottom: 8px;"> | |
| <h4 style="color: #ff1493; font-size: 13px; margin: 0 0 5px 0;">π Notes & Guide</h4> | |
| <p style="color: #d81b60; font-size: 11px; margin: 0 0 3px 0;"><b>Pitch:</b> Adjust voice pitch</p> | |
| <p style="color: #d81b60; font-size: 11px; margin: 0 0 3px 0;"><b>Algorithm:</b> F0 extraction method</p> | |
| <p style="color: #d81b60; font-size: 11px; margin: 0 0 3px 0;"><b>Retrieval:</b> Voice similarity (0-1)</p> | |
| <p style="color: #d81b60; font-size: 11px; margin: 0 0 3px 0;"><b>Filter:</b> Noise reduction</p> | |
| <p style="color: #d81b60; font-size: 11px; margin: 0 0 3px 0;"><b>Volume:</b> Volume stability</p> | |
| <p style="color: #d81b60; font-size: 11px; margin: 0;"><b>Protect:</b> Protect voice</p> | |
| </div> | |
| """) | |
| with gr.Column(): | |
| gr.HTML(""" | |
| <div style="font-family: 'Arial'; border: 1px solid #ffd6e7; border-radius: 8px; padding: 12px; background: #fff0f7; border-left: 4px solid #ff69b4;"> | |
| <h4 style="color: #ff1493; font-size: 13px; margin: 0 0 5px 0;">π RECOMMENDED</h4> | |
| <p style="color: #d81b60; font-size: 11px; margin: 0 0 3px 0;"><b>Pitch:</b> <span style="color: #ff1493; font-weight: bold;">+12</span></p> | |
| <p style="color: #d81b60; font-size: 11px; margin: 0 0 3px 0;"><b>Algorithm:</b> <span style="color: #ff1493; font-weight: bold;">RMVPE</span></p> | |
| <p style="color: #d81b60; font-size: 11px; margin: 0 0 3px 0;"><b>Retrieval:</b> <span style="color: #ff1493; font-weight: bold;">0.75</span></p> | |
| <p style="color: #d81b60; font-size: 11px; margin: 0 0 3px 0;"><b>Filter:</b> <span style="color: #ff1493; font-weight: bold;">7</span></p> | |
| <p style="color: #d81b60; font-size: 11px; margin: 0 0 3px 0;"><b>Volume:</b> <span style="color: #ff1493; font-weight: bold;">0.76</span></p> | |
| <p style="color: #d81b60; font-size: 11px; margin: 0;"><b>Protect:</b> <span style="color: #ff1493; font-weight: bold;">0.33</span></p> | |
| </div> | |
| """) | |
| # Speed section | |
| with gr.Column(elem_classes="speed-section"): | |
| gr.HTML('<div class="speed-title">β‘ VOICE SPEED CONTROL β‘</div>') | |
| speed_slider = gr.Slider( | |
| 0.5, 2.0, | |
| value=1.0, | |
| step=0.1, | |
| label="Speed" | |
| ) | |
| gr.HTML(""" | |
| <div class="speed-notes-box"> | |
| <div class="speed-notes-title">βοΈ Speed Voice βοΈ</div> | |
| <div class="speed-notes-content"> | |
| β’ <b>Left (0.5):</b> Slow down voice<br> | |
| β’ <b>Center (1.0):</b> Normal speed<br> | |
| β’ <b>Right (2.0):</b> Speed up voice<br> | |
| </div> | |
| </div> | |
| """) | |
| # Loading indicator | |
| gr.HTML( | |
| '<div class="arona-loading-container">' | |
| '<div class="loading-text-pink">Ready to Generate!</div>' | |
| '<img class="loading-gif-small" src="https://huggingface.co/spaces/Library-Anime/DATE-A-LIVE/resolve/main/kurumi-tokisaki.gif">' | |
| '</div>' | |
| ) | |
| # 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( | |
| '<div class="footer-text">' | |
| '<div>π DESIGNED BY MUTSUMI-CHAN π</div>' | |
| '<div style="font-weight:700; color:#b07d8b;">Date A Live - RCV v1.0 β’ Pink Edition</div>' | |
| '</div>' | |
| ) | |
| # 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 | |
| ) |