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()