Spaces:
Sleeping
Sleeping
| # app.py | |
| import os | |
| import sys | |
| import subprocess | |
| import logging | |
| # каб лагі ішлі адразу ў stdout | |
| os.environ.setdefault("PYTHONUNBUFFERED", "1") | |
| import spaces | |
| import gradio as gr | |
| import torch | |
| import numpy as np | |
| from huggingface_hub import hf_hub_download | |
| # ----------------------------- | |
| # Лагаванне толькі ў stdout | |
| # ----------------------------- | |
| def _setup_logging(): | |
| handler = logging.StreamHandler(sys.stdout) | |
| handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) | |
| logging.basicConfig(level=logging.INFO, handlers=[handler], force=True) | |
| logging.captureWarnings(True) | |
| logger = logging.getLogger("be-xtts-app") | |
| logger.propagate = False | |
| logger.setLevel(logging.INFO) | |
| return logger | |
| logger = _setup_logging() | |
| # --------------------------------------------------------- | |
| # 1) Клануем fork coqui-ai-TTS з падтрымкай беларускай | |
| # --------------------------------------------------------- | |
| REPO_URL = "https://github.com/tuteishygpt/coqui-ai-TTS.git" | |
| REPO_DIR = "coqui-ai-TTS" | |
| if not os.path.exists(REPO_DIR): | |
| subprocess.run(["git", "clone", REPO_URL, REPO_DIR], check=True) | |
| repo_root = os.path.abspath(REPO_DIR) | |
| if repo_root not in sys.path: | |
| sys.path.insert(0, repo_root) | |
| # --------------------------------------------------------- | |
| # 2) Імпарты з TTS | |
| # --------------------------------------------------------- | |
| from TTS.tts.configs.xtts_config import XttsConfig | |
| from TTS.tts.models.xtts import Xtts | |
| from TTS.tts.layers.xtts.tokenizer import ( | |
| split_sentence, | |
| VoiceBpeTokenizer, | |
| ) | |
| # --------------------------------------------------------- | |
| # 3) Шляхі да файлаў мадэлі | |
| # --------------------------------------------------------- | |
| repo_id = "archivartaunik/BE_XTTS_V2_10ep250k" | |
| model_dir = "./model" | |
| os.makedirs(model_dir, exist_ok=True) | |
| checkpoint_file = os.path.join(model_dir, "model.pth") | |
| config_file = os.path.join(model_dir, "config.json") | |
| vocab_file = os.path.join(model_dir, "vocab.json") | |
| default_voice_file = os.path.join(model_dir, "voice.wav") | |
| speakers_file = os.path.join(model_dir, "speakers_xtts.pth") | |
| if not os.path.exists(checkpoint_file): | |
| hf_hub_download(repo_id, filename="model.pth", local_dir=model_dir) | |
| if not os.path.exists(config_file): | |
| hf_hub_download(repo_id, filename="config.json", local_dir=model_dir) | |
| if not os.path.exists(vocab_file): | |
| hf_hub_download(repo_id, filename="vocab.json", local_dir=model_dir) | |
| if not os.path.exists(default_voice_file): | |
| hf_hub_download(repo_id, filename="voice.wav", local_dir=model_dir) | |
| # новае: падцягваем speakers_xtts.pth | |
| if not os.path.exists(speakers_file): | |
| try: | |
| hf_hub_download(repo_id, filename="speakers_xtts.pth", local_dir=model_dir) | |
| except Exception as e: | |
| logger.warning("Не атрымалася спампаваць speakers_xtts.pth: %s", e) | |
| # --------------------------------------------------------- | |
| # 4) Загрузка мадэлі і токенайзера | |
| # --------------------------------------------------------- | |
| config = XttsConfig() | |
| config.load_json(config_file) | |
| XTTS_MODEL = Xtts.init_from_config(config) | |
| XTTS_MODEL.load_checkpoint( | |
| config, | |
| checkpoint_path=checkpoint_file, | |
| vocab_path=vocab_file, | |
| use_deepspeed=False, | |
| ) | |
| device = "cuda:0" if torch.cuda.is_available() else "cpu" | |
| XTTS_MODEL.to(device) | |
| sampling_rate = int(XTTS_MODEL.config.audio["sample_rate"]) | |
| # Ініцыялізацыя VoiceBpeTokenizer і падкладанне ў мадэль | |
| tokenizer = VoiceBpeTokenizer(vocab_file=vocab_file) | |
| XTTS_MODEL.tokenizer = tokenizer | |
| # Базавыя значэнні для кандыцыянавання | |
| CFG_GPT_COND = int(getattr(XTTS_MODEL.config, "gpt_cond_len", 6)) | |
| CFG_MAX_REF = int(getattr(XTTS_MODEL.config, "max_ref_len", 20)) | |
| CFG_NORM = bool(getattr(XTTS_MODEL.config, "sound_norm_refs", True)) | |
| # --------------------------------------------------------- | |
| # 4.1) Загрузка speakers_xtts.pth | |
| # --------------------------------------------------------- | |
| SPEAKERS_DB: dict[str, dict] = {} | |
| SPEAKER_CHOICES: list[str] = ["— з аўдыё (reference) —"] | |
| if os.path.exists(speakers_file): | |
| try: | |
| raw = torch.load(speakers_file, map_location="cpu") | |
| # магчымыя фарматы: | |
| # 1) {"speakers": {name: {...}}} | |
| # 2) {name: {...}} | |
| if isinstance(raw, dict) and "speakers" in raw and isinstance(raw["speakers"], dict): | |
| speakers_dict = raw["speakers"] | |
| else: | |
| speakers_dict = raw | |
| valid_count = 0 | |
| if isinstance(speakers_dict, dict): | |
| for name, val in speakers_dict.items(): | |
| if ( | |
| isinstance(val, dict) | |
| and "gpt_cond_latent" in val | |
| and "speaker_embedding" in val | |
| ): | |
| SPEAKERS_DB[str(name)] = { | |
| "gpt_cond_latent": val["gpt_cond_latent"], | |
| "speaker_embedding": val["speaker_embedding"], | |
| } | |
| valid_count += 1 | |
| if valid_count > 0: | |
| S_NAMES = sorted(SPEAKERS_DB.keys()) | |
| SPEAKER_CHOICES.extend(S_NAMES) | |
| logger.info("Загружана %d галасоў з speakers_xtts.pth", valid_count) | |
| else: | |
| logger.warning( | |
| "speakers_xtts.pth загружаны, але не знойдзена ніводнага " | |
| "галасу з ключамі 'gpt_cond_latent' і 'speaker_embedding'." | |
| ) | |
| except Exception as e: | |
| logger.exception("Памылка пры загрузцы speakers_xtts.pth: %s", e) | |
| else: | |
| logger.warning("speakers_xtts.pth не знойдзены па шляху: %s", speakers_file) | |
| # --------------------------------------------------------- | |
| # Утыліты | |
| # --------------------------------------------------------- | |
| def clip_for_log(s: str, limit: int = 600): | |
| s = (s or "").replace("\n", " ").strip() | |
| return s if len(s) <= limit else s[:limit] + " ... [clipped]" | |
| def log_after_chunk(idx: int, text: str, ui_logs: list): | |
| line = f"[TEXT] chunk {idx}: AFTER :: {clip_for_log(text)}" | |
| logger.info(line) | |
| print(line, flush=True) # дублюем у stdout, гарантуем бачнасць | |
| ui_logs.append(line) | |
| # --------------------------------------------------------- | |
| # 5) Функцыя TTS (лагі толькі AFTER) | |
| # + падтрымка speakers_xtts.pth | |
| # + асобнае аўдыё "Прыклад прасодыі" толькі для gpt_cond_latent | |
| # --------------------------------------------------------- | |
| def text_to_speech( | |
| belarusian_story: str, | |
| speaker_audio_file: str | None, | |
| prosody_audio_file: str | None, # НОВЫ ПАРАМЕТР | |
| preset_speaker: str = "— з аўдыё (reference) —", | |
| language: str = "be", | |
| preprocess_text_flag: bool = True, | |
| gpt_cond_len: int = CFG_GPT_COND, | |
| max_ref_len: int = CFG_MAX_REF, | |
| sound_norm_refs: bool = CFG_NORM, | |
| temperature: float = 0.2, | |
| length_penalty: float = 1.0, | |
| repetition_penalty: float = 7.0, | |
| top_k: int = 30, | |
| top_p: float = 0.8, | |
| ): | |
| """ | |
| Вяртае: (sr, waveform), LOG_TEXT | |
| Лагі ўключаюць ТОЛЬКІ радкі '[TEXT] chunk N: AFTER :: ...' | |
| Параметр preset_speaker: | |
| - калі значэнне з SPEAKERS_DB — бярэцца голас з speakers_xtts.pth | |
| - калі '— з аўдыё (reference) —' — выкарыстоўваецца speaker_audio_file / voice.wav | |
| Дадаткова: | |
| - калі prosody_audio_file загружаны, ён выкарыстоўваецца ТОЛЬКІ для gpt_cond_latent | |
| - speaker_embedding вызначаецца так, як рэалізавана раней: | |
| * альбо з SPEAKERS_DB | |
| * альбо з reference-аудыё (speaker_audio_file / voice.wav) | |
| """ | |
| if not belarusian_story or belarusian_story.strip() == "": | |
| raise gr.Error("Увядзі хоць нейкі тэкст 🙂") | |
| lang_short = (language or "be").split("-")[0] | |
| chunk_limit = tokenizer.char_limits.get(lang_short, 250) | |
| # 1) падзел на чанкі | |
| try: | |
| tts_texts = split_sentence( | |
| belarusian_story.strip(), | |
| lang=lang_short, | |
| text_split_length=chunk_limit, | |
| ) | |
| tts_texts = [s.strip() for s in tts_texts if s and s.strip()] | |
| if not tts_texts: | |
| raise gr.Error("Не атрымалася падзяліць тэкст на сказы/чанкі.") | |
| except Exception as e: | |
| logger.exception("Памылка пры падзеле тэксту") | |
| raise gr.Error(f"Памылка пры падзеле тэксту: {e}") | |
| # 2) поўная апрацоўка (лагі толькі AFTER) | |
| ui_logs = [] | |
| if preprocess_text_flag: | |
| processed = [] | |
| for idx, s in enumerate(tts_texts, start=1): | |
| tokenizer.check_input_length(s, lang_short) # можа вывесці WARN у stdout | |
| s_proc = tokenizer.preprocess_text(s, lang_short) | |
| log_after_chunk(idx, s_proc, ui_logs) # ЛОГ ТОЛЬКІ AFTER | |
| processed.append(s_proc) | |
| tts_texts = processed | |
| # 3) атрыманне латэнтаў голасу: | |
| # - speaker_embedding: "як зараз рэалізавана" | |
| # - gpt_cond_latent: альбо з таго ж месца, альбо з prosody_audio_file | |
| use_preset = ( | |
| isinstance(preset_speaker, str) | |
| and preset_speaker in SPEAKERS_DB | |
| ) | |
| gpt_cond_latent = None | |
| speaker_embedding = None | |
| # 3a) speaker_embedding (і, калі няма prosody_audio_file, gpt_cond_latent) | |
| if use_preset: | |
| # галасавы прэсэт з speakers_xtts.pth | |
| try: | |
| sp = SPEAKERS_DB[preset_speaker] | |
| speaker_embedding = sp["speaker_embedding"].to(device) | |
| if not prosody_audio_file: | |
| # калі асобны прыклад прасодыі НЕ зададзены, | |
| # то gpt_cond_latent таксама бярэм з прэсэта (як раней) | |
| gpt_cond_latent = sp["gpt_cond_latent"].to(device) | |
| except Exception as e: | |
| logger.exception( | |
| "Памылка пры выкарыстанні галасу '%s' з speakers_xtts.pth", preset_speaker | |
| ) | |
| raise gr.Error( | |
| f"Памылка пры выкарыстанні падрыхтаванага галасу '{preset_speaker}': {e}" | |
| ) | |
| else: | |
| # reference-аудыё (Прыклад голасу / voice.wav) | |
| ref_path = speaker_audio_file | |
| if not ref_path or ( | |
| not isinstance(ref_path, str) | |
| and getattr(ref_path, "name", "") == "" | |
| ): | |
| ref_path = default_voice_file | |
| try: | |
| ref_gpt_cond_latent, ref_speaker_embedding = XTTS_MODEL.get_conditioning_latents( | |
| audio_path=ref_path, | |
| gpt_cond_len=int(gpt_cond_len), | |
| max_ref_length=int(max_ref_len), | |
| sound_norm_refs=bool(sound_norm_refs), | |
| ) | |
| speaker_embedding = ref_speaker_embedding.to(device) | |
| if not prosody_audio_file: | |
| # калі асобны прыклад прасодыі НЕ зададзены, | |
| # то gpt_cond_latent таксама бярэм з reference-аудыё (як раней) | |
| gpt_cond_latent = ref_gpt_cond_latent.to(device) | |
| except Exception as e: | |
| logger.exception("Памылка пры атрыманні латэнтаў голасу з reference-аудыё") | |
| raise gr.Error(f"Памылка пры атрыманні латэнтаў голасу: {e}") | |
| # 3b) Калі загружаны "Прыклад прасодыі" — выкарыстоўваем яго ТОЛЬКІ для gpt_cond_latent | |
| if prosody_audio_file: | |
| prosody_path = prosody_audio_file | |
| if not isinstance(prosody_path, str) and getattr(prosody_path, "name", ""): | |
| prosody_path = prosody_path.name | |
| try: | |
| prosody_gpt_cond_latent, _ = XTTS_MODEL.get_conditioning_latents( | |
| audio_path=prosody_path, | |
| gpt_cond_len=int(gpt_cond_len), | |
| max_ref_length=int(max_ref_len), | |
| sound_norm_refs=bool(sound_norm_refs), | |
| ) | |
| gpt_cond_latent = prosody_gpt_cond_latent.to(device) | |
| except Exception as e: | |
| logger.exception("Памылка пры атрыманні прасодыі з 'Прыклад прасодыі'") | |
| raise gr.Error(f"Памылка пры атрыманні прасодыі з аўдыё: {e}") | |
| # праверка, што абодва латэнты ёсць | |
| if gpt_cond_latent is None or speaker_embedding is None: | |
| raise gr.Error( | |
| "Не атрымалася вызначыць gpt_cond_latent або speaker_embedding. " | |
| "Праверце налады галасу і файлы аўдыё." | |
| ) | |
| # 4) генерацыя | |
| all_wavs = [] | |
| for text in tts_texts: | |
| try: | |
| with torch.no_grad(): | |
| wav_chunk = XTTS_MODEL.inference( | |
| text=text, | |
| language=lang_short, | |
| gpt_cond_latent=gpt_cond_latent, | |
| speaker_embedding=speaker_embedding, | |
| temperature=float(temperature), | |
| length_penalty=float(length_penalty), | |
| repetition_penalty=float(repetition_penalty), | |
| top_k=int(top_k), | |
| top_p=float(top_p), | |
| ) | |
| all_wavs.append(wav_chunk["wav"]) | |
| except Exception as e: | |
| logger.exception("Памылка пры генерырацыі аўдыя") | |
| raise gr.Error(f"Памылка пры генерырацыі аўдыя: {e}") | |
| if not all_wavs: | |
| raise gr.Error("Нічога не згенеравалася — праверце ўваходныя даныя.") | |
| try: | |
| out_wav = np.concatenate(all_wavs).astype(np.float32) | |
| except Exception as e: | |
| logger.exception("Памылка пры аб'яднанні аўдыя") | |
| raise gr.Error(f"Памылка пры аб'яднанні аўдыя: {e}") | |
| return (sampling_rate, out_wav), "\n".join(ui_logs) | |
| # --------------------------------------------------------- | |
| # 5.1) Прэв'ю выбранага галасу (кароткая фраза) | |
| # --------------------------------------------------------- | |
| def preview_speaker( | |
| preset_speaker: str = "— з аўдыё (reference) —", | |
| language: str = "be", | |
| ): | |
| """ | |
| Генеруе кароткі прыклад для выбранага прэсэта/рефэрэнса. | |
| Вяртае толькі (sr, waveform) без лагаў. | |
| """ | |
| lang_short = (language or "be").split("-")[0] | |
| sample_texts = { | |
| "be": "Гэта прыклад беларускага голасу.", | |
| "ru": "Это пример голоса.", | |
| "uk": "Це приклад голосу.", | |
| "pl": "To jest przykładowy głos.", | |
| "en": "This is a sample voice.", | |
| "de": "Dies ist eine Beispielstimme.", | |
| "fr": "Ceci est une voix d'exemple.", | |
| "es": "Esta es una voz de ejemplo.", | |
| } | |
| sample_text = sample_texts.get(lang_short, "This is a sample voice.") | |
| # Выкарыстоўваем тыя ж налады, што і ў асноўнай функцыі | |
| (audio, _logs) = text_to_speech( | |
| belarusian_story=sample_text, | |
| speaker_audio_file=None, # для прэсэта не трэба | |
| prosody_audio_file=None, # асобны прыклад прасодыі для прэв'ю не патрабуецца | |
| preset_speaker=preset_speaker, | |
| language=language, | |
| preprocess_text_flag=True, | |
| gpt_cond_len=CFG_GPT_COND, | |
| max_ref_len=CFG_MAX_REF, | |
| sound_norm_refs=CFG_NORM, | |
| temperature=0.2, | |
| length_penalty=1.0, | |
| repetition_penalty=7.0, | |
| top_k=30, | |
| top_p=0.8, | |
| ) | |
| return audio | |
| # --------------------------------------------------------- | |
| # 6) UI (Gradio Blocks) | |
| # --------------------------------------------------------- | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# Belarusian TTS Demo (XTTSv2 + tokenizer.py) — лагі толькі AFTER") | |
| # НОВЫ РАДОК: тэкст + прыклад голасу + прыклад прасодыі | |
| with gr.Row(): | |
| txt = gr.Textbox(lines=8, label="Тэкст") | |
| ref = gr.Audio(type="filepath", label="Прыклад голасу (≥7 с)") | |
| prosody = gr.Audio( | |
| type="filepath", | |
| label="Прыклад прасодыі (≥7 с, неабавязкова)", | |
| ) | |
| # выбар галасу з speakers_xtts.pth | |
| speaker_dropdown = gr.Dropdown( | |
| label="Падрыхтаваныя галасы (speakers_xtts.pth)", | |
| choices=SPEAKER_CHOICES, | |
| value=SPEAKER_CHOICES[0], | |
| ) | |
| # Аўдыё для прэв'ю галасу | |
| preview_audio = gr.Audio(type="numpy", label="Прэв'ю выбранага галасу") | |
| # Кнопка для праслухоўвання выбранага прэсэта | |
| preview_btn = gr.Button("▶️ Прайграць выбраны голас") | |
| with gr.Row(): | |
| language = gr.Dropdown( | |
| label="Мова (language)", | |
| choices=[ | |
| "be","ru","uk","pl","cs","en","de","fr","es", | |
| "it","pt","tr","vi","zh","ja","ko","nl","hu","ar","hi" | |
| ], | |
| value="be", | |
| ) | |
| preprocess_text_flag = gr.Checkbox( | |
| value=True, | |
| label="Апрацоўваць тэкст праз tokenizer.py" | |
| ) | |
| with gr.Accordion("Параметры кандыцыянавання (для reference-аудыё)", open=False): | |
| with gr.Row(): | |
| gpt_cond_len = gr.Slider( | |
| 1, max(1, CFG_GPT_COND*3), | |
| step=1, | |
| value=CFG_GPT_COND, | |
| label="gpt_cond_len (сек.)" | |
| ) | |
| max_ref_len = gr.Slider( | |
| 1, max(1, CFG_MAX_REF*3), | |
| step=1, | |
| value=CFG_MAX_REF, | |
| label="max_ref_len (сек.)" | |
| ) | |
| sound_norm_refs = gr.Checkbox( | |
| value=CFG_NORM, | |
| label="sound_norm_refs" | |
| ) | |
| with gr.Accordion("Параметры генерацыі", open=True): | |
| with gr.Row(): | |
| temperature = gr.Slider( | |
| 0.0, 2.0, | |
| value=0.2, | |
| step=0.01, | |
| label="temperature" | |
| ) | |
| top_k = gr.Slider( | |
| 1, 100, | |
| value=30, | |
| step=1, | |
| label="top_k" | |
| ) | |
| with gr.Row(): | |
| top_p = gr.Slider( | |
| 0.0, 1.0, | |
| value=0.8, | |
| step=0.01, | |
| label="top_p" | |
| ) | |
| length_penalty = gr.Slider( | |
| 0.5, 3.5, | |
| value=1.0, | |
| step=0.05, | |
| label="length_penalty" | |
| ) | |
| repetition_penalty = gr.Slider( | |
| 0.5, 20.0, | |
| value=7.0, | |
| step=0.1, | |
| label="repetition_penalty" | |
| ) | |
| out_audio = gr.Audio(type="numpy", label="Згенераванае аўдыя") | |
| out_logs = gr.Textbox(lines=16, label="Лагі (толькі AFTER)") | |
| btn = gr.Button("🔊 Генераваць") | |
| # асноўная генерацыя | |
| btn.click( | |
| fn=text_to_speech, | |
| inputs=[ | |
| txt, | |
| ref, | |
| prosody, # НОВЫ INPUT | |
| speaker_dropdown, | |
| language, | |
| preprocess_text_flag, | |
| gpt_cond_len, | |
| max_ref_len, | |
| sound_norm_refs, | |
| temperature, | |
| length_penalty, | |
| repetition_penalty, | |
| top_k, | |
| top_p, | |
| ], | |
| outputs=[out_audio, out_logs], | |
| ) | |
| # прэв'ю выбранага галасу | |
| preview_btn.click( | |
| fn=preview_speaker, | |
| inputs=[speaker_dropdown, language], | |
| outputs=preview_audio, | |
| ) | |
| # --------------------------------------------------------- | |
| # 7) Запуск | |
| # --------------------------------------------------------- | |
| if __name__ == "__main__": | |
| demo.launch() | |