# =========================================================
# PROJECT SEKAI TTS
# BABY GREEN THEME UI WITH ORIGINAL DOWNLOAD FEATURE
# =========================================================
import os
import random
import logging
import re
import numpy as np
import torch
import shutil
from torch import no_grad, LongTensor
import commons
import utils
import gradio as gr
from models import SynthesizerTrn
from text import text_to_sequence, _clean_text
from huggingface_hub import hf_hub_download
# =========================================================
# 1. LOGGING
# =========================================================
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# =========================================================
# 2. CONFIG & ROMAJI UTILS
# =========================================================
HF_TOKEN = os.getenv("HF_TOKEN")
REPO_ID = "Plana-Archive/Anime-Models"
SUBFOLDER = "Anime-VITS/Prosekai-TTS/saved_model"
device = torch.device("cpu")
try:
import pykakasi
kks = pykakasi.kakasi()
def to_romaji(text):
if not text or text == "None":
return ""
try:
result = kks.convert(str(text))
return "".join([item["hepburn"].capitalize() for item in result])
except:
return str(text)
except:
def to_romaji(text):
return str(text)
# =========================================================
# 3. FITUR DOWNLOAD ASLI BAWAAN APP (6).PY (TIDAK DIUBAH)
# =========================================================
logger.info("[*] Downloading model assets...")
config_path = hf_hub_download(
repo_id=REPO_ID,
filename="config.json",
subfolder=SUBFOLDER,
token=HF_TOKEN
)
model_path = hf_hub_download(
repo_id=REPO_ID,
filename="model.pth",
subfolder=SUBFOLDER,
token=HF_TOKEN
)
cover_path = hf_hub_download(
repo_id=REPO_ID,
filename="cover.png",
subfolder=SUBFOLDER,
token=HF_TOKEN
)
# =========================================================
# 4. ENGINE LOAD MODEL
# =========================================================
hps = utils.get_hparams_from_file(config_path)
model = 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
).to(device)
utils.load_checkpoint(model_path, model, None)
model.eval()
speaker_names = [name for name in hps.speakers if name != "None"]
display_names = [to_romaji(name) for name in speaker_names]
speaker_map = {romaji: original for romaji, original in zip(display_names, speaker_names)}
# =========================================================
# 5. HELPER FUNCTIONS
# =========================================================
def get_text(text, hps, is_phoneme):
text_norm = text_to_sequence(
text, hps.symbols, [] if is_phoneme else hps.data.text_cleaners
)
if hps.data.add_blank:
text_norm = commons.intersperse(text_norm, 0)
return LongTensor(text_norm)
def tts_execute(text, speaker_romaji, speed, noise_scale, noise_scale_w, is_phoneme):
if not speaker_romaji:
return "Pilih karakter terlebih dahulu.", None
try:
original_name = speaker_map[speaker_romaji]
speaker_id = hps.speakers.index(original_name)
stn_tst = get_text(text, hps, is_phoneme)
with no_grad():
x_tst = stn_tst.unsqueeze(0).to(device)
x_tst_lengths = LongTensor([stn_tst.size(0)]).to(device)
sid = LongTensor([speaker_id]).to(device)
audio = model.infer(
x_tst, x_tst_lengths,
sid=sid,
noise_scale=noise_scale,
noise_scale_w=noise_scale_w,
length_scale=1.0 / speed
)[0][0, 0].data.cpu().float().numpy()
return "Voice berhasil dibuat.", (hps.data.sampling_rate, (audio * 32767).astype(np.int16))
except Exception as e:
return f"Error: {e}", None
def get_random_jp():
return random.choice([
"こんにちは、今日はいい天気ですね。",
"みんなで一緒に歌おう!",
"ワンダショ、最高のステージにしよう。",
"練習、頑張ろうね。きっと上手くなるよ。",
"また明日も会えるかな?楽しみにしてる。"
])
# --- MENCONTEK TAMPILAN REFINED CARD GRID LAYOUT DARI APP.PY ---
def get_char_info_html():
html = """
"""
for i, name in enumerate(display_names):
html += f"""
"""
html += "
"
return html
# =========================================================
# 6. UI STYLE (BABY GREEN THEME FROM APP (7).PY)
# =========================================================
css = """
:root {
--primary-600: #a8e6cf !important;
--accent-600: #a8e6cf !important;
}
.gradio-container, .gradio-container * {
--loader-color: #dcedc1 !important;
}
.ba-header-container {
border: 1.5px solid #e1f0e5;
border-radius: 12px;
margin-bottom: 12px;
background: white;
overflow: hidden;
line-height: 0;
}
.ba-header-container img {
width: 100%;
height: auto;
}
.status-container {
border: 1.5px solid #e1f0e5;
border-radius: 12px;
padding: 15px 22px;
margin-bottom: 20px;
background: white;
}
.status-title {
color: #88d498 !important;
font-weight: 800;
font-size: 16px;
margin-bottom: 8px;
}
.text-green-bold { color: #88d498 !important; font-weight: 900 !important; }
/* Pulsing Dot Effect */
.dot {
height: 8px;
width: 8px;
background-color: #88d498;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
box-shadow: 0 0 0 0 rgba(136, 212, 152, 1);
animation: pulse-green 2s infinite;
}
@keyframes pulse-green {
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(136, 212, 152, 0.7); }
70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(136, 212, 152, 0); }
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(136, 212, 152, 0); }
}
.slim-card { max-width: 480px; margin: 0 auto; background: transparent; padding: 10px; }
.scroll-box { height: 200px; overflow-y: auto; border: 1px solid #f0f4f8; border-radius: 12px; padding: 10px; background: #fafbfc; margin-bottom: 10px; }
.char-btn { background: white !important; border: 1px solid #e2e8f0 !important; border-left: 5px solid #a8e6cf !important; text-align: left !important; padding: 8px !important; font-size: 12px !important; margin-bottom: 4px !important; width: 100%; color: #4a5568 !important; }
.warning-card { background: #fffcf0; border: 2px dashed #ffd3b6; border-radius: 10px; padding: 12px; margin-bottom: 15px; text-align: center; }
.jp-btn { background: #f8fafc !important; border: 1px solid #cbd5e1 !important; color: #475569 !important; font-weight: 700 !important; border-radius: 10px !important; margin-bottom: 10px; font-size: 12px !important; width: 100%; }
.gen-btn { background: #a8e6cf !important; color: #4a5568 !important; font-weight: 700 !important; border-radius: 12px !important; height: 45px !important; width: 100%; border: none !important; cursor: pointer; }
.info-header-custom { background: #dcedc1 !important; color: #4a5568 !important; border: none !important; border-radius: 8px 8px 0 0 !important; padding: 12px 15px !important; width: 100% !important; cursor: pointer; font-weight: 800 !important; margin-top: 5px; }
.credit-footer { margin-top: 25px; padding: 15px; background: white; border-radius: 12px; text-align: center; border-bottom: 4px solid #a8e6cf; color: #94a3b8; font-weight: 700; font-size: 12px; letter-spacing: 2px; }
.gif-container { display: flex; justify-content: center; margin-top: 15px; }
.gif-container img { border-radius: 12px; max-width: 100%; height: auto; border: 1px solid #e1f0e5; }
"""
# =========================================================
# 7. GRADIO UI LAYOUT
# =========================================================
with gr.Blocks() as demo:
with gr.Column(elem_classes="slim-card"):
# Header Banner & System Status
gr.HTML("""
System Status
Model : LOADED ✅
System : Online
""")
gr.Markdown(f"### 📂 Project SEKAI - Milk Pastel TTS")
# Tampilkan cover image yang sudah terdownload asli jika ada
if os.path.exists(cover_path):
gr.Image(cover_path, show_label=False, interactive=False, height=140)
sel_name = gr.State("")
char_display = gr.Markdown("📍 *Silakan pilih karakter...*")
# Scroll box penampung daftar karakter (menggunakan display_names hasil download asli)
with gr.Column(elem_classes="scroll-box"):
if display_names:
for name in display_names:
btn = gr.Button(f"👤 {name}", elem_classes="char-btn")
btn.click(fn=lambda n=name: (n, f"📍 Selected: **{n}**"), outputs=[sel_name, char_display])
gr.HTML("""
🔖 PERINGATAN MINNA 🔖
Cara pakai: klik character di atas, masukkan text, lalu Generate! ✨
""")
# Input Teks & Fitur Random Text
txt_in = gr.TextArea(label="Input Text", value="こんにちは。みんなで楽しく歌いましょう。", lines=3)
gr.Button("🎲 INPUTS RANDOM TEXT 🎲", elem_classes="jp-btn").click(get_random_jp, outputs=[txt_in])
# Kecepatan bicara
spd = gr.Slider(0.6, 1.8, value=1.0, step=0.05, label="Kecepatan bicara")
# Parameter Advanced (Sembunyi dalam Accordion)
with gr.Accordion("🎛️ Advanced (noise & expressivity)", open=False):
noise_scale = gr.Slider(0.3, 1.2, value=0.667, step=0.01, label="Noise Scale (tone randomness)")
noise_scale_w = gr.Slider(0.5, 1.3, value=0.8, step=0.01, label="Duration Variation (w)")
phoneme_mode = gr.Checkbox(label="Mode input fonem (langsung ke fonem)")
# Tombol utama eksekusi suara
btn_gen = gr.Button("🫒 GENERATE VOICE 🫒", elem_classes="gen-btn")
status_out = gr.Textbox(label="Status", interactive=False)
aud_out = gr.Audio(label="Voice Output", type="numpy")
# Animasi GIF tambahan & Informasi Karakter
gr.HTML(f"""
""")
gr.HTML(get_char_info_html())
# Logika tombol saat di-klik
btn_gen.click(
fn=tts_execute,
inputs=[txt_in, sel_name, spd, noise_scale, noise_scale_w, phoneme_mode],
outputs=[status_out, aud_out]
)
gr.HTML("""""")
# =========================================================
# 8. LAUNCH RUN
# =========================================================
if __name__ == "__main__":
demo.queue().launch(
server_name="0.0.0.0",
server_port=7860,
css=css
)