Plana-Archive's picture
Migrated content from repo: Plana-Archive/Anime-Spaces
d4b1959 verified
Raw
History Blame Contribute Delete
41.7 kB
import gradio as gr
import os
import torch
import commons
import utils
from models import SynthesizerTrn
import numpy as np
import json
import requests
import logging
import traceback
import sys
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Fungsi untuk mendownload file dari Hugging Face
def download_file(url, local_path):
"""Download file dari URL ke local path"""
try:
response = requests.get(url, stream=True)
response.raise_for_status()
os.makedirs(os.path.dirname(local_path), exist_ok=True)
with open(local_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
logger.info(f"File downloaded: {local_path}")
return True
except Exception as e:
logger.error(f"Error downloading {url}: {e}")
return False
# Base URL dari Hugging Face
BASE_URL = "https://huggingface.co/spaces/Rosmontis-Chan/Waifu-Anime-TTS/resolve/main"
# File yang diperlukan - dari folder 7/
FILES_TO_DOWNLOAD = {
"config.json": f"{BASE_URL}/saved_model/7/config.json",
"model.pth": f"{BASE_URL}/saved_model/7/model.pth",
"cover.png": f"{BASE_URL}/saved_model/7/cover.png"
}
# Path lokal
LOCAL_DIR = "saved_model"
LOCAL_PATHS = {
"config": os.path.join(LOCAL_DIR, "7", "config.json"),
"model": os.path.join(LOCAL_DIR, "7", "model.pth"),
"cover": os.path.join(LOCAL_DIR, "7", "cover.png")
}
# Buat folder jika belum ada
os.makedirs(os.path.join(LOCAL_DIR, "7"), exist_ok=True)
# Download file jika belum ada
logger.info("Checking for required files...")
for file_name, url in FILES_TO_DOWNLOAD.items():
# Tentukan local path berdasarkan file_name
if file_name == "config.json":
local_path = LOCAL_PATHS["config"]
elif file_name == "model.pth":
local_path = LOCAL_PATHS["model"]
elif file_name == "cover.png":
local_path = LOCAL_PATHS["cover"]
else:
continue
if not os.path.exists(local_path):
logger.info(f"Downloading {file_name} from {url}...")
success = download_file(url, local_path)
if not success:
logger.error(f"Failed to download {file_name}")
else:
logger.info(f"{os.path.basename(local_path)} already exists")
# Mapping untuk nama karakter dengan Romaji
CHARACTER_MAPPING = {
"鷹倉杏璃": "Takakura Anri",
"ι·Ήε€‰ζιˆ΄": "Takakura Anzu",
"γ‚’γƒšγ‚€γƒͺγ‚’": "Apeiria",
"ε€‰η§‘ζ˜Žζ—₯香": "Kurashina Asuka",
"ATRI": "ATRI",
"をむラ": "Aira",
"ζ–°ε ‚ε½©ιŸ³": "Shindo Ayane",
"ε§«ι‡Žζ˜Ÿε₯": "Himeno Seina",
"ε°ιž γ‚†γ„": "Komari Yui",
"聖代橋氷織": "Seidaihashi Hiori",
"ζœ‰ε‚ηœŸη™½": "Arisaka Mashiro",
"η™½ε’²ηΎŽη΅΅η‘ ": "Shirasaki Mieru",
"δΊŒιšŽε ‚ηœŸη΄…": "Nikaido Shinku"
}
# Fungsi untuk mendapatkan display name dengan Romaji
def get_display_name(name):
"""Format nama karakter dengan Romaji dalam kurung"""
if name in CHARACTER_MAPPING:
return f"{name} ({CHARACTER_MAPPING[name]})"
return f"{name}"
# Fungsi untuk memisahkan nama Jepang dan Romaji
def split_name_display(display_name):
"""Pisahkan nama Jepang dan Romaji dari display name"""
if '(' in display_name and ')' in display_name:
parts = display_name.split('(')
japanese = parts[0].strip()
romaji = parts[1].replace(')', '').strip()
return japanese, romaji
return display_name, ""
# Fungsi untuk membaca parameter dari config.json
def read_config_file(config_path):
"""Membaca config.json dan mengembalikan dictionary dengan parameter"""
try:
with open(config_path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
logger.info(f"Config keys found: {list(config_data.keys())}")
return config_data
except Exception as e:
logger.error(f"Error reading config file: {e}")
return None
# Fungsi untuk mendapatkan parameter dengan fallback
def get_config_param(config, keys, default=None):
"""Mendapatkan parameter dari config dengan key bertingkat"""
if isinstance(config, dict):
current = config
for key in keys:
if isinstance(current, dict) and key in current:
current = current[key]
else:
return default
return current
return default
# Fungsi konversi teks ke urutan angka - VERSI DIPERBAIKI UNTUK JEPANG
def get_text(text, hps):
"""Convert text to sequence - FIXED VERSION with Japanese support"""
try:
# Impor modul text
try:
from text import text_to_sequence
text_module_available = True
except ImportError as e:
logger.error(f"Cannot import text module: {e}")
text_module_available = False
# Dapatkan symbols dari hps
symbols = getattr(hps, 'symbols', [])
if not symbols:
# Fallback symbols dari config (1).json
symbols = ["_", ",", ".", "!", "?", "-", "A", "E", "I", "N", "O", "Q", "U", "a", "b", "d", "e", "f", "g", "h", "i", "j", "k", "m", "n", "o", "p", "r", "s", "t", "u", "v", "w", "y", "z", "Κƒ", "Κ§", "↓", "↑", " "]
logger.warning(f"Using fallback symbols, count: {len(symbols)}")
# Bersihkan teks
text = str(text).strip()
if not text:
logger.error("Empty text provided to get_text")
return torch.LongTensor([0])
logger.info(f"Original text: {text}")
text_norm = []
# Coba menggunakan text_cleaners jika ada (japanese_cleaners)
if text_module_available and hasattr(hps, 'text_cleaners') and hps.text_cleaners:
try:
logger.info(f"Using text cleaners: {hps.text_cleaners}")
text_norm = text_to_sequence(text, symbols, hps.text_cleaners)
logger.info(f"text_to_sequence with cleaners succeeded, length: {len(text_norm)}")
except Exception as e:
logger.warning(f"text_to_sequence with cleaners failed: {e}, trying without cleaners")
try:
text_norm = text_to_sequence(text, symbols)
except Exception as e2:
logger.warning(f"text_to_sequence without cleaners also failed: {e2}")
text_module_available = False
elif text_module_available:
try:
text_norm = text_to_sequence(text, symbols)
logger.info(f"text_to_sequence without cleaners succeeded, length: {len(text_norm)}")
except Exception as e:
logger.warning(f"text_to_sequence without cleaners failed: {e}")
text_module_available = False
# Jika masih gagal, gunakan mapping manual (hanya untuk debugging)
if not text_norm:
logger.warning("Using manual symbol mapping as fallback")
symbol_to_id = {s: i for i, s in enumerate(symbols)}
# Untuk teks Jepang, manual mapping tidak akan berfungsi karena karakter tidak ada di symbols
# Kita coba konversi teks ke romaji sederhana (hanya untuk karakter latin)
# Sebagai fallback ekstrem, kita buat sequence default
text_norm = []
text_lower = text.lower()
for char in text_lower:
if char in symbol_to_id:
text_norm.append(symbol_to_id[char])
elif char == ' ':
continue
# Jika sequence kosong, buat sequence default "a" (atau simbol pertama)
if not text_norm:
logger.error("Text sequence is empty after all attempts, using default 'a'")
# Gunakan simbol 'a' jika ada, atau simbol pertama
symbol_to_id = {s: i for i, s in enumerate(symbols)}
default_char = 'a' if 'a' in symbol_to_id else symbols[0] if symbols else '_'
text_norm = [symbol_to_id.get(default_char, 0)]
# Tambahkan blank jika add_blank True
add_blank = getattr(hps, 'add_blank', False)
if add_blank:
text_norm = commons.intersperse(text_norm, 0)
logger.info(f"Added blanks, sequence length: {len(text_norm)}")
logger.info(f"Final sequence length: {len(text_norm)}")
return torch.LongTensor(text_norm)
except Exception as e:
logger.error(f"Error in get_text: {e}")
logger.error(traceback.format_exc())
# Return minimal sequence
return torch.LongTensor([0, 1, 2])
# Memuat model
def load_model():
try:
if not os.path.exists(LOCAL_PATHS["config"]):
logger.error(f"Config file not found at {LOCAL_PATHS['config']}")
return None, None, [], []
if not os.path.exists(LOCAL_PATHS["model"]):
logger.error(f"Model file not found at {LOCAL_PATHS['model']}")
return None, None, [], []
logger.info("Loading model...")
# Baca config.json
config_data = read_config_file(LOCAL_PATHS["config"])
if config_data is None:
return None, None, [], []
# Buat hps object sederhana
class HParams:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
# Ekstrak parameter dari config
n_speakers = get_config_param(config_data, ['n_speakers'], 13)
# Ambil symbols dari config
symbols = get_config_param(config_data, ['symbols'], [])
if not symbols:
# Fallback symbols dari config (1).json
symbols = ["_", ",", ".", "!", "?", "-", "A", "E", "I", "N", "O", "Q", "U", "a", "b", "d", "e", "f", "g", "h", "i", "j", "k", "m", "n", "o", "p", "r", "s", "t", "u", "v", "w", "y", "z", "Κƒ", "Κ§", "↓", "↑", " "]
sampling_rate = get_config_param(config_data, ['data', 'sampling_rate'], 22050)
filter_length = get_config_param(config_data, ['data', 'filter_length'], 1024)
hop_length = get_config_param(config_data, ['data', 'hop_length'], 256)
segment_size = get_config_param(config_data, ['train', 'segment_size'], 8192)
# Dapatkan parameter model
model_params = get_config_param(config_data, ['model'], {})
# Cari text_cleaners jika ada
text_cleaners = get_config_param(config_data, ['data', 'text_cleaners'], None)
# Cari add_blank jika ada
add_blank = get_config_param(config_data, ['data', 'add_blank'], False)
# Ambil speakers dari config
speakers = get_config_param(config_data, ['speakers'], [])
if not speakers:
# Fallback ke CHARACTER_MAPPING
speakers = list(CHARACTER_MAPPING.keys())
# Buat hps object
hps = HParams(
n_speakers=n_speakers,
symbols=symbols,
sampling_rate=sampling_rate,
filter_length=filter_length,
hop_length=hop_length,
segment_size=segment_size,
model=model_params,
add_blank=add_blank,
text_cleaners=text_cleaners
)
logger.info(f"Model parameters: n_speakers={n_speakers}, sampling_rate={sampling_rate}, symbols_count={len(symbols)}")
# Buat model
net_g = SynthesizerTrn(
len(symbols),
filter_length // 2 + 1,
segment_size // hop_length,
n_speakers=n_speakers,
**model_params
)
# Load model weights
try:
checkpoint = torch.load(LOCAL_PATHS["model"], map_location='cpu')
# Handle different checkpoint formats
if 'state_dict' in checkpoint:
state_dict = checkpoint['state_dict']
elif 'model' in checkpoint:
state_dict = checkpoint['model']
elif 'net_g' in checkpoint:
state_dict = checkpoint['net_g']
else:
state_dict = checkpoint
# Remove prefixes jika ada
new_state_dict = {}
for k, v in state_dict.items():
name = k.replace('module.', '').replace('net_g.', '')
new_state_dict[name] = v
net_g.load_state_dict(new_state_dict, strict=False)
logger.info("Model weights loaded successfully")
except Exception as e:
logger.error(f"Error loading model weights: {e}")
logger.error(traceback.format_exc())
return None, None, [], []
net_g.eval()
logger.info("Model loaded successfully")
# Ambil daftar speaker dari config
speaker_choices = []
speaker_display_names = []
for speaker in speakers:
if speaker and str(speaker).strip():
speaker_choices.append(str(speaker))
display_name = get_display_name(str(speaker))
speaker_display_names.append(display_name)
logger.info(f"Loaded {len(speaker_choices)} speakers from config")
return hps, net_g, speaker_choices, speaker_display_names
except Exception as e:
logger.error(f"Error loading model: {e}")
logger.error(traceback.format_exc())
return None, None, [], []
# Load model saat startup
hps, net_g, speaker_choices, speaker_display_names = load_model()
# Path untuk gambar
COVER_PATH = LOCAL_PATHS["cover"] if os.path.exists(LOCAL_PATHS["cover"]) else None
def tts_generate(speaker_index, text, speed):
if net_g is None or hps is None:
logger.error("Model not loaded")
return None, COVER_PATH
text = str(text).strip() if text else ""
if not text:
logger.error("Text is empty")
return None, COVER_PATH
if not speaker_choices or speaker_index >= len(speaker_choices):
speaker_index = 0
try:
speaker_name = speaker_choices[speaker_index] if speaker_index < len(speaker_choices) else "Unknown"
logger.info(f"Generating TTS for speaker {speaker_index}: {speaker_name}")
logger.info(f"Processing text: {text[:50]}...")
# Konversi teks ke sequence
stn_tst = get_text(text, hps)
logger.info(f"Text sequence length: {stn_tst.size(0)}")
with torch.no_grad():
x_tst = stn_tst.unsqueeze(0)
x_tst_lengths = torch.LongTensor([stn_tst.size(0)])
sid = torch.LongTensor([speaker_index])
speed = float(speed)
if speed <= 0 or speed > 10:
speed = 1.0
logger.info(f"Using speed factor: {speed}")
# Generate audio
try:
audio = net_g.infer(
x_tst,
x_tst_lengths,
sid=sid,
noise_scale=0.667,
noise_scale_w=0.8,
length_scale=1.0/speed
)[0][0,0].data.cpu().float().numpy()
logger.info(f"Audio generated, shape: {audio.shape}")
except Exception as infer_error:
logger.error(f"Inference error: {infer_error}")
logger.error(traceback.format_exc())
return None, COVER_PATH
# Normalisasi Audio
if len(audio) > 0:
audio_max = np.abs(audio).max()
if audio_max > 0:
audio = (audio / audio_max * 32767).astype(np.int16)
logger.info(f"Audio normalized, max: {audio_max}")
else:
logger.warning("Audio is silent (max=0)")
audio = np.zeros(44100, dtype=np.int16)
else:
logger.warning("Audio is empty")
audio = np.zeros(44100, dtype=np.int16)
# Gunakan sampling_rate dari config
sampling_rate = getattr(hps, 'sampling_rate', 22050)
logger.info(f"Audio generation complete: {len(audio)} samples at {sampling_rate}Hz")
return (sampling_rate, audio), COVER_PATH
except Exception as e:
logger.error(f"Error generating audio: {e}")
logger.error(traceback.format_exc())
return None, COVER_PATH
# Refresh model function
def refresh_model():
global hps, net_g, speaker_choices, speaker_display_names
logger.info("Refreshing model...")
hps, net_g, speaker_choices, speaker_display_names = load_model()
if speaker_display_names and len(speaker_display_names) > 0:
return gr.update(choices=speaker_display_names, value=speaker_display_names[0]), f"βœ… Model refreshed! {len(speaker_display_names)} speakers loaded"
else:
return gr.update(choices=["No speakers found"], value="No speakers found"), "❌ Failed to load speakers"
def test_generation():
if net_g is None:
return None, "❌ Model not loaded"
# Gunakan teks Jepang untuk test
test_text = "こんにけは"
try:
result, _ = tts_generate(0, test_text, 1.0)
if result:
speaker_display = speaker_display_names[0] if speaker_display_names else "Default"
return result, f"βœ… Test successful with {speaker_display}!"
else:
return None, "❌ Test failed - no audio generated"
except Exception as e:
return None, f"❌ Test error: {str(e)[:100]}"
# --- TEMA HIJAU DAN PUTIH YANG CERAH DAN INDAH ---
with gr.Blocks(css="""
/* Reset dan dasar */
* {
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #f0fff4 0%, #e6fffa 100%);
min-height: 100vh;
margin: 0;
padding: 20px;
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
color: #2d3748;
}
/* Container utama */
.gradio-container {
background: white !important;
border-radius: 24px !important;
box-shadow:
0 20px 60px rgba(72, 187, 120, 0.15),
0 1px 3px rgba(0, 0, 0, 0.05) !important;
border: 1px solid #e2e8f0 !important;
max-width: 1200px !important;
margin: 20px auto !important;
padding: 0 !important;
overflow: hidden;
position: relative;
}
/* Header yang elegan - JUDUL YOSUGA NO SORA */
.header-style {
text-align: center;
padding: 50px 30px 40px !important;
background: linear-gradient(135deg, #ffffff 0%, #f7fff9 100%);
border-bottom: 1px solid #e2e8f0;
margin: 0 !important;
position: relative;
overflow: hidden;
}
.header-style::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #38a169 0%, #48bb78 50%, #68d391 100%);
}
.header-style h1 {
font-family: 'Segoe UI', sans-serif;
font-weight: 800;
letter-spacing: -0.5px;
color: transparent;
margin: 0 0 10px 0;
font-size: 3.2rem;
background: linear-gradient(135deg, #38a169 0%, #68d391 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 2px 4px rgba(56, 161, 105, 0.1);
}
.header-style p {
color: #718096;
font-size: 1.3rem;
letter-spacing: 1px;
margin-top: 15px;
font-weight: 400;
opacity: 0.9;
text-transform: uppercase;
}
/* Premium badge */
.premium-badge {
display: inline-block;
background: linear-gradient(135deg, #ffd700 0%, #ffecb3 100%);
color: #8d6e00;
padding: 8px 25px;
border-radius: 25px;
font-size: 1rem;
font-weight: 800;
letter-spacing: 2px;
margin-left: 20px;
vertical-align: middle;
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
border: 2px solid #ffd700;
}
/* Tombol utama - gradien hijau elegan */
.btn-lux {
background: linear-gradient(135deg, #38a169 0%, #48bb78 100%) !important;
color: white !important;
border-radius: 20px !important;
border: none !important;
height: 65px !important;
font-weight: 700 !important;
font-size: 1.2rem !important;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
margin: 20px 0 !important;
box-shadow:
0 8px 25px rgba(56, 161, 105, 0.3),
0 2px 6px rgba(0, 0, 0, 0.1) !important;
position: relative;
overflow: hidden;
letter-spacing: 0.5px;
}
.btn-lux:hover {
transform: translateY(-3px) !important;
box-shadow:
0 15px 35px rgba(56, 161, 105, 0.4),
0 5px 15px rgba(0, 0, 0, 0.1) !important;
background: linear-gradient(135deg, #2f855a 0%, #38a169 100%) !important;
}
/* Tombol sekunder - hijau muda modern */
.btn-secondary {
background: linear-gradient(135deg, #f0fff4 0%, #e6fffa 100%) !important;
color: #2d3748 !important;
border-radius: 16px !important;
border: 2px solid #c6f6d5 !important;
height: 50px !important;
font-weight: 600 !important;
cursor: pointer;
transition: all 0.2s ease !important;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.05) !important;
}
.btn-secondary:hover {
background: linear-gradient(135deg, #ffffff 0%, #f0fff4 100%) !important;
border-color: #9ae6b4 !important;
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.08) !important;
}
/* Textbox - clean design dengan sentuhan hijau */
.textbox-style textarea {
background: #f7fff9 !important;
border: 2px solid #e2e8f0 !important;
color: #2d3748 !important;
border-radius: 20px !important;
padding: 25px !important;
font-size: 1.1rem !important;
min-height: 200px !important;
transition: all 0.3s ease !important;
font-family: 'Segoe UI', sans-serif;
line-height: 1.7;
}
.textbox-style textarea:focus {
border-color: #48bb78 !important;
background: white !important;
box-shadow: 0 0 0 4px rgba(72, 187, 120, 0.15) !important;
outline: none !important;
}
/* Dropdown - modern dengan font untuk karakter Jepang */
.dropdown-style select {
background: #f7fff9 !important;
color: #2d3748 !important;
border: 2px solid #e2e8f0 !important;
border-radius: 16px !important;
padding: 15px 20px !important;
font-size: 1.1rem !important;
font-weight: 600;
transition: all 0.3s ease;
font-family: 'Segoe UI', 'Hiragino Sans', 'Yu Gothic', sans-serif !important;
}
.dropdown-style .gr-dropdown {
font-family: 'Segoe UI', 'Hiragino Sans', 'Yu Gothic', sans-serif !important;
}
.dropdown-style option {
font-family: 'Segoe UI', 'Hiragino Sans', 'Yu Gothic', sans-serif !important;
padding: 12px !important;
}
.dropdown-style select:focus {
border-color: #48bb78 !important;
background: white !important;
box-shadow: 0 0 0 4px rgba(72, 187, 120, 0.15) !important;
}
/* Styling khusus untuk teks karakter */
.character-name {
font-family: 'Hiragino Sans', 'Yu Gothic', sans-serif;
font-weight: 600;
color: #2d3748;
}
.character-romaji {
font-family: 'Segoe UI', Arial, sans-serif;
color: #718096;
font-size: 0.95em;
font-weight: 400;
font-style: italic;
}
/* Slider - design hijau */
.slider-style .gr-slider {
background: #f7fff9 !important;
border-radius: 12px !important;
border: 2px solid #e2e8f0 !important;
}
.slider-style .gr-slider .gr-slider-track {
background: linear-gradient(90deg, #38a169 0%, #68d391 100%) !important;
}
/* Image container */
.image-container {
border-radius: 24px !important;
overflow: hidden !important;
border: 3px solid white !important;
box-shadow:
0 15px 35px rgba(0, 0, 0, 0.08),
0 3px 10px rgba(0, 0, 0, 0.05) !important;
background: white !important;
transition: transform 0.3s ease, box-shadow 0.3s ease;
width: 100% !important;
height: 450px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.image-container img {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
object-position: center center !important;
border-radius: 21px !important;
}
/* Status boxes - SAMA SEPERTI APP.PY AWAL */
.status-box {
background: #f8f9fa !important;
border: 2px solid #e9ecef !important;
border-radius: 16px !important;
padding: 20px !important;
margin: 15px 0 !important;
color: #2c3e50;
font-family: 'SF Mono', 'Monaco', monospace;
font-size: 0.9rem;
line-height: 1.5;
}
.success-box {
background: linear-gradient(135deg, #f0fff4 0%, #e6fffa 100%) !important;
border: 2px solid #c6f6d5 !important;
color: #22543d !important;
border-radius: 16px !important;
padding: 20px !important;
margin: 15px 0 !important;
}
.error-box {
background: linear-gradient(135deg, #fff5f5 0%, #ffe6e6 100%) !important;
border: 2px solid #fed7d7 !important;
color: #742a2a !important;
border-radius: 16px !important;
padding: 20px !important;
margin: 15px 0 !important;
}
/* Footer */
.footer-style {
text-align: center;
margin-top: 40px;
padding: 30px;
color: #718096;
font-size: 0.95rem;
letter-spacing: 0.5px;
border-top: 1px solid #e2e8f0;
background: #f7fff9;
border-radius: 0 0 24px 24px;
}
/* Audio player custom */
audio {
border-radius: 20px !important;
background: #f7fff9 !important;
border: 2px solid #e2e8f0 !important;
padding: 15px !important;
width: 100% !important;
}
/* Label styling */
label {
font-weight: 700 !important;
color: #2d3748 !important;
margin-bottom: 12px !important;
font-size: 1.1rem !important;
letter-spacing: 0.3px;
}
/* Card container */
.card {
background: white;
border-radius: 24px;
padding: 30px;
box-shadow:
0 8px 25px rgba(0, 0, 0, 0.05),
0 2px 6px rgba(0, 0, 0, 0.03);
border: 1px solid #e2e8f0;
margin-bottom: 25px;
}
/* Grid layout */
.grid-container {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 35px;
padding: 35px;
}
@media (max-width: 768px) {
.grid-container {
grid-template-columns: 1fr;
gap: 25px;
padding: 25px;
}
.header-style h1 {
font-size: 2.5rem;
}
.header-style p {
font-size: 1.1rem;
}
.image-container {
height: 380px !important;
}
}
/* Loading animation hijau */
.loading-spinner {
display: inline-block;
width: 24px;
height: 24px;
border: 3px solid rgba(56, 161, 105, 0.2);
border-radius: 50%;
border-top-color: #38a169;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Character info */
.character-info {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
}
.character-japanese {
font-size: 1.2em;
font-weight: 600;
color: #2d3748;
}
/* Example box */
.example-box {
background: linear-gradient(135deg, #f7fff9 0%, #e6fffa 100%);
border: 2px solid #c6f6d5;
border-radius: 16px;
padding: 20px;
margin: 15px 0;
}
.example-japanese {
font-family: 'Hiragino Sans', sans-serif;
font-size: 1.1em;
color: #2d3748;
margin-bottom: 5px;
}
.example-romaji {
color: #718096;
font-size: 0.95em;
font-style: italic;
margin-bottom: 10px;
}
/* Gradient text */
.gradient-text {
background: linear-gradient(135deg, #38a169 0%, #68d391 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Character card */
.character-card {
background: white;
border-radius: 16px;
padding: 20px;
border: 2px solid #e2e8f0;
transition: all 0.3s ease;
}
.character-card:hover {
border-color: #9ae6b4;
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
}
""") as demo:
gr.HTML("""
<div class='header-style'>
<h1>πŸ’šWAIFU ANIME - TTSπŸ’š</h1>
<p>Generate Voice Anime</span></p>
</div>
""")
with gr.Row(elem_classes="grid-container"):
with gr.Column(scale=1):
# Status Box
status_md = gr.Markdown(f"""
<div class="{'success-box' if net_g else 'error-box'}">
<strong>⚑ SYSTEM STATUS</strong><br>
<span style="color: {'#38a169' if net_g else '#e53e3e'}">●</span> Model: {'βœ… LOADED' if net_g else '❌ NOT LOADED'}<br>
<span style="color: {'#38a169' if speaker_choices else '#e53e3e'}">●</span> Characters: {len(speaker_choices) if speaker_choices else 0}<br>
<span style="color: {'#38a169' if os.path.exists(COVER_PATH) else '#e53e3e'}">●</span> Cover: {'βœ… PRESENT' if os.path.exists(COVER_PATH) else '❌ MISSING'}<br>
<hr style="margin: 15px 0; border-color: rgba(56, 161, 105, 0.2)">
<small class="gradient-text">v3.0 β€’ Ready for generation</small>
</div>
""", elem_classes="card")
# Cover Image
if COVER_PATH and os.path.exists(COVER_PATH):
img_disp = gr.Image(
value=COVER_PATH,
show_label=False,
interactive=False,
elem_classes="image-container hover-lift"
)
else:
img_disp = gr.Image(
value=None,
show_label=False,
interactive=False,
elem_classes="image-container"
)
gr.Markdown(f"""
<div class="error-box">
<strong>⚠️ WARNING:</strong> Cover image not found!<br>
Expected at: {LOCAL_PATHS["cover"]}<br>
Please check if file exists or try refreshing.
</div>
""")
# Controls
with gr.Group(elem_classes="card"):
# Dropdown untuk pilihan karakter
spk_input = gr.Dropdown(
choices=speaker_display_names if speaker_display_names else [],
label="🎭 SELECT CHARACTER",
value=speaker_display_names[0] if speaker_display_names else "No characters",
type="index",
elem_classes="dropdown-style",
info="Japanese name with Romaji pronunciation"
)
# Display informasi karakter terpilih
if speaker_display_names:
current_jp, current_romaji = split_name_display(speaker_display_names[0])
gr.Markdown(f"""
<div class="success-box">
<strong>🎯 SELECTED CHARACTER:</strong><br>
<div class="character-info">
<span class="character-japanese">{current_jp}</span>
<span class="character-romaji">{current_romaji}</span>
</div>
</div>
""")
with gr.Row():
refresh_btn = gr.Button("πŸ”„ Refresh Model", elem_classes="btn-secondary")
test_btn = gr.Button("πŸ”Š Test Voice", elem_classes="btn-secondary")
refresh_status = gr.Markdown("", elem_classes="status-box")
speed_sl = gr.Slider(
0.5, 2.0, 1.0, step=0.1,
label="⚑ SPEED CONTROL",
elem_classes="slider-style",
info="Lower = slower, Higher = faster"
)
gr.Markdown("""
<div class="example-box">
<strong>πŸ’‘ PENTING:</strong><br>
Model ini menggunakan <strong>Japanese text cleaners</strong>!<br><br>
Gunakan teks bahasa Jepang (Hiragana, Katakana, Kanji).<br>
Contoh: γ“γ‚“γ«γ‘γ―γ€ε…ƒζ°—γ§γ™γ‹οΌŸ
</div>
""")
with gr.Column(scale=2):
# Text Input - DIUBAH UNTUK TEKS JEPANG
with gr.Group(elem_classes="card"):
text_in = gr.Textbox(
label="πŸ“ MASUKKAN TEKS BAHASA JEPANG",
placeholder="Contoh: γ“γ‚“γ«γ‘γ―γ€ε…ƒζ°—γ§γ™γ‹οΌŸ",
lines=8,
elem_classes="textbox-style",
max_lines=12,
info="Masukkan teks dalam bahasa Jepang (Hiragana, Katakana, Kanji)"
)
# Example texts - DIUBAH KE CONTOH JEPANG
with gr.Accordion("πŸ’‘ CONTOH TEKS", open=False):
gr.Markdown("""
<div class="example-box">
<div class="example-japanese">こんにけは</div>
<div class="example-romaji">(Konnichiwa)</div>
<em>Halo / Selamat siang</em>
</div>
<div class="example-box">
<div class="example-japanese">γŠγ―γ‚ˆγ†γ”γ–γ„γΎγ™</div>
<div class="example-romaji">(Ohayou gozaimasu)</div>
<em>Selamat pagi</em>
</div>
<div class="example-box">
<div class="example-japanese">γ‚γ‚ŠγŒγ¨γ†</div>
<div class="example-romaji">(Arigatou)</div>
<em>Terima kasih</em>
</div>
<div class="example-box">
<div class="example-japanese">ε₯½γγ§γ™</div>
<div class="example-romaji">(Suki desu)</div>
<em>Aku suka kamu</em>
</div>
""")
# Generate Button
gen_btn = gr.Button("✨ GENERATE VOICE ✨", elem_classes="btn-lux", scale=1)
# Audio Output
with gr.Group(elem_classes="card"):
audio_out = gr.Audio(
label="🎧 SUARA YANG DIHASILKAN",
type="numpy",
interactive=False
)
# Controls Row
with gr.Row():
clear_btn = gr.Button("πŸ—‘οΈ Hapus Semua", elem_classes="btn-secondary")
# Character Info Panel
with gr.Accordion("🎭 INFORMASI KARAKTER", open=False):
if speaker_display_names:
char_info_html = "<div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px;'>"
for i, display_name in enumerate(speaker_display_names):
jp_name, romaji = split_name_display(display_name)
char_info_html += f"""
<div class="character-card">
<div style="font-weight: 600; color: #38a169; margin-bottom: 10px;">Karakter {i+1}</div>
<div style="font-size: 1.1em; font-family: 'Hiragino Sans'; margin: 5px 0;">{jp_name}</div>
<div style="color: #718096; font-size: 0.9em;">{romaji}</div>
<div style="margin-top: 10px; font-size: 0.85em; color: #718096;">
<span style="color: #38a169;">●</span> Suara Premium
</div>
</div>
"""
char_info_html += "</div>"
gr.HTML(char_info_html)
else:
gr.Markdown("Tidak ada informasi karakter.")
# Footer
gr.HTML("""
<div class='footer-style'>
DIBUAT OLEH PLANA-CHAN<br>
<small style="opacity: 0.7;">WAIFU ANIME Premium TTS v3.0 β€’ Powered by VITS</small>
</div>
""")
# Event handlers
def on_speaker_change(speaker_index):
"""Update character info when speaker changes"""
if speaker_display_names and speaker_index < len(speaker_display_names):
display_name = speaker_display_names[0]
jp_name, romaji = split_name_display(display_name)
return f"""
<div class="success-box">
<strong>🎯 SELECTED CHARACTER:</strong><br>
<div class="character-info">
<span class="character-japanese">{jp_name}</span>
<span class="character-romaji">{romaji}</span>
</div>
</div>
"""
return ""
# Connect buttons
gen_btn.click(
tts_generate,
inputs=[spk_input, text_in, speed_sl],
outputs=[audio_out, img_disp]
)
refresh_btn.click(
refresh_model,
inputs=[],
outputs=[spk_input, refresh_status]
).then(
lambda: f"""
<div class="success-box">
<strong>⚑ SYSTEM STATUS</strong><br>
<span style="color: {'#38a169' if net_g else '#e53e3e'}">●</span> Model: {'βœ… LOADED' if net_g else '❌ NOT LOADED'}<br>
<span style="color: {'#38a169' if speaker_choices else '#e53e3e'}">●</span> Characters: {len(speaker_choices) if speaker_choices else 0}<br>
<span style="color: {'#38a169' if os.path.exists(COVER_PATH) else '#e53e3e'}">●</span> Cover: {'βœ… PRESENT' if os.path.exists(COVER_PATH) else '❌ MISSING'}<br>
<hr style="margin: 15px 0; border-color: rgba(56, 161, 105, 0.2)">
<small class="gradient-text">v3.0 β€’ Model refreshed at {gr.utils.get_current_time()}</small>
</div>
""",
outputs=[status_md]
)
test_btn.click(
test_generation,
inputs=[],
outputs=[audio_out, refresh_status]
)
clear_btn.click(
lambda: ("", None, ""),
outputs=[text_in, audio_out, refresh_status]
)
# Update character info when speaker changes
spk_input.change(
on_speaker_change,
inputs=[spk_input],
outputs=[status_md]
)
# Launch settings
if __name__ == "__main__":
print("=" * 70)
print("WAIFU-ANIME-TTS - PREMIUM VOICE GENERATION")
print("=" * 70)
print(f"Model Status: {'βœ… LOADED' if net_g else '❌ NOT LOADED'}")
print(f"Characters: {len(speaker_choices) if speaker_choices else 0}")
if speaker_display_names:
for i, name in enumerate(speaker_display_names):
print(f" {i+1}. {name}")
# Cek file gambar
if os.path.exists(LOCAL_PATHS["cover"]):
print(f"Cover Image: βœ… PRESENT at {LOCAL_PATHS['cover']}")
else:
print(f"Cover Image: ❌ MISSING - Expected at {LOCAL_PATHS['cover']}")
print("=" * 70)
print("Starting server on http://localhost:7860")
print("=" * 70)
try:
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
debug=False,
show_error=True,
quiet=True,
show_api=False
)
except Exception as e:
print(f"Failed to launch: {e}")
print("Trying alternative port...")
demo.launch(
server_name="0.0.0.0",
server_port=7861,
share=False,
debug=True,
show_error=True
)