Spaces:
Paused
Paused
Download app.py from recycleactor/tts: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/recycleactor/tts/resolve/main/app.py
- Command line
-
hf download hf://spaces/recycleactor/tts/app.py
-
curl -L -o app.py https://huggingface.co/spaces/recycleactor/tts/resolve/main/app.py
10.4 kB
| # ==== F5-TTS_RUSSIAN — свой минимальный Gradio Space, по образу GigaAM ==== | |
| # | |
| # Деплой (как GigaAM): | |
| # 1. huggingface.co -> New Space -> SDK: Gradio -> Hardware: ZeroGPU | |
| # (бесплатно, но с очередью и лимитом времени на вызов — для одной | |
| # реплики этого достаточно; если не подойдёт, платный T4 small). | |
| # 2. Положить рядом с этим файлом: | |
| # - requirements.txt (см. ниже) | |
| # - ref_voice.wav — 5-15 секунд ЧИСТОЙ речи голосом, которым должен | |
| # говорить бот (без музыки/шума на фоне, лучше строгая, ровная | |
| # дикция — F5-TTS клонирует именно тембр и манеру произношения) | |
| # - переписать REF_TEXT ниже на ТОЧНУЮ дословную расшифровку того, что | |
| # реально сказано на ref_voice.wav (лишний/неточный текст — главная | |
| # причина кривых результатов у F5-TTS) | |
| # 3. Закоммитить (через веб-интерфейс Space или git push) — Space сам | |
| # соберётся и на первый вызов подтянет веса модели с HF (holodный | |
| # старт может занять пару минут — это нормально). | |
| # 4. В переменных окружения бота на Render задать: | |
| # TTS_SPACE=твой-юзернейм/имя-space | |
| # | |
| # requirements.txt: | |
| # f5-tts | |
| # gradio | |
| # soundfile | |
| # spaces | |
| import os | |
| import subprocess | |
| import tempfile | |
| # ZeroGPU требует, чтобы `spaces` импортировался ДО любого CUDA-пакета | |
| # (torch тянется транзитивно через f5_tts) — иначе падает с | |
| # "CUDA has been initialized before importing the `spaces` package". | |
| # На этом Space хардвер всё ещё ZeroGPU (HF ставит spaces сам, | |
| # независимо от requirements.txt), поэтому просто держим декоратор | |
| # рабочим, а не пытаемся его обойти. | |
| try: | |
| import spaces | |
| GPU_DECORATOR = spaces.GPU(duration=60) | |
| except Exception: | |
| def GPU_DECORATOR(func): | |
| return func | |
| import gradio as gr | |
| import soundfile as sf | |
| from f5_tts.api import F5TTS | |
| from ruaccent import RUAccent | |
| # ---- Ударения и произношение ---- | |
| # F5TTS_v1_Base_v2 обучен с полной разметкой ударений (символ "+" перед | |
| # ударной гласной, см. карточку модели) — без неё модель периодически | |
| # путает омографы и часть слов звучит "смазанно". RUAccent расставляет | |
| # ударения автоматически в этом же формате, поэтому просто прогоняем | |
| # через него весь текст перед синтезом. | |
| # | |
| # Отдельная история — заимствования типа "тест", где буква "е" на самом | |
| # деле читается как твёрдое "э" (тэст, а не тест как в слове "тесто"). | |
| # Обычные правила русского языка это не знают, RUAccent тоже (это не | |
| # вопрос ударения, а вопрос твёрдости согласного), поэтому модель | |
| # смягчает согласный и на слух "тест" путается с "тесто". Чинится | |
| # точечно через custom_dict — сюда же дописывать новые проблемные | |
| # слова по мере обнаружения (формат: 'слово': 'правильное произношение | |
| # с ударением через +'). | |
| _CUSTOM_PRONUNCIATION = { | |
| "тест": "тэст", | |
| "теста": "тэста", | |
| "тесту": "тэсту", | |
| "тесты": "тэсты", | |
| "тестов": "тэстов", | |
| "тестом": "тэстом", | |
| "тестами": "тэстами", | |
| "тестах": "тэстах", | |
| "тестирование": "тэстир+ование", | |
| "тестировать": "тэстир+овать", | |
| "тестирую": "тэстир+ую", | |
| "тестируем": "тэстир+уем", | |
| "тестируется": "тэстир+уется", | |
| } | |
| # Загружается один раз при старте контейнера — это лёгкая CPU-модель, | |
| # GPU (ZeroGPU) ей не нужен, поэтому не прячем её внутрь @spaces.GPU | |
| # и не тратим на неё выделенное GPU-время. | |
| _accentizer = RUAccent() | |
| _accentizer.load( | |
| omograph_model_size="turbo3.1", | |
| use_dictionary=True, | |
| custom_dict=_CUSTOM_PRONUNCIATION, | |
| ) | |
| def _prepare_text(text: str) -> str: | |
| text = text.strip() | |
| # Если текст не заканчивается знаком завершения предложения, F5-TTS | |
| # иногда финиширует раньше времени и обрезает последнее слово/слог — | |
| # модель ориентируется на пунктуацию, чтобы понять, что фраза | |
| # закончена. LLM-ответы бота не всегда кончаются точкой (обрывки, | |
| # смайлики без знака и т.п.), поэтому подстраховываемся тут. | |
| if text and text[-1] not in ".!?…": | |
| text += "." | |
| return _accentizer.process_all(text) | |
| # ---- Модель ---- | |
| # F5TTS_v1_Base_v2 — самый свежий из выложенных чекпоинтов Misha24-10 на | |
| # момент написания; если на HF появится более новый (проверить | |
| # huggingface.co/Misha24-10/F5-TTS_RUSSIAN) — можно смело поменять здесь. | |
| REPO = "Misha24-10/F5-TTS_RUSSIAN" | |
| CKPT_SUBPATH = "F5TTS_v1_Base_v2/model_last_inference.safetensors" | |
| VOCAB_SUBPATH = "F5TTS_v1_Base/vocab.txt" | |
| from huggingface_hub import hf_hub_download | |
| # Сами файлы весов можно тянуть на старте — это обычное скачивание, | |
| # GPU для этого не нужен. | |
| ckpt_path = hf_hub_download(REPO, CKPT_SUBPATH) | |
| vocab_path = hf_hub_download(REPO, VOCAB_SUBPATH) | |
| # ---- Референсный голос ---- | |
| REF_AUDIO = os.path.join(os.path.dirname(__file__), "ref_voice.wav") | |
| # !!! ОБЯЗАТЕЛЬНО заменить на точный текст, реально сказанный в ref_voice.wav | |
| REF_TEXT = "Обычно я говорю спокойно и ровно, стараясь не торопиться и чётко проговаривать каждое слово. Мне нравится, когда речь звучит естественно, без лишней суеты." | |
| # А вот сам F5TTS(...) грузит чекпоинт СРАЗУ на CUDA-устройство — на | |
| # ZeroGPU видеокарта физически подключена к контейнеру только внутри | |
| # вызова функции, помеченной @spaces.GPU. Если создать модель на | |
| # верхнем уровне модуля (т.е. при старте контейнера), CUDA ещё не | |
| # подключена -> "RuntimeError: No CUDA GPUs are available". Поэтому | |
| # создаём модель лениво, при первом реальном запросе, уже внутри | |
| # GPU-функции. | |
| _tts = None | |
| # Telegram рисует нормальный voice-плеер (waveform, длительность, кнопка | |
| # расшифровки речи) только если реально получает OGG-контейнер с Opus | |
| # внутри. soundfile пишет только WAV (или OGG/Vorbis, что тоже не | |
| # подходит), поэтому финальную перекодировку в OGG/Opus делаем через | |
| # ffmpeg — он уже есть в образе HF Spaces, доп. системные зависимости | |
| # не нужны. | |
| def _to_ogg_opus(wav_path: str) -> str: | |
| ogg_path = tempfile.mktemp(suffix=".ogg") | |
| subprocess.run( | |
| [ | |
| "ffmpeg", "-y", | |
| "-i", wav_path, | |
| "-c:a", "libopus", | |
| "-b:a", "32k", | |
| "-ar", "48000", | |
| "-ac", "1", | |
| ogg_path, | |
| ], | |
| check=True, | |
| capture_output=True, | |
| ) | |
| return ogg_path | |
| def _get_tts(): | |
| global _tts | |
| if _tts is None: | |
| _tts = F5TTS( | |
| model="F5TTS_v1_Base", | |
| ckpt_file=ckpt_path, | |
| vocab_file=vocab_path, | |
| ) | |
| return _tts | |
| def tts_infer(text: str) -> str: | |
| if not text or not text.strip(): | |
| raise gr.Error("пустой текст") | |
| tts = _get_tts() | |
| gen_text = _prepare_text(text) | |
| wav, sr, _ = tts.infer( | |
| ref_file=REF_AUDIO, | |
| ref_text=REF_TEXT, | |
| gen_text=gen_text, | |
| # nfe_step повыше — чище результат, меньше заиканий/кривляний | |
| # (дефолт 32 маловат для русской модели с ударениями RUAccent). | |
| nfe_step=64, | |
| # speed чуть ниже 1.0 — модель закладывает больше времени на | |
| # символ при оценке длительности, снижает риск обрезки конца | |
| # фразы (см. _prepare_text про пунктуацию — работает в паре). | |
| speed=0.93, | |
| ) | |
| wav_path = tempfile.mktemp(suffix=".wav") | |
| sf.write(wav_path, wav, sr) | |
| return _to_ogg_opus(wav_path) | |
| demo = gr.Interface( | |
| fn=tts_infer, | |
| inputs=gr.Textbox(label="Текст"), | |
| outputs=gr.Audio(type="filepath", label="Озвучка"), | |
| api_name="tts", # именно этот путь бот дёргает как "/tts" через @gradio/client | |
| title="F5-TTS_RUSSIAN — озвучка для бота", | |
| ) | |
| demo.queue().launch() | |