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 # Setup logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # Fungsi untuk mendownload file dari Hugging Face 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 dari Hugging Face BASE_URL = "https://huggingface.co/spaces/Rosmontis-Chan/Waifu-Anime-TTS/resolve/main" # File yang diperlukan - dari folder 7/ 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" } # Path lokal 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") } # Buat folder jika belum ada os.makedirs(os.path.join(LOCAL_DIR, "7"), exist_ok=True) # Download file jika belum ada logger.info("Checking for required files...") for file_name, url in FILES_TO_DOWNLOAD.items(): # Tentukan local path berdasarkan file_name 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") # Mapping untuk nama karakter dengan Romaji 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" } # Fungsi untuk mendapatkan display name dengan Romaji 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}" # Fungsi untuk memisahkan nama Jepang dan Romaji 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, "" # Fungsi untuk membaca parameter dari config.json 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 # Fungsi untuk mendapatkan parameter dengan fallback 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 # Fungsi konversi teks ke urutan angka - VERSI DIPERBAIKI UNTUK JEPANG def get_text(text, hps): """Convert text to sequence - FIXED VERSION with Japanese support""" try: # Impor modul text 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 # Dapatkan symbols dari hps symbols = getattr(hps, 'symbols', []) if not symbols: # Fallback symbols dari config (1).json 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)}") # Bersihkan teks 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 = [] # Coba menggunakan text_cleaners jika ada (japanese_cleaners) 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 # Jika masih gagal, gunakan mapping manual (hanya untuk debugging) if not text_norm: logger.warning("Using manual symbol mapping as fallback") symbol_to_id = {s: i for i, s in enumerate(symbols)} # Untuk teks Jepang, manual mapping tidak akan berfungsi karena karakter tidak ada di symbols # Kita coba konversi teks ke romaji sederhana (hanya untuk karakter latin) # Sebagai fallback ekstrem, kita buat sequence default 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 # Jika sequence kosong, buat sequence default "a" (atau simbol pertama) if not text_norm: logger.error("Text sequence is empty after all attempts, using default 'a'") # Gunakan simbol 'a' jika ada, atau simbol pertama 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)] # Tambahkan blank jika add_blank True 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 minimal sequence return torch.LongTensor([0, 1, 2]) # Memuat model 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...") # Baca config.json config_data = read_config_file(LOCAL_PATHS["config"]) if config_data is None: return None, None, [], [] # Buat hps object sederhana class HParams: def __init__(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) # Ekstrak parameter dari config n_speakers = get_config_param(config_data, ['n_speakers'], 13) # Ambil symbols dari config symbols = get_config_param(config_data, ['symbols'], []) if not symbols: # Fallback symbols dari config (1).json 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) # Dapatkan parameter model model_params = get_config_param(config_data, ['model'], {}) # Cari text_cleaners jika ada text_cleaners = get_config_param(config_data, ['data', 'text_cleaners'], None) # Cari add_blank jika ada add_blank = get_config_param(config_data, ['data', 'add_blank'], False) # Ambil speakers dari config speakers = get_config_param(config_data, ['speakers'], []) if not speakers: # Fallback ke CHARACTER_MAPPING speakers = list(CHARACTER_MAPPING.keys()) # Buat hps object 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)}") # Buat model net_g = SynthesizerTrn( len(symbols), filter_length // 2 + 1, segment_size // hop_length, n_speakers=n_speakers, **model_params ) # Load model weights try: checkpoint = torch.load(LOCAL_PATHS["model"], map_location='cpu') # Handle different checkpoint formats 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 # Remove prefixes jika ada 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") # Ambil daftar speaker dari config 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, [], [] # Load model saat startup hps, net_g, speaker_choices, speaker_display_names = load_model() # Path untuk gambar 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]}...") # Konversi teks ke sequence 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}") # Generate audio 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 # Normalisasi Audio 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) # Gunakan sampling_rate dari config 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 # Refresh model function 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" # Gunakan teks Jepang untuk test 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]}" # --- TEMA HIJAU DAN PUTIH YANG CERAH DAN INDAH --- 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("""
Generate Voice Anime