2ch commited on
Commit
7d6516a
·
verified ·
0 Parent(s):
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
MANIFEST.in ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ include requirements.txt
2
+ recursive-include tts_colab/static *
3
+ recursive-include tts_colab/templates *
pyproject.toml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tts_colab"
7
+ version = "0.1.5"
8
+ dependencies = [
9
+ "pydub==0.25.1",
10
+ "soundfile==0.13.1",
11
+ "f5-tts==1.1.7",
12
+ "ruaccent==1.5.8.3",
13
+ "numpy<=1.26.4",
14
+ "huggingface_hub",
15
+ "requests",
16
+ "flask",
17
+ "waitress",
18
+ "onnx_asr"
19
+ ]
20
+
21
+ [tool.setuptools]
22
+ package-dir = {"" = "."}
23
+ packages = ["tts_colab"]
24
+
25
+ [tool.setuptools.package-data]
26
+ "tts_colab" = ["static/css/*", "static/js/*", "static/generated/*", "templates/*"]
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pydub==0.25.1
2
+ soundfile==0.13.1
3
+ f5-tts==1.1.7
4
+ ruaccent==1.5.8.3
5
+ numpy<=1.26.4
6
+ onnx_asr
7
+ huggingface_hub
8
+ requests
9
+ flask
10
+ waitress
11
+
setup.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from setuptools import setup, find_packages
2
+
3
+ def parse_requirements():
4
+ requirements = []
5
+ with open('requirements.txt', 'r') as f:
6
+ for line in f:
7
+ line = line.strip()
8
+ if not line or line.startswith('#') or line.startswith('-'):
9
+ continue
10
+
11
+ requirements.append(line)
12
+ return requirements
13
+
14
+
15
+ setup(
16
+ name="tts_colab",
17
+ version="0.1.5",
18
+ packages=find_packages(),
19
+ include_package_data=True,
20
+ package_data={
21
+ 'tts_colab': [
22
+ 'static/css/*.css',
23
+ 'static/js/*.js',
24
+ 'static/generated/*',
25
+ 'templates/*.html'
26
+ ],
27
+ },
28
+ install_requires=parse_requirements(),
29
+ entry_points={
30
+ 'console_scripts': [
31
+ 'tts-colab=tts_colab.tts:main',
32
+ ],
33
+ },
34
+ author="prolapse",
35
+ author_email="[email protected]",
36
+ description="Text-to-Speech web application for Colab",
37
+ keywords="tts colab text-to-speech",
38
+ classifiers=[
39
+ "Development Status :: 4 - Beta",
40
+ "Intended Audience :: Developers",
41
+ "License :: OSI Approved :: MIT License",
42
+ "Programming Language :: Python :: 3",
43
+ "Operating System :: OS Independent",
44
+ ],
45
+ )
tts_colab/__init__.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from random import randint
3
+ from os import environ
4
+
5
+
6
+ tmp = Path(__file__).parent / 'tmp'
7
+ tmp.mkdir(exist_ok=True)
8
+ (tmp / 'transformers').mkdir(exist_ok=True)
9
+ (tmp / 'hub').mkdir(exist_ok=True)
10
+
11
+ environ['appdata'] = tmp.resolve().as_posix()
12
+ environ['userprofile'] = tmp.resolve().as_posix()
13
+ environ['temp'] = tmp.resolve().as_posix()
14
+ environ['PYTHONWARNINGS'] = 'ignore::FutureWarning'
15
+
16
+ environ['HF_HOME'] = tmp.resolve().as_posix()
17
+ environ['TRANSFORMERS_CACHE'] = (tmp / 'transformers').resolve().as_posix()
18
+ environ['HUGGINGFACE_HUB_CACHE'] = (tmp / 'hub').resolve().as_posix()
19
+
20
+
21
+ if 'MPLBACKEND' in environ:
22
+ del environ['MPLBACKEND']
23
+
24
+ from waitress import serve
25
+ from tts_colab.share import try_all
26
+ from tts_colab.tts import app
27
+
28
+
29
+ def start(port: int = randint(7860, 8999)):
30
+ try:
31
+ from IPython.display import clear_output
32
+
33
+ clear_output()
34
+ except:
35
+ pass
36
+
37
+ print(f'ссылки на WebUI:\n{try_all(port)}')
38
+
39
+ serve(app, host='0.0.0.0', port=port)
40
+
41
+
42
+
tts_colab/__main__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from tts_colab import *
2
+
3
+ if __name__ == '__main__':
4
+ start()
tts_colab/share.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from atexit import register as exit_register
2
+ from os import close, read, name as os_name
3
+ from pathlib import Path
4
+ from pty import openpty
5
+ from re import compile, search
6
+ from shutil import move
7
+ from subprocess import PIPE, Popen, STDOUT, check_output, CalledProcessError
8
+ from sys import stdout
9
+ from time import sleep, time
10
+ from urllib.parse import unquote, urlparse
11
+
12
+ from requests import get as get_url, head as get_head
13
+ from requests.structures import CaseInsensitiveDict
14
+
15
+ WORK_FOLDER = Path('/content')
16
+
17
+ claudflare_bin = WORK_FOLDER / 'claudflared'
18
+ gradio_bin = WORK_FOLDER / 'frpc_linux_amd64'
19
+ links_file = WORK_FOLDER / 'links.txt'
20
+
21
+
22
+
23
+ def determine_archive_format(filepath: str | Path) -> str | None:
24
+ filepath = Path(filepath)
25
+ zip_signature = bytes([0x50, 0x4B, 0x03, 0x04])
26
+ seven_z_signature = bytes([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C])
27
+ lzma_xz_signature = bytes([0xFD, 0x37, 0x7A, 0x58, 0x5A])
28
+ tgz_signature = bytes([0x1F, 0x8B])
29
+ tbz_signature = bytes([0x42, 0x5A, 0x68])
30
+ ustar_signature = bytes([0x75, 0x73, 0x74, 0x61, 0x72])
31
+ with filepath.open('rb') as file:
32
+ header = file.read(262)
33
+ if header.startswith(zip_signature):
34
+ return 'zip'
35
+ elif header.startswith(seven_z_signature) or header.startswith(lzma_xz_signature):
36
+ return '7z'
37
+ elif header.startswith(tgz_signature):
38
+ return 'tar.gz'
39
+ elif header.startswith(tbz_signature):
40
+ return 'tar.bz2'
41
+ elif header[0x101:0x101 + len(ustar_signature)] == ustar_signature:
42
+ return 'tar'
43
+ return None
44
+
45
+
46
+ def unpack_archive(archive_path: str | Path, dest_path: str | Path, rm_archive: bool = True):
47
+ if str(archive_path).startswith(('https://', 'http://')):
48
+ archive_path = download(archive_path, save_path=WORK_FOLDER, progress=False)
49
+ if not Path(archive_path).exists():
50
+ raise RuntimeError(f'архив {archive_path} не найден.')
51
+ archive_path = Path(archive_path)
52
+ dest_path = Path(dest_path)
53
+
54
+ if not archive_path.exists():
55
+ raise RuntimeError(f'архив {archive_path} не найден.')
56
+
57
+ determine_format = determine_archive_format(archive_path)
58
+
59
+ try:
60
+ if determine_format == '7z' or archive_path.suffix == '.7z':
61
+ run(f'7z -bso0 -bd -slp -y x {str(archive_path)} -o{str(dest_path)}')
62
+ archive_path.unlink(missing_ok=True) if rm_archive else None
63
+ elif determine_format == 'tar' or archive_path.suffix in ['.tar']:
64
+ run(f'tar -xvpf {str(archive_path)} -C {str(dest_path)}')
65
+ archive_path.unlink(missing_ok=True) if rm_archive else None
66
+ elif determine_format == 'tar.gz' or archive_path.suffix in ['.tar.gz', '.tar.bz2', '.tar.xz']:
67
+ result = run(f'tar -xvzpf {str(archive_path)} -C {str(dest_path)}')
68
+ if result['status_code'] != 0:
69
+ run(f'gzip -d {str(archive_path)}', dest_path)
70
+ archive_path.unlink(missing_ok=True) if rm_archive else None
71
+ elif determine_format == 'zip' or archive_path.suffix == '.zip':
72
+ run(f'unzip {str(archive_path)} -d {str(dest_path)}')
73
+ archive_path.unlink(missing_ok=True) if rm_archive else None
74
+ else:
75
+ run(f'7z -bso0 -bd -slp -y x {str(archive_path)} -o{str(dest_path)}')
76
+ archive_path.unlink(missing_ok=True) if rm_archive else None
77
+ except:
78
+ try:
79
+ run(f'7z -bso0 -bd -mmt4 -slp -y x {str(archive_path)} -o{str(dest_path)}')
80
+ archive_path.unlink(missing_ok=True) if rm_archive else None
81
+ except:
82
+ raise RuntimeError(
83
+ f'формат архива {archive_path.suffix} не определен, нужно задать формат в "determine_format".')
84
+
85
+
86
+ def run(command: str, cwd: str | Path | None = None, live_output: bool = False) -> dict:
87
+ process = Popen(command, shell=True, cwd=cwd, stdout=PIPE, stderr=STDOUT)
88
+ encodings = ['iso-8859-5', 'windows-1251', 'iso-8859-1', 'cp866', 'koi8-r', 'mac_cyrillic']
89
+ is_progress_bar_pattern = r'\d+%|\d+/\d+'
90
+
91
+ def decode_output(output_data):
92
+ for encoding in encodings:
93
+ try:
94
+ return output_data.decode(encoding)
95
+ except UnicodeDecodeError:
96
+ continue
97
+ return output_data.decode('utf-8', errors='replace')
98
+
99
+ final_output = []
100
+ last_progress_bar = ''
101
+
102
+ def process_output():
103
+ nonlocal last_progress_bar
104
+ for line in iter(process.stdout.readline, b''):
105
+ try:
106
+ line_decoded = line.decode('utf-8').strip()
107
+ except UnicodeDecodeError:
108
+ line_decoded = decode_output(line.strip())
109
+
110
+ if search(is_progress_bar_pattern, line_decoded):
111
+ last_progress_bar = line_decoded
112
+ if live_output:
113
+ stdout.write('\r' + line_decoded)
114
+ else:
115
+ final_output.append(line_decoded)
116
+ if live_output:
117
+ stdout.write('\n' + line_decoded)
118
+
119
+ if live_output: stdout.flush()
120
+
121
+ process_output()
122
+ process.wait()
123
+ if last_progress_bar:
124
+ final_output.append(last_progress_bar)
125
+ return {
126
+ 'status_code': process.returncode,
127
+ 'output': '\n'.join(final_output)
128
+ }
129
+
130
+
131
+ def is_valid_url(url: str) -> bool:
132
+ headers = None
133
+ for attempt in range(2):
134
+ try:
135
+ with get_url(url, headers=headers, allow_redirects=True, stream=True) as response:
136
+ if 200 <= response.status_code < 300:
137
+ return True
138
+ except:
139
+ try:
140
+ response = get_head(url, headers=headers, allow_redirects=True)
141
+ if 200 <= response.status_code < 300:
142
+ return True
143
+ except:
144
+ pass
145
+ else:
146
+ break
147
+ return False
148
+
149
+
150
+ def get_filename_from_headers(requests_headers: CaseInsensitiveDict) -> str | None:
151
+ content_disposition = requests_headers.get('content-disposition')
152
+ if not content_disposition:
153
+ return requests_headers.get('filename')
154
+ parts = content_disposition.split(';')
155
+ filename = None
156
+ for part in parts:
157
+ part = part.strip()
158
+ if part.startswith('filename*='):
159
+ encoding, _, encoded_filename = part[len('filename*='):].partition("''")
160
+ filename = unquote(encoded_filename, encoding=encoding)
161
+ break
162
+ elif part.startswith('filename='):
163
+ filename = part[len('filename='):].strip('"')
164
+ break
165
+ return filename
166
+
167
+
168
+ def download(url: str, filename: str | Path | None = None, save_path: str | Path | None = None,
169
+ progress: bool = True) -> Path | None:
170
+ headers = None
171
+ url_with_header = url.replace('"', '').replace("'", '').split('--header=')
172
+ if len(url_with_header) > 1:
173
+ url = (url_with_header[0]).strip()
174
+ header = url_with_header[1]
175
+ headers = {
176
+ header.split(':')[0].strip(): header.split(':')[1].strip()
177
+ }
178
+ if is_valid_url(url):
179
+ save_path = Path(save_path) if save_path else Path.cwd()
180
+ save_path.mkdir(parents=True, exist_ok=True)
181
+
182
+ with get_url(url, stream=True, allow_redirects=True, headers=headers) as request:
183
+ file_size = int(request.headers.get('content-length', 0))
184
+ file_name = filename or get_filename_from_headers(request.headers) or Path(urlparse(request.url).path).name
185
+ file_path = save_path / file_name
186
+
187
+ chunk_size = max(4096, file_size // 2000)
188
+ downloaded_size = 0
189
+ try:
190
+ with open(file_path, 'wb') as fp:
191
+ start = time()
192
+ for chunk in request.iter_content(chunk_size=chunk_size):
193
+ if chunk:
194
+ fp.write(chunk)
195
+ if progress:
196
+ downloaded_size += len(chunk)
197
+ percent_completed = downloaded_size / file_size * 100
198
+ elapsed_time = time() - start
199
+ print(f'\rзагрузка {file_name}: {percent_completed:.2f}% | {elapsed_time:.2f} сек.',
200
+ end='')
201
+ except Exception as e:
202
+ raise RuntimeError(f'не удалось загрузить файл по ссылке {url}:\n{e}')
203
+ return file_path
204
+ else:
205
+ raise RuntimeError(f'недействительная ссылка на файл: {url}')
206
+
207
+
208
+ def is_process_running(process_name: str | Path) -> bool:
209
+ try:
210
+ output = check_output(['pgrep', '-f', str(process_name)], text=True)
211
+ return len(output.strip().split('\n')) > 0
212
+ except CalledProcessError:
213
+ return False
214
+
215
+
216
+ def move_path(old_path: Path | str, new_path: Path | str):
217
+ old, new = Path(old_path), Path(new_path)
218
+ if old_path.exists():
219
+ try:
220
+ old_path.replace(new)
221
+ except:
222
+ try:
223
+ move(old, new)
224
+ except:
225
+ if os_name == 'posix':
226
+ run(f'mv "{old}" "{new}"')
227
+ else:
228
+ run(f'move "{old}" "{new}"')
229
+ else:
230
+ raise RuntimeError(f'не найден исходный путь для перемещения: {old}')
231
+
232
+
233
+ def terminate_process(process_name: str, process_obj: Popen):
234
+ process_obj.stdout.close()
235
+ process_obj.stderr.close()
236
+ process_obj.terminate()
237
+ run(f'pkill -f {process_name}')
238
+
239
+
240
+ def is_ipv4(address: str) -> bool:
241
+ ipv4_pattern = compile(r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')
242
+ if ipv4_pattern.match(address):
243
+ return True
244
+ else:
245
+ return False
246
+
247
+
248
+ def get_revproxy_url(bin_url: str, need_unpack: bool, bin_path: Path, start_commands: list, read_from_stderr: bool,
249
+ lines_to_read: int, url_pattern: str = r'https://\S+', write_link: bool = False) -> str:
250
+ if not bin_path.exists():
251
+ files_before = {item.name for item in WORK_FOLDER.iterdir() if item.is_file()}
252
+ if need_unpack:
253
+ unpack_archive(download(bin_url, progress=False), WORK_FOLDER, rm_archive=True)
254
+ else:
255
+ download(bin_url, save_path=bin_path.parent, progress=False)
256
+ files_after = {item.name for item in WORK_FOLDER.iterdir() if item.is_file()}
257
+ new_files = files_after - files_before
258
+ if len(new_files) == 1:
259
+ unpacked_file = WORK_FOLDER / new_files.pop()
260
+ move_path(unpacked_file, bin_path)
261
+ else:
262
+ unpacked_files = ', '.join(new_files)
263
+ raise RuntimeError(f'ошибка при определении нового файла после распаковки!\n'
264
+ f'ожидалось что за время работы добавится один файл (распакованный бинарник),\n'
265
+ f'а добавилсь эти: {unpacked_files}')
266
+ bin_path.chmod(0o777)
267
+
268
+ if is_process_running(bin_path.name):
269
+ run(f'pkill -f {bin_path.name}')
270
+
271
+ process = Popen(start_commands, stdout=PIPE, stderr=PIPE, text=True)
272
+ lines = []
273
+ stream = process.stderr if read_from_stderr else process.stdout
274
+ for _ in range(lines_to_read):
275
+ line = stream.readline()
276
+ # print(f'# {line}')
277
+ if len(lines) == lines_to_read or not line:
278
+ break
279
+ lines.append(line.strip())
280
+
281
+ ipv4 = next((line for line in lines if is_ipv4(line)), None)
282
+
283
+ urls = [search(url_pattern, url).group() for url in lines if search(url_pattern, url)]
284
+
285
+ if urls:
286
+ exit_register(terminate_process, bin_path.name, process)
287
+ if write_link:
288
+ links_file.write_text(urls[0])
289
+ return f'{urls[0]}\n пароль(IP): {ipv4}' if ipv4 else urls[0]
290
+ else:
291
+ run(f'pkill -f {bin_path.name}')
292
+ output = '\n'.join(lines)
293
+ raise RuntimeError(f'ссылку получить не удалось, вывод работы бинарника:\n{output}')
294
+
295
+
296
+
297
+
298
+
299
+ def get_cloudflared_url(port: int) -> str:
300
+ return get_revproxy_url(
301
+ bin_url='https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64',
302
+ need_unpack=False,
303
+ bin_path=Path(claudflare_bin),
304
+ start_commands=[f'{claudflare_bin}', 'tunnel', '--url', f'http://127.0.0.1:{port}'],
305
+ read_from_stderr=True,
306
+ lines_to_read=6,
307
+ url_pattern=r'(?P<url>https?://\S+\.trycloudflare\.com)'
308
+ )
309
+
310
+
311
+ def get_gradio_url(port: int) -> str | None:
312
+ max_attempts = 3
313
+ for attempt in range(max_attempts):
314
+ try:
315
+ response = get_url('https://api.gradio.app/v2/tunnel-request')
316
+ if response and response.status_code == 200:
317
+ remote_host, remote_port = response.json()[0]['host'], int(response.json()[0]['port'])
318
+ com = [f'{gradio_bin}', 'http', '-n', 'random', '-l', f'{port}', '-i', '127.0.0.1', '--uc', '--sd',
319
+ 'random', '--ue', '--server_addr', f'{remote_host}:{remote_port}', '--disable_log_color']
320
+ return get_revproxy_url(
321
+ bin_url='https://cdn-media.huggingface.co/frpc-gradio-0.1/frpc_linux_amd64',
322
+ need_unpack=False,
323
+ bin_path=Path(gradio_bin),
324
+ start_commands=com,
325
+ read_from_stderr=False,
326
+ lines_to_read=3,
327
+ )
328
+ except Exception as e:
329
+ if attempt < max_attempts - 1:
330
+ print(f'попытка {attempt + 1} получить ссылку градио провалилась: {e}. пробуем еще...')
331
+ sleep(5)
332
+ return None
333
+ else:
334
+ raise RuntimeError(f'после трех попыток поднять туннель градио не удалось: {e}')
335
+ return None
336
+
337
+
338
+ proxies_functions = {
339
+ 'cloudflared': get_cloudflared_url,
340
+ 'gradio': get_gradio_url
341
+ }
342
+
343
+
344
+ def try_all(port: int) -> str:
345
+ results = []
346
+ for proxy_name, proxy_func in proxies_functions.items():
347
+ try:
348
+ url = proxy_func(port)
349
+ results.append(url)
350
+ except Exception as e:
351
+ print(f'{proxy_name}: {e}')
352
+ return '\n'.join(results)
353
+
354
+
355
+
tts_colab/static/css/style.css ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-color: #0d1117;
3
+ --surface-color: rgba(22, 27, 34, 0.7);
4
+ --border-color: rgba(255, 255, 255, 0.1);
5
+ --primary-gradient: linear-gradient(90deg, #00c6ff, #0072ff);
6
+ --primary-color-static: #008cff;
7
+ --secondary-color: #30363d;
8
+ --secondary-hover: #484f58;
9
+ --text-color: #c9d1d9;
10
+ --text-secondary-color: #8b949e;
11
+ --success-color: #238636;
12
+ --error-color: #da3633;
13
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
14
+ }
15
+
16
+ * { box-sizing: border-box; margin: 0; padding: 0; }
17
+ body { font-family: var(--font-family); background-color: var(--bg-color); color: var(--text-color); line-height: 1.6; font-size: 15px; overflow: hidden; }
18
+ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: var(--bg-color); } ::-webkit-scrollbar-thumb { background: var(--secondary-color); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: var(--secondary-hover); }
19
+
20
+ .main-layout { display: grid; grid-template-columns: 450px 1fr; height: 100vh; }
21
+ .controls-panel { background-color: var(--bg-color); border-right: 1px solid var(--border-color); padding: 1.5rem 2rem; overflow-y: auto; display: flex; flex-direction: column; }
22
+ .results-panel { padding: 2.5rem; overflow-y: auto; display: flex; flex-direction: column; justify-content: center; align-items: center; }
23
+
24
+ header { text-align: left; margin-bottom: 2rem; }
25
+ header h1 { font-size: 1.8rem; font-weight: 700; }
26
+ header h1 a { background: var(--primary-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-decoration: none; transition: opacity 0.3s; }
27
+ header h1 a:hover { opacity: 0.8; }
28
+
29
+ .card { background-color: var(--surface-color); border: 1px solid var(--border-color); border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); }
30
+ .card h3 { margin-bottom: 1.5rem; font-size: 1.2rem; }
31
+
32
+ .form-group { margin-bottom: 1.25rem; } .form-group:last-child { margin-bottom: 0; } .form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; }
33
+
34
+ input, textarea, select { width: 100%; padding: 0.7rem; background-color: #0d1117; border: 1px solid var(--secondary-color); border-radius: 6px; color: var(--text-color); font-family: inherit; font-size: 0.95rem; transition: all 0.2s; }
35
+ input:focus, textarea:focus, select:focus { outline: none; border-color: var(--primary-color-static); box-shadow: 0 0 0 3px rgba(0, 140, 255, 0.3); }
36
+ textarea { resize: vertical; min-height: 80px; }
37
+
38
+ .info-text { font-size: 0.85rem; color: var(--text-secondary-color); background-color: rgba(0, 140, 255, 0.05); padding: 0.75rem; border-radius: 8px; border-left: 3px solid var(--primary-color-static); margin-top: auto; }
39
+
40
+ .accordion { padding: 0; } .accordion summary { padding: 0.5rem 0; cursor: pointer; list-style: none; display: flex; justify-content: space-between; align-items: center; }
41
+ .accordion.card summary { padding: 1rem 1.5rem; } .accordion.card[open] { padding-bottom: 1.5rem; }
42
+ .accordion summary h3, .accordion summary label { margin: 0; font-size: 1.2rem; font-weight: 600; }
43
+ .accordion.text-accordion summary label { font-size: 1rem; font-weight: 500; }
44
+ .accordion summary::after { content: '▶'; font-size: 0.8em; transform: rotate(0deg); transition: transform 0.3s; }
45
+ .accordion[open] summary::after { transform: rotate(90deg); }
46
+ .accordion-content { padding-top: 1rem; } .accordion.card .accordion-content { padding: 0 1.5rem; } .accordion.text-accordion .accordion-content { padding-top: 0.5rem; }
47
+
48
+ .file-drop-area { position: relative; text-align: center; padding: 1.5rem 1rem; border: 2px dashed var(--secondary-color); border-radius: 8px; transition: all 0.3s; }
49
+ .file-drop-area.is-active { border-color: var(--primary-color-static); background-color: rgba(0, 140, 255, 0.1); }
50
+ .file-msg { color: var(--text-secondary-color); font-size: 0.9rem; }
51
+ .file-drop-area input[type="file"] { position: absolute; width: 100%; height: 100%; top: 0; left: 0; opacity: 0; cursor: pointer; }
52
+ .audio-preview { margin-top: 1rem; }
53
+
54
+ .settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
55
+ .settings-grid-bottom { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: end; margin-top: 1rem; }
56
+ .slider-group label { display: flex; justify-content: space-between; font-size: 0.9rem; }
57
+ input[type="range"] { -webkit-appearance: none; width: 100%; height: 4px; background: var(--secondary-color); border-radius: 2px; outline: none; padding: 0; }
58
+ input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; background: var(--primary-color-static); cursor: pointer; border-radius: 50%; border: 2px solid var(--bg-color); transition: background-color 0.2s; }
59
+
60
+ .checkbox-group { display: flex; align-items: center; gap: 0.5rem; padding-bottom: 0.5rem; }
61
+ .checkbox-group label { margin-bottom: 0; }
62
+ .checkbox-group input { width: auto; }
63
+ .radio-group { display: flex; gap: 1.5rem; background-color: #0d1117; border: 1px solid var(--secondary-color); border-radius: 6px; padding: 0.5rem 0.75rem; }
64
+ .radio-group div { display: flex; align-items: center; gap: 0.5rem; }
65
+ .radio-group label { margin: 0; font-weight: normal; font-size: 0.9rem; }
66
+ .radio-group input[type="radio"] { width: auto; }
67
+
68
+ .actions { display: flex; gap: 1rem; margin: 1rem 0; }
69
+ button { flex-grow: 1; padding: 0.8rem 1rem; font-size: 1rem; font-weight: 600; border: none; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; }
70
+ .primary-btn { background: var(--primary-gradient); color: white; }
71
+ .primary-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(0, 140, 255, 0.3); }
72
+ .accent-btn { background-color: var(--secondary-color); color: var(--text-color); width: 100%; margin-top: 0.75rem; flex-grow: 0; font-size: 0.9rem; padding: 0.6rem; }
73
+ .accent-btn:hover:not(:disabled) { background-color: var(--secondary-hover); }
74
+ button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none;}
75
+
76
+ #results-placeholder { text-align: center; color: var(--text-secondary-color); } #results-placeholder h2 { color: var(--text-color); margin-bottom: 0.5rem; }
77
+ #results-container { width: 100%; max-width: 700px; } .result-item { margin-bottom: 2rem; } .result-item h4 { margin-bottom: 0.75rem; color: var(--text-secondary-color); font-weight: 500; }
78
+ #spectrogram-output { width: 100%; border-radius: 8px; border: 1px solid var(--border-color); }
79
+ #status-container { width: 100%; max-width: 700px; margin-top: 1.5rem; padding: 1rem; border-radius: 8px; text-align: center; font-weight: 500; display: none; }
80
+ #status-container.success { background-color: rgba(35, 134, 54, 0.2); color: #3fb950; border: 1px solid var(--success-color); }
81
+ #status-container.error { background-color: rgba(218, 54, 51, 0.2); color: #f85149; border: 1px solid var(--error-color); }
82
+ .hidden { display: none !important; }
83
+
84
+ #generation-progress { width: 100%; max-width: 700px; }
85
+ .loader-content { background-color: var(--surface-color); padding: 2rem; border-radius: 12px; border: 1px solid var(--border-color); text-align: center; }
86
+ .loader-content h3 { font-size: 1.3rem; margin-bottom: 0.5rem; } .loader-content p { color: var(--text-secondary-color); margin-bottom: 1.5rem; }
87
+ .progress-bar-container { width: 100%; height: 8px; background-color: var(--secondary-color); border-radius: 4px; overflow: hidden; }
88
+ .progress-bar { width: 0%; height: 100%; background: var(--primary-gradient); transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
89
+ .loader-footer { display: flex; justify-content: space-between; margin-top: 0.75rem; color: var(--text-secondary-color); font-size: 0.9rem; }
90
+
91
+ .custom-audio-player { background: var(--surface-color); border: 1px solid var(--border-color); border-radius: 8px; padding: 0.75rem 1rem; display: flex; align-items: center; gap: 1rem; backdrop-filter: blur(5px); }
92
+ .play-pause-btn { background: none; border: none; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
93
+ .play-pause-btn svg { width: 24px; height: 24px; fill: var(--text-color); transition: transform 0.2s; }
94
+ .play-pause-btn:hover svg { fill: white; }
95
+ .player-controls { display: flex; align-items: center; gap: 0.75rem; width: 100%; }
96
+ .time-display { font-size: 0.85rem; color: var(--text-secondary-color); font-family: monospace; font-feature-settings: 'tnum'; }
97
+ .progress-bar-wrapper { flex-grow: 1; height: 6px; background-color: var(--secondary-color); border-radius: 3px; cursor: pointer; }
98
+ .progress-bar-fill { width: 0%; height: 100%; background: var(--primary-gradient); border-radius: 3px; transition: width 0.1s linear; }
99
+ .volume-wrapper { position: relative; display: flex; align-items: center; }
100
+ .volume-btn { background: none; border: none; cursor: pointer; padding: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; }
101
+ .volume-btn svg { width: 20px; height: 20px; fill: var(--text-secondary-color); transition: fill 0.2s; }
102
+ .volume-btn:hover svg { fill: var(--text-color); }
103
+ .volume-slider-wrapper { position: absolute; bottom: 150%; left: 50%; transform: translateX(-50%); background: var(--surface-color); border: 1px solid var(--border-color); border-radius: 6px; padding: 0.75rem 0.5rem; visibility: hidden; opacity: 0; transition: all 0.2s; }
104
+ .volume-wrapper:hover .volume-slider-wrapper { visibility: visible; opacity: 1; }
105
+ .volume-slider { -webkit-appearance: none; appearance: none; width: 8px; height: 100px; background: var(--secondary-color); border-radius: 4px; outline: none; writing-mode: bt-lr; -webkit-appearance: slider-vertical; }
106
+ .volume-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; background: var(--primary-color-static); cursor: pointer; border-radius: 50%; border: 2px solid var(--bg-color); }
107
+
108
+ .download-btn { background: none; border: none; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
109
+ .download-btn svg { width: 22px; height: 22px; fill: var(--text-secondary-color); transition: all 0.2s; }
110
+ .download-btn:hover svg { fill: var(--primary-color-static); transform: scale(1.1); }
111
+
112
+ @media (max-width: 1024px) { .main-layout { grid-template-columns: 1fr; } .controls-panel { border-right: none; height: auto; } .results-panel { height: auto; padding: 2rem; } }
tts_colab/static/js/script.js ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const ttsForm = document.getElementById('tts-form');
3
+ const generateBtn = document.getElementById('generate-btn');
4
+ const processTextBtn = document.getElementById('process-text-btn');
5
+ const generationProgress = document.getElementById('generation-progress');
6
+ const statusContainer = document.getElementById('status-container');
7
+ const resultsContainer = document.getElementById('results-container');
8
+ const resultsPlaceholder = document.getElementById('results-placeholder');
9
+ const audioOutputContainer = document.getElementById('audio-output-container');
10
+ const spectrogramOutput = document.getElementById('spectrogram-output');
11
+ const fileInput = document.getElementById('ref_audio');
12
+ const fileDropArea = document.querySelector('.file-drop-area');
13
+ const fileMsg = document.querySelector('.file-msg');
14
+ const refAudioPreview = document.getElementById('ref-audio-preview');
15
+ const refTextInput = document.getElementById('ref_text');
16
+ const genTextInput = document.getElementById('gen_text');
17
+ const loaderStatusText = document.getElementById('loader-status-text');
18
+ const timerElement = document.getElementById('timer');
19
+ const progressBar = document.querySelector('.progress-bar');
20
+ const progressPercent = document.getElementById('progress-percent');
21
+ let timerInterval, pollInterval;
22
+
23
+ const createCustomPlayer = (audioSrc) => {
24
+ const playerContainer = document.createElement('div');
25
+ playerContainer.className = 'custom-audio-player';
26
+ const audio = new Audio(audioSrc);
27
+ audio.style.display = 'none';
28
+
29
+ playerContainer.innerHTML = `
30
+ <button type="button" class="play-pause-btn">
31
+ <svg viewBox="0 0 24 24" class="play-icon"><path d="M8 5v14l11-7z"></path></svg>
32
+ <svg viewBox="0 0 24 24" class="pause-icon" style="display: none;"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path></svg>
33
+ </button>
34
+ <div class="player-controls">
35
+ <span class="time-display current-time">00:00</span>
36
+ <div class="progress-bar-wrapper">
37
+ <div class="progress-bar-fill"></div>
38
+ </div>
39
+ <span class="time-display duration">00:00</span>
40
+ </div>
41
+ <div class="volume-wrapper">
42
+ <button type="button" class="volume-btn">
43
+ <svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"></path></svg>
44
+ </button>
45
+ <div class="volume-slider-wrapper">
46
+ <input type="range" class="volume-slider" min="0" max="1" step="0.01" value="1" style="writing-mode: bt-lr; -webkit-appearance: slider-vertical;">
47
+ </div>
48
+ </div>
49
+ <a href="${audioSrc}" class="download-btn" download title="Скачать аудио">
50
+ <svg viewBox="0 0 24 24"><path d="M5 20h14v-2H5v2zM19 9h-4V3H9v6H5l7 7 7-7z"></path></svg>
51
+ </a>
52
+ `;
53
+ playerContainer.appendChild(audio);
54
+
55
+ const playPauseBtn = playerContainer.querySelector('.play-pause-btn');
56
+ const playIcon = playerContainer.querySelector('.play-icon');
57
+ const pauseIcon = playerContainer.querySelector('.pause-icon');
58
+ const currentTimeEl = playerContainer.querySelector('.current-time');
59
+ const durationEl = playerContainer.querySelector('.duration');
60
+ const progressBarFill = playerContainer.querySelector('.progress-bar-fill');
61
+ const progressBarWrapper = playerContainer.querySelector('.progress-bar-wrapper');
62
+ const volumeSlider = playerContainer.querySelector('.volume-slider');
63
+ let animationFrameId;
64
+
65
+ const formatTime = (time) => { const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; };
66
+
67
+ const updateProgress = () => {
68
+ currentTimeEl.textContent = formatTime(audio.currentTime);
69
+ progressBarFill.style.width = `${(audio.currentTime / audio.duration) * 100}%`;
70
+ animationFrameId = requestAnimationFrame(updateProgress);
71
+ };
72
+
73
+ const togglePlay = () => {
74
+ if (audio.paused) { audio.play(); playIcon.style.display = 'none'; pauseIcon.style.display = 'block'; }
75
+ else { audio.pause(); playIcon.style.display = 'block'; pauseIcon.style.display = 'none'; }
76
+ };
77
+
78
+ audio.addEventListener('loadedmetadata', () => { durationEl.textContent = formatTime(audio.duration); });
79
+ audio.addEventListener('play', () => { animationFrameId = requestAnimationFrame(updateProgress); });
80
+ audio.addEventListener('pause', () => { cancelAnimationFrame(animationFrameId); });
81
+ audio.addEventListener('ended', () => {
82
+ cancelAnimationFrame(animationFrameId); playIcon.style.display = 'block'; pauseIcon.style.display = 'none';
83
+ progressBarFill.style.width = '0%'; currentTimeEl.textContent = '00:00';
84
+ });
85
+
86
+ playPauseBtn.addEventListener('click', togglePlay);
87
+ progressBarWrapper.addEventListener('click', (e) => { const rect = progressBarWrapper.getBoundingClientRect(); const clickX = e.clientX - rect.left; audio.currentTime = audio.duration * (clickX / rect.width); });
88
+ volumeSlider.addEventListener('input', (e) => { audio.volume = e.target.value; });
89
+
90
+ return playerContainer;
91
+ };
92
+
93
+ const sliders = [ { id: 'speed', valueId: 'speed-value' }, { id: 'nfe', valueId: 'nfe-value' }, { id: 'cross_fade', valueId: 'cross-fade-value' } ];
94
+ sliders.forEach(s => { const el = document.getElementById(s.id); if (el) el.addEventListener('input', () => document.getElementById(s.valueId).textContent = el.value); });
95
+
96
+ function handleFile(file) { if (file && file.type.startsWith('audio/')) { fileMsg.textContent = file.name; refAudioPreview.innerHTML = ''; const player = createCustomPlayer(URL.createObjectURL(file)); refAudioPreview.appendChild(player); refTextInput.value = ''; } }
97
+ fileInput.addEventListener('change', () => handleFile(fileInput.files[0]));
98
+ fileDropArea.addEventListener('dragover', (e) => { e.preventDefault(); fileDropArea.classList.add('is-active'); });
99
+ fileDropArea.addEventListener('dragleave', () => fileDropArea.classList.remove('is-active'));
100
+ fileDropArea.addEventListener('drop', (e) => { e.preventDefault(); fileDropArea.classList.remove('is-active'); const f = e.dataTransfer.files; if (f.length) { fileInput.files = f; handleFile(f[0]); } });
101
+
102
+ function showStatus(message, isError = false) { statusContainer.textContent = message; statusContainer.className = isError ? 'error' : 'success'; statusContainer.style.display = 'block'; }
103
+
104
+ function startLoader() {
105
+ resultsPlaceholder.classList.add('hidden'); resultsContainer.classList.add('hidden'); generationProgress.classList.remove('hidden');
106
+ let seconds = 0; timerElement.textContent = '00:00'; progressBar.style.width = '0%'; progressPercent.textContent = '0%'; loaderStatusText.textContent = 'Инициализация...';
107
+ timerInterval = setInterval(() => { seconds++; const m = String(Math.floor(seconds/60)).padStart(2,'0'); const s = String(seconds%60).padStart(2,'0'); timerElement.textContent = `${m}:${s}`; }, 1000);
108
+ }
109
+
110
+ function stopLoader() { clearInterval(timerInterval); clearInterval(pollInterval); }
111
+
112
+ function pollStatus(taskId) {
113
+ pollInterval = setInterval(async () => {
114
+ try {
115
+ const response = await fetch(`/status/${taskId}`); const data = await response.json();
116
+ progressBar.style.width = `${data.progress || 0}%`; progressPercent.textContent = `${data.progress || 0}%`;
117
+ if (data.description) loaderStatusText.textContent = data.description;
118
+ if (data.status === 'complete' || data.status === 'error') {
119
+ stopLoader(); generationProgress.classList.add('hidden'); generateBtn.disabled = false;
120
+ if (data.status === 'complete') {
121
+ const result = data.result; resultsContainer.classList.remove('hidden');
122
+ audioOutputContainer.innerHTML = ''; const player = createCustomPlayer(result.audio_url); audioOutputContainer.appendChild(player);
123
+ spectrogramOutput.src = result.spectrogram_url; refTextInput.value = result.ref_text; genTextInput.value = result.gen_text;
124
+ showStatus('Синтез успешно завершен!', false);
125
+ } else { showStatus(`Ошибка: ${data.error}`, true); resultsPlaceholder.classList.remove('hidden'); }
126
+ }
127
+ } catch (error) { stopLoader(); generationProgress.classList.add('hidden'); resultsPlaceholder.classList.remove('hidden'); showStatus('Ошибка сети.', true); generateBtn.disabled = false; }
128
+ }, 1000);
129
+ }
130
+
131
+ processTextBtn.addEventListener('click', async () => {
132
+ processTextBtn.disabled = true; processTextBtn.innerHTML = 'Обработка...';
133
+ try {
134
+ const response = await fetch('/process-text', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ gen_text: genTextInput.value }) });
135
+ const data = await response.json();
136
+ if (data.success) { genTextInput.value = data.gen_text; showStatus('Ударения для текста генерации расставлены.', false); }
137
+ else { showStatus(data.error || 'Ошибка обработки текста.', true); }
138
+ } catch (error) { showStatus('Ошибка сети.', true); }
139
+ finally { processTextBtn.disabled = false; processTextBtn.innerHTML = '✏️ Расставить ударения'; }
140
+ });
141
+
142
+ ttsForm.addEventListener('submit', async (e) => {
143
+ e.preventDefault();
144
+ generateBtn.disabled = true; statusContainer.style.display = 'none';
145
+ startLoader(); const formData = new FormData(ttsForm);
146
+ formData.set('remove_silence', document.getElementById('remove_silence').checked);
147
+ try {
148
+ const response = await fetch('/synthesize', { method: 'POST', body: formData });
149
+ const data = await response.json();
150
+ if (response.ok && data.success) { pollStatus(data.task_id); }
151
+ else { stopLoader(); generationProgress.classList.add('hidden'); resultsPlaceholder.classList.remove('hidden'); showStatus(data.error || 'Не удалось запустить задачу.', true); generateBtn.disabled = false; }
152
+ } catch (error) { stopLoader(); generationProgress.classList.add('hidden'); resultsPlaceholder.classList.remove('hidden'); showStatus('Критическая ошибка сети.', true); generateBtn.disabled = false; }
153
+ });
154
+ });
tts_colab/templates/index.html ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ESpeech-TTS-AINetSD Interface</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
11
+ </head>
12
+ <body>
13
+ <div class="main-layout">
14
+ <aside class="controls-panel">
15
+ <header><h1>ESpeech-TTS</h1></header>
16
+ <form id="tts-form" enctype="multipart/form-data">
17
+ <div class="card">
18
+ <h3>Модель и данные</h3>
19
+ <div class="form-group"><label for="model_choice">Модель</label><select id="model_choice" name="model_choice">{% for model in models %}<option value="{{ model }}">{{ model }}</option>{% endfor %}</select></div>
20
+ <div class="form-group"><label for="ref_audio">Референтное аудио (до 12 сек)</label><div class="file-drop-area"><span class="file-msg">Перетащите файл или кликните</span><input type="file" id="ref_audio" name="ref_audio" accept="audio/*" required></div><div id="ref-audio-preview" class="audio-preview"></div></div>
21
+ <details class="accordion text-accordion"><summary><label for="ref_text">Текст референса (необязательно)</label></summary><div class="accordion-content"><textarea id="ref_text" name="ref_text" rows="3" placeholder="Оставьте пустым для ASR"></textarea></div></details>
22
+ <div class="form-group"><label for="gen_text">Текст для генерации</label><textarea id="gen_text" name="gen_text" rows="5" placeholder="Введите текст..." required></textarea><button type="button" id="process-text-btn" class="secondary-btn accent-btn">✏️ Расставить ударения</button></div>
23
+ </div>
24
+ <details class="card accordion">
25
+ <summary><h3>Настройки</h3></summary>
26
+ <div class="accordion-content">
27
+ <div class="settings-grid">
28
+ <div class="form-group slider-group"><label for="speed">Скорость: <span id="speed-value">1.0</span></label><input type="range" id="speed" name="speed" min="0.3" max="2.0" value="1.0" step="0.1"></div>
29
+ <div class="form-group slider-group"><label for="nfe">Шаги NFE: <span id="nfe-value">48</span></label><input type="range" id="nfe" name="nfe" min="4" max="64" value="48" step="2"></div>
30
+ <div class="form-group slider-group"><label for="cross_fade">Cross-Fade: <span id="cross-fade-value">0.15</span></label><input type="range" id="cross_fade" name="cross_fade" min="0.0" max="1.0" value="0.15" step="0.01"></div>
31
+ <div class="form-group"><label for="seed">Сид</label><input type="number" id="seed" name="seed" value="-1" placeholder="-1 для случайного"></div>
32
+ </div>
33
+ <div class="settings-grid-bottom">
34
+ <div class="form-group checkbox-group"><input type="checkbox" id="remove_silence" name="remove_silence"><label for="remove_silence">Удалить тишину</label></div>
35
+ <div class="form-group">
36
+ <label>Формат вывода</label>
37
+ <div class="radio-group">
38
+ <div><input type="radio" id="format_wav" name="output_format" value="wav"><label for="format_wav">WAV</label></div>
39
+ <div><input type="radio" id="format_mp3" name="output_format" value="mp3" checked><label for="format_mp3">MP3</label></div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </details>
45
+ <div class="actions"><button type="submit" id="generate-btn" class="primary-btn"><span>🎤 Сгенерировать</span></button></div>
46
+ </form>
47
+ <div class="info-text">💡 Можно использовать `+` для ручного ударения</div>
48
+ </aside>
49
+ <main class="results-panel">
50
+ <div id="results-placeholder"><h2>Результат генерации</h2><p>Здесь появится сгенерированное аудио и его спектрограмма.</p></div>
51
+ <div id="generation-progress" class="hidden"><div class="loader-content"><h3>Идёт синтез...</h3><p id="loader-status-text">Инициализация...</p><div class="progress-bar-container"><div class="progress-bar"></div></div><div class="loader-footer"><span id="progress-percent">0%</span><span id="timer">00:00</span></div></div></div>
52
+ <div id="results-container" class="hidden">
53
+ <div class="result-item">
54
+ <h4>Сгенерированное Аудио</h4>
55
+ <div id="audio-output-container"></div>
56
+ </div>
57
+ <div class="result-item">
58
+ <h4>Спектрограмма</h4>
59
+ <img id="spectrogram-output" src="" alt="Spectrogram">
60
+ </div>
61
+ </div>
62
+ <div id="status-container"></div>
63
+ </main>
64
+ </div>
65
+ <script src="{{ url_for('static', filename='js/script.js') }}"></script>
66
+ </body>
67
+ </html>
tts_colab/tts.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gc
2
+ import tempfile
3
+ import threading
4
+ import time
5
+ import traceback
6
+ import uuid
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import onnx_asr
11
+ import soundfile as sf
12
+ import torch
13
+ import torchaudio
14
+ from f5_tts.infer.utils_infer import (
15
+ infer_process, load_model, load_vocoder, preprocess_ref_audio_text,
16
+ remove_silence_for_generated_wav, save_spectrogram, tempfile_kwargs,
17
+ )
18
+ from f5_tts.model import DiT
19
+ from flask import Flask, render_template, request, jsonify
20
+ from huggingface_hub import hf_hub_download
21
+ from pydub import AudioSegment
22
+ from ruaccent import RUAccent
23
+
24
+
25
+
26
+
27
+ COLAB_ROOT = Path('/content')
28
+ GENERATED_FOLDER = Path(__file__).parent / 'static' / 'generated'
29
+
30
+ GENERATED_FOLDER.mkdir(exist_ok=True)
31
+
32
+ app = Flask(__name__)
33
+
34
+ TASK_STATUS = {}
35
+
36
+ MODEL_REPOS = {
37
+ "ESpeech-TTS-1 [RL] V2": {"repo_id": "ESpeech/ESpeech-TTS-1_RL-V2", "filename": "espeech_tts_rlv2.pt"},
38
+ "ESpeech-TTS-1 [RL] V1": {"repo_id": "ESpeech/ESpeech-TTS-1_RL-V1", "filename": "espeech_tts_rlv1.pt"},
39
+ "ESpeech-TTS-1 [SFT] 95K": {"repo_id": "ESpeech/ESpeech-TTS-1_SFT-95K", "filename": "espeech_tts_95k.pt"},
40
+ "ESpeech-TTS-1 [SFT] 265K": {"repo_id": "ESpeech/ESpeech-TTS-1_SFT-256K", "filename": "espeech_tts_256k.pt"},
41
+ "ESpeech-TTS-1 PODCASTER [SFT]": {"repo_id": "ESpeech/ESpeech-TTS-1_podcaster",
42
+ "filename": "espeech_tts_podcaster.pt"},
43
+ }
44
+ VOCAB_REPO = "ESpeech/ESpeech-TTS-1_podcaster"
45
+ VOCAB_FILENAME = "vocab.txt"
46
+ MODEL_CFG = dict(dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4)
47
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
48
+ _cached_local_paths = {}
49
+ _loaded_models_on_device = {}
50
+
51
+
52
+ def hf_download_file(repo_id: str, filename: str):
53
+ try:
54
+ p = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model")
55
+ return p
56
+ except Exception as e:
57
+ app.logger.error(f"Ошибка загрузки: {e}")
58
+ raise
59
+
60
+
61
+ def get_vocab_path():
62
+ key = f"{VOCAB_REPO}::{VOCAB_FILENAME}"
63
+ p = _cached_local_paths.get(key)
64
+ if p and Path(p).exists(): return p
65
+ p = hf_download_file(VOCAB_REPO, VOCAB_FILENAME)
66
+ _cached_local_paths[key] = p
67
+ return p
68
+
69
+
70
+ def get_model_local_path(choice: str):
71
+ repo = MODEL_REPOS[choice]
72
+ key = f"{repo['repo_id']}::{repo['filename']}"
73
+ p = _cached_local_paths.get(key)
74
+ if p and Path(p).exists(): return p
75
+ p = hf_download_file(repo["repo_id"], repo["filename"])
76
+ _cached_local_paths[key] = p
77
+ return p
78
+
79
+
80
+ def get_model_on_device(choice: str):
81
+ if choice in _loaded_models_on_device: return _loaded_models_on_device[choice]
82
+ model_file, vocab_file = get_model_local_path(choice), get_vocab_path()
83
+ model = load_model(DiT, MODEL_CFG, model_file, vocab_file=vocab_file)
84
+ model.to(DEVICE).float().eval()
85
+ _loaded_models_on_device[choice] = model
86
+ return model
87
+
88
+
89
+ class CustomProgressTracker:
90
+ def __init__(self, task_id, progress_range=(0, 100)):
91
+ self.task_id = task_id
92
+ self.start, self.end = progress_range
93
+ self.range = self.end - self.start
94
+ self.last_update_time = time.time()
95
+ self.tqdm = lambda x: x
96
+
97
+ def __call__(self, progress, desc=""):
98
+ current_time = time.time()
99
+ if current_time - self.last_update_time < 0.1: return
100
+ percent_in_range = (progress[0] / progress[1]) if progress[1] > 0 else 0
101
+ scaled_percent = int(self.start + percent_in_range * self.range)
102
+ TASK_STATUS[self.task_id].update({"progress": scaled_percent, "description": desc})
103
+ self.last_update_time = current_time
104
+
105
+
106
+ def process_text_with_accent(text, accentizer):
107
+ if not text or not text.strip() or '+' in text: return text
108
+ return accentizer.process_all(text)
109
+
110
+
111
+ def update_task_progress(task_id, progress, description):
112
+ TASK_STATUS[task_id].update({"progress": progress, "description": description})
113
+ time.sleep(0.05)
114
+
115
+
116
+ def run_synthesis(task_id, params):
117
+ try:
118
+ model_choice, ref_audio_path, ref_text, gen_text, remove_silence, seed, cross_fade_duration, nfe_step, speed, output_format = (
119
+ params['model_choice'], params['ref_audio_path'], params['ref_text'], params['gen_text'],
120
+ params['remove_silence'], params['seed'], params['cross_fade_duration'], params['nfe_step'],
121
+ params['speed'], params['output_format']
122
+ )
123
+ ref_audio_path = Path(ref_audio_path)
124
+ if seed is None or seed < 0 or seed > 2 ** 31 - 1: seed = np.random.randint(0, 2 ** 31 - 1)
125
+ torch.manual_seed(int(seed))
126
+ if not ref_text or not ref_text.strip():
127
+ update_task_progress(task_id, 5, 'Транскрипция (ASR)...')
128
+ waveform, sample_rate = torchaudio.load(ref_audio_path)
129
+ waveform = waveform.numpy()
130
+ if waveform.dtype == np.int16:
131
+ waveform = waveform / 2 ** 15
132
+ elif waveform.dtype == np.int32:
133
+ waveform = waveform / 2 ** 31
134
+ if waveform.ndim == 2: waveform = waveform.mean(axis=0)
135
+ ref_text = asr_model.recognize(waveform, sample_rate=sample_rate)
136
+ update_task_progress(task_id, 15, 'Транскрипция завершена')
137
+ processed_ref_text = process_text_with_accent(ref_text, accentizer)
138
+ model = get_model_on_device(model_choice)
139
+ update_task_progress(task_id, 20, 'Предобработка аудио...')
140
+ ref_audio_proc, processed_ref_text_final = preprocess_ref_audio_text(ref_audio_path, processed_ref_text,
141
+ show_info=app.logger.info)
142
+ update_task_progress(task_id, 30, 'Запуск синтеза...')
143
+ progress_tracker = CustomProgressTracker(task_id, progress_range=(30, 90))
144
+ final_wave, final_sample_rate, combined_spectrogram = infer_process(
145
+ ref_audio_proc, processed_ref_text_final, gen_text, model, vocoder,
146
+ cross_fade_duration=cross_fade_duration, nfe_step=nfe_step, speed=speed,
147
+ show_info=app.logger.info, progress=progress_tracker)
148
+ update_task_progress(task_id, 95, 'Постобработка...')
149
+ if remove_silence:
150
+ with tempfile.NamedTemporaryFile(suffix=".wav", **tempfile_kwargs) as f:
151
+ temp_path = f.name
152
+ sf.write(temp_path, final_wave, final_sample_rate)
153
+ remove_silence_for_generated_wav(temp_path)
154
+ final_wave_tensor, _ = torchaudio.load(temp_path)
155
+ final_wave = final_wave_tensor.squeeze().cpu().numpy()
156
+ unique_id = uuid.uuid4()
157
+ wav_filename = f'{unique_id}.wav'
158
+ wav_path = GENERATED_FOLDER / wav_filename
159
+ sf.write(wav_path, final_wave, final_sample_rate)
160
+
161
+ audio_url_final = f'/static/generated/{wav_filename}'
162
+ if output_format == 'mp3':
163
+ update_task_progress(task_id, 98, 'Конвертация в MP3...')
164
+ mp3_filename = f'{unique_id}.mp3'
165
+ mp3_path = GENERATED_FOLDER / mp3_filename
166
+ sound = AudioSegment.from_wav(wav_path)
167
+ sound.export(mp3_path, format="mp3", bitrate="192k")
168
+ audio_url_final = f'/static/generated/{mp3_filename}'
169
+ wav_path.unlink()
170
+
171
+ spectrogram_filename = f'{unique_id}.png'
172
+ spectrogram_path = GENERATED_FOLDER / spectrogram_filename
173
+ save_spectrogram(combined_spectrogram, spectrogram_path)
174
+ if DEVICE.type == "cuda": torch.cuda.empty_cache()
175
+ gc.collect()
176
+ ref_audio_path.unlink()
177
+
178
+ TASK_STATUS[task_id].update({
179
+ "status": "complete", "progress": 100, "description": "Готово!", "result": {
180
+ 'audio_url': audio_url_final, 'spectrogram_url': f'/static/generated/{spectrogram_filename}',
181
+ 'ref_text': processed_ref_text_final, 'gen_text': gen_text}})
182
+ except Exception as e:
183
+ app.logger.error(f"Ошибка в задаче {task_id}: {e}")
184
+ traceback.print_exc()
185
+ TASK_STATUS[task_id].update({"status": "error", "error": str(e)})
186
+ if params['ref_audio_path']:
187
+ Path(params['ref_audio_path']).unlink(missing_ok=True)
188
+
189
+
190
+ @app.route('/')
191
+ def index(): return render_template('index.html', models=MODEL_REPOS.keys())
192
+
193
+
194
+ @app.route('/process-text', methods=['POST'])
195
+ def process_text_endpoint():
196
+ data = request.json
197
+ gen_text = process_text_with_accent(data.get('gen_text', ''), accentizer)
198
+ return jsonify({'success': True, 'gen_text': gen_text})
199
+
200
+
201
+ @app.route('/synthesize', methods=['POST'])
202
+ def synthesize_start():
203
+ task_id = str(uuid.uuid4())
204
+ ref_audio_file = request.files.get('ref_audio')
205
+ if not ref_audio_file: return jsonify({'success': False, 'error': 'Аудиофайл не найден.'}), 400
206
+ _, ext = ref_audio_file.filename.split('.')
207
+ temp_audio_path = Path(tempfile.gettempdir()) / f"{task_id}{ext}"
208
+ ref_audio_file.save(temp_audio_path)
209
+ params = {
210
+ 'model_choice': request.form.get('model_choice'), 'ref_audio_path': temp_audio_path,
211
+ 'ref_text': request.form.get('ref_text', ''), 'gen_text': request.form.get('gen_text', ''),
212
+ 'remove_silence': request.form.get('remove_silence') == 'true', 'seed': int(request.form.get('seed', -1)),
213
+ 'cross_fade_duration': float(request.form.get('cross_fade', 0.15)),
214
+ 'nfe_step': int(request.form.get('nfe', 48)), 'speed': float(request.form.get('speed', 1.0)),
215
+ 'output_format': request.form.get('output_format', 'wav'),
216
+ }
217
+ TASK_STATUS[task_id] = {"status": "processing", "progress": 0, "description": "В очереди..."}
218
+ thread = threading.Thread(target=run_synthesis, args=(task_id, params))
219
+ thread.daemon = True
220
+ thread.start()
221
+ return jsonify({'success': True, 'task_id': task_id})
222
+
223
+
224
+ @app.route('/status/<task_id>')
225
+ def get_status(task_id):
226
+ return jsonify(TASK_STATUS.get(task_id, {"status": "not_found"}))
227
+
228
+
229
+ print(f"--- Используемое устройство: {DEVICE} ---")
230
+ print("--- Загрузка RUAccent... ---")
231
+ accentizer = RUAccent()
232
+ accentizer.load(omograph_model_size='turbo3.1', use_dictionary=True, tiny_mode=False)
233
+ print("--- Загрузка ASR (nemo-fastconformer-ru-rnnt)... ---")
234
+ asr_model = onnx_asr.load_model("nemo-fastconformer-ru-rnnt")
235
+ vocoder = load_vocoder()
236
+ try:
237
+ vocoder.to(DEVICE).float()
238
+ except:
239
+ print("--- Вокодер остался на CPU ---")
240
+
241
+
242
+