-
Notifications
You must be signed in to change notification settings - Fork 303
/
app.py
232 lines (208 loc) · 11.1 KB
/
app.py
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import argparse
import os
import regex as re
import socket
import subprocess
import sys
import unidic
from lib.conf import *
from lib.lang import language_mapping, default_language_code
def check_python_version():
current_version = sys.version_info[:2] # (major, minor)
if current_version < min_python_version or current_version > max_python_version:
error = f'''********** Error: Your OS Python version is not compatible! (current: {current_version[0]}.{current_version[1]})
Please create a virtual python environment verrsion {min_python_version[0]}.{min_python_version[1]} or {max_python_version[0]}.{max_python_version[1]}
with conda or python -v venv **********'''
print(error)
return False
else:
return True
def check_and_install_requirements(file_path):
if not os.path.exists(file_path):
print(f'Warning: File {file_path} not found. Skipping package check.')
try:
from importlib.metadata import version, PackageNotFoundError
with open(file_path, 'r') as f:
contents = f.read().replace('\r', '\n')
packages = [pkg.strip() for pkg in contents.splitlines() if pkg.strip()]
missing_packages = []
for package in packages:
# Extract package name without version specifier
pkg_name = re.split(r'[<>=]', package)[0].strip()
try:
installed_version = version(pkg_name)
except PackageNotFoundError:
print(f'{package} is missing.')
missing_packages.append(package)
pass
if missing_packages:
print('\nInstalling missing packages...')
try:
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'pip'] + missing_packages)
except subprocess.CalledProcessError as e:
print(f'Failed to install packages: {e}')
return False
return True
except Exception as e:
raise(f'An error occurred: {e}')
def check_dictionary():
unidic_path = unidic.DICDIR
dicrc = os.path.join(unidic_path, 'dicrc')
if not os.path.exists(dicrc) or os.path.getsize(dicrc) == 0:
try:
print('UniDic dictionary not found or incomplete. Downloading now...')
subprocess.run(['python', '-m', 'unidic', 'download'], check=True)
except subprocess.CalledProcessError as e:
print(f'Failed to download UniDic dictionary. Error: {e}')
raise SystemExit('Unable to continue without UniDic. Exiting...')
return True
def is_port_in_use(port):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
return s.connect_ex(('0.0.0.0', port)) == 0
def main():
global is_gui_process
# Convert the list of languages to a string to display in the help text
lang_list_str = ', '.join(list(language_mapping.keys()))
# Argument parser to handle optional parameters with descriptions
parser = argparse.ArgumentParser(
description='Convert eBooks to Audiobooks using a Text-to-Speech model. You can either launch the Gradio interface or run the script in headless mode for direct conversion.',
epilog='''
Example usage:
Windows:
headless:
ebook2audiobook.cmd --headless --ebook 'path_to_ebook'
Graphic Interface:
ebook2audiobook.cmd
Linux/Mac:
headless:
./ebook2audiobook.sh --headless --ebook 'path_to_ebook'
Graphic Interface:
./ebook2audiobook.sh
''',
formatter_class=argparse.RawTextHelpFormatter
)
options = [
'--script_mode', '--share', '--headless',
'--session', '--ebook', '--ebooks_dir',
'--voice', '--language', '--device', '--custom_model',
'--temperature', '--length_penalty', '--repetition_penalty',
'--top_k', '--top_p', '--speed',
'--enable_text_splitting', '--fine_tuned',
'--version', '--help'
]
parser.add_argument(options[0], type=str,
help='Force the script to run in NATIVE or DOCKER_UTILS')
parser.add_argument(options[1], action='store_true',
help='Enable a public shareable Gradio link. Default to False.')
parser.add_argument(options[2], nargs='?', const=True, default=False,
help='Run in headless mode. Default to True if the flag is present without a value, False otherwise.')
parser.add_argument(options[3], type=str,
help='Session to reconnect in case of interruption (headless mode only)')
parser.add_argument(options[4], type=str,
help='Path to the ebook file for conversion. Required in headless mode.')
parser.add_argument(options[5], nargs='?', const='default', type=str,
help=f'Path to the directory containing ebooks for batch conversion. Default to "{os.path.basename(ebooks_dir)}" if "default" is provided.')
parser.add_argument(options[6], type=str, default=None,
help='Path to the target voice file for TTS. Optional, must be 24khz for XTTS and 16khz for fairseq models, uses a default voice if not provided.')
parser.add_argument(options[7], type=str, default=default_language_code,
help=f'Language for the audiobook conversion. Options: {lang_list_str}. Default to English (eng).')
parser.add_argument(options[8], type=str, default='cpu', choices=['cpu', 'gpu'],
help=f'Type of processor unit for the audiobook conversion. If not specified: check first if gpu available, if not cpu is selected.')
parser.add_argument(options[9], type=str,
help=f'Path to the custom model (.zip file containing {default_model_files}). Required if using a custom model.')
parser.add_argument(options[10], type=float, default=0.65,
help='Temperature for the model. Default to 0.65. Higher temperatures lead to more creative outputs.')
parser.add_argument(options[11], type=float, default=1.0,
help='A length penalty applied to the autoregressive decoder. Default to 1.0. Not applied to custom models.')
parser.add_argument(options[12], type=float, default=2.5,
help='A penalty that prevents the autoregressive decoder from repeating itself. Default to 2.5')
parser.add_argument(options[13], type=int, default=50,
help='Top-k sampling. Lower values mean more likely outputs and increased audio generation speed. Default to 50')
parser.add_argument(options[14], type=float, default=0.8,
help='Top-p sampling. Lower values mean more likely outputs and increased audio generation speed. Default to 0.8')
parser.add_argument(options[15], type=float, default=1.0,
help='Speed factor for the speech generation. Default to 1.0')
parser.add_argument(options[16], action='store_true',
help='Enable splitting text into sentences. Default to False.')
parser.add_argument(options[17], type=str, default=default_fine_tuned,
help='Name of the fine tuned model. Optional, uses the standard model according to the TTS engine and language.')
parser.add_argument(options[18], action='version',version=f'ebook2audiobook version {version}',
help='Show the version of the script and exit')
for arg in sys.argv:
if arg.startswith('--') and arg not in options:
print(f'Error: Unrecognized option "{arg}"')
sys.exit(1)
args = vars(parser.parse_args())
# Check if the port is already in use to prevent multiple launches
if not args['headless'] and is_port_in_use(interface_port):
print(f'Error: Port {interface_port} is already in use. The web interface may already be running.')
sys.exit(1)
args['script_mode'] = args['script_mode'] if args['script_mode'] else NATIVE
args['share'] = args['share'] if args['share'] else False
if args['script_mode'] == NATIVE:
check_pkg = check_and_install_requirements(requirements_file)
if check_pkg:
if not check_dictionary():
sys.exit(1)
else:
print('Some packages could not be installed')
sys.exit(1)
from lib.functions import web_interface, convert_ebook
# Conditions based on the --headless flag
if args['headless']:
args['is_gui_process'] = False
args['audiobooks_dir'] = audiobooks_cli_dir
# Condition to stop if both --ebook and --ebooks_dir are provided
if args['ebook'] and args['ebooks_dir']:
print('Error: You cannot specify both --ebook and --ebooks_dir in headless mode.')
sys.exit(1)
# Condition 1: If --ebooks_dir exists, check value and set 'ebooks_dir'
if args['ebooks_dir']:
new_ebooks_dir = None
if args['ebooks_dir'] == 'default':
print(f'Using the default ebooks_dir: {ebooks_dir}')
new_ebooks_dir = os.path.abspath(ebooks_dir)
else:
# Check if the directory exists
if os.path.exists(args['ebooks_dir']):
new_ebooks_dir = os.path.abspath(args['ebooks_dir'])
else:
print(f'Error: The provided --ebooks_dir "{args["ebooks_dir"]}" does not exist.')
sys.exit(1)
if os.path.exists(new_ebooks_dir):
for file in os.listdir(new_ebooks_dir):
# Process files with supported ebook formats
if any(file.endswith(ext) for ext in ebook_formats):
full_path = os.path.join(new_ebooks_dir, file)
print(f'Processing eBook file: {full_path}')
args['ebook'] = full_path
progress_status, audiobook_file = convert_ebook(args)
if audiobook_file is None:
print(f'Conversion failed: {progress_status}')
sys.exit(1)
else:
print(f'Error: The directory {new_ebooks_dir} does not exist.')
sys.exit(1)
elif args['ebook']:
progress_status, audiobook_file = convert_ebook(args)
if audiobook_file is None:
print(f'Conversion failed: {progress_status}')
sys.exit(1)
else:
print('Error: In headless mode, you must specify either an ebook file using --ebook or an ebook directory using --ebooks_dir.')
sys.exit(1)
else:
args['is_gui_process'] = True
passed_arguments = sys.argv[1:]
allowed_arguments = {'--share', '--script_mode'}
passed_args_set = {arg for arg in passed_arguments if arg.startswith('--')}
if passed_args_set.issubset(allowed_arguments):
web_interface(args)
else:
print('Error: In non-headless mode, no option or only --share can be passed')
sys.exit(1)
if __name__ == '__main__':
if not check_python_version():
sys.exit(1)
else:
main()