NSW-CHUPA / app.py
Plana-Archive's picture
Update app.py
1062a85 verified
import os
import shutil
import datetime
from pathlib import Path
import random
import time
import torch
import gradio as gr
from huggingface_hub import snapshot_download, login
# --- BAGIAN 1: LOGIKA MODEL ---
from style_bert_vits2.constants import (
DEFAULT_LENGTH, DEFAULT_LINE_SPLIT, DEFAULT_NOISE,
DEFAULT_NOISEW, DEFAULT_SPLIT_INTERVAL, Languages
)
from style_bert_vits2.logging import logger
from style_bert_vits2.tts_model import TTSModelHolder
from style_bert_vits2.nlp import bert_models
os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
# Login dengan token Hugging Face
def hf_login():
"""Login ke Hugging Face Hub dengan token"""
token = os.environ.get("HF_TOKEN")
if token:
try:
login(token=token, add_to_git_credential=True)
logger.info("✅ Berhasil login ke Hugging Face Hub dengan token")
return True
except Exception as e:
logger.warning(f"⚠️ Gagal login dengan token: {e}")
return False
else:
logger.warning("⚠️ HF_TOKEN tidak ditemukan, mencoba akses tanpa token")
return False
# Login saat aplikasi dimulai
hf_login()
def setup_bert_structure():
"""Setup struktur folder BERT sesuai yang diharapkan style_bert_vits2"""
try:
# Path yang diharapkan oleh style_bert_vits2 untuk model JP
bert_jp_path = Path("./bert/bert-base-japanese-v3")
# Cek apakah sudah ada struktur yang benar
required_files = ["config.json", "pytorch_model.bin", "vocab.txt"]
has_all_files = all((bert_jp_path / file).exists() for file in required_files)
if has_all_files:
logger.info(f"✅ Model BERT JP sudah ada di {bert_jp_path}")
return True
logger.info("📥 Mengunduh atau mengatur ulang struktur BERT...")
# Coba unduh dari repo premium
REPO_ID = "Plana-Archive/Premium-Model"
SOURCE_SUBFOLDER = "SBV2-Chupa-Demo/bert"
token = os.environ.get("HF_TOKEN")
try:
if token:
temp_dir = snapshot_download(
repo_id=REPO_ID,
allow_patterns=[f"{SOURCE_SUBFOLDER}/**/*"],
token=token,
local_dir_use_symlinks=False
)
logger.info("✅ BERT assets berhasil diunduh dari repo premium")
else:
temp_dir = snapshot_download(
repo_id=REPO_ID,
allow_patterns=[f"{SOURCE_SUBFOLDER}/**/*"],
local_dir_use_symlinks=False
)
logger.info("✅ BERT assets berhasil diunduh tanpa token")
# Cari file BERT dalam struktur yang diunduh
src_path = Path(temp_dir) / SOURCE_SUBFOLDER
# Jika tidak ditemukan, coba cari di lokasi lain
if not src_path.exists():
# Coba cari folder bert di berbagai lokasi
possible_locations = [
Path(temp_dir) / "bert",
Path(temp_dir) / "BERT",
Path(temp_dir) / "SBV2-Chupa-Demo/bert",
Path(temp_dir) / "sbv2-chupa-demo/bert",
]
for loc in possible_locations:
if loc.exists():
src_path = loc
break
if src_path.exists():
# Pastikan folder bert ada
bert_dir = Path("./bert")
bert_dir.mkdir(exist_ok=True)
# Salin semua isi folder bert
for item in src_path.rglob("*"):
if item.is_file():
rel_path = item.relative_to(src_path)
dest_path = bert_dir / rel_path
dest_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item, dest_path)
logger.info(f"✅ BERT assets disalin ke ./bert")
# Verifikasi struktur
if bert_jp_path.exists():
logger.info(f"✅ Struktur BERT JP valid: {bert_jp_path}")
return True
else:
# Coba buat struktur yang diharapkan
logger.info("🔄 Membuat struktur BERT JP yang diharapkan...")
# Cari model BERT dalam folder bert
bert_base_dir = None
for subdir in bert_dir.iterdir():
if subdir.is_dir() and "japanese" in subdir.name.lower():
bert_base_dir = subdir
break
if bert_base_dir and bert_base_dir.exists():
# Salin ke lokasi yang diharapkan
bert_jp_path.mkdir(parents=True, exist_ok=True)
for item in bert_base_dir.iterdir():
if item.is_file():
shutil.copy2(item, bert_jp_path / item.name)
logger.info(f"✅ Model BERT JP disalin ke {bert_jp_path}")
return True
except Exception as e:
logger.warning(f"⚠️ Gagal mengunduh BERT dari repo premium: {e}")
# Fallback: Unduh model BERT dasar dari Hugging Face
logger.info("🔄 Mencoba mengunduh model BERT dasar...")
try:
# Download bert-base-japanese-v3 dari huggingface
from transformers import AutoTokenizer, AutoModel
import huggingface_hub
token = os.environ.get("HF_TOKEN")
# Download tokenizer dan model
logger.info("📥 Mengunduh tokenizer BERT JP...")
tokenizer = AutoTokenizer.from_pretrained(
"cl-tohoku/bert-base-japanese-v3",
use_auth_token=token if token else None
)
logger.info("📥 Mengunduh model BERT JP...")
model = AutoModel.from_pretrained(
"cl-tohoku/bert-base-japanese-v3",
use_auth_token=token if token else None
)
# Simpan ke disk
bert_jp_path.mkdir(parents=True, exist_ok=True)
tokenizer.save_pretrained(str(bert_jp_path))
model.save_pretrained(str(bert_jp_path))
logger.info(f"✅ Model BERT dasar berhasil diunduh ke {bert_jp_path}")
return True
except Exception as e:
logger.error(f"❌ Gagal mengunduh model BERT dasar: {e}")
return False
except Exception as e:
logger.error(f"❌ Error dalam setup BERT: {e}")
return False
def download_model_assets():
"""Download model TTS assets dari repo privat"""
REPO_ID = "Plana-Archive/Premium-Model"
SOURCE_SUBFOLDER = "SBV2-Chupa-Demo/model_assets"
DEST_FOLDER = Path("./model_assets")
# Cek apakah sudah ada
if DEST_FOLDER.exists():
model_files = list(DEST_FOLDER.glob("*.pth")) + list(DEST_FOLDER.glob("*.json"))
if model_files:
logger.info(f"✅ Model assets sudah ada di {DEST_FOLDER}")
return True
try:
logger.info(f"📥 Mengunduh model assets dari {REPO_ID}/{SOURCE_SUBFOLDER}")
token = os.environ.get("HF_TOKEN")
if token:
temp_dir = snapshot_download(
repo_id=REPO_ID,
allow_patterns=[f"{SOURCE_SUBFOLDER}/**/*"],
token=token,
local_dir_use_symlinks=False
)
else:
temp_dir = snapshot_download(
repo_id=REPO_ID,
allow_patterns=[f"{SOURCE_SUBFOLDER}/**/*"],
local_dir_use_symlinks=False
)
# Temukan dan salin model assets
src_path = Path(temp_dir) / SOURCE_SUBFOLDER
if not src_path.exists():
# Coba cari di lokasi lain
possible_locations = [
Path(temp_dir) / "model_assets",
Path(temp_dir) / "SBV2-Chupa-Demo",
Path(temp_dir) / "sbv2-chupa-demo",
]
for loc in possible_locations:
if loc.exists():
# Cari file .pth dan .json
model_files = list(loc.rglob("*.pth")) + list(loc.rglob("*.json"))
if model_files:
DEST_FOLDER.mkdir(exist_ok=True)
for file in model_files:
shutil.copy2(file, DEST_FOLDER / file.name)
logger.info(f"✅ Model files disalin ke {DEST_FOLDER}")
return True
if src_path.exists():
DEST_FOLDER.mkdir(exist_ok=True)
for item in src_path.iterdir():
if item.is_file():
shutil.copy2(item, DEST_FOLDER / item.name)
logger.info(f"✅ Model assets disalin ke {DEST_FOLDER}")
return True
except Exception as e:
logger.error(f"❌ Gagal mengunduh model assets: {e}")
# Buat folder kosong
DEST_FOLDER.mkdir(exist_ok=True)
return False
# Setup semua assets
try:
if setup_bert_structure():
logger.info("✅ Setup BERT berhasil")
else:
logger.error("❌ Setup BERT gagal")
if download_model_assets():
logger.info("✅ Download model assets berhasil")
else:
logger.warning("⚠️ Download model assets gagal, menggunakan folder kosong")
except Exception as e:
logger.error(f"❌ Error saat setup assets: {e}")
# --- BAGIAN 2: CSS CUSTOM (ESTETIK & CLEAN) ---
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;
max-width: 1200px !important;
margin: 0 auto !important;
}
footer { display: none !important; }
/* Verifikasi CSS */
.age-verification-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #fff5f9 0%, #ffeaf3 100%);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s ease;
}
.age-verification-box {
background: white;
border-radius: 20px;
padding: 25px 30px;
max-width: 420px;
width: 90%;
text-align: center;
box-shadow: 0 15px 40px rgba(255, 105, 180, 0.2);
border: 2px solid #ffe4ec;
}
.verification-image {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #ffe4ec;
margin: 0 auto 15px;
display: block;
}
.age-verification-title {
font-family: 'Quicksand', sans-serif;
font-weight: 800;
font-size: 22px;
color: #ff69b4;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.verification-message {
font-size: 14px;
line-height: 1.5;
color: #7b4d5a;
margin: 15px 0;
padding: 12px;
background: #fffafc;
border-radius: 10px;
border: 1px solid #ffe4ec;
}
.age-verification-buttons {
display: flex;
gap: 12px;
margin-top: 20px;
justify-content: center;
}
.age-btn {
padding: 10px 25px !important;
border-radius: 8px !important;
font-weight: 700 !important;
font-size: 14px !important;
transition: all 0.2s ease !important;
border: 2px solid transparent !important;
min-width: 110px;
flex: 1;
height: 40px !important;
}
.age-btn-yes {
background: linear-gradient(135deg, #ff69b4 0%, #ff1493 100%) !important;
color: white !important;
}
.age-btn-yes:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(255, 105, 180, 0.3) !important;
}
.age-btn-no {
background: white !important;
color: #ff69b4 !important;
border-color: #ffe4ec !important;
}
.age-btn-no:hover:not(:disabled) {
background: #fff5f9 !important;
border-color: #ff69b4 !important;
transform: translateY(-2px);
}
.age-btn:disabled {
opacity: 0.7;
cursor: not-allowed !important;
}
.loading-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #ffffff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
.loading-spinner-no {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #ff69b4;
border-radius: 50%;
border-top-color: transparent;
animation: spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Loading Screen */
.loading-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
animation: fadeIn 0.3s ease;
}
.loading-content {
text-align: center;
padding: 40px;
border-radius: 20px;
background: white;
box-shadow: 0 10px 30px rgba(255, 105, 180, 0.1);
border: 2px solid #ffe4ec;
}
.loading-logo {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #ffe4ec;
margin-bottom: 20px;
}
.loading-title {
font-family: 'Quicksand', sans-serif;
font-weight: 700;
font-size: 18px;
color: #ff69b4;
margin-bottom: 15px;
}
.loading-message {
font-size: 14px;
color: #7b4d5a;
margin-bottom: 20px;
}
.loading-large-spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 3px solid #ffe4ec;
border-radius: 50%;
border-top-color: #ff69b4;
animation: spin 1s linear infinite;
margin-top: 10px;
}
/* Loading Screen untuk Tidak */
.loading-screen-no {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #fff5f9 0%, #ffeaf3 100%);
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
animation: fadeIn 0.3s ease;
}
.loading-content-no {
text-align: center;
padding: 40px;
border-radius: 20px;
background: white;
box-shadow: 0 10px 30px rgba(255, 105, 180, 0.2);
border: 2px solid #ff69b4;
}
.loading-title-no {
font-family: 'Quicksand', sans-serif;
font-weight: 700;
font-size: 18px;
color: #ff1493;
margin-bottom: 15px;
}
.loading-message-no {
font-size: 14px;
color: #7b4d5a;
margin-bottom: 20px;
}
.loading-large-spinner-no {
display: inline-block;
width: 40px;
height: 40px;
border: 3px solid #ff69b4;
border-radius: 50%;
border-top-color: #ff1493;
animation: spin 1s linear infinite;
margin-top: 10px;
}
/* Main Content Layout */
.main-container {
display: flex;
flex-direction: column;
gap: 15px;
width: 100%;
}
.header-img-container {
text-align: center;
padding: 10px 0;
}
.header-img {
width: 100%;
max-width: 500px;
border-radius: 15px;
margin: 0 auto;
display: block;
}
.status-card {
background: #ffffff;
border: 2px solid #ffe4ec;
border-radius: 14px;
padding: 15px 10px;
margin: 0 auto 20px auto;
max-width: 400px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4px 15px rgba(255, 105, 180, 0.1);
}
.content-row {
display: flex;
flex-direction: row;
gap: 20px;
width: 100%;
align-items: flex-start;
}
.sidebar-column {
flex: 1;
border-right: 3px solid #ff69b4 !important;
padding-right: 20px !important;
min-width: 300px;
}
.main-column {
flex: 2;
min-width: 500px;
}
.pink-accordion {
border: 1px solid #ffe4ec !important;
border-radius: 10px !important;
overflow: hidden;
margin-top: 10px;
}
.pink-accordion .label-wrap {
background: linear-gradient(135deg, #ff69b4 0%, #ff1493 100%) !important;
padding: 10px !important;
}
.pink-accordion .label-wrap span {
color: white !important;
font-weight: 700 !important;
}
.generate-btn {
background: linear-gradient(135deg, #ff69b4 0%, #ff1493 100%) !important;
color: white !important;
border-radius: 10px !important;
font-weight: 700 !important;
height: 45px !important;
font-size: 1em !important;
margin-top: 15px;
border: none !important;
width: 100% !important;
}
.output-audio-box {
border: none !important;
box-shadow: none !important;
background: transparent !important;
padding: 1px 1px 8px 1px !important;
}
.output-audio-box > div {
border: 1px solid #ffe4ec !important;
border-bottom: 1px solid #ffe4ec !important;
border-radius: 10px !important;
background: #ffffff !important;
overflow: hidden !important;
}
/* --- CHATBOT SIMULATION CSS --- */
.chat-container {
background: #ffffff;
border: 2px solid #ffe4ec;
border-radius: 15px;
padding: 20px;
margin-top: 20px;
box-shadow: 0 5px 15px rgba(255, 105, 180, 0.05);
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #fff0f7;
padding-bottom: 10px;
margin-bottom: 15px;
}
.chat-title {
color: #ff69b4;
font-weight: 800;
font-size: 15px;
}
.chat-status {
background: #e6fffa;
color: #38b2ac;
padding: 2px 10px;
border-radius: 20px;
font-size: 10px;
font-weight: 700;
letter-spacing: 1px;
}
.chat-bubble-bot {
background: #fff5f8;
border: 1px solid #ffe4ec;
padding: 12px;
border-radius: 15px 15px 15px 0px;
color: #7b4d5a;
font-size: 13px;
line-height: 1.6;
}
.chat-button-group {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 15px;
}
.chat-btn-opt {
background: white !important;
border: 1px solid #ffe4ec !important;
color: #7b4d5a !important;
text-align: left !important;
font-size: 12px !important;
padding: 8px 12px !important;
min-height: unset !important;
border-radius: 8px !important;
}
.chat-btn-opt:hover {
border-color: #ff69b4 !important;
color: #ff69b4 !important;
background: #fffafc !important;
}
.note-box {
background: #fffafa;
padding: 20px;
border-radius: 12px;
margin-top: 25px;
border: 1px solid #ffe4ec;
}
.note-title {
color: #ff69b4;
font-weight: 800;
font-size: 14px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
text-transform: uppercase;
letter-spacing: 1px;
}
.note-content {
color: #7b4d5a;
font-size: 13px;
line-height: 1.6;
}
.footer-container {
text-align: center;
margin-top: 30px;
padding: 25px;
border-top: 2px solid #ffe4ec;
background: #fffafc;
}
.dot-online {
height: 8px;
width: 8px;
background-color: #ff69b4;
border-radius: 50%;
display: inline-block;
animation: blink 1.5s infinite;
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0.4; }
100% { opacity: 1; }
}
/* Responsive */
@media (max-width: 900px) {
.content-row {
flex-direction: column;
}
.sidebar-column {
border-right: none !important;
border-bottom: 3px solid #ff69b4 !important;
padding-right: 0 !important;
padding-bottom: 20px !important;
min-width: 100%;
}
.main-column {
min-width: 100%;
}
.age-btn {
padding: 8px 20px !important;
min-width: 100px;
}
}
"""
def get_random_text():
return random.choice([
"ちゅぱ、ちゅるる、ぢゅ、んく、れーれゅれろれろれろ、じゅぽぽぽぽぽ……ちゅううう!",
"んっ……ぷはっ……はぁ……ぺろっ、ちゅ、ちゅうぅ……。",
"あむっ、んぐっ、んちゅ……はぁ、じゅるっ……。"
])
# --- BAGIAN 3: APLIKASI UTAMA ---
def create_inference_app(model_holder: TTSModelHolder) -> gr.Blocks:
model_names = model_holder.model_names
current_model_name = model_names[0] if model_names else None
initial_pth_files = [str(f) for f in model_holder.model_files_dict[current_model_name]] if current_model_name else []
current_model_path = initial_pth_files[0] if initial_pth_files else None
if current_model_name and current_model_path:
model_holder.get_model(current_model_name, current_model_path)
# Variabel pesan awal
welcome_msg = '''
<div class="chat-bubble-bot">
(*≧ω≦*) Halo! Saya Mutsumi 🌥️.<br>
Saya bisa membantu hal ini :<br>
• Cara menggunakan TTS<br>
• Informasi bahasa<br>
• Efek input non-Jepang<br>
Klik tombol di bawah untuk melihat detail!
</div>
'''
def tts_fn(text, sdp_ratio, noise_scale, noise_scale_w, length_scale):
try:
sr, audio = model_holder.current_model.infer(
text=text, language="JP", sdp_ratio=sdp_ratio,
noise=noise_scale, noise_w=noise_scale_w, length=length_scale,
line_split=False, split_interval=0.5, speaker_id=0,
)
return "Generation Complete! ✅", (sr, audio)
except Exception as e: return f"Error: {e}", None
def chat_respond(choice):
responses = {
1: '''<div class="chat-bubble-bot">
<b>Cara Menggunakan:</b><br>
1. Masukkan teks Jepang di input<br>
2. Settings jika perlu<br>
3. Atur kecepatan jika perlu<br>
4. Klik 'Generate Voice'<br>
5. dan hasil muncul!<br><br>
🌥️ <b>NOTES :</b> tidak di sarankan mengubah settingan kecuali bagian atur kecepatan aja.
</div>''',
2: '''<div class="chat-bubble-bot">
Model ini hanya support <b>bahasa Jepang</b> aja sementara lainnya tidak bisa.
</div>''',
3: '''<div class="chat-bubble-bot">
⚠️ <b>Input Bukan Jepang</b> ⚠️<br>
Karakter akan terdengar aneh karena model khusus bahasa Jepang.
</div>'''
}
return responses.get(choice, welcome_msg)
with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="pink")) as app:
# --- LOADING SCREEN (untuk Yes) ---
with gr.Column(visible=False, elem_id="loading-screen-yes") as loading_screen_yes:
with gr.Column(elem_classes="loading-screen"):
with gr.Column(elem_classes="loading-content"):
gr.HTML('<img src="https://huggingface.co/spaces/Plana-Archive/NSW-CHUPA/resolve/main/Plana-Archive.PNG" class="loading-logo">')
gr.HTML('<div class="loading-title">Memuat Aplikasi...</div>')
gr.HTML('<div class="loading-message">Harap tunggu sebentar, aplikasi sedang dimuat.</div>')
gr.HTML('<div class="loading-large-spinner"></div>')
# --- LOADING SCREEN (untuk Tidak) ---
with gr.Column(visible=False, elem_id="loading-screen-no") as loading_screen_no:
with gr.Column(elem_classes="loading-screen-no"):
with gr.Column(elem_classes="loading-content-no"):
gr.HTML('<img src="https://huggingface.co/spaces/Plana-Archive/NSW-CHUPA/resolve/main/Plana-Archive.PNG" class="loading-logo">')
gr.HTML('<div class="loading-title-no">⚠️ Peringatan Minna ⚠️</div>')
gr.HTML('<div class="loading-message-no">Silahkan kembali jika belum cukup umurmu, Hargai Author yah! 🌥️</div>')
gr.HTML('<div class="loading-large-spinner-no"></div>')
# --- VERIFIKASI USIA OVERLAY ---
with gr.Column(visible=True, elem_id="age-verification") as verification_overlay:
with gr.Column(elem_classes="age-verification-overlay"):
with gr.Column(elem_classes="age-verification-box"):
gr.HTML('<img src="https://huggingface.co/spaces/Plana-Archive/NSW-CHUPA/resolve/main/Plana-Archive.PNG" class="verification-image">')
gr.HTML('<div class="age-verification-title">🍂 Verifikasi Usia 🍂</div>')
gr.HTML('''
<div class="verification-message">
Model ini hanya untuk 18+ Tahun. Pastikan umur kamu sudah cukup dewasa untuk pakai model ini.<br>
Apakah usiamu sudah mencapai 18+ tahun? (⸝⸝╸▵╺⸝⸝)
</div>
''')
with gr.Row(elem_classes="age-verification-buttons"):
btn_no = gr.Button("Tidak", variant="secondary", elem_classes="age-btn age-btn-no")
btn_yes = gr.Button("Yes", variant="primary", elem_classes="age-btn age-btn-yes")
# --- KONTEN UTAMA (tersembunyi sampai verifikasi) ---
with gr.Column(visible=False, elem_id="main-content") as main_content:
with gr.Column(elem_classes="main-container"):
gr.HTML('<div class="header-img-container"><img src="https://huggingface.co/spaces/Plana-Archive/sbv2-chupa-demo-space/resolve/main/Plana-Archive.PNG" class="header-img"></div>')
# STATUS CARD
gr.HTML('''
<div class="status-card">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
<span class="dot-online"></span><b style="color: #ff69b4;">Voice Conversion System Online</b>
</div>
<div style="display: flex; width: 100%; border-top: 1px solid #fff0f7; padding-top: 10px; justify-content: space-around;">
<div style="text-align:center;"><span style="font-size: 13px; font-weight: 600; color: #7b4d5a;">👥 1 Students</span><br><span style="font-size: 11px; color: #b07d8b;">Ready</span></div>
<div style="text-align:center;"><span style="font-size: 13px; font-weight: 600; color: #7b4d5a;">📊 Total Models</span><br><span style="font-size: 11px; color: #b07d8b;">Database: 1</span></div>
</div>
</div>
''')
with gr.Row(elem_classes="content-row"):
# SIDEBAR
with gr.Column(scale=1, elem_classes="sidebar-column"):
gr.Markdown("### ⚙️ Audio Settings")
length_scale = gr.Slider(0.1, 2, value=1.0, step=0.1, label="Kecepatan")
sdp_ratio = gr.Slider(0, 1, value=0.2, step=0.1, label="SDP Ratio")
with gr.Accordion("🔮 Advanced Settings 🔮", open=False, elem_classes="pink-accordion"):
noise_scale = gr.Slider(0.1, 2, value=0.6, step=0.1, label="Noise")
noise_scale_w = gr.Slider(0.1, 2, value=0.8, step=0.1, label="Noise_W")
# MAIN CONTENT
with gr.Column(scale=2, elem_classes="main-column"):
with gr.Group():
text_input = gr.TextArea(label="Input Teks (JP)", value="ちゅぱ、ちゅるる...", lines=10)
random_button = gr.Button("🎲 KLIK ACAK ISI TEXT 🎲", variant="secondary")
tts_button = gr.Button("Generate Voice", variant="primary", elem_classes="generate-btn")
# --- CHATBOT AREA ---
with gr.Column(elem_classes="chat-container"):
gr.HTML('''
<div class="chat-header">
<span class="chat-title">Plana AI - Asisten</span>
<span class="chat-status">● CONNECTED</span>
</div>
''')
chat_display = gr.HTML(welcome_msg)
with gr.Column(elem_classes="chat-button-group"):
btn_opt1 = gr.Button("1. Cara pakai VITS ini", elem_classes="chat-btn-opt")
btn_opt2 = gr.Button("2. Support bahasa apa aja VITS ini", elem_classes="chat-btn-opt")
btn_opt3 = gr.Button("3. Apa yang terjadi jika inputs bukan Jepang", elem_classes="chat-btn-opt")
with gr.Row():
text_output = gr.Textbox(label="Status", interactive=False)
audio_output = gr.Audio(label="Output", interactive=False, elem_classes="output-audio-box")
gr.HTML('''
<div class="note-box">
<div class="note-title">📑 TENTANG SETTINGS 📑</div>
<div class="note-content">
<b>Kecepatan:</b> Mengatur tempo bicara (Default 1.0).<br>
<b>SDP Ratio:</b> Ritme bicara. Angka tinggi lebih ekspresif.<br>
<b>Noise:</b> Stabilitas suara. Kurangi jika suara pecah.<br>
<b>Noise_W:</b> Dinamika durasi antar kata.
</div>
</div>
''')
# FOOTER
gr.HTML('''
<div class="footer-container">
<div style="font-family: 'Quicksand', sans-serif; font-size: 14px; color: #b07d8b; font-weight: 700;">
👅 NSW - VITS Anime • TTS 👅
</div>
<div style="font-size: 11px; color: #d0a4b0; margin-top: 5px;">By Mutsumi • Style-Bert-VITS2</div>
</div>
''')
# --- EVENT HANDLERS VERIFIKASI ---
def show_loading_yes():
"""Tampilkan loading screen untuk tombol Yes"""
return {
verification_overlay: gr.Column(visible=False),
loading_screen_yes: gr.Column(visible=True)
}
def show_main_content():
"""Tampilkan konten utama setelah loading"""
time.sleep(5) # Tunggu 5 detik
return {
loading_screen_yes: gr.Column(visible=False),
main_content: gr.Column(visible=True)
}
def show_loading_no():
"""Tampilkan loading screen untuk tombol Tidak"""
return {
verification_overlay: gr.Column(visible=False),
loading_screen_no: gr.Column(visible=True)
}
def redirect_to_huggingface():
"""Redirect ke Hugging Face setelah delay"""
time.sleep(3) # Tunggu 3 detik untuk efek loading
return {
loading_screen_no: gr.Column(visible=False),
verification_overlay: gr.Column(visible=True)
}
# Event Handlers Utama
random_button.click(get_random_text, outputs=[text_input])
tts_button.click(
tts_fn,
inputs=[text_input, sdp_ratio, noise_scale, noise_scale_w, length_scale],
outputs=[text_output, audio_output]
)
# Chatbot Handlers
btn_opt1.click(fn=lambda: chat_respond(1), outputs=chat_display)
btn_opt2.click(fn=lambda: chat_respond(2), outputs=chat_display)
btn_opt3.click(fn=lambda: chat_respond(3), outputs=chat_display)
# Verifikasi Handlers
# Handler untuk tombol Yes dengan loading screen
btn_yes.click(
fn=show_loading_yes,
outputs=[verification_overlay, loading_screen_yes]
).then(
fn=show_main_content,
outputs=[loading_screen_yes, main_content]
)
# Handler untuk tombol No dengan loading screen dan redirect
btn_no.click(
fn=show_loading_no,
outputs=[verification_overlay, loading_screen_no]
).then(
fn=redirect_to_huggingface,
outputs=[loading_screen_no, verification_overlay]
).then(
# JavaScript untuk membuka tab baru ke Hugging Face
fn=None,
inputs=[],
outputs=[],
js="window.open('https://huggingface.co/Plana-Archive', '_blank');"
)
return app
if __name__ == "__main__":
# Load BERT model dan tokenizer untuk JP
try:
bert_models.load_model(Languages.JP)
bert_models.load_tokenizer(Languages.JP)
logger.info("✅ BERT model dan tokenizer berhasil dimuat")
except Exception as e:
logger.error(f"❌ Gagal memuat BERT model: {e}")
# Coba load dengan path eksplisit
bert_path = Path("./bert/bert-base-japanese-v3")
if bert_path.exists():
try:
from style_bert_vits2.nlp import bert_models
import sys
# Tambahkan path bert ke sys.path
sys.path.insert(0, str(bert_path.parent))
logger.info(f"🔄 Mencoba memuat BERT dari {bert_path}")
# Reload module
import importlib
importlib.reload(bert_models)
bert_models.load_model(Languages.JP)
bert_models.load_tokenizer(Languages.JP)
logger.info("✅ BERT model berhasil dimuat dengan path eksplisit")
except Exception as e2:
logger.error(f"❌ Masih gagal memuat BERT: {e2}")
# Fallback ke model dasar
logger.info("🔄 Fallback ke model dasar...")
else:
logger.error("❌ Path BERT tidak ditemukan")
device = "cuda" if torch.cuda.is_available() else "cpu"
model_holder = TTSModelHolder(Path("model_assets"), device)
create_inference_app(model_holder).launch()