| 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) |
|
|
| |
| def download_required_weights(): |
| repo_id = "Plana-Archive/Anime-RCV" |
| |
| |
| 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 |
| |
| |
| 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) |
| |
| |
| 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 = [] |
| |
| |
| 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) |
| |
| |
| 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" |
| } |
| |
| |
| 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", "") |
| |
| |
| display_title = category_short_names.get(cat_key, category_title) |
| |
| |
| base_path = os.path.dirname(folder_path) |
| target_folder = os.path.basename(folder_path) |
| |
| models = [] |
| |
| |
| 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 |
| |
| |
| 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): |
| |
| pth_files = glob.glob(os.path.join(char_dir, "*.pth")) |
| if pth_files: |
| |
| 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() |
|
|
| |
| |
| |
| 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 = """ |
| @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: |
| |
| gr.HTML('<div class="header-img-container"><img src="https://huggingface.co/spaces/Library-Anime/Waifu-Anime-RCV/resolve/main/BanG Dream.PNG" class="header-img"></div>') |
| |
| |
| 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;">Waifu Anime RCV System Online</b> |
| </div> |
| <div class="status-details-container"> |
| <div class="status-detail-item"> |
| <span class="status-text-main">π₯ {total_models} Waifus</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 style="margin-top: 10px; padding: 8px; background: #f0f8ff; border-radius: 8px; border: 1px dashed #4169e1; width: 100%;"> |
| <span style="font-size: 10px; color: #4169e1;">π₯ Model download completed! Ready to generate voices.</span> |
| </div> |
| </div> |
| ''') |
| else: |
| gr.HTML(f''' |
| <div class="error-box"> |
| <h3>β οΈ No Models Loaded</h3> |
| <p>Please check console logs for details.</p> |
| </div> |
| ''') |
| |
| |
| 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>') |
| |
| |
| with gr.Tabs(elem_classes="model-inner-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(): |
| |
| with gr.Column(scale=1): |
| if cover and os.path.exists(cover): |
| 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> |
| ''') |
| else: |
| gr.HTML(f''' |
| <div style="display:flex;flex-direction:column;align-items:center;padding:20px;background:white;border-radius:20px;border:1px solid #ffeef4;"> |
| <div style="width:200px;height:260px;background:#f0f0f0;border-radius:15px;display:flex;align-items:center;justify-content:center;"> |
| <span style="color:#b07d8b;">No Image</span> |
| </div> |
| <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> |
| ''') |
| |
| |
| with gr.Column(scale=2): |
| |
| 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 |
| ) |
| |
| |
| with gr.Row(): |
| with gr.Column(): |
| vc_pitch = 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 |
| ) |
| |
| |
| 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;">Untuk cewek ubah jadi (+12)</span></p> |
| <p style="color: #d81b60; font-size: 11px; margin: 0 0 3px 0;"><b>Pitch:</b> <span style="color: #ff1493; font-weight: bold;">Untuk Cowok ubah jadi (0)</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> |
| """) |
| |
| |
| 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> |
| """) |
| |
| |
| 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/Waifu-Anime-RCV/resolve/main/soyo-nagasaki.gif">' |
| '</div>' |
| ) |
| |
| |
| 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" |
| ) |
| |
| |
| gr.HTML(f''' |
| <div class="peringatan-box"> |
| <div class="peringatan-title">π PERINGATAN MINNA π</div> |
| <div class="peringatan-content">Setelah di Generate Voice, audionya akan muncul beberapa detik dan tunggu aja ya!</div> |
| <div class="Jika waktu di play-audio jadi stuk, itu bukan Error tapi sedang memuat dan tunggu aja beberapa detik!</div> |
| </div> |
| ''') |
| |
| |
| vc_convert.click( |
| fn=vc_fn, |
| inputs=[ |
| vc_audio_mode, vc_input, vc_upload, tts_text, |
| vc_microphone_mode, vc_upload, vc_pitch, f0method0, |
| index_rate1, filter_radius0, resample_sr0, rms_mix_rate0, |
| protect0, speed_slider |
| ], |
| outputs=[vc_log, vc_output] |
| ) |
| |
| |
| vc_audio_mode.change( |
| fn=change_audio_mode, |
| inputs=[vc_audio_mode], |
| outputs=[vc_input, vc_microphone_mode, vc_upload, tts_text] |
| ) |
| |
| |
| vc_microphone_mode.change( |
| fn=use_microphone, |
| inputs=vc_microphone_mode, |
| outputs=vc_upload |
| ) |
| |
| |
| with gr.Row(): |
| with gr.Column(scale=1): |
| pass |
| with gr.Column(scale=3): |
| gr.HTML(""" |
| <div class="video-demo-container"> |
| <div class="video-demo-title">β
(ON) MODE YURI - SAKI π</div> |
| <video class="video-demo-player" controls autoplay loop muted playsinline> |
| <source src="https://huggingface.co/spaces/BanG-Dream-MyGO/README/resolve/main/Sakiko%20Chan.mp4" type="video/mp4"> |
| Your browser does not support the video tag. |
| </video> |
| </div> |
| """) |
| with gr.Column(scale=1): |
| pass |
| |
| |
| gr.HTML( |
| '<div class="footer-text">' |
| '<div>π· DESIGNED BY MUTSUMI-CHAN π·</div>' |
| '<div style="font-weight:700; color:#b07d8b;">Waifu Anime RCV β’ Premium TTS Service β’ 2026</div>' |
| '</div>' |
| ) |
| |
| |
| 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; |
| } |
| } |
| } |
| } |
| """ |
| ) |
| |
| app.queue(max_size=10).launch(server_name="0.0.0.0", server_port=7860) |