Plana-Archive's picture
Migrated content from repo: Plana-Archive/Anime-Spaces
d4b1959 verified
Raw
History Blame Contribute Delete
27.9 kB
"""
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"""
<div class="info-content-area" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 15px; background: white; border: 1px solid #f0f4f8; border-top: none; border-radius: 0 0 10px 10px; max-height: 300px; overflow-y: auto;">
<div style="border: 1px solid #f0f4f8; border-radius: 12px; padding: 10px; border-left: 5px solid #1299ff; background: #fff;">
<div style="font-weight: 800; color: #2d3748; font-size: 14px; margin-bottom: 2px;">{char_name}</div>
<div style="color: #a0aec0; font-size: 11px;">{title}</div>
<div style="color: #a0aec0; font-size: 10px;">{author} β€’ {version}</div>
</div>
</div>
"""
if __name__ == '__main__':
load_hubert()
categories = load_model()
total_models = sum(len(m) for _, _, m in categories)
with gr.Blocks(css=css) as demo:
with gr.Column(elem_classes="slim-card"):
# Header ala app(2).py
gr.HTML("""
<div class="ba-header-container">
<h1>Library Anime</h1>
<p>πŸ‚ Blue Archive - Real-time Voice Conversion πŸ‚</p>
</div>
<div class="status-container">
<div class="status-title">System Status</div>
<div class="status-item"><span style="color:#4a5568">Model :</span> <span class="text-green-bold">&nbsp;LOADED βœ…</span></div>
<div class="status-item"><span style="color:#4a5568">Total Characters :</span> <span class="text-blue-status">&nbsp;{total_models}</span></div>
</div>
""")
# Tabs (hanya satu kategori, tapi biarkan dinamis)
with gr.Tabs(elem_classes="tabs"):
for cat_title, cat_folder, models in categories:
with gr.Tab(f"{cat_title}"):
selected_state = gr.State(None) # (char_name, cover_path, cache_key)
cover_img = gr.Image(label="Character Cover", interactive=False, height=200, visible=True)
selected_name_display = gr.Markdown("πŸ“ *Silakan pilih karakter...*")
# Scroll box karakter
gr.Markdown("### πŸ“Œ Select Character")
with gr.Column(elem_classes="scroll-box"):
for (char_name, title, author, cover_path, version, cache_key) in models:
btn = gr.Button(f"πŸ‘€ {char_name}", elem_classes="char-btn")
btn.click(
fn=lambda n=char_name, c=cover_path, k=cache_key, t=title, a=author, v=version: (n, c, k, t, a, v),
outputs=[selected_state]
).then(
fn=lambda state: (state[1] if state else None, f"πŸ“ Selected: **{state[0]}**" if state else "πŸ“ *Silakan pilih karakter...*"),
inputs=[selected_state],
outputs=[cover_img, selected_name_display]
)
# Input teks & kontrol
txt_in = gr.TextArea(label="Input Text (Japanese / English)", value="γ“γ‚“γ«γ‘γ―γ€ε…ˆη”ŸοΌ", lines=3)
gr.Button("🎲 RANDOM TEXT 🎲", elem_classes="jp-btn").click(get_random_jp, outputs=[txt_in])
# Sliders
speed_slider = gr.Slider(0.5, 2.0, value=1.0, step=0.1, label="Speed Audio")
pitch_slider = gr.Slider(-12, 12, value=12, step=1, label="Pitch Shift (semitones) – Recommended +12 for female, 0 for male")
# Advanced settings dalam accordion
with gr.Accordion("βš™οΈ Advanced Settings", open=False):
f0method = gr.Radio(label="F0 Method", choices=f0method_mode, value="rmvpe" if "rmvpe" in f0method_mode else "pm")
index_rate = gr.Slider(0, 1, value=0.75, label="Index Rate")
filter_radius = gr.Slider(0, 7, value=7, step=1, label="Filter Radius")
resample_sr = gr.Slider(0, 48000, value=0, label="Resample SR (0 = none)")
rms_mix_rate = gr.Slider(0, 1, value=0.76, label="Volume Mix")
protect = gr.Slider(0, 0.5, value=0.33, label="Voice Protect")
# Generate button & outputs
gen_btn = gr.Button("🎐 GENERATE VOICE 🎐", elem_classes="gen-btn")
status_log = gr.Textbox(label="Status", interactive=False)
audio_output = gr.Audio(label="Voice Output")
# GIF Kurumi
gr.HTML("""
<div class="gif-container">
<img src="https://huggingface.co/spaces/Plana-Archive/MOE-TTS/resolve/main/kurumi-tokisaki.gif" alt="Kurumi GIF">
</div>
""")
# Expandable character info (dummy dulu, diisi saat pilih karakter)
gr.HTML("""
<button onclick="document.getElementById('char-info-area').style.display = (document.getElementById('char-info-area').style.display === 'none') ? 'grid' : 'none';" class="info-header-custom">
πŸ“‘ Character Information πŸ“‘
</button>
""")
char_info_html_placeholder = gr.HTML('<div id="char-info-area" style="display: none;">Select a character first</div>')
# Update info saat pilih karakter
def update_char_info(state):
if state is None:
return '<div id="char-info-area" style="display: none;">No character selected</div>'
name, cover, key, title, author, version = state
return char_info_html(name, title, author, version)
selected_state.change(
fn=update_char_info,
inputs=[selected_state],
outputs=[char_info_html_placeholder]
)
# Fungsi generate
async def generate_voice(state, text, pitch, speed, f0m, idx_r, filt_r, resamp, rms, prot):
if state is None:
return "❌ Please select a character first", None
name, cover, key, title, author, version = state
vc_fn = vc_fn_map.get(key)
if vc_fn is None:
return "❌ Character function not found", None
if not text.strip():
return "❌ Please enter some text", None
last_status, last_audio = None, None
async for status, audio in vc_fn(
vc_audio_mode="TTS Audio",
vc_input=None,
vc_upload=None,
tts_text=text,
f0_up_key=pitch,
f0_method=f0m,
index_rate=idx_r,
filter_radius=filt_r,
resample_sr=resamp,
rms_mix_rate=rms,
protect=prot,
speed=speed
):
last_status, last_audio = status, audio
return last_status, last_audio
gen_btn.click(
fn=generate_voice,
inputs=[selected_state, txt_in, pitch_slider, speed_slider, f0method, index_rate, filter_radius, resample_sr, rms_mix_rate, protect],
outputs=[status_log, audio_output]
)
gr.HTML("""<div class="credit-footer">πŸŒ₯️ CREATED BY MUTSUMI πŸŒ₯️</div>""")
# Launch
demo.queue().launch(
server_name="0.0.0.0" if os.getenv('SPACE_ID') else "127.0.0.1",
server_port=7860,
show_api=False
)