| 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 |
|
|
| |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
| logger = logging.getLogger(__name__) |
|
|
| |
| 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 = "https://huggingface.co/spaces/Rosmontis-Chan/Yosuga-no-Sora/resolve/main/saved_model" |
|
|
| |
| 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" |
| } |
|
|
| |
| 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") |
| } |
|
|
| |
| 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") |
|
|
| |
| CHARACTER_MAPPING = { |
| "春日野穹": "Kasugano Sora", |
| "天女目瑛": "Amatsume Akira", |
| "依媛奈緒": "Yorihime Nao", |
| "渚一葉": "Ichika Nagisa", |
| "Kasugano Sora": "春日野穹", |
| "Amatsume Akira": "天女目瑛", |
| "Yorihime Nao": "依媛奈緒", |
| "Ichika Nagisa": "渚一葉", |
| } |
|
|
| |
| def get_display_name(name): |
| """Format nama karakter dengan Romaji dalam kurung""" |
| if name in CHARACTER_MAPPING: |
| return f"{name} ({CHARACTER_MAPPING[name]})" |
| |
| for jp_name, romaji in CHARACTER_MAPPING.items(): |
| if romaji == name: |
| return f"{jp_name} ({name})" |
| |
| return name |
|
|
| |
| 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) |
|
|
| |
| 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, [], [] |
|
|
| |
| hps, net_g, speaker_choices, speaker_display_names = load_model() |
|
|
| |
| 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 |
| |
| |
| 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 |
|
|
| |
| 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: |
| |
| 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]}***" |
| |
| 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 |
|
|
| |
| 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]}" |
|
|
| |
| def get_random_jp(): |
| texts = [ |
| "こんにちは!", |
| "お元気ですか?", |
| "先生、お疲れ様です!", |
| "大好きだよ!", |
| "また明日ね。", |
| "お兄ちゃん、大好き!", |
| "一緒にいようね。", |
| "今日はいい天気ですね。" |
| ] |
| return random.choice(texts) |
|
|
| |
| 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; |
| } |
| } |
| """ |
|
|
| |
| with gr.Blocks(css=css, title="Yosuga no Sora TTS") as demo: |
| |
| 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_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> |
| """) |
| |
| |
| 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") |
| |
| |
| 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]) |
| |
| |
| 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_btn = gr.Button("🎲 INPUTS RANDOM TEXT 🎲", elem_classes="jp-btn") |
| |
| |
| 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> |
| """) |
| |
| |
| |
| char_info_html = gr.HTML(value="") |
| |
| 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() |
| |
| |
| 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_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_in = gr.Textbox( |
| label="📝 ENTER JAPANESE TEXT", |
| placeholder="例: お兄ちゃん、大好きだよ... (onii-chan, daisuki da yo)", |
| lines=8, |
| elem_classes="textbox-style" |
| ) |
| |
| |
| 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 |
| """) |
| |
| |
| gen_btn = gr.Button("✨ GENERATE VOICE ✨", elem_classes="btn-lux") |
| |
| |
| audio_out = gr.Audio(label="🎧 GENERATED VOICE", type="numpy", interactive=False) |
| |
| |
| clear_btn = gr.Button("🗑️ Clear All", elem_classes="btn-secondary") |
|
|
| |
| 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> |
| """) |
| |
| |
| |
| gen_btn.click( |
| tts_generate, |
| inputs=[selected_char_idx, text_in, speed_sl], |
| outputs=[audio_out, img_disp] |
| ) |
| |
| |
| random_btn.click(get_random_jp, outputs=[text_in]) |
| |
| |
| clear_btn.click(lambda: ("", None, ""), outputs=[text_in, audio_out, refresh_status]) |
| |
| |
| test_btn.click(test_generation, outputs=[audio_out, refresh_status]) |
| |
| |
| refresh_btn.click( |
| refresh_model, |
| outputs=[char_radio, selected_char_idx, selected_char_display, char_info_html, refresh_status, img_disp] |
| ) |
|
|
| |
| 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 |
| ) |