File size: 12,586 Bytes
6f192dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
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
        '''
        from lib.functions import check_missing_files, download_model
        for mod in models.keys():
            if mod == 'xtts':
                mod_exists, err, list = check_missing_files(models[mod]['local'], models[mod]['files'])
                if mod_exists:
                    print('All specified xtts base model files are present in the folder.')
                else:
                    print('The following files are missing:', list)
                    print(f'Downloading {mod} files . . .')
                    download_model(models[mod]['local'], models[mod]['url'])
        '''
        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' --voice 'path_to_voice'
    Graphic Interface:
    ebook2audiobook.cmd
Linux/Mac:
    headless:
    ./ebook2audiobook.sh --headless --ebook 'path_to_ebook' --voice 'path_to_voice'
    Graphic Interface:
    ./ebook2audiobook.sh
        ''',
        formatter_class=argparse.RawTextHelpFormatter
    )
    options = [
        '--script_mode', '--share', '--headless', 
        '--session', '--ebook', '--ebooks_dir',
        '--voice', '--language', '--device', 
        #'--custom_model',
        #'--custom_model_url',
        '--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, 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='Path to the custom model file (.pth). Required if using a custom model.')
    parser.add_argument(options[10], type=str,
                        help=("URL to download the custom model as a zip file. Optional, but will be used if provided. "
                              "Examples include David Attenborough's model: "
                              "'https://huggingface.co/drewThomasson/xtts_David_Attenborough_fine_tune/resolve/main/Finished_model_files.zip?download=true'. "
                              "More XTTS fine-tunes can be found on my Hugging Face at 'https://huggingface.co/drewThomasson'."))
    """
    parser.add_argument(options[9], 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[10], 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[11], type=float, default=2.5,
                        help='A penalty that prevents the autoregressive decoder from repeating itself. Default to 2.5')
    parser.add_argument(options[12], 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[13], 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[14], type=float, default=1.0,
                        help='Speed factor for the speech generation. Default to 1.0')
    parser.add_argument(options[15], 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[16], action='store_true',
                        help='Enable splitting text into sentences. Default to False.')
    parser.add_argument(options[17], 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 = parser.parse_args()

    # Check if the port is already in use to prevent multiple launches
    if not args.headless and is_port_in_use(gradio_interface_port):
        print(f'Error: Port {gradio_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:
            print('Package requirements ok')
            if check_dictionary():
                print ('Dictionary ok')
            else:
                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()