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