Plana-Archive's picture
Migrated content from repo: Plana-Archive/Anime-Spaces
d4b1959 verified
Raw
History Blame Contribute Delete
31.9 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
import random
import re
# 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/Yosuga-no-Sora/resolve/main/saved_model"
# File yang diperlukan - SEMUA FILE DARI HUGGING FACE
FILES_TO_DOWNLOAD = {
"config.json": f"{BASE_URL}/3/config.json",
"model.pth": f"{BASE_URL}/3/model.pth",
"cover.png": f"{BASE_URL}/3/cover.png"
}
# Path lokal
LOCAL_DIR = "saved_model/3"
LOCAL_PATHS = {
"config": os.path.join(LOCAL_DIR, "config.json"),
"model": os.path.join(LOCAL_DIR, "model.pth"),
"cover": os.path.join(LOCAL_DIR, "cover.png")
}
# Download file jika belum ada
logger.info("Checking for required files...")
for file_name, url in FILES_TO_DOWNLOAD.items():
local_path = os.path.join(LOCAL_DIR, file_name)
if not os.path.exists(local_path):
logger.info(f"Downloading {file_name}...")
success = download_file(url, local_path)
if not success:
logger.error(f"Failed to download {file_name}")
else:
logger.info(f"{file_name} already exists")
# Mapping untuk nama karakter Jepang ke Romaji
CHARACTER_MAPPING = {
"春日野穹": "Kasugano Sora",
"天女目瑛": "Amatsume Akira",
"依媛奈緒": "Yorihime Nao",
"渚一葉": "Ichika Nagisa",
"Kasugano Sora": "春日野穹",
"Amatsume Akira": "天女目瑛",
"Yorihime Nao": "依媛奈緒",
"Ichika Nagisa": "渚一葉",
}
# 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]})"
# Jika tidak ada mapping, cek apakah ini nama Romaji
for jp_name, romaji in CHARACTER_MAPPING.items():
if romaji == name:
return f"{jp_name} ({name})"
# Jika tidak ditemukan, kembalikan aslinya
return name
# Fungsi konversi teks ke urutan angka dengan error handling yang lebih baik
def get_text(text, hps):
"""Convert text to sequence"""
try:
from text import text_to_sequence
logger.info(f"Processing text: {text[:50]}...")
try:
text_norm = text_to_sequence(text, hps.symbols, hps.data.text_cleaners)
logger.info("Using text_to_sequence with symbols and cleaners")
except TypeError as e:
logger.warning(f"Version 1 failed: {e}. Trying alternative...")
try:
text_norm = text_to_sequence(text, cleaner_names=hps.data.text_cleaners)
logger.info("Using text_to_sequence with cleaner_names only")
except TypeError as e2:
logger.warning(f"Version 2 failed: {e2}. Trying simple version...")
text_norm = text_to_sequence(text)
logger.info("Using simple text_to_sequence")
if hps.data.add_blank:
text_norm = commons.intersperse(text_norm, 0)
text_norm = torch.LongTensor(text_norm)
logger.info(f"Text converted to sequence of length: {len(text_norm)}")
return text_norm
except Exception as e:
logger.error(f"Error in get_text: {e}")
logger.error(traceback.format_exc())
try:
if hasattr(hps, 'symbols'):
symbol_to_id = {s: i for i, s in enumerate(hps.symbols)}
text_norm = []
for char in text:
if char in symbol_to_id:
text_norm.append(symbol_to_id[char])
if hps.data.add_blank:
text_norm = commons.intersperse(text_norm, 0)
logger.info(f"Fallback successful: {len(text_norm)} tokens")
return torch.LongTensor(text_norm)
except Exception as e2:
logger.error(f"Fallback also failed: {e2}")
logger.warning("All methods failed, using default text")
default_text = "こんにちは"
text_norm = text_to_sequence(default_text, hps.symbols, hps.data.text_cleaners)
if hps.data.add_blank:
text_norm = commons.intersperse(text_norm, 0)
return torch.LongTensor(text_norm)
# 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...")
hps = utils.get_hparams_from_file(LOCAL_PATHS["config"])
net_g = SynthesizerTrn(
len(hps.symbols),
hps.data.filter_length // 2 + 1,
hps.train.segment_size // hps.data.hop_length,
n_speakers=hps.data.n_speakers,
**hps.model
)
try:
utils.load_checkpoint(LOCAL_PATHS["model"], net_g, None)
logger.info("Model loaded using utils.load_checkpoint")
except Exception as e:
logger.warning(f"Standard load failed: {e}. Trying alternative...")
checkpoint = torch.load(LOCAL_PATHS["model"], map_location='cpu')
state_dict = {}
for k, v in checkpoint.items():
if isinstance(k, str):
if k.startswith('net_g.'):
state_dict[k[6:]] = v
elif k.startswith('module.'):
state_dict[k[7:]] = v
else:
state_dict[k] = v
missing_keys, unexpected_keys = net_g.load_state_dict(state_dict, strict=False)
if missing_keys:
logger.warning(f"Missing keys: {missing_keys}")
if unexpected_keys:
logger.warning(f"Unexpected keys: {unexpected_keys}")
net_g.eval()
speaker_choices = []
speaker_display_names = []
if hasattr(hps, 'speakers') and hps.speakers:
for s in hps.speakers:
if s and str(s) != "None" and str(s).strip():
speaker_choices.append(str(s))
display_name = get_display_name(str(s))
speaker_display_names.append(display_name)
if not speaker_choices:
speaker_choices = ["春日野穹", "天女目瑛", "依媛奈緒", "渚一葉"]
speaker_display_names = [get_display_name(name) for name in speaker_choices]
logger.info(f"Model loaded successfully with {len(speaker_choices)} speakers")
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 or not text:
logger.error("Model not loaded or text is empty")
return None, COVER_PATH
if not speaker_choices or speaker_index >= len(speaker_choices):
speaker_index = 0
logger.warning("Invalid speaker index, using 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"Text: {text[:100]}...")
logger.info(f"Speed: {speed}")
text = str(text).strip()
if not text:
logger.error("Empty text provided")
return None, COVER_PATH
# Validasi Japanese characters
has_japanese = bool(re.search(r'[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f]', text))
if not has_japanese:
logger.warning("Text may not contain Japanese characters")
stn_tst = get_text(text, hps)
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.warning(f"Speed out of range, set to 1.0")
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()
if len(audio) > 0:
audio_max = np.abs(audio).max()
if audio_max > 0:
audio = (audio / audio_max * 32767).astype(np.int16)
else:
audio = np.zeros(44100, dtype=np.int16)
else:
audio = np.zeros(44100, dtype=np.int16)
return (hps.data.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 (kembalikan tuple lengkap untuk update)
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:
# Kembalikan: radio_update, selected_idx, selected_display_text, info_html, status_msg, cover_img_update
radio_update = gr.update(choices=speaker_display_names, value=speaker_display_names[0])
selected_idx = 0
selected_display_text = f"📍 *Selected: **{speaker_display_names[0]}***"
# Buat HTML info karakter
info_html = '<div class="char-info-grid">'
for jp, disp in zip(speaker_choices, speaker_display_names):
romaji = disp.split('(')[-1].rstrip(')') if '(' in disp else ""
info_html += f"""
<div class="char-card">
<div class="char-jp">{jp}</div>
<div class="char-romaji">{romaji}</div>
</div>
"""
info_html += '</div>'
status_msg = f"✅ Model refreshed! Loaded {len(speaker_display_names)} speakers"
cover_update = gr.update(value=COVER_PATH) if COVER_PATH else gr.update()
return radio_update, selected_idx, selected_display_text, info_html, status_msg, cover_update
else:
radio_update = gr.update(choices=["No characters"], value="No characters")
selected_idx = 0
selected_display_text = "📍 *Selected: None*"
info_html = "<div class='char-info-grid'>No characters available</div>"
status_msg = "❌ Failed to load speakers"
cover_update = gr.update()
return radio_update, selected_idx, selected_display_text, info_html, status_msg, cover_update
# Test TTS function
def test_generation():
if net_g is None:
return None, "❌ Model not loaded"
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]}"
# Fungsi untuk mendapatkan teks random
def get_random_jp():
texts = [
"こんにちは!",
"お元気ですか?",
"先生、お疲れ様です!",
"大好きだよ!",
"また明日ね。",
"お兄ちゃん、大好き!",
"一緒にいようね。",
"今日はいい天気ですね。"
]
return random.choice(texts)
# --- CSS GABUNGAN (asli + gaya dari app(1).py) ---
css = """
/* Reset dan dasar */
* {
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%);
min-height: 100vh;
margin: 0;
padding: 20px;
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
color: #333;
}
.gradio-container {
background: white !important;
border-radius: 24px !important;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.05) !important;
border: 1px solid rgba(255, 255, 255, 0.9) !important;
backdrop-filter: blur(20px);
max-width: 1200px !important;
margin: 20px auto !important;
padding: 0 !important;
overflow: hidden;
}
.header-style {
text-align: center;
padding: 50px 30px 40px !important;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
margin: 0 !important;
position: relative;
overflow: hidden;
}
.header-style::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
}
.header-style h1 {
font-family: 'Segoe UI', sans-serif;
font-weight: 700;
letter-spacing: 1px;
color: #2c3e50;
margin: 0 0 10px 0;
font-size: 2.8rem;
text-shadow: 0 1px 2px rgba(0,0,0,0.1);
background: linear-gradient(135deg, #2c3e50 0%, #4a6491 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-style p {
color: #7f8c8d;
font-size: 1.1rem;
letter-spacing: 3px;
margin-top: 15px;
font-weight: 300;
text-transform: uppercase;
opacity: 0.9;
}
.btn-lux {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
border-radius: 16px !important;
border: none !important;
height: 60px !important;
font-weight: 600 !important;
font-size: 1.1rem !important;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
margin: 15px 0 !important;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3), 0 1px 3px rgba(0, 0, 0, 0.1) !important;
}
.btn-lux:hover {
transform: translateY(-2px) !important;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4), 0 2px 5px rgba(0, 0, 0, 0.1) !important;
}
.btn-secondary {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important;
color: #495057 !important;
border-radius: 12px !important;
border: 1px solid #dee2e6 !important;
height: 45px !important;
font-weight: 500 !important;
cursor: pointer;
transition: all 0.2s ease !important;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05) !important;
}
.btn-secondary:hover {
background: linear-gradient(135deg, #ffffff 0%, #f1f3f5 100%) !important;
border-color: #adb5bd !important;
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08) !important;
}
/* Radio group untuk karakter (seperti tombol) */
.character-radio-group {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) !important;
gap: 10px !important;
background: #fafbfc !important;
padding: 15px !important;
border-radius: 16px !important;
border: 2px solid #e9ecef !important;
margin-bottom: 20px !important;
}
.character-radio-group .gr-radio {
background: white !important;
border: 1px solid #e2e8f0 !important;
border-left: 5px solid #a8e6cf !important;
border-radius: 12px !important;
padding: 12px 10px !important;
margin: 0 !important;
cursor: pointer !important;
transition: all 0.2s !important;
font-weight: 500 !important;
color: #4a5568 !important;
}
.character-radio-group .gr-radio:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
border-color: #cbd5e0 !important;
}
.character-radio-group .gr-radio.selected {
background: #e6fffa !important;
border-left-color: #319795 !important;
border-color: #81e6d9 !important;
color: #234e52 !important;
}
/* Warning card */
.warning-card {
background: #fffcf0;
border: 2px dashed #ffd3b6;
border-radius: 16px;
padding: 15px;
margin: 20px 0;
text-align: center;
}
.warning-title {
color: #f5a623;
font-weight: 800;
font-size: 13px;
letter-spacing: 1px;
}
.warning-text {
color: #855d1a;
font-size: 12px;
font-weight: 600;
margin-top: 5px;
}
/* Tombol random (jp-btn) */
.jp-btn {
background: #f8fafc !important;
border: 1px solid #cbd5e1 !important;
color: #475569 !important;
font-weight: 700 !important;
border-radius: 10px !important;
margin-bottom: 15px !important;
font-size: 12px !important;
width: 100%;
transition: all 0.2s;
}
.jp-btn:hover {
background: #f1f5f9 !important;
border-color: #94a3b8 !important;
}
/* GIF container */
.gif-container {
display: flex;
justify-content: center;
margin: 20px 0;
}
.gif-container img {
border-radius: 16px;
max-width: 100%;
height: auto;
border: 1px solid #e1f0e5;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
/* Character Information Panel - selalu terlihat */
.char-info-panel {
background: white;
border: 2px solid #e1f0e5;
border-radius: 16px;
padding: 15px;
margin: 20px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
}
.char-info-title {
font-weight: 800;
color: #2d3748;
border-bottom: 2px solid #a8e6cf;
display: inline-block;
padding-bottom: 5px;
margin-bottom: 15px;
font-size: 1.1rem;
}
.char-info-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
max-height: 300px;
overflow-y: auto;
padding: 5px;
}
.char-card {
border: 1px solid #e1f0e5;
border-radius: 12px;
padding: 10px;
border-left: 5px solid #a8e6cf;
background: #fff;
}
.char-jp {
font-weight: 700;
color: #2d3748;
font-size: 14px;
}
.char-romaji {
color: #a0aec0;
font-size: 11px;
margin-top: 4px;
}
/* Textbox */
.textbox-style textarea {
background: #f8f9fa !important;
border: 2px solid #e9ecef !important;
color: #2c3e50 !important;
border-radius: 16px !important;
padding: 20px !important;
font-size: 1rem !important;
min-height: 180px !important;
transition: all 0.3s ease !important;
font-family: 'Segoe UI', sans-serif;
}
.textbox-style textarea:focus {
border-color: #667eea !important;
background: white !important;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
}
/* Slider */
.slider-style .gr-slider {
background: #f8f9fa !important;
border-radius: 10px !important;
border: 2px solid #e9ecef !important;
}
.slider-style .gr-slider .gr-slider-track {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%) !important;
}
/* Image container */
.image-container {
border-radius: 20px !important;
overflow: hidden !important;
border: 2px solid white !important;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.05) !important;
background: white !important;
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: 18px !important;
}
/* Status boxes */
.status-box {
background: #f8f9fa !important;
border: 2px solid #e9ecef !important;
border-radius: 16px !important;
padding: 20px !important;
margin: 15px 0 !important;
color: #2c3e50;
}
.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;
}
.footer-style {
text-align: center;
margin-top: 40px;
padding: 25px;
color: #7f8c8d;
font-size: 0.9rem;
border-top: 1px solid #f1f2f6;
background: #f8f9fa;
border-radius: 0 0 24px 24px;
}
@media (max-width: 768px) {
.grid-container {
grid-template-columns: 1fr !important;
gap: 20px !important;
padding: 20px !important;
}
.image-container {
height: 350px !important;
}
.character-radio-group {
grid-template-columns: 1fr !important;
}
}
"""
# --- MEMBANGUN UI ---
with gr.Blocks(css=css, title="Yosuga no Sora TTS") as demo:
# Header
gr.HTML("""
<div class='header-style'>
<h1>KASUGANO SORA</h1>
<p>YOSUGA NO SORA PREMIUM TTS</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: 10px 0; border-color: rgba(0,0,0,0.1)">
<small>v3.0 • Ready for generation</small>
</div>
""")
# Cover Image
img_disp = gr.Image(value=COVER_PATH, show_label=False, interactive=False, elem_classes="image-container") if COVER_PATH else gr.Image(value=None, show_label=False, interactive=False, elem_classes="image-container")
# --- SELECT CHARACTER (Radio bergaya tombol) ---
gr.Markdown("### 🎭 Select Character")
selected_char_idx = gr.State(value=0 if speaker_choices else None)
selected_char_display = gr.Markdown(f"📍 *Selected: **{speaker_display_names[0] if speaker_display_names else 'None'}***")
if speaker_display_names:
char_radio = gr.Radio(
choices=speaker_display_names,
value=speaker_display_names[0],
label=None,
type="index",
elem_classes="character-radio-group"
)
else:
char_radio = gr.Radio(choices=["No characters"], value="No characters", label=None, type="index", elem_classes="character-radio-group")
def update_selected(idx):
if idx is not None and speaker_display_names and idx < len(speaker_display_names):
return idx, f"📍 *Selected: **{speaker_display_names[idx]}***"
return 0, "📍 *Selected: None*"
char_radio.change(update_selected, inputs=[char_radio], outputs=[selected_char_idx, selected_char_display])
# --- WARNING CARD ---
gr.HTML("""
<div class="warning-card">
<div class="warning-title">🔖 PERINGATAN MINNA 🔖</div>
<div class="warning-text">Cara pakai: klik character, masukkan text, lalu Generate! ✨</div>
</div>
""")
# --- Random Text Button ---
random_btn = gr.Button("🎲 INPUTS RANDOM TEXT 🎲", elem_classes="jp-btn")
# --- GIF ---
gr.HTML("""
<div class="gif-container">
<img src="https://huggingface.co/spaces/Plana-Archive/MOE-TTS/resolve/main/kurumi-tokisaki.gif" alt="Kurumi GIF">
</div>
""")
# --- Character Information Panel (selalu terlihat) ---
# Panel ini akan diupdate saat refresh, jadi kita buat sebagai komponen HTML
char_info_html = gr.HTML(value="")
# Fungsi untuk mengisi awal
def build_char_info_html():
if speaker_choices and speaker_display_names:
info_html = '<div class="char-info-panel"><div class="char-info-title">📑 Character Information</div><div class="char-info-grid">'
for jp, disp in zip(speaker_choices, speaker_display_names):
romaji = disp.split('(')[-1].rstrip(')') if '(' in disp else ""
info_html += f"""
<div class="char-card">
<div class="char-jp">{jp}</div>
<div class="char-romaji">{romaji}</div>
</div>
"""
info_html += '</div></div>'
return info_html
else:
return '<div class="char-info-panel"><div class="char-info-title">📑 Character Information</div>No characters available</div>'
char_info_html.value = build_char_info_html()
# --- Refresh & Test Buttons ---
with gr.Row():
refresh_btn = gr.Button("🔄 Refresh Model", elem_classes="btn-secondary")
test_btn = gr.Button("🔊 Test Audio", elem_classes="btn-secondary")
refresh_status = gr.Markdown("", elem_classes="status-box")
# Speed slider
speed_sl = gr.Slider(0.5, 2.0, 1.0, step=0.1, label="⚡ SPEED CONTROL", elem_classes="slider-style")
with gr.Column(scale=2):
# Text Input
text_in = gr.Textbox(
label="📝 ENTER JAPANESE TEXT",
placeholder="例: お兄ちゃん、大好きだよ... (onii-chan, daisuki da yo)",
lines=8,
elem_classes="textbox-style"
)
# Example texts accordion
with gr.Accordion("💡 Example Texts with Romaji", open=False):
gr.Markdown("""
**Common Phrases:**
- こんにちは、世界 (konnichiwa, sekai) - Hello, world
- お元気ですか? (ogenki desu ka?) - How are you?
- 今日はいい天気ですね (kyou wa ii tenki desu ne) - Nice weather today
- ありがとうございます (arigatou gozaimasu) - Thank you
**Yosuga no Sora Phrases:**
- お兄ちゃん、待って! (onii-chan, matte!) - Brother, wait!
- 私、ずっと好きでした (watashi, zutto suki deshita) - I've always loved you
- 一緒にいようね (issho ni iyou ne) - Let's stay together
""")
# Generate Button
gen_btn = gr.Button("✨ GENERATE VOICE ✨", elem_classes="btn-lux")
# Audio Output
audio_out = gr.Audio(label="🎧 GENERATED VOICE", type="numpy", interactive=False)
# Clear Button
clear_btn = gr.Button("🗑️ Clear All", elem_classes="btn-secondary")
# Footer
gr.HTML("""
<div class='footer-style'>
CREATED BY PLANA-CHAN<br>
<small style="opacity: 0.7;">Yosuga no Sora Premium TTS v3.0 • Powered by VITS</small>
</div>
""")
# --- EVENT HANDLERS ---
# Generate
gen_btn.click(
tts_generate,
inputs=[selected_char_idx, text_in, speed_sl],
outputs=[audio_out, img_disp]
)
# Random text
random_btn.click(get_random_jp, outputs=[text_in])
# Clear
clear_btn.click(lambda: ("", None, ""), outputs=[text_in, audio_out, refresh_status])
# Test
test_btn.click(test_generation, outputs=[audio_out, refresh_status])
# Refresh: update radio, state, selected display, panel info, status, dan cover
refresh_btn.click(
refresh_model,
outputs=[char_radio, selected_char_idx, selected_char_display, char_info_html, refresh_status, img_disp]
)
# --- LAUNCH ---
if __name__ == "__main__":
print("=" * 60)
print("YOSUGA NO SORA TTS - PREMIUM VOICE GENERATION")
print("=" * 60)
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}")
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("=" * 60)
print("Starting server on http://localhost:7860")
print("Press Ctrl+C to stop")
print("=" * 60)
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
show_error=True
)