| |
| |
| |
| |
|
|
| 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 |
|
|
| |
| |
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s - %(levelname)s - %(message)s" |
| ) |
| logger = logging.getLogger(__name__) |
|
|
| |
| |
| |
| 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) |
|
|
| |
| |
| |
| 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 |
| ) |
|
|
| |
| |
| |
| 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)} |
|
|
| |
| |
| |
| 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([ |
| "こんにちは、今日はいい天気ですね。", |
| "みんなで一緒に歌おう!", |
| "ワンダショ、最高のステージにしよう。", |
| "練習、頑張ろうね。きっと上手くなるよ。", |
| "また明日も会えるかな?楽しみにしてる。" |
| ]) |
|
|
| |
| def get_char_info_html(): |
| html = """<div class="info-content-area" id="info-content-main" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; padding: 16px; background: white; border: 1.5px solid #e1f0e5; border-top: none; border-radius: 0 0 16px 16px; max-height: 320px; overflow-y: auto; box-sizing: border-box;">""" |
| for i, name in enumerate(display_names): |
| html += f""" |
| <div style="border: 1px solid #e1f0e5; border-radius: 14px; padding: 12px 16px; border-left: 5px solid #a8e6cf; background: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.01); display: flex; flex-direction: column; justify-content: center; box-sizing: border-box; min-width: 0;"> |
| <div style="font-weight: 700; color: #3e5349; font-size: 14px; margin-bottom: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{name}</div> |
| <div style="color: #9bb7a8; font-size: 11px; font-weight: 500;">Character {i+1}</div> |
| </div> |
| """ |
| html += "</div>" |
| return html |
|
|
| |
| |
| |
| 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; } |
| """ |
|
|
| |
| |
| |
| with gr.Blocks() as demo: |
| with gr.Column(elem_classes="slim-card"): |
| |
| gr.HTML(""" |
| <div class="ba-header-container"> |
| <img src="https://huggingface.co/spaces/Plana-Archive/MOE-TTS/resolve/main/Mutsumi.jpg" alt="Mutsumi Banner"> |
| </div> |
| <div class="status-container"> |
| <div class="status-title">System Status</div> |
| <div class="status-item"><span style="color:#4a5568">Model :</span> <span class="text-green-bold"> LOADED ✅</span></div> |
| <div class="status-item"><span style="color:#4a5568">System :</span> <span style="color:#88d498; font-weight:700;"><span class="dot"></span>Online</span></div> |
| </div> |
| """) |
| |
| gr.Markdown(f"### 📂 Project SEKAI - Milk Pastel TTS") |
| |
| |
| 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...*") |
| |
| |
| 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(""" |
| <div class="warning-card"> |
| <div style="color:#f5a623;font-weight:800;font-size:13px;">🔖 PERINGATAN MINNA 🔖</div> |
| <div style="color:#855d1a;font-size:11px;font-weight:600;"> |
| Cara pakai: klik character di atas, masukkan text, lalu Generate! ✨ |
| </div> |
| </div> |
| """) |
| |
| |
| 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]) |
| |
| |
| spd = gr.Slider(0.6, 1.8, value=1.0, step=0.05, label="Kecepatan bicara") |
| |
| |
| 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)") |
| |
| |
| 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") |
| |
| |
| gr.HTML(f""" |
| <div class="gif-container"> |
| <img src="https://huggingface.co/spaces/Plana-Archive/MOE-TTS/resolve/main/kurumi-tokisaki.gif" alt="Kurumi GIF"> |
| </div> |
| <button onclick="document.getElementById('info-content-main').style.display = (document.getElementById('info-content-main').style.display === 'none') ? 'grid' : 'none';" class="info-header-custom"> |
| 📑 Character Information 📑 |
| </button> |
| """) |
| gr.HTML(get_char_info_html()) |
| |
| |
| 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("""<div class="credit-footer">🍏 CREATED BY MUTSUMI 🍏</div>""") |
|
|
| |
| |
| |
| if __name__ == "__main__": |
| demo.queue().launch( |
| server_name="0.0.0.0", |
| server_port=7860, |
| css=css |
| ) |