| import os |
| import sys |
| import logging |
| import torch |
| import utils |
| import commons |
| import gradio as gr |
| from models import SynthesizerTrn |
| from text import text_to_sequence |
| from text.symbols import symbols |
|
|
| |
| logging.getLogger('matplotlib').setLevel(logging.WARNING) |
| device = torch.device("cpu") |
|
|
| def get_text(text, hps): |
| text_norm = text_to_sequence(text, hps.data.text_cleaners) |
| if hps.data.add_blank: |
| text_norm = commons.intersperse(text_norm, 0) |
| return torch.LongTensor(text_norm) |
|
|
| def create_tts_fn(model, hps, speaker_ids): |
| def tts_fn(text, speaker_idx, speed): |
| if not text: return "Teks tidak boleh kosong!", None |
| try: |
| speaker_id = speaker_ids[speaker_idx] |
| stn_tst = get_text(text, hps) |
| with torch.no_grad(): |
| x_tst = stn_tst.unsqueeze(0).to(device) |
| x_tst_lengths = torch.LongTensor([stn_tst.size(0)]).to(device) |
| sid = torch.LongTensor([speaker_id]).to(device) |
| audio = model.infer( |
| x_tst, x_tst_lengths, sid=sid, |
| noise_scale=.667, noise_scale_w=0.8, |
| length_scale=1.0/speed |
| )[0][0,0].data.cpu().float().numpy() |
| return "Berhasil Membuat β
!", (hps.data.sampling_rate, audio) |
| except Exception as e: |
| return f"Error: {str(e)}", None |
| return tts_fn |
|
|
| |
| css = """ |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800&display=swap'); |
| |
| body, .gradio-container { |
| background-color: #fdfdfd !important; |
| font-family: 'Inter', sans-serif !important; |
| } |
| footer { display: none !important; } |
| |
| /* Header */ |
| .main-header { text-align: center; padding: 20px 0 5px 0; } |
| .main-header h1 { font-weight: 800; color: #1299ff; font-size: 2.4rem; margin: 0; } |
| .main-header p { color: #8a99af; letter-spacing: 2px; font-weight: 600; font-size: 10px; margin-top: 2px; } |
| |
| /* Menghilangkan Shadow & Border Halus pada Input */ |
| .gradio-container .block, .gradio-container .form, .gradio-container input, .gradio-container textarea, .gradio-container .secondary { |
| box-shadow: none !important; |
| border: 1px solid #e2e8f0 !important; |
| background-color: #ffffff !important; |
| } |
| |
| /* --- Label Biru & Putih Susu (Target Semua Label Termasuk Hasil Audio) --- */ |
| .gradio-container .label-wrap span, |
| .gradio-container span.svelte-1gfkn6j, |
| .gradio-container label span { |
| background-color: #e6f4ff !important; |
| color: #1299ff !important; |
| border-radius: 8px !important; |
| padding: 2px 10px !important; |
| box-shadow: none !important; |
| border: none !important; |
| font-weight: 700 !important; |
| font-size: 12px !important; |
| } |
| |
| /* --- Audio Player (Wadah Putih Bersih) --- */ |
| .gradio-container .audio-container { |
| background-color: #ffffff !important; |
| border: 1px solid #e2e8f0 !important; |
| } |
| |
| /* Status Box */ |
| .status-card { |
| background: #ffffff; |
| border: 1px solid #eef2f7; |
| border-radius: 12px; |
| padding: 12px 20px; |
| margin: 0 auto 15px auto; |
| max-width: 380px; |
| } |
| .status-item { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #4a5568; margin: 2px 0; } |
| .dot { height: 6px; width: 6px; background-color: #1299ff; border-radius: 50%; display: inline-block; } |
| |
| /* Character Info Grid */ |
| .char-scroll-box { max-height: 280px; overflow-y: auto; padding: 5px; } |
| .char-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } |
| .char-card { |
| background: white; |
| border-left: 4px solid #1299ff; |
| padding: 10px; |
| border-radius: 10px; |
| border-top: 1px solid #f0f4f8; |
| border-right: 1px solid #f0f4f8; |
| border-bottom: 1px solid #f0f4f8; |
| cursor: pointer; |
| transition: all 0.2s; |
| } |
| .char-card:hover { background: #f0f7ff; transform: translateY(-2px); border-color: #0072ff; } |
| .char-name-jp { font-weight: 700; font-size: 13px; color: #1a202c; display: block; } |
| .char-name-en { font-size: 11px; color: #718096; } |
| |
| /* Tombol Generate */ |
| .generate-btn { |
| background: linear-gradient(135deg, #00c6ff 0%, #0072ff 100%) !important; |
| border: none !important; color: white !important; font-weight: 700 !important; |
| border-radius: 12px !important; height: 45px !important; |
| } |
| |
| #custom-img { border-radius: 15px !important; border: 3px solid #fff; } |
| """ |
|
|
| if __name__ == '__main__': |
| config_path = "saved_model/config.json" |
| model_path = "saved_model/model.pth" |
| cover_path = "saved_model/cover.png" |
| |
| hps = utils.get_hparams_from_file(config_path) |
| posterior_channels = 80 if ("use_mel_posterior_encoder" in hps.model.keys() and hps.model.use_mel_posterior_encoder) else hps.data.filter_length // 2 + 1 |
|
|
| model = SynthesizerTrn( |
| len(symbols), posterior_channels, |
| 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() |
|
|
| speakers = [name for name in hps.speakers if name != "None"] |
| speaker_ids = [i for i, name in enumerate(hps.speakers) if name != "None"] |
|
|
| with gr.Blocks(css=css, theme=gr.themes.Soft()) as app: |
| |
| |
| gr.HTML("""<div class="main-header"><h1>Blue Archive</h1><p>PREMIUM TEXT-TO-SPEECH SERVICE</p></div>""") |
| |
| |
| gr.HTML(f""" |
| <div class="status-card"> |
| <b style="font-size: 13px; color: #1299ff; display: block; margin-bottom: 4px;">System Status</b> |
| <div class="status-item"><span class="dot"></span> Model: LOADED</div> |
| <div class="status-item"><span class="dot"></span> Characters: {len(speakers)}</div> |
| <div style="font-size: 10px; color: #8a99af; margin-top: 4px; border-top: 1px solid #f7fafc; padding-top: 3px;">v3.0 β’ Ready for generation</div> |
| </div> |
| """) |
| |
| |
| |
| hidden_choice = gr.Textbox(visible=False, elem_id="hidden_choice") |
|
|
| with gr.Row(): |
| with gr.Column(scale=1): |
| if os.path.exists(cover_path): |
| gr.Image(cover_path, elem_id="custom-img", show_label=False, interactive=False) |
| |
| input_text = gr.TextArea(label="Input Teks (Japanese)", placeholder="ε
ηγδ½γγζδΌγγγΎγγγγοΌ", lines=3) |
| |
| with gr.Row(): |
| |
| speaker_sel = gr.Dropdown(label="π·οΈ Pilih Karakter", choices=speakers, type="index", value=speakers[0], elem_id="spk_drop") |
| speed_sl = gr.Slider(label="Kecepatan", minimum=0.5, maximum=2.0, value=1.0, step=0.1) |
| |
| btn = gr.Button("π Generate Voice π", variant="primary", elem_classes="generate-btn") |
|
|
| with gr.Column(scale=1): |
| out_msg = gr.Textbox(label="Status") |
| out_audio = gr.Audio(label="Hasil Audio", type="numpy") |
|
|
| with gr.Accordion("π Character Information π", open=True): |
| |
| char_html = "".join([ |
| f'<div class="char-card" onclick="selectChar({i})">' |
| f'<span class="char-name-jp">{name}</span>' |
| f'<span class="char-name-en">Character {i+1}</span>' |
| f'</div>' for i, name in enumerate(speakers) |
| ]) |
| gr.HTML(f'<div class="char-scroll-box"><div class="char-grid">{char_html}</div></div>') |
|
|
| gr.HTML(""" |
| <div style="text-align: center; padding: 15px; margin-top: 25px;"> |
| <div style="font-weight: 800; color: #2d3748; font-size: 13px;">CREATED BY PLANA-CHAN</div> |
| <div style="color: #718096; font-size: 10px; margin-top: 2px;">Blue Archive TTS v3.0 β’ Powered by VITS</div> |
| </div> |
| """) |
|
|
| |
| |
| def move_selection(idx): |
| try: |
| |
| return int(idx) |
| except: |
| return 0 |
| |
| |
| hidden_choice.change(fn=move_selection, inputs=hidden_choice, outputs=speaker_sel) |
|
|
| |
| btn.click(create_tts_fn(model, hps, speaker_ids), inputs=[input_text, speaker_sel, speed_sl], outputs=[out_msg, out_audio]) |
|
|
| |
| |
| app.load(None, None, None, js=""" |
| () => { |
| window.selectChar = (index) => { |
| // Cari elemen textarea di dalam hidden_choice |
| // Gradio versi baru kadang pakai input, kadang textarea |
| let el = document.querySelector("#hidden_choice textarea"); |
| if (!el) el = document.querySelector("#hidden_choice input"); |
| |
| if (el) { |
| el.value = index; |
| // Dispatch event 'input' agar Gradio mendeteksi perubahan |
| el.dispatchEvent(new Event('input', { bubbles: true })); |
| } else { |
| console.error("Hidden input not found"); |
| } |
| } |
| } |
| """) |
|
|
| app.queue().launch() |