| 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 |
|
|
| |
| 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/Waifu-Anime-TTS/resolve/main" |
|
|
| |
| FILES_TO_DOWNLOAD = { |
| "config.json": f"{BASE_URL}/saved_model/7/config.json", |
| "model.pth": f"{BASE_URL}/saved_model/7/model.pth", |
| "cover.png": f"{BASE_URL}/saved_model/7/cover.png" |
| } |
|
|
| |
| LOCAL_DIR = "saved_model" |
| LOCAL_PATHS = { |
| "config": os.path.join(LOCAL_DIR, "7", "config.json"), |
| "model": os.path.join(LOCAL_DIR, "7", "model.pth"), |
| "cover": os.path.join(LOCAL_DIR, "7", "cover.png") |
| } |
|
|
| |
| os.makedirs(os.path.join(LOCAL_DIR, "7"), exist_ok=True) |
|
|
| |
| logger.info("Checking for required files...") |
| for file_name, url in FILES_TO_DOWNLOAD.items(): |
| |
| if file_name == "config.json": |
| local_path = LOCAL_PATHS["config"] |
| elif file_name == "model.pth": |
| local_path = LOCAL_PATHS["model"] |
| elif file_name == "cover.png": |
| local_path = LOCAL_PATHS["cover"] |
| else: |
| continue |
| |
| if not os.path.exists(local_path): |
| logger.info(f"Downloading {file_name} from {url}...") |
| success = download_file(url, local_path) |
| if not success: |
| logger.error(f"Failed to download {file_name}") |
| else: |
| logger.info(f"{os.path.basename(local_path)} already exists") |
|
|
| |
| CHARACTER_MAPPING = { |
| "ι·Ήεζη": "Takakura Anri", |
| "ι·Ήεζι΄": "Takakura Anzu", |
| "γ’γγ€γͺγ’": "Apeiria", |
| "εη§ζζ₯ι¦": "Kurashina Asuka", |
| "ATRI": "ATRI", |
| "γ’γ€γ©": "Aira", |
| "ζ°ε 彩ι³": "Shindo Ayane", |
| "ε§«ιζε₯": "Himeno Seina", |
| "ε°ι γγ": "Komari Yui", |
| "θ代ζ©ζ°·ηΉ": "Seidaihashi Hiori", |
| "ζεηη½": "Arisaka Mashiro", |
| "η½ε²ηΎη΅΅η ": "Shirasaki Mieru", |
| "δΊιε ηη΄
": "Nikaido Shinku" |
| } |
|
|
| |
| def get_display_name(name): |
| """Format nama karakter dengan Romaji dalam kurung""" |
| if name in CHARACTER_MAPPING: |
| return f"{name} ({CHARACTER_MAPPING[name]})" |
| return f"{name}" |
|
|
| |
| def split_name_display(display_name): |
| """Pisahkan nama Jepang dan Romaji dari display name""" |
| if '(' in display_name and ')' in display_name: |
| parts = display_name.split('(') |
| japanese = parts[0].strip() |
| romaji = parts[1].replace(')', '').strip() |
| return japanese, romaji |
| return display_name, "" |
|
|
| |
| def read_config_file(config_path): |
| """Membaca config.json dan mengembalikan dictionary dengan parameter""" |
| try: |
| with open(config_path, 'r', encoding='utf-8') as f: |
| config_data = json.load(f) |
| |
| logger.info(f"Config keys found: {list(config_data.keys())}") |
| return config_data |
| except Exception as e: |
| logger.error(f"Error reading config file: {e}") |
| return None |
|
|
| |
| def get_config_param(config, keys, default=None): |
| """Mendapatkan parameter dari config dengan key bertingkat""" |
| if isinstance(config, dict): |
| current = config |
| for key in keys: |
| if isinstance(current, dict) and key in current: |
| current = current[key] |
| else: |
| return default |
| return current |
| return default |
|
|
| |
| def get_text(text, hps): |
| """Convert text to sequence - FIXED VERSION with Japanese support""" |
| try: |
| |
| try: |
| from text import text_to_sequence |
| text_module_available = True |
| except ImportError as e: |
| logger.error(f"Cannot import text module: {e}") |
| text_module_available = False |
| |
| |
| symbols = getattr(hps, 'symbols', []) |
| if not symbols: |
| |
| symbols = ["_", ",", ".", "!", "?", "-", "A", "E", "I", "N", "O", "Q", "U", "a", "b", "d", "e", "f", "g", "h", "i", "j", "k", "m", "n", "o", "p", "r", "s", "t", "u", "v", "w", "y", "z", "Κ", "Κ§", "β", "β", " "] |
| logger.warning(f"Using fallback symbols, count: {len(symbols)}") |
| |
| |
| text = str(text).strip() |
| if not text: |
| logger.error("Empty text provided to get_text") |
| return torch.LongTensor([0]) |
| |
| logger.info(f"Original text: {text}") |
| |
| text_norm = [] |
| |
| |
| if text_module_available and hasattr(hps, 'text_cleaners') and hps.text_cleaners: |
| try: |
| logger.info(f"Using text cleaners: {hps.text_cleaners}") |
| text_norm = text_to_sequence(text, symbols, hps.text_cleaners) |
| logger.info(f"text_to_sequence with cleaners succeeded, length: {len(text_norm)}") |
| except Exception as e: |
| logger.warning(f"text_to_sequence with cleaners failed: {e}, trying without cleaners") |
| try: |
| text_norm = text_to_sequence(text, symbols) |
| except Exception as e2: |
| logger.warning(f"text_to_sequence without cleaners also failed: {e2}") |
| text_module_available = False |
| elif text_module_available: |
| try: |
| text_norm = text_to_sequence(text, symbols) |
| logger.info(f"text_to_sequence without cleaners succeeded, length: {len(text_norm)}") |
| except Exception as e: |
| logger.warning(f"text_to_sequence without cleaners failed: {e}") |
| text_module_available = False |
| |
| |
| if not text_norm: |
| logger.warning("Using manual symbol mapping as fallback") |
| symbol_to_id = {s: i for i, s in enumerate(symbols)} |
| |
| |
| |
| text_norm = [] |
| text_lower = text.lower() |
| for char in text_lower: |
| if char in symbol_to_id: |
| text_norm.append(symbol_to_id[char]) |
| elif char == ' ': |
| continue |
| |
| |
| if not text_norm: |
| logger.error("Text sequence is empty after all attempts, using default 'a'") |
| |
| symbol_to_id = {s: i for i, s in enumerate(symbols)} |
| default_char = 'a' if 'a' in symbol_to_id else symbols[0] if symbols else '_' |
| text_norm = [symbol_to_id.get(default_char, 0)] |
| |
| |
| add_blank = getattr(hps, 'add_blank', False) |
| if add_blank: |
| text_norm = commons.intersperse(text_norm, 0) |
| logger.info(f"Added blanks, sequence length: {len(text_norm)}") |
| |
| logger.info(f"Final sequence length: {len(text_norm)}") |
| return torch.LongTensor(text_norm) |
| |
| except Exception as e: |
| logger.error(f"Error in get_text: {e}") |
| logger.error(traceback.format_exc()) |
| |
| return torch.LongTensor([0, 1, 2]) |
|
|
| |
| 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...") |
| |
| |
| config_data = read_config_file(LOCAL_PATHS["config"]) |
| if config_data is None: |
| return None, None, [], [] |
| |
| |
| class HParams: |
| def __init__(self, **kwargs): |
| for key, value in kwargs.items(): |
| setattr(self, key, value) |
| |
| |
| n_speakers = get_config_param(config_data, ['n_speakers'], 13) |
| |
| |
| symbols = get_config_param(config_data, ['symbols'], []) |
| if not symbols: |
| |
| symbols = ["_", ",", ".", "!", "?", "-", "A", "E", "I", "N", "O", "Q", "U", "a", "b", "d", "e", "f", "g", "h", "i", "j", "k", "m", "n", "o", "p", "r", "s", "t", "u", "v", "w", "y", "z", "Κ", "Κ§", "β", "β", " "] |
| |
| sampling_rate = get_config_param(config_data, ['data', 'sampling_rate'], 22050) |
| filter_length = get_config_param(config_data, ['data', 'filter_length'], 1024) |
| hop_length = get_config_param(config_data, ['data', 'hop_length'], 256) |
| segment_size = get_config_param(config_data, ['train', 'segment_size'], 8192) |
| |
| |
| model_params = get_config_param(config_data, ['model'], {}) |
| |
| |
| text_cleaners = get_config_param(config_data, ['data', 'text_cleaners'], None) |
| |
| |
| add_blank = get_config_param(config_data, ['data', 'add_blank'], False) |
| |
| |
| speakers = get_config_param(config_data, ['speakers'], []) |
| if not speakers: |
| |
| speakers = list(CHARACTER_MAPPING.keys()) |
| |
| |
| hps = HParams( |
| n_speakers=n_speakers, |
| symbols=symbols, |
| sampling_rate=sampling_rate, |
| filter_length=filter_length, |
| hop_length=hop_length, |
| segment_size=segment_size, |
| model=model_params, |
| add_blank=add_blank, |
| text_cleaners=text_cleaners |
| ) |
| |
| logger.info(f"Model parameters: n_speakers={n_speakers}, sampling_rate={sampling_rate}, symbols_count={len(symbols)}") |
| |
| |
| net_g = SynthesizerTrn( |
| len(symbols), |
| filter_length // 2 + 1, |
| segment_size // hop_length, |
| n_speakers=n_speakers, |
| **model_params |
| ) |
| |
| |
| try: |
| checkpoint = torch.load(LOCAL_PATHS["model"], map_location='cpu') |
| |
| |
| if 'state_dict' in checkpoint: |
| state_dict = checkpoint['state_dict'] |
| elif 'model' in checkpoint: |
| state_dict = checkpoint['model'] |
| elif 'net_g' in checkpoint: |
| state_dict = checkpoint['net_g'] |
| else: |
| state_dict = checkpoint |
| |
| |
| new_state_dict = {} |
| for k, v in state_dict.items(): |
| name = k.replace('module.', '').replace('net_g.', '') |
| new_state_dict[name] = v |
| |
| net_g.load_state_dict(new_state_dict, strict=False) |
| logger.info("Model weights loaded successfully") |
| |
| except Exception as e: |
| logger.error(f"Error loading model weights: {e}") |
| logger.error(traceback.format_exc()) |
| return None, None, [], [] |
| |
| net_g.eval() |
| logger.info("Model loaded successfully") |
| |
| |
| speaker_choices = [] |
| speaker_display_names = [] |
| |
| for speaker in speakers: |
| if speaker and str(speaker).strip(): |
| speaker_choices.append(str(speaker)) |
| display_name = get_display_name(str(speaker)) |
| speaker_display_names.append(display_name) |
| |
| logger.info(f"Loaded {len(speaker_choices)} speakers from config") |
| 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: |
| logger.error("Model not loaded") |
| return None, COVER_PATH |
| |
| text = str(text).strip() if text else "" |
| if not text: |
| logger.error("Text is empty") |
| return None, COVER_PATH |
| |
| if not speaker_choices or speaker_index >= len(speaker_choices): |
| speaker_index = 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"Processing text: {text[:50]}...") |
| |
| |
| stn_tst = get_text(text, hps) |
| logger.info(f"Text sequence length: {stn_tst.size(0)}") |
| |
| 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.info(f"Using speed factor: {speed}") |
| |
| |
| try: |
| 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() |
| logger.info(f"Audio generated, shape: {audio.shape}") |
| except Exception as infer_error: |
| logger.error(f"Inference error: {infer_error}") |
| logger.error(traceback.format_exc()) |
| return None, COVER_PATH |
| |
| |
| if len(audio) > 0: |
| audio_max = np.abs(audio).max() |
| if audio_max > 0: |
| audio = (audio / audio_max * 32767).astype(np.int16) |
| logger.info(f"Audio normalized, max: {audio_max}") |
| else: |
| logger.warning("Audio is silent (max=0)") |
| audio = np.zeros(44100, dtype=np.int16) |
| else: |
| logger.warning("Audio is empty") |
| audio = np.zeros(44100, dtype=np.int16) |
| |
| |
| sampling_rate = getattr(hps, 'sampling_rate', 22050) |
| |
| logger.info(f"Audio generation complete: {len(audio)} samples at {sampling_rate}Hz") |
| return (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: |
| return gr.update(choices=speaker_display_names, value=speaker_display_names[0]), f"β
Model refreshed! {len(speaker_display_names)} speakers loaded" |
| else: |
| return gr.update(choices=["No speakers found"], value="No speakers found"), "β Failed to load speakers" |
|
|
| 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]}" |
|
|
| |
| with gr.Blocks(css=""" |
| /* Reset dan dasar */ |
| * { |
| box-sizing: border-box; |
| } |
| |
| body { |
| background: linear-gradient(135deg, #f0fff4 0%, #e6fffa 100%); |
| min-height: 100vh; |
| margin: 0; |
| padding: 20px; |
| font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; |
| color: #2d3748; |
| } |
| |
| /* Container utama */ |
| .gradio-container { |
| background: white !important; |
| border-radius: 24px !important; |
| box-shadow: |
| 0 20px 60px rgba(72, 187, 120, 0.15), |
| 0 1px 3px rgba(0, 0, 0, 0.05) !important; |
| border: 1px solid #e2e8f0 !important; |
| max-width: 1200px !important; |
| margin: 20px auto !important; |
| padding: 0 !important; |
| overflow: hidden; |
| position: relative; |
| } |
| |
| /* Header yang elegan - JUDUL YOSUGA NO SORA */ |
| .header-style { |
| text-align: center; |
| padding: 50px 30px 40px !important; |
| background: linear-gradient(135deg, #ffffff 0%, #f7fff9 100%); |
| border-bottom: 1px solid #e2e8f0; |
| margin: 0 !important; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .header-style::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| height: 4px; |
| background: linear-gradient(90deg, #38a169 0%, #48bb78 50%, #68d391 100%); |
| } |
| |
| .header-style h1 { |
| font-family: 'Segoe UI', sans-serif; |
| font-weight: 800; |
| letter-spacing: -0.5px; |
| color: transparent; |
| margin: 0 0 10px 0; |
| font-size: 3.2rem; |
| background: linear-gradient(135deg, #38a169 0%, #68d391 100%); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| text-shadow: 0 2px 4px rgba(56, 161, 105, 0.1); |
| } |
| |
| .header-style p { |
| color: #718096; |
| font-size: 1.3rem; |
| letter-spacing: 1px; |
| margin-top: 15px; |
| font-weight: 400; |
| opacity: 0.9; |
| text-transform: uppercase; |
| } |
| |
| /* Premium badge */ |
| .premium-badge { |
| display: inline-block; |
| background: linear-gradient(135deg, #ffd700 0%, #ffecb3 100%); |
| color: #8d6e00; |
| padding: 8px 25px; |
| border-radius: 25px; |
| font-size: 1rem; |
| font-weight: 800; |
| letter-spacing: 2px; |
| margin-left: 20px; |
| vertical-align: middle; |
| box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3); |
| border: 2px solid #ffd700; |
| } |
| |
| /* Tombol utama - gradien hijau elegan */ |
| .btn-lux { |
| background: linear-gradient(135deg, #38a169 0%, #48bb78 100%) !important; |
| color: white !important; |
| border-radius: 20px !important; |
| border: none !important; |
| height: 65px !important; |
| font-weight: 700 !important; |
| font-size: 1.2rem !important; |
| cursor: pointer; |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; |
| margin: 20px 0 !important; |
| box-shadow: |
| 0 8px 25px rgba(56, 161, 105, 0.3), |
| 0 2px 6px rgba(0, 0, 0, 0.1) !important; |
| position: relative; |
| overflow: hidden; |
| letter-spacing: 0.5px; |
| } |
| |
| .btn-lux:hover { |
| transform: translateY(-3px) !important; |
| box-shadow: |
| 0 15px 35px rgba(56, 161, 105, 0.4), |
| 0 5px 15px rgba(0, 0, 0, 0.1) !important; |
| background: linear-gradient(135deg, #2f855a 0%, #38a169 100%) !important; |
| } |
| |
| /* Tombol sekunder - hijau muda modern */ |
| .btn-secondary { |
| background: linear-gradient(135deg, #f0fff4 0%, #e6fffa 100%) !important; |
| color: #2d3748 !important; |
| border-radius: 16px !important; |
| border: 2px solid #c6f6d5 !important; |
| height: 50px !important; |
| font-weight: 600 !important; |
| cursor: pointer; |
| transition: all 0.2s ease !important; |
| box-shadow: 0 3px 8px rgba(0, 0, 0, 0.05) !important; |
| } |
| |
| .btn-secondary:hover { |
| background: linear-gradient(135deg, #ffffff 0%, #f0fff4 100%) !important; |
| border-color: #9ae6b4 !important; |
| transform: translateY(-2px); |
| box-shadow: 0 6px 15px rgba(0, 0, 0, 0.08) !important; |
| } |
| |
| /* Textbox - clean design dengan sentuhan hijau */ |
| .textbox-style textarea { |
| background: #f7fff9 !important; |
| border: 2px solid #e2e8f0 !important; |
| color: #2d3748 !important; |
| border-radius: 20px !important; |
| padding: 25px !important; |
| font-size: 1.1rem !important; |
| min-height: 200px !important; |
| transition: all 0.3s ease !important; |
| font-family: 'Segoe UI', sans-serif; |
| line-height: 1.7; |
| } |
| |
| .textbox-style textarea:focus { |
| border-color: #48bb78 !important; |
| background: white !important; |
| box-shadow: 0 0 0 4px rgba(72, 187, 120, 0.15) !important; |
| outline: none !important; |
| } |
| |
| /* Dropdown - modern dengan font untuk karakter Jepang */ |
| .dropdown-style select { |
| background: #f7fff9 !important; |
| color: #2d3748 !important; |
| border: 2px solid #e2e8f0 !important; |
| border-radius: 16px !important; |
| padding: 15px 20px !important; |
| font-size: 1.1rem !important; |
| font-weight: 600; |
| transition: all 0.3s ease; |
| font-family: 'Segoe UI', 'Hiragino Sans', 'Yu Gothic', sans-serif !important; |
| } |
| |
| .dropdown-style .gr-dropdown { |
| font-family: 'Segoe UI', 'Hiragino Sans', 'Yu Gothic', sans-serif !important; |
| } |
| |
| .dropdown-style option { |
| font-family: 'Segoe UI', 'Hiragino Sans', 'Yu Gothic', sans-serif !important; |
| padding: 12px !important; |
| } |
| |
| .dropdown-style select:focus { |
| border-color: #48bb78 !important; |
| background: white !important; |
| box-shadow: 0 0 0 4px rgba(72, 187, 120, 0.15) !important; |
| } |
| |
| /* Styling khusus untuk teks karakter */ |
| .character-name { |
| font-family: 'Hiragino Sans', 'Yu Gothic', sans-serif; |
| font-weight: 600; |
| color: #2d3748; |
| } |
| |
| .character-romaji { |
| font-family: 'Segoe UI', Arial, sans-serif; |
| color: #718096; |
| font-size: 0.95em; |
| font-weight: 400; |
| font-style: italic; |
| } |
| |
| /* Slider - design hijau */ |
| .slider-style .gr-slider { |
| background: #f7fff9 !important; |
| border-radius: 12px !important; |
| border: 2px solid #e2e8f0 !important; |
| } |
| |
| .slider-style .gr-slider .gr-slider-track { |
| background: linear-gradient(90deg, #38a169 0%, #68d391 100%) !important; |
| } |
| |
| /* Image container */ |
| .image-container { |
| border-radius: 24px !important; |
| overflow: hidden !important; |
| border: 3px solid white !important; |
| box-shadow: |
| 0 15px 35px rgba(0, 0, 0, 0.08), |
| 0 3px 10px rgba(0, 0, 0, 0.05) !important; |
| background: white !important; |
| transition: transform 0.3s ease, box-shadow 0.3s ease; |
| 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: 21px !important; |
| } |
| |
| /* Status boxes - SAMA SEPERTI APP.PY AWAL */ |
| .status-box { |
| background: #f8f9fa !important; |
| border: 2px solid #e9ecef !important; |
| border-radius: 16px !important; |
| padding: 20px !important; |
| margin: 15px 0 !important; |
| color: #2c3e50; |
| font-family: 'SF Mono', 'Monaco', monospace; |
| font-size: 0.9rem; |
| line-height: 1.5; |
| } |
| |
| .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; |
| margin: 15px 0 !important; |
| } |
| |
| .error-box { |
| background: linear-gradient(135deg, #fff5f5 0%, #ffe6e6 100%) !important; |
| border: 2px solid #fed7d7 !important; |
| color: #742a2a !important; |
| border-radius: 16px !important; |
| padding: 20px !important; |
| margin: 15px 0 !important; |
| } |
| |
| /* Footer */ |
| .footer-style { |
| text-align: center; |
| margin-top: 40px; |
| padding: 30px; |
| color: #718096; |
| font-size: 0.95rem; |
| letter-spacing: 0.5px; |
| border-top: 1px solid #e2e8f0; |
| background: #f7fff9; |
| border-radius: 0 0 24px 24px; |
| } |
| |
| /* Audio player custom */ |
| audio { |
| border-radius: 20px !important; |
| background: #f7fff9 !important; |
| border: 2px solid #e2e8f0 !important; |
| padding: 15px !important; |
| width: 100% !important; |
| } |
| |
| /* Label styling */ |
| label { |
| font-weight: 700 !important; |
| color: #2d3748 !important; |
| margin-bottom: 12px !important; |
| font-size: 1.1rem !important; |
| letter-spacing: 0.3px; |
| } |
| |
| /* Card container */ |
| .card { |
| background: white; |
| border-radius: 24px; |
| padding: 30px; |
| box-shadow: |
| 0 8px 25px rgba(0, 0, 0, 0.05), |
| 0 2px 6px rgba(0, 0, 0, 0.03); |
| border: 1px solid #e2e8f0; |
| margin-bottom: 25px; |
| } |
| |
| /* Grid layout */ |
| .grid-container { |
| display: grid; |
| grid-template-columns: 1fr 2fr; |
| gap: 35px; |
| padding: 35px; |
| } |
| |
| @media (max-width: 768px) { |
| .grid-container { |
| grid-template-columns: 1fr; |
| gap: 25px; |
| padding: 25px; |
| } |
| |
| .header-style h1 { |
| font-size: 2.5rem; |
| } |
| |
| .header-style p { |
| font-size: 1.1rem; |
| } |
| |
| .image-container { |
| height: 380px !important; |
| } |
| } |
| |
| /* Loading animation hijau */ |
| .loading-spinner { |
| display: inline-block; |
| width: 24px; |
| height: 24px; |
| border: 3px solid rgba(56, 161, 105, 0.2); |
| border-radius: 50%; |
| border-top-color: #38a169; |
| animation: spin 1s ease-in-out infinite; |
| } |
| |
| @keyframes spin { |
| to { transform: rotate(360deg); } |
| } |
| |
| /* Character info */ |
| .character-info { |
| display: flex; |
| flex-direction: column; |
| gap: 8px; |
| padding: 8px 0; |
| } |
| |
| .character-japanese { |
| font-size: 1.2em; |
| font-weight: 600; |
| color: #2d3748; |
| } |
| |
| /* Example box */ |
| .example-box { |
| background: linear-gradient(135deg, #f7fff9 0%, #e6fffa 100%); |
| border: 2px solid #c6f6d5; |
| border-radius: 16px; |
| padding: 20px; |
| margin: 15px 0; |
| } |
| |
| .example-japanese { |
| font-family: 'Hiragino Sans', sans-serif; |
| font-size: 1.1em; |
| color: #2d3748; |
| margin-bottom: 5px; |
| } |
| |
| .example-romaji { |
| color: #718096; |
| font-size: 0.95em; |
| font-style: italic; |
| margin-bottom: 10px; |
| } |
| |
| /* Gradient text */ |
| .gradient-text { |
| background: linear-gradient(135deg, #38a169 0%, #68d391 100%); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| |
| /* Character card */ |
| .character-card { |
| background: white; |
| border-radius: 16px; |
| padding: 20px; |
| border: 2px solid #e2e8f0; |
| transition: all 0.3s ease; |
| } |
| |
| .character-card:hover { |
| border-color: #9ae6b4; |
| transform: translateY(-5px); |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); |
| } |
| """) as demo: |
| |
| gr.HTML(""" |
| <div class='header-style'> |
| <h1>πWAIFU ANIME - TTSπ</h1> |
| <p>Generate Voice Anime</span></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: 15px 0; border-color: rgba(56, 161, 105, 0.2)"> |
| <small class="gradient-text">v3.0 β’ Ready for generation</small> |
| </div> |
| """, elem_classes="card") |
| |
| |
| if COVER_PATH and os.path.exists(COVER_PATH): |
| img_disp = gr.Image( |
| value=COVER_PATH, |
| show_label=False, |
| interactive=False, |
| elem_classes="image-container hover-lift" |
| ) |
| else: |
| img_disp = gr.Image( |
| value=None, |
| show_label=False, |
| interactive=False, |
| elem_classes="image-container" |
| ) |
| gr.Markdown(f""" |
| <div class="error-box"> |
| <strong>β οΈ WARNING:</strong> Cover image not found!<br> |
| Expected at: {LOCAL_PATHS["cover"]}<br> |
| Please check if file exists or try refreshing. |
| </div> |
| """) |
| |
| |
| with gr.Group(elem_classes="card"): |
| |
| spk_input = gr.Dropdown( |
| choices=speaker_display_names if speaker_display_names else [], |
| label="π SELECT CHARACTER", |
| value=speaker_display_names[0] if speaker_display_names else "No characters", |
| type="index", |
| elem_classes="dropdown-style", |
| info="Japanese name with Romaji pronunciation" |
| ) |
| |
| |
| if speaker_display_names: |
| current_jp, current_romaji = split_name_display(speaker_display_names[0]) |
| gr.Markdown(f""" |
| <div class="success-box"> |
| <strong>π― SELECTED CHARACTER:</strong><br> |
| <div class="character-info"> |
| <span class="character-japanese">{current_jp}</span> |
| <span class="character-romaji">{current_romaji}</span> |
| </div> |
| </div> |
| """) |
| |
| with gr.Row(): |
| refresh_btn = gr.Button("π Refresh Model", elem_classes="btn-secondary") |
| test_btn = gr.Button("π Test Voice", 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", |
| info="Lower = slower, Higher = faster" |
| ) |
| |
| gr.Markdown(""" |
| <div class="example-box"> |
| <strong>π‘ PENTING:</strong><br> |
| Model ini menggunakan <strong>Japanese text cleaners</strong>!<br><br> |
| Gunakan teks bahasa Jepang (Hiragana, Katakana, Kanji).<br> |
| Contoh: γγγ«γ‘γ―γε
ζ°γ§γγοΌ |
| </div> |
| """) |
| |
| with gr.Column(scale=2): |
| |
| with gr.Group(elem_classes="card"): |
| text_in = gr.Textbox( |
| label="π MASUKKAN TEKS BAHASA JEPANG", |
| placeholder="Contoh: γγγ«γ‘γ―γε
ζ°γ§γγοΌ", |
| lines=8, |
| elem_classes="textbox-style", |
| max_lines=12, |
| info="Masukkan teks dalam bahasa Jepang (Hiragana, Katakana, Kanji)" |
| ) |
| |
| |
| with gr.Accordion("π‘ CONTOH TEKS", open=False): |
| gr.Markdown(""" |
| <div class="example-box"> |
| <div class="example-japanese">γγγ«γ‘γ―</div> |
| <div class="example-romaji">(Konnichiwa)</div> |
| <em>Halo / Selamat siang</em> |
| </div> |
| |
| <div class="example-box"> |
| <div class="example-japanese">γγ―γγγγγγΎγ</div> |
| <div class="example-romaji">(Ohayou gozaimasu)</div> |
| <em>Selamat pagi</em> |
| </div> |
| |
| <div class="example-box"> |
| <div class="example-japanese">γγγγ¨γ</div> |
| <div class="example-romaji">(Arigatou)</div> |
| <em>Terima kasih</em> |
| </div> |
| |
| <div class="example-box"> |
| <div class="example-japanese">ε₯½γγ§γ</div> |
| <div class="example-romaji">(Suki desu)</div> |
| <em>Aku suka kamu</em> |
| </div> |
| """) |
| |
| |
| gen_btn = gr.Button("β¨ GENERATE VOICE β¨", elem_classes="btn-lux", scale=1) |
| |
| |
| with gr.Group(elem_classes="card"): |
| audio_out = gr.Audio( |
| label="π§ SUARA YANG DIHASILKAN", |
| type="numpy", |
| interactive=False |
| ) |
| |
| |
| with gr.Row(): |
| clear_btn = gr.Button("ποΈ Hapus Semua", elem_classes="btn-secondary") |
| |
| |
| with gr.Accordion("π INFORMASI KARAKTER", open=False): |
| if speaker_display_names: |
| char_info_html = "<div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px;'>" |
| for i, display_name in enumerate(speaker_display_names): |
| jp_name, romaji = split_name_display(display_name) |
| char_info_html += f""" |
| <div class="character-card"> |
| <div style="font-weight: 600; color: #38a169; margin-bottom: 10px;">Karakter {i+1}</div> |
| <div style="font-size: 1.1em; font-family: 'Hiragino Sans'; margin: 5px 0;">{jp_name}</div> |
| <div style="color: #718096; font-size: 0.9em;">{romaji}</div> |
| <div style="margin-top: 10px; font-size: 0.85em; color: #718096;"> |
| <span style="color: #38a169;">β</span> Suara Premium |
| </div> |
| </div> |
| """ |
| char_info_html += "</div>" |
| gr.HTML(char_info_html) |
| else: |
| gr.Markdown("Tidak ada informasi karakter.") |
|
|
| |
| gr.HTML(""" |
| <div class='footer-style'> |
| DIBUAT OLEH PLANA-CHAN<br> |
| <small style="opacity: 0.7;">WAIFU ANIME Premium TTS v3.0 β’ Powered by VITS</small> |
| </div> |
| """) |
| |
| |
| def on_speaker_change(speaker_index): |
| """Update character info when speaker changes""" |
| if speaker_display_names and speaker_index < len(speaker_display_names): |
| display_name = speaker_display_names[0] |
| jp_name, romaji = split_name_display(display_name) |
| |
| return f""" |
| <div class="success-box"> |
| <strong>π― SELECTED CHARACTER:</strong><br> |
| <div class="character-info"> |
| <span class="character-japanese">{jp_name}</span> |
| <span class="character-romaji">{romaji}</span> |
| </div> |
| </div> |
| """ |
| return "" |
| |
| |
| gen_btn.click( |
| tts_generate, |
| inputs=[spk_input, text_in, speed_sl], |
| outputs=[audio_out, img_disp] |
| ) |
| |
| refresh_btn.click( |
| refresh_model, |
| inputs=[], |
| outputs=[spk_input, refresh_status] |
| ).then( |
| lambda: f""" |
| <div class="success-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: 15px 0; border-color: rgba(56, 161, 105, 0.2)"> |
| <small class="gradient-text">v3.0 β’ Model refreshed at {gr.utils.get_current_time()}</small> |
| </div> |
| """, |
| outputs=[status_md] |
| ) |
| |
| test_btn.click( |
| test_generation, |
| inputs=[], |
| outputs=[audio_out, refresh_status] |
| ) |
| |
| clear_btn.click( |
| lambda: ("", None, ""), |
| outputs=[text_in, audio_out, refresh_status] |
| ) |
| |
| |
| spk_input.change( |
| on_speaker_change, |
| inputs=[spk_input], |
| outputs=[status_md] |
| ) |
|
|
| |
| if __name__ == "__main__": |
| print("=" * 70) |
| print("WAIFU-ANIME-TTS - PREMIUM VOICE GENERATION") |
| print("=" * 70) |
| 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("=" * 70) |
| print("Starting server on http://localhost:7860") |
| print("=" * 70) |
| |
| try: |
| demo.launch( |
| server_name="0.0.0.0", |
| server_port=7860, |
| share=False, |
| debug=False, |
| show_error=True, |
| quiet=True, |
| show_api=False |
| ) |
| except Exception as e: |
| print(f"Failed to launch: {e}") |
| print("Trying alternative port...") |
| demo.launch( |
| server_name="0.0.0.0", |
| server_port=7861, |
| share=False, |
| debug=True, |
| show_error=True |
| ) |