Spaces:
Running
Running
| 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() |