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 = '''