Spaces:
Paused
Paused
File size: 10,370 Bytes
18cc23b 2964929 18cc23b 1b70de7 678c874 1b70de7 678c874 1b70de7 7fc2a4b 417e452 18cc23b a1adacb 18cc23b 8c96708 18cc23b d1678b4 18cc23b 8c96708 2964929 8c96708 18cc23b 678c874 18cc23b 8c96708 7fc2a4b 18cc23b 7fc2a4b 417e452 18cc23b 2964929 18cc23b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | # ==== 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
@GPU_DECORATOR
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()
|