File size: 11,525 Bytes
6466c67
518cdf9
fc032f5
 
518cdf9
 
fc032f5
 
 
 
 
150a0eb
 
3939bf8
 
150a0eb
fc032f5
 
 
 
 
 
 
 
518cdf9
fc032f5
 
 
 
 
518cdf9
fc032f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518cdf9
fc032f5
 
 
518cdf9
fc032f5
 
 
 
518cdf9
fc032f5
 
518cdf9
fc032f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518cdf9
fc032f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518cdf9
fc032f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518cdf9
fc032f5
 
 
aecf640
fc032f5
 
 
 
 
 
 
 
 
 
 
 
 
 
518cdf9
fc032f5
 
518cdf9
fc032f5
 
 
 
 
690e666
fc032f5
 
 
518cdf9
fc032f5
 
 
 
 
 
e61b434
fc032f5
 
d069ecb
fc032f5
 
 
 
 
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
233
234
235
236
237
238
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

# To download the voices folder
import download_github_folder
# Call the main function to execute the downloading process
download_github_folder.main()

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