venSpeakerPy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- venSpeakerPy/__init__.py +3 -0
- venSpeakerPy/__main__.py +99 -0
- venSpeakerPy/lib_helper.py +16 -0
- venSpeakerPy/lib_sl_text.py +138 -0
- venSpeakerPy/lib_speak.py +251 -0
- venspeakerpy-0.1.0.dist-info/METADATA +168 -0
- venspeakerpy-0.1.0.dist-info/RECORD +10 -0
- venspeakerpy-0.1.0.dist-info/WHEEL +5 -0
- venspeakerpy-0.1.0.dist-info/licenses/LICENSE +21 -0
- venspeakerpy-0.1.0.dist-info/top_level.txt +1 -0
venSpeakerPy/__init__.py
ADDED
venSpeakerPy/__main__.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import pathlib
|
|
3
|
+
|
|
4
|
+
from .lib_helper import settings_selero
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main(args):
|
|
8
|
+
from .lib_speak import Speaker
|
|
9
|
+
|
|
10
|
+
sample_rate = (
|
|
11
|
+
args.sample_rate
|
|
12
|
+
if args.sample_rate
|
|
13
|
+
else settings_selero[args.language]["sample_rate"]
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
sp = Speaker(**settings_selero[args.language]["sp"])
|
|
17
|
+
|
|
18
|
+
file = pathlib.Path(args.file)
|
|
19
|
+
text = file.read_text()
|
|
20
|
+
|
|
21
|
+
if args.type_out == "speak":
|
|
22
|
+
sp.speak(text, sample_rate=sample_rate, speed=args.speed)
|
|
23
|
+
elif args.type_out == "mp3":
|
|
24
|
+
sp.to_mp3(
|
|
25
|
+
text=text,
|
|
26
|
+
name_text=args.name_text if args.name_text else file.name,
|
|
27
|
+
sample_rate=sample_rate,
|
|
28
|
+
audio_dir=args.audio_dir / "mp3",
|
|
29
|
+
speed=args.speed,
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
raise KeyError("Не указано - Каким образом вывести сентезированный текст")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
parser = argparse.ArgumentParser(
|
|
37
|
+
description="🎙️ SpeakerPy: CLI для синтеза речи с использованием SpeakerPy 🎙️",
|
|
38
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
39
|
+
epilog="Примеры использования:\n"
|
|
40
|
+
" python -m speakerpy -l ru -t speak -f ./books/example.txt;\n"
|
|
41
|
+
" python -m speakerpy -l ru -t mp3 -f ./books/example.txt;\n"
|
|
42
|
+
" python -m speakerpy -l en -t speak -f ./books/example.txt;\n"
|
|
43
|
+
" python -m speakerpy -l en -t mp3 -f ./books/example.txt;\n",
|
|
44
|
+
)
|
|
45
|
+
y = "\033[33m"
|
|
46
|
+
r = "\033[0m"
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"-l",
|
|
49
|
+
"--language",
|
|
50
|
+
choices=list(settings_selero.keys()),
|
|
51
|
+
required=True,
|
|
52
|
+
help=f"{y}Язык синтеза (ru | en){r}",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"-f",
|
|
56
|
+
"--file",
|
|
57
|
+
type=str,
|
|
58
|
+
required=True,
|
|
59
|
+
help=f"{y}Путь к файлу с текстом для озвучивания{r}",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"-t",
|
|
63
|
+
"--type_out",
|
|
64
|
+
choices=["mp3", "speak"],
|
|
65
|
+
required=True,
|
|
66
|
+
help=f"{y}Каким образом вывести синтезированный текст (mp3 | speak){r}",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"-sr",
|
|
70
|
+
"--sample_rate",
|
|
71
|
+
type=int,
|
|
72
|
+
default=48000,
|
|
73
|
+
help=f"{y}Частота дискретизации (по умолчанию: 48000){r}",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"-s",
|
|
77
|
+
"--speed",
|
|
78
|
+
type=float,
|
|
79
|
+
default=1.0,
|
|
80
|
+
help=f"{y}Скорость чтения (по умолчанию: 1.0){r}",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"-n",
|
|
84
|
+
"--name_text",
|
|
85
|
+
type=str,
|
|
86
|
+
default="",
|
|
87
|
+
help=f"{y}Имя для текста, с таким именем сохранится итоговый mp3 файл (по умолчанию: имя файла с текстом){r}",
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"-a",
|
|
91
|
+
"--audio_dir",
|
|
92
|
+
type=str,
|
|
93
|
+
default=pathlib.Path(__file__).parent.parent,
|
|
94
|
+
help=f"{y}Папка для сохранения готовых аудио файлов (по умолчанию: текущая папка){r}",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
args = parser.parse_args()
|
|
98
|
+
|
|
99
|
+
main(args)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from time import process_time
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def timeit(func):
|
|
5
|
+
"""Декоратор замера времени выполнения функции"""
|
|
6
|
+
|
|
7
|
+
def wrapper(*args, **kwargs):
|
|
8
|
+
start_time = process_time()
|
|
9
|
+
result = func(*args, **kwargs)
|
|
10
|
+
end_time = process_time()
|
|
11
|
+
print(
|
|
12
|
+
f"Время выполнения функции '{func.__name__}': {end_time - start_time} секунд"
|
|
13
|
+
)
|
|
14
|
+
return result
|
|
15
|
+
|
|
16
|
+
return wrapper
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pathlib
|
|
3
|
+
import re
|
|
4
|
+
from string import punctuation, whitespace
|
|
5
|
+
from typing import Generator, Literal
|
|
6
|
+
|
|
7
|
+
import nltk
|
|
8
|
+
from num2words import num2words
|
|
9
|
+
from transliterate import translit
|
|
10
|
+
|
|
11
|
+
nltk.download("punkt")
|
|
12
|
+
nltk.download("punkt_tab")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def nltk_remove_other_model_but_russian(directory: str | pathlib.Path):
|
|
16
|
+
"""Удалить все языки кроме русского
|
|
17
|
+
|
|
18
|
+
directory: Путь к директории в которой искать файлы
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
extension = ".pickle" # замените на нужное расширение
|
|
22
|
+
|
|
23
|
+
# обходим все файлы в указанной директории
|
|
24
|
+
for root, dirs, files in os.walk(directory):
|
|
25
|
+
for file in files:
|
|
26
|
+
# если файл имеет нужное расширение, выводим его путь
|
|
27
|
+
if file.endswith(extension) and not file.startswith("russian"):
|
|
28
|
+
abs_path = os.path.join(root, file)
|
|
29
|
+
os.remove(abs_path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SeleroText:
|
|
33
|
+
"""
|
|
34
|
+
Класс для хранения, и удобного использования текста,
|
|
35
|
+
который вы хотите синтезировать в голос
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
#: Флаг, который используется для чтения модели nltk единожды
|
|
39
|
+
IF_INIT = False
|
|
40
|
+
#: Путь для хранения моделей nltk
|
|
41
|
+
download_dir = pathlib.Path(__file__).parent.parent / "models/nltk_store"
|
|
42
|
+
###
|
|
43
|
+
#
|
|
44
|
+
#: Максимальное количество сиголов в куске текста. Если больше, то это уже новый кусок
|
|
45
|
+
_max_chunk_symbols = 1000
|
|
46
|
+
#: Символы которые нужно исключать
|
|
47
|
+
exclude_characters = whitespace + punctuation
|
|
48
|
+
|
|
49
|
+
def __init__(self, text: str, to_language: Literal["ru"]) -> None:
|
|
50
|
+
"""Преобразовать текст в подходящий формат."""
|
|
51
|
+
self.text = text
|
|
52
|
+
self.to_language = to_language
|
|
53
|
+
###
|
|
54
|
+
#
|
|
55
|
+
# Инициализация моделей nltk
|
|
56
|
+
#
|
|
57
|
+
if not self.IF_INIT:
|
|
58
|
+
os.makedirs(self.download_dir, exist_ok=True)
|
|
59
|
+
# nltk.download(
|
|
60
|
+
# "punkt",
|
|
61
|
+
# download_dir=self.download_dir,
|
|
62
|
+
# )
|
|
63
|
+
if self.to_language in ("ru"):
|
|
64
|
+
# Удалить все модели языки кроме русского языка
|
|
65
|
+
nltk_remove_other_model_but_russian(self.download_dir)
|
|
66
|
+
self.IF_INIT = True
|
|
67
|
+
#
|
|
68
|
+
self._normal()
|
|
69
|
+
|
|
70
|
+
def chunk(self) -> Generator[str, None, None]:
|
|
71
|
+
"""Поделить текст на куски"""
|
|
72
|
+
|
|
73
|
+
# 3. Разделения по предложениям
|
|
74
|
+
for t in self._punkt(self.text):
|
|
75
|
+
if t:
|
|
76
|
+
# 4. Разделения длинных предложений
|
|
77
|
+
for t2 in self._split_long_string(t, self._max_chunk_symbols):
|
|
78
|
+
# В тексте должны быть буквы
|
|
79
|
+
if re.search(r"\w", t2):
|
|
80
|
+
yield t2
|
|
81
|
+
|
|
82
|
+
##########################
|
|
83
|
+
#
|
|
84
|
+
#
|
|
85
|
+
def _normal(self) -> str:
|
|
86
|
+
"""Преобразовать текст в подходящий формат для синтеза
|
|
87
|
+
|
|
88
|
+
to_language: В какой язык сделать тралит
|
|
89
|
+
"""
|
|
90
|
+
# 1. Транскрипция иностранного языка
|
|
91
|
+
if self.to_language in ("ru"):
|
|
92
|
+
self.text = translit(self.text, self.to_language)
|
|
93
|
+
# 2. Текстовое описание цифр
|
|
94
|
+
self.text = self._nums_to_text()
|
|
95
|
+
|
|
96
|
+
def _transcription(self) -> str:
|
|
97
|
+
"""Транскрипция иностранного языка (Типа hello->хелоо)"""
|
|
98
|
+
self.text = translit(self.text, self.to_language)
|
|
99
|
+
|
|
100
|
+
def _nums_to_text(self) -> str:
|
|
101
|
+
"""Преобразует числа в буквы: 1 -> один, 23 -> двадцать три"""
|
|
102
|
+
return re.sub(
|
|
103
|
+
r"(\d+)",
|
|
104
|
+
lambda x: num2words(int(x.group(0)), lang=self.to_language),
|
|
105
|
+
self.text,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _punkt(text: str) -> Generator[str, None, None]:
|
|
110
|
+
"""Разделения по предложениям"""
|
|
111
|
+
for t in nltk.sent_tokenize(text, language="russian"):
|
|
112
|
+
# Оставляем в строке только буквы, цифры и знаки пунктуации.
|
|
113
|
+
yield re.sub(r"[^a-zA-Zа-яА-Я0-9.,!?;:\-\–\'\"\s]", "", t)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _split_long_string(text: str, slice_length: int) -> Generator[str, None, None]:
|
|
117
|
+
"""Поделить текст на части.
|
|
118
|
+
|
|
119
|
+
:param text: Текст
|
|
120
|
+
:param slice_length: Через сколько символов делить строку
|
|
121
|
+
"""
|
|
122
|
+
words = text.split()
|
|
123
|
+
current_slice = []
|
|
124
|
+
current_length = 0
|
|
125
|
+
for word in words:
|
|
126
|
+
# Добавляем текущее слово к текущей "кусочной" строке,
|
|
127
|
+
# если она еще не началась или длина слова и пробела не превышает slice_length.
|
|
128
|
+
if not current_slice or current_length + len(word) + 1 <= slice_length:
|
|
129
|
+
current_slice.append(word)
|
|
130
|
+
current_length += len(word) + 1
|
|
131
|
+
else:
|
|
132
|
+
# Иначе, если текущая "кусочная" строка уже достаточно длинная,
|
|
133
|
+
# то добавляем ее в результат и начинаем новую "кусочную" строку с текущего слова.
|
|
134
|
+
yield " ".join(current_slice)
|
|
135
|
+
current_slice = [word]
|
|
136
|
+
current_length = len(word)
|
|
137
|
+
# Добавляем последнюю "кусочную" строку в результат.
|
|
138
|
+
yield " ".join(current_slice)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from typing import Generator, Literal
|
|
5
|
+
from scipy.signal import resample_poly
|
|
6
|
+
|
|
7
|
+
import sounddevice
|
|
8
|
+
import soundfile
|
|
9
|
+
import torch
|
|
10
|
+
from silero import silero_tts
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from lib_helper import timeit
|
|
14
|
+
from lib_sl_text import SeleroText
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SpeakerBase:
|
|
18
|
+
#: Для синхронизации потоков озвучки текста
|
|
19
|
+
_th_lock_speak = threading.Lock()
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
model_id,
|
|
24
|
+
language: Literal["ru", "en"],
|
|
25
|
+
speaker: Literal["aidar", "baya", "kseniya", "xenia", "random"],
|
|
26
|
+
device="cpu",
|
|
27
|
+
) -> None:
|
|
28
|
+
self.language = language
|
|
29
|
+
self.model_id = model_id
|
|
30
|
+
# Модель
|
|
31
|
+
self.model = self._init_models(
|
|
32
|
+
model_id=model_id,
|
|
33
|
+
language=language,
|
|
34
|
+
device=device,
|
|
35
|
+
)
|
|
36
|
+
# Голос
|
|
37
|
+
self.speaker = speaker
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _init_models(
|
|
41
|
+
model_id: str,
|
|
42
|
+
language: Literal["ru"] = "ru",
|
|
43
|
+
device: Literal[
|
|
44
|
+
"cpu",
|
|
45
|
+
"cuda",
|
|
46
|
+
"ipu",
|
|
47
|
+
"xpu",
|
|
48
|
+
"mkldnn",
|
|
49
|
+
"opengl",
|
|
50
|
+
"opencl",
|
|
51
|
+
"ideep",
|
|
52
|
+
"hip",
|
|
53
|
+
"ve",
|
|
54
|
+
"fpga",
|
|
55
|
+
"ort",
|
|
56
|
+
"xla",
|
|
57
|
+
"lazy",
|
|
58
|
+
"vulkan",
|
|
59
|
+
"mps",
|
|
60
|
+
] = "cpu",
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
Инициализация ИИ модели
|
|
64
|
+
|
|
65
|
+
model_id: Доступные модели https://github.com/snakers4/silero-models/blob/master/models.yml (четверку тут использовать не получится)
|
|
66
|
+
"""
|
|
67
|
+
_device = torch.device(device)
|
|
68
|
+
_model, *_any = silero_tts(language=language, speaker=model_id, device=_device)
|
|
69
|
+
return _model
|
|
70
|
+
|
|
71
|
+
def _synthesize_text(
|
|
72
|
+
self, text: str, sample_rate: int, put_accent: bool = True, put_yo: bool = True
|
|
73
|
+
) -> torch.Tensor:
|
|
74
|
+
"""Синтезировать текст"""
|
|
75
|
+
audio: torch.Tensor = self.model.apply_tts(
|
|
76
|
+
text=text,
|
|
77
|
+
speaker=self.speaker,
|
|
78
|
+
sample_rate=sample_rate,
|
|
79
|
+
put_accent=put_accent,
|
|
80
|
+
put_yo=put_yo,
|
|
81
|
+
)
|
|
82
|
+
return audio
|
|
83
|
+
|
|
84
|
+
def _speak(
|
|
85
|
+
self,
|
|
86
|
+
th_name: int,
|
|
87
|
+
text: str,
|
|
88
|
+
audio: torch.Tensor,
|
|
89
|
+
sample_rate: int,
|
|
90
|
+
speed: float = 1.0,
|
|
91
|
+
):
|
|
92
|
+
"""Озвучить текст
|
|
93
|
+
|
|
94
|
+
:param audio: Аудио
|
|
95
|
+
:param sample_rate: Частота
|
|
96
|
+
:param speed: Скорость воспроизведения [от 1.0 до 2.0]
|
|
97
|
+
"""
|
|
98
|
+
_speed = sample_rate * speed
|
|
99
|
+
# Блокировка потока. Может говорить только один поток
|
|
100
|
+
with self._th_lock_speak:
|
|
101
|
+
# print(f"Th: {th_name}\t| {text}")
|
|
102
|
+
sounddevice.play(audio, samplerate=_speed)
|
|
103
|
+
time.sleep((len(audio) / _speed + 0.02))
|
|
104
|
+
sounddevice.stop()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Speaker(SpeakerBase):
|
|
108
|
+
def speak(
|
|
109
|
+
self,
|
|
110
|
+
text: str,
|
|
111
|
+
sample_rate: int,
|
|
112
|
+
speed: float = 1.0,
|
|
113
|
+
*,
|
|
114
|
+
volume: float = 1.0,
|
|
115
|
+
put_accent: bool = True,
|
|
116
|
+
put_yo: bool = False,
|
|
117
|
+
normalize: bool = False,
|
|
118
|
+
limiting: bool = False,
|
|
119
|
+
) -> Generator[tuple[bytes, str], None, None]:
|
|
120
|
+
"""
|
|
121
|
+
Генерация и озвучка текста с поддержкой тегов <long:x:duration>
|
|
122
|
+
|
|
123
|
+
:param text: Текст для озвучки (поддерживает <long:буква:время>)
|
|
124
|
+
:param sample_rate: Частота дискретизации
|
|
125
|
+
:param speed: Скорость воспроизведения
|
|
126
|
+
:param volume: Громкость (0.0 - 2.0)
|
|
127
|
+
:param put_accent: Автоматическая постановка ударения
|
|
128
|
+
:param put_yo: Автоматическая постановка буквы ё
|
|
129
|
+
:param normalize: Нормализация звука (вряд ли вообще понадобится, но пусть будет)
|
|
130
|
+
:param limiting: Ограничение звука (вряд ли вообще понадобится, но пусть будет)
|
|
131
|
+
:return: Чанки аудио в формате bytes
|
|
132
|
+
"""
|
|
133
|
+
if not (0.0 <= volume <= 2.0):
|
|
134
|
+
raise ValueError("Параметр volume должен быть в диапазоне от 0.0 до 2.0")
|
|
135
|
+
sl_text = SeleroText(text, to_language=self.language)
|
|
136
|
+
|
|
137
|
+
# Генерация чанков с выводом байтов по мере готовности
|
|
138
|
+
chunks = list(sl_text.chunk())
|
|
139
|
+
for i, _chunk_text in enumerate(chunks):
|
|
140
|
+
audio: torch.Tensor = self._synthesize_text(
|
|
141
|
+
_chunk_text,
|
|
142
|
+
sample_rate=sample_rate,
|
|
143
|
+
put_accent=put_accent,
|
|
144
|
+
put_yo=put_yo,
|
|
145
|
+
)
|
|
146
|
+
scaled_audio = (audio.numpy() / 5 * volume).astype(np.float32)
|
|
147
|
+
|
|
148
|
+
adjusted_audio = resample_poly(scaled_audio, up=100, down=int(100 * speed)) if speed != 1 else scaled_audio
|
|
149
|
+
|
|
150
|
+
normal = self.normalize(adjusted_audio) if normalize else adjusted_audio
|
|
151
|
+
limited = self.limiter(normal) if limiting else normal
|
|
152
|
+
|
|
153
|
+
if len(limited) > 0:
|
|
154
|
+
yield adjusted_audio.tobytes(), _chunk_text
|
|
155
|
+
|
|
156
|
+
# Если это последний чанк — добавим тишину
|
|
157
|
+
if i == len(chunks) - 1:
|
|
158
|
+
silence = np.zeros(int(sample_rate * 0.15), dtype=np.float32) # 150 мс тишины
|
|
159
|
+
yield silence.tobytes(), ""
|
|
160
|
+
|
|
161
|
+
@timeit
|
|
162
|
+
def to_wav(
|
|
163
|
+
self,
|
|
164
|
+
text: str,
|
|
165
|
+
name_text: str,
|
|
166
|
+
sample_rate: int,
|
|
167
|
+
audio_dir: pathlib.Path | str,
|
|
168
|
+
speed: float = 1.0,
|
|
169
|
+
volume: float = 1.0,
|
|
170
|
+
put_accent=True
|
|
171
|
+
) -> pathlib.Path:
|
|
172
|
+
"""
|
|
173
|
+
Синтезировать и сохранить звук в WAV файл
|
|
174
|
+
|
|
175
|
+
:param text: Текст
|
|
176
|
+
:param name_text: Имя текста (используется в имени файла)
|
|
177
|
+
:param sample_rate: Частота дискретизации
|
|
178
|
+
:param audio_dir: Куда сохранить файл
|
|
179
|
+
:param speed: Скорость речи
|
|
180
|
+
:param volume: Громкость
|
|
181
|
+
:return: Путь к сохраненному WAV-файлу
|
|
182
|
+
"""
|
|
183
|
+
name_text = name_text.replace(" ", "_")
|
|
184
|
+
audio_dir = pathlib.Path(audio_dir)
|
|
185
|
+
audio_dir.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
|
|
187
|
+
output_file = audio_dir / (
|
|
188
|
+
name_text
|
|
189
|
+
+ ".wav"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if output_file.exists():
|
|
193
|
+
print("Cache___Wav: 0 \t| ", output_file)
|
|
194
|
+
return output_file
|
|
195
|
+
|
|
196
|
+
full_audio = b''.join(self.speak(text=text, sample_rate=sample_rate, speed=speed, volume=volume, put_accent=put_accent, put_yo=True))
|
|
197
|
+
|
|
198
|
+
# Сохраняем как WAV
|
|
199
|
+
with soundfile.SoundFile(output_file, 'w', samplerate=sample_rate, channels=1, subtype='PCM_16') as f:
|
|
200
|
+
audio_array = np.frombuffer(full_audio, dtype=np.float32)
|
|
201
|
+
f.write(audio_array)
|
|
202
|
+
|
|
203
|
+
print("Build___Wav: 0 \t| ", output_file)
|
|
204
|
+
return output_file
|
|
205
|
+
|
|
206
|
+
def normalize(self, audio, target=0.95):
|
|
207
|
+
peak = np.max(np.abs(audio))
|
|
208
|
+
if peak > 0:
|
|
209
|
+
audio = audio / peak * target
|
|
210
|
+
return audio
|
|
211
|
+
|
|
212
|
+
def limiter(self, audio, threshold=0.9):
|
|
213
|
+
audio = np.clip(audio, -threshold, threshold)
|
|
214
|
+
return audio / threshold
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == '__main__':
|
|
218
|
+
import sounddevice as sd
|
|
219
|
+
import queue
|
|
220
|
+
|
|
221
|
+
audio_queue = queue.Queue()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def audio_player():
|
|
225
|
+
try:
|
|
226
|
+
with sd.OutputStream(samplerate=48000, channels=1, dtype='float32') as stream:
|
|
227
|
+
while True:
|
|
228
|
+
speech_chunk = audio_queue.get()
|
|
229
|
+
if speech_chunk is None:
|
|
230
|
+
break
|
|
231
|
+
audio_array = np.frombuffer(speech_chunk, dtype=np.float32)
|
|
232
|
+
stream.write(audio_array)
|
|
233
|
+
except KeyboardInterrupt:
|
|
234
|
+
print("Stop speaker")
|
|
235
|
+
finally:
|
|
236
|
+
sd.stop()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
player_thread = threading.Thread(target=audio_player, daemon=True)
|
|
240
|
+
player_thread.start()
|
|
241
|
+
speaker = Speaker(model_id="ru_v3", language="ru", speaker="baya", device="cpu")
|
|
242
|
+
while True:
|
|
243
|
+
text = input('Text: ')
|
|
244
|
+
text = text.replace('ё', 'е')
|
|
245
|
+
|
|
246
|
+
if text.startswith('!w '):
|
|
247
|
+
text = text.replace('!w ', '')
|
|
248
|
+
speaker.to_wav(text=text.text, sample_rate=48000, speed=1.1, volume=0.7, name_text='output', audio_dir='output')
|
|
249
|
+
else:
|
|
250
|
+
for chunk, text in speaker.speak(text=text.text, sample_rate=48000, speed=1.1, volume=0.7):
|
|
251
|
+
audio_queue.put(chunk)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: venSpeakerPy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python TTS package using Silero models for Russian language
|
|
5
|
+
Home-page: https://github.com/VenalusDev/ven-speaker-py
|
|
6
|
+
Author: VenalusDev
|
|
7
|
+
Author-email: Venalusdev@mail.ru
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy>=1.24
|
|
15
|
+
Requires-Dist: scipy<2.0,>=1.10
|
|
16
|
+
Requires-Dist: sounddevice>=0.4.7
|
|
17
|
+
Requires-Dist: soundfile>=0.12.1
|
|
18
|
+
Requires-Dist: torch<3.0,>=2.1
|
|
19
|
+
Requires-Dist: nltk<4.0,>=3.9
|
|
20
|
+
Requires-Dist: num2words<0.6,>=0.5
|
|
21
|
+
Requires-Dist: transliterate>=1.10.2
|
|
22
|
+
Requires-Dist: silero>=0.4
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: author-email
|
|
25
|
+
Dynamic: classifier
|
|
26
|
+
Dynamic: description
|
|
27
|
+
Dynamic: description-content-type
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
Dynamic: requires-dist
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
Dynamic: summary
|
|
33
|
+
|
|
34
|
+
# ven-speaker-py
|
|
35
|
+
|
|
36
|
+
**Форк библиотеки `speakerpy` с модификациями и улучшениями для потоковой работы с аудио и TTS.**
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## 🔹 Описание
|
|
42
|
+
|
|
43
|
+
ven-speaker-py — это Python-библиотека для синтеза речи с модификациями:
|
|
44
|
+
|
|
45
|
+
- Потоковая передача аудио в байтовом виде.
|
|
46
|
+
- Исправленная функция ~to_mp3~->`to_wav`.
|
|
47
|
+
- Поддержка гибкой настройки скорости, громкости и sample rate.
|
|
48
|
+
- Постепенно добавляются новые функции и улучшения под конкретные задачи.
|
|
49
|
+
|
|
50
|
+
Библиотека подходит для создания голосовых приложений, озвучивания текста в реальном времени и интеграции с другими проектами.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## Особенности и преимущества SpeakerPy
|
|
54
|
+
|
|
55
|
+
- **Транскрипция чисел и английских слов**: SpeakerPy способен озвучивать числа и английские слова через транскрипцию.
|
|
56
|
+
- **Синтез больших объемов текста**: SpeakerPy может синтезировать большие объемы текста без проблем.
|
|
57
|
+
- **Корректное деление текста на куски**: благодаря использованию библиотеки nltk, SpeakerPy корректно делит большие тексты на предложения.
|
|
58
|
+
- **Кеширование синтезированного текста**: SpeakerPy синтезирует и хранит текст по кускам, что позволяет кешировать уже ранее синтезированный текст и избежать повторного синтезирования. После первой генерации все последующие обычно занимают менее четверти секунды (имеется в виду не полная генерация, а момент, когда начинает зачитываться речь) на обычном процессоре!
|
|
59
|
+
- **Работа в автономном режиме**: SpeakerPy синтезирует звук локально, без подключения к интернету, что позволяет использовать его в автономном режиме.
|
|
60
|
+
- **Сохранение синтезированного текста в MP3**: SpeakerPy предоставляет возможность сохранять синтезированный текст в формате MP3.
|
|
61
|
+
|
|
62
|
+
## ⚙️ Установка через pip
|
|
63
|
+
|
|
64
|
+
`ven-speaker-py` можно установить с помощью pip:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install venspeakerpy
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 🛠️ Основные возможности
|
|
71
|
+
|
|
72
|
+
- **Преобразование текста в речь.**
|
|
73
|
+
- **Потоковая генерация аудио чанков.**
|
|
74
|
+
- **Поддержка сохранения в различные форматы, включая mp3.**
|
|
75
|
+
- **Настройка скорости, громкости и sample rate.**
|
|
76
|
+
- **Интеграция в голосовые боты и приложения реального времени.**
|
|
77
|
+
|
|
78
|
+
### 🚀 Системные требования
|
|
79
|
+
|
|
80
|
+
- 2 ГБ ОЗУ
|
|
81
|
+
- Наличие процессора :D
|
|
82
|
+
|
|
83
|
+
## 💡 Основные компоненты
|
|
84
|
+
|
|
85
|
+
Проект состоит из двух основных компонентов: Lib Speak и Lib SL Text.
|
|
86
|
+
|
|
87
|
+
### Lib Speak
|
|
88
|
+
|
|
89
|
+
Lib Speak включает классы `Speaker` и `SpeakerBase` для синтеза речи с использованием моделей Silero. Основные методы класса `Speaker`:
|
|
90
|
+
|
|
91
|
+
1. `speak`: ~~произнесение текста.~~ Теперь это потоковая передача звука в байтах, что позволяет использовать звук где-то еще (например, для трансляции в Discord)
|
|
92
|
+
2. `to_wav`: конвертация текста в аудиофайл wav.
|
|
93
|
+
|
|
94
|
+
#### 📄 Пример использования функции speak
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from lib_speak import Speaker
|
|
98
|
+
from lib_sl_text import SeleroText
|
|
99
|
+
import sounddevice as sd
|
|
100
|
+
import queue
|
|
101
|
+
import threading
|
|
102
|
+
import numpy as np
|
|
103
|
+
|
|
104
|
+
speaker = Speaker(model_id="ru_v3", language="ru", speaker="baya", device="cpu")
|
|
105
|
+
|
|
106
|
+
def text_to_speech(txt: str):
|
|
107
|
+
audio_queue = queue.Queue()
|
|
108
|
+
|
|
109
|
+
def audio_player():
|
|
110
|
+
with sd.OutputStream(samplerate=48000, channels=1, dtype='float32') as stream:
|
|
111
|
+
while True:
|
|
112
|
+
speech_chunk = audio_queue.get()
|
|
113
|
+
if speech_chunk is None:
|
|
114
|
+
break
|
|
115
|
+
audio_array = np.frombuffer(speech_chunk, dtype=np.float32)
|
|
116
|
+
stream.write(audio_array)
|
|
117
|
+
|
|
118
|
+
player_thread = threading.Thread(target=audio_player, daemon=True)
|
|
119
|
+
player_thread.start()
|
|
120
|
+
|
|
121
|
+
for chunk, text in speaker.speak(text=text.text, sample_rate=48000, speed=1.1, volume=0.7):
|
|
122
|
+
audio_queue.put(chunk)
|
|
123
|
+
|
|
124
|
+
audio_queue.put(None)
|
|
125
|
+
player_thread.join()
|
|
126
|
+
|
|
127
|
+
text = SeleroText("Пример текста для синтеза речи")
|
|
128
|
+
text_to_speech(text)
|
|
129
|
+
|
|
130
|
+
#p.s. я не проверял этот код, просто куски из своего проекта сюда напихал в надежде, что у вас заработает, так что удачи.
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### Пример использования функции to_wav
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from lib_speak import Speaker
|
|
138
|
+
|
|
139
|
+
text = SeleroText("Пример текста для синтеза речи")
|
|
140
|
+
speaker = Speaker(model_id="ru_v3", language="ru", speaker="aidar", device="cpu")
|
|
141
|
+
speaker.to_wav(text=text.text, sample_rate=48000, speed=1.0, volume=1.0, name_text='output', audio_dir='output')
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Lib SL Text
|
|
145
|
+
|
|
146
|
+
Lib SL Text содержит класс `SeleroText` для обработки текста перед синтезом речи. Основной метод класса `SeleroText`:
|
|
147
|
+
|
|
148
|
+
1. `chunk`: разделение текста на блоки.
|
|
149
|
+
|
|
150
|
+
#### Использование класса SeleroText
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
text = SeleroText("Привет, мир!", to_language="ru")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Разделение текста на куски с помощью метода `chunk()`:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
for chunk in text.chunk():
|
|
160
|
+
print(chunk)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Ресурсы и ссылки
|
|
164
|
+
|
|
165
|
+
- <a href="https://github.com/snakers4/silero-models/blob/master/models.yml" target="_new">Silero Models на GitHub</a>
|
|
166
|
+
- <a href="https://www.sphinx-doc.org/" target="_new">Sphinx</a>
|
|
167
|
+
- <a href="https://github.com/readthedocs/sphinx_rtd_theme" target="_new">Sphinx RTD Theme</a>
|
|
168
|
+
- <a href="https://readthedocs.org/" target="_new">Read the Docs</a>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
venSpeakerPy/__init__.py,sha256=4mVo6EsL_AqkHIiJNvHAPafPbvmLWMiFlQXizNkusSs,55
|
|
2
|
+
venSpeakerPy/__main__.py,sha256=jdd_SruDTASmB_3yYVUgee9-KQPLtetx0J8lChLCnFE,3319
|
|
3
|
+
venSpeakerPy/lib_helper.py,sha256=JZo9zuRc57X-UhWxieTw9fy0P57opWHjqJV8aPHlqms,472
|
|
4
|
+
venSpeakerPy/lib_sl_text.py,sha256=afQ4iBYzWBQYSjHmmfApeyWUzig9x7x9F4OhfIYDshE,6026
|
|
5
|
+
venSpeakerPy/lib_speak.py,sha256=VwD8No_bnE8nnH1O6PC9OuFiXUvaj9IPplsOZJEQUeQ,9080
|
|
6
|
+
venspeakerpy-0.1.0.dist-info/licenses/LICENSE,sha256=KhpHAy226In6QqqMvi2X6H3ZAFum2Tc0KK2PI-Bu9Rk,1088
|
|
7
|
+
venspeakerpy-0.1.0.dist-info/METADATA,sha256=8qMgVr5TrVetwJO_AXRKRJit9CKev0O3GxeWCi6y3TA,8177
|
|
8
|
+
venspeakerpy-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
venspeakerpy-0.1.0.dist-info/top_level.txt,sha256=aHqh6je2O9RLUo4Z7rIdpAR516r0asGToNwoZr5MbIE,13
|
|
10
|
+
venspeakerpy-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 VenalusDev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
venSpeakerPy
|