""" app.py - Blue Archive RVC dengan tampilan ala app(2).py (biru, rapi) =============================================================== Fitur: - Download otomatis model Blue Archive dari Hugging Face (jika belum ada) - Load semua karakter (42 karakter) dengan model RVC - Tampilan: header biru "Library Anime", status LOADED, tabs - Pilih karakter dari scroll box, masukkan teks, atur speed & pitch - Generate voice menggunakan TTS (Edge TTS) + konversi suara - Output audio, GIF Kurumi, expandable character info """ 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 import random 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 # ============================= # 1. ENV & TOKEN # ============================= 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") # ============================= # 2. DOWNLOAD MODEL (BLUE ARCHIVE) # ============================= def download_required_weights(): print("=" * 50) print("π BLUE ARCHIVE VOICE CONVERSION") print("=" * 50) target_dir = "weights" blue_archive_dir = os.path.join(target_dir, "Blue-Archive") if os.path.exists(blue_archive_dir): model_files = [] for root, dirs, files in os.walk(blue_archive_dir): for f in files: if f.endswith(".pth"): model_files.append(os.path.join(root, f)) if len(model_files) >= 1: print(f"β Models already exist: {len(model_files)} .pth files") return True try: from huggingface_hub import snapshot_download repo_id = "Plana-Archive/Premium-Model" print(f"π₯ Downloading from: {repo_id}") snapshot_download( repo_id=repo_id, allow_patterns=["Blue Archive - RCV/weights/**"], local_dir=".", local_dir_use_symlinks=False, token=HF_TOKEN, max_workers=2 ) print("β Download completed") source_dir = "Blue Archive - RCV/weights" if os.path.exists(source_dir): os.makedirs(target_dir, exist_ok=True) 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 default folder_info_path = os.path.join(target_dir, "folder_info.json") if not os.path.exists(folder_info_path): folder_info = { "Blue-Archive": { "title": "Blue Archive - RCV Collection", "folder_path": "Blue-Archive", "description": "Official RVC Weights by Plana-Archive", "enable": True } } with open(folder_info_path, "w") as f: json.dump(folder_info, f, indent=2) create_model_info_from_files(target_dir) return True except Exception as e: print(f"β οΈ Download failed: {e}") return False def create_model_info_from_files(base_path): blue_archive_dir = os.path.join(base_path, "Blue-Archive") if not os.path.exists(blue_archive_dir): return model_info = {} for char_folder in os.listdir(blue_archive_dir): char_path = os.path.join(blue_archive_dir, char_folder) if not os.path.isdir(char_path): continue pth_files = [f for f in os.listdir(char_path) if f.endswith('.pth')] idx_files = [f for f in os.listdir(char_path) if f.endswith('.index')] img_files = [f for f in os.listdir(char_path) if f.lower().endswith(('.png','.jpg','.jpeg'))] if not pth_files: continue char_name_fmt = re.sub(r"([a-z])([A-Z])", r"\1 \2", char_folder) model_info[char_folder] = { "enable": True, "model_path": pth_files[0], "title": f"Blue Archive - {char_name_fmt}", "cover": img_files[0] if img_files else "cover.png", "feature_retrieval_library": idx_files[0] if idx_files else "", "author": "Plana-Archive" } with open(os.path.join(blue_archive_dir, "model_info.json"), "w") as f: json.dump(model_info, f, indent=2) print(f"β Created model_info.json with {len(model_info)} characters") download_required_weights() # ============================= # 3. KONFIGURASI & GLOBAL # ============================= config = Config() logging.getLogger("numba").setLevel(logging.WARNING) logging.getLogger("fairseq").setLevel(logging.WARNING) model_cache = {} hubert_loaded = False hubert_model = None vc_fn_map = {} # key -> async generator function 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) return re.sub(r'\s*-\s*\d+\s*epochs', '', title, flags=re.IGNORECASE) # ============================= # 4. AUDIO LOAD & PREPROCESS # ============================= def _load_audio_input(vc_audio_mode, vc_input, vc_upload, tts_text): 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!") sr, 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 sr != 16000: audio = librosa.resample(audio, orig_sr=sr, target_sr=16000) return audio.astype(np.float32), 16000, None elif vc_audio_mode == "TTS Audio": if not tts_text or not tts_text.strip(): raise ValueError("Please enter text for TTS!") temp_file = f"tts_temp_{int(time.time())}.wav" async def tts_task(): await edge_tts.Communicate(tts_text, "ja-JP-NanamiNeural").save(temp_file) asyncio.run(asyncio.wait_for(tts_task(), timeout=15)) 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): if speed == 1.0: return audio return librosa.effects.time_stretch(audio.astype(np.float32), rate=speed) def preprocess_audio(audio): if np.max(np.abs(audio)) > 1.0: audio = audio / np.max(np.abs(audio)) * 0.9 return audio.astype(np.float32) # ============================= # 5. VC FUNCTION (ASYNC GENERATOR) # ============================= def create_vc_fn(model_key, tgt_sr, net_g, vc, if_f0, version, file_index): async 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 # ============================= # 6. LOAD MODEL RVC # ============================= def load_model(): print("\n" + "=" * 50) print("π΅ LOADING VOICE MODELS") print("=" * 50) categories = [] base_path = "weights" if not os.path.exists(base_path): return categories folder_info_path = f"{base_path}/folder_info.json" if not os.path.exists(folder_info_path): folder_info = { "Blue-Archive": { "title": "Blue Archive - RCV Collection", "folder_path": "Blue-Archive", "description": "Official RVC Weights", "enable": True } } with open(folder_info_path, "w") as f: json.dump(folder_info, f, indent=2) with open(folder_info_path, "r") as f: folder_info = json.load(f) for cat_name, cat_info in folder_info.items(): if not cat_info.get('enable', True): continue cat_title = cat_info['title'] cat_folder = cat_info['folder_path'] models = [] model_info_path = f"{base_path}/{cat_folder}/model_info.json" if not os.path.exists(model_info_path): create_model_info_from_files(base_path) with open(model_info_path, "r") as f: models_info = json.load(f) for char_name, info in models_info.items(): if not info.get('enable', True): continue cache_key = f"{cat_folder}_{char_name}" char_dir = f"{base_path}/{cat_folder}/{char_name}" model_path = f"{char_dir}/{info['model_path']}" cover_path = f"{char_dir}/{info['cover']}" index_path = f"{char_dir}/{info['feature_retrieval_library']}" # Fallback jika file tidak persis if os.path.exists(char_dir): actual = os.listdir(char_dir) if not os.path.exists(model_path): pths = [f for f in actual if f.endswith('.pth')] if pths: model_path = f"{char_dir}/{pths[0]}" if not os.path.exists(cover_path): imgs = [f for f in actual if f.lower().endswith(('.png','.jpg','.jpeg'))] if imgs: cover_path = f"{char_dir}/{imgs[0]}" if not os.path.exists(index_path): idxs = [f for f in actual if f.endswith('.index')] if idxs: index_path = f"{char_dir}/{idxs[0]}" if not os.path.exists(model_path): print(f"β Skipping {char_name} - model not found") continue if cache_key in model_cache: tgt_sr, net_g, vc, if_f0, version, idx_path = model_cache[cache_key] else: try: print(f"β³ Loading {char_name}...") 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) except Exception as e: print(f"β Error {char_name}: {e}") continue vc_func = create_vc_fn(cache_key, tgt_sr, net_g, vc, if_f0, version, index_path) vc_fn_map[cache_key] = vc_func models.append(( char_name, info['title'], info['author'], cover_path, version, cache_key )) if models: categories.append([cat_title, cat_folder, models]) total = sum(len(m) for _, _, m in categories) print(f"π― Total models loaded: {total}") return categories def load_hubert(): global hubert_model, hubert_loaded if hubert_loaded: return print("π§ Loading HuBERT...") 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 ready") # ============================= # 7. UI - GAYA BIRU APP(2).PY # ============================= css = """ :root { --primary-600: #1299ff !important; --accent-600: #1299ff !important; } .gradio-container, .gradio-container * { --loader-color: #A2D2FF !important; } .ba-header-container { border: 1.5px solid #e1e8f0; border-radius: 12px; padding: 20px 10px; margin-bottom: 12px; background: white; text-align: center; } .ba-header-container h1 { color: #1299ff !important; font-weight: 700 !important; font-size: 42px !important; margin: 0; } .status-container { border: 1.5px solid #e1e8f0; border-radius: 12px; padding: 15px 22px; margin-bottom: 20px; background: white; } .status-title { color: #1299ff !important; font-weight: 800; font-size: 16px; margin-bottom: 8px; } .text-green-bold { color: #28a745 !important; font-weight: 900 !important; } .text-blue-status { color: #1299ff !important; } .slim-card { max-width: 480px; margin: 0 auto; background: transparent; padding: 10px; } .tabs > .tab-nav { display: flex !important; overflow-x: auto !important; white-space: nowrap !important; } .scroll-box { height: 280px; overflow-y: auto; border: 1px solid #f0f4f8; border-radius: 12px; padding: 10px; background: #fafbfc; margin-bottom: 10px; } .char-btn { background: white !important; border: 1px solid #e2e8f0 !important; border-left: 5px solid #1299ff !important; text-align: left !important; padding: 8px !important; font-size: 12px !important; margin-bottom: 4px !important; width: 100%; color: #4a5568 !important; } .warning-card { background: #fff9f0; border: 2px dashed #f5a623; border-radius: 10px; padding: 12px; margin-bottom: 15px; text-align: center; } .jp-btn { background: #f8fafc !important; border: 1px solid #cbd5e1 !important; color: #475569 !important; font-weight: 700 !important; border-radius: 10px !important; margin-bottom: 10px; font-size: 12px !important; width: 100%; } .gen-btn { background: #1299ff !important; color: white !important; font-weight: 700 !important; border-radius: 12px !important; height: 45px !important; width: 100%; border: none !important; cursor: pointer; } .info-header-custom { background: #1299ff !important; color: white !important; border: none !important; border-radius: 8px 8px 0 0 !important; padding: 12px 15px !important; width: 100% !important; cursor: pointer; font-weight: 800 !important; margin-top: 5px; } .credit-footer { margin-top: 25px; padding: 15px; background: white; border-radius: 12px; text-align: center; border-bottom: 4px solid #1299ff; color: #94a3b8; font-weight: 700; font-size: 12px; letter-spacing: 2px; } .gif-container { display: flex; justify-content: center; margin-top: 15px; } .gif-container img { border-radius: 12px; max-width: 100%; height: auto; border: 1px solid #e1e8f0; } .accordion-custom { margin-top: 12px; } input[type="range"] { accent-color: #1299ff !important; } """ # Helper random teks (opsional) def get_random_jp(): return random.choice(["γγγ«γ‘γ―οΌ", "γε ζ°γ§γγοΌ", "ε ηγγη²γζ§γ§γοΌ", "ε€§ε₯½γγ γοΌ", "γΎγζζ₯γγ", "γγ£γγΌοΌ", "γγγγ§γγοΌ"]) def char_info_html(char_name, title, author, version): return f"""
π Blue Archive - Real-time Voice Conversion π