CoolFace
Apppublic

uxoxo/eb2ab

sourceHugging Faceapache-2.0updated 11mo agoView on Hugging Face
0likes
app.py332 linesDownload Raw Back to root
1import argparse2import filecmp3import importlib.util4import os5import shutil6import socket7import subprocess8import sys9import tempfile10 11from pathlib import Path12from lib import *13 14def check_virtual_env(script_mode):15    current_version = sys.version_info[:2]  # (major, minor)16    if str(os.path.basename(sys.prefix)) == 'python_env' or script_mode == FULL_DOCKER or current_version >= min_python_version and current_version <= max_python_version:17        return True  18    error = f'''***********19Wrong launch! ebook2audiobook must run in its own virtual environment!20NOTE: If you are running a Docker so you are probably using an old version of ebook2audiobook.21To solve this issue go to download the new version at https://github.com/DrewThomasson/ebook2audiobook22If the directory python_env does not exist in the ebook2audiobook root directory,23run your command with "./ebook2audiobook.sh" for Linux and Mac or "ebook2audiobook.cmd" for Windows24to install it all automatically.25{install_info}26***********'''27    print(error)28    return False29 30def check_python_version():31    current_version = sys.version_info[:2]  # (major, minor)32    if current_version < min_python_version or current_version > max_python_version:33        error = f'''***********34Wrong launch: Your OS Python version is not compatible! (current: {current_version[0]}.{current_version[1]})35In order to install and/or use ebook2audiobook correctly you must run 36"./ebook2audiobook.sh" for Linux and Mac or "ebook2audiobook.cmd" for Windows.37{install_info}38***********'''39        print(error)40        return False41    else:42        return True43 44def check_and_install_requirements(file_path):45    if not os.path.exists(file_path):46        error = f'Warning: File {file_path} not found. Skipping package check.'47        print(error)48        return False49    try:50        from importlib.metadata import version, PackageNotFoundError51        try:52            from packaging.specifiers import SpecifierSet53        except ImportError:54            subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--no-cache-dir', 'packaging'])55            from packaging.specifiers import SpecifierSet56        import regex as re57        from tqdm import tqdm58        with open(file_path, 'r') as f:59            contents = f.read().replace('\r', '\n')60            packages = [61                pkg.strip()62                for pkg in contents.splitlines()63                if pkg.strip() and re.search(r'[a-zA-Z0-9]', pkg)64            ]65        missing_packages = []66        for package in packages:67            # remove extras so '[lang]==x.y' becomes 'pkg==x.y'68            clean_pkg = re.sub(r'\[.*?\]', '', package)69            pkg_name  = re.split(r'[<>=]', clean_pkg, 1)[0].strip()70            try:71                installed_version = version(pkg_name)72                if pkg_name == 'num2words':73                    code = "ZH_CN"74                    spec = importlib.util.find_spec(f"num2words.lang_{code}")75                    if spec is None:76                        missing_packages.append(package)77            except PackageNotFoundError:78                error = f'{package} is missing.'79                print(error)80                missing_packages.append(package)81            else:82                # get specifier from clean_pkg, not from the raw string83                spec_str = clean_pkg[len(pkg_name):].strip()84                if spec_str:85                    spec = SpecifierSet(spec_str)86                    if installed_version not in spec:87                        error = (f'{pkg_name} (installed {installed_version}) does not satisfy "{spec_str}".')88                        print(error)89                        missing_packages.append(package)90        if missing_packages:91            msg = '\nInstalling missing or upgrade packages...\n'92            print(msg)93            tmp_dir = tempfile.mkdtemp()94            os.environ['TMPDIR'] = tmp_dir95            result = subprocess.call([sys.executable, '-m', 'pip', 'cache', 'purge'])96            subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'pip'])97            with tqdm(total=len(packages),98                      desc='Installation 0.00%',99                      bar_format='{desc}: {n_fmt}/{total_fmt} ',100                      unit='step') as t:101                for package in tqdm(missing_packages, desc="Installing", unit="pkg"):102                    try:103                        if package == 'num2words':104                            pkgs = ['git+https://github.com/savoirfairelinux/num2words.git', '--force']105                        else:106                            pkgs = [package]107                        subprocess.check_call([108                            sys.executable, '-m', 'pip', 'install',109                            '--no-cache-dir', '--use-pep517',110                            *pkgs111                        ])112                        t.update(1)113                    except subprocess.CalledProcessError as e:114                        error = f'Failed to install {package}: {e}'115                        print(error)116                        return False117            msg = '\nAll required packages are installed.'118            print(msg)119        return True120    except Exception as e:121        error = f'check_and_install_requirements() error: {e}'122        raise SystemExit(error)123        return False124       125def check_dictionary():126    import unidic127    unidic_path = unidic.DICDIR128    dicrc = os.path.join(unidic_path, 'dicrc')129    if not os.path.exists(dicrc) or os.path.getsize(dicrc) == 0:130        try:131            error = 'UniDic dictionary not found or incomplete. Downloading now...'132            print(error)133            subprocess.run(['python', '-m', 'unidic', 'download'], check=True)134        except subprocess.CalledProcessError as e:135            error = f'Failed to download UniDic dictionary. Error: {e}. Unable to continue without UniDic. Exiting...'136            raise SystemExit(error)137            return False138    return True139 140def is_port_in_use(port):141    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:142        return s.connect_ex(('0.0.0.0', port)) == 0143 144def main():145    # Argument parser to handle optional parameters with descriptions146    parser = argparse.ArgumentParser(147        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.',148        epilog='''149Example usage:    150Windows:151    Gradio/GUI:152    ebook2audiobook.cmd153    Headless mode:154    ebook2audiobook.cmd --headless --ebook '/path/to/file'155Linux/Mac:156    Gradio/GUI:157    ./ebook2audiobook.sh158    Headless mode:159    ./ebook2audiobook.sh --headless --ebook '/path/to/file'160    161Tip: to add of silence (1.4 seconds) into your text just use "###" or "[pause]".162        ''',163        formatter_class=argparse.RawTextHelpFormatter164    )165    options = [166        '--script_mode', '--session', '--share', '--headless', 167        '--ebook', '--ebooks_dir', '--language', '--voice', '--device', '--tts_engine', 168        '--custom_model', '--fine_tuned', '--output_format',169        '--temperature', '--length_penalty', '--num_beams', '--repetition_penalty', '--top_k', '--top_p', '--speed', '--enable_text_splitting',170        '--text_temp', '--waveform_temp',171        '--output_dir', '--version', '--workflow', '--help'172    ]173    tts_engine_list_keys = [k for k in TTS_ENGINES.keys()]174    tts_engine_list_values = [k for k in TTS_ENGINES.values()]175    all_group = parser.add_argument_group('**** The following options are for all modes', 'Optional')176    all_group.add_argument(options[0], type=str, help=argparse.SUPPRESS)177    parser.add_argument(options[1], type=str, help='''Session to resume the conversion in case of interruption, crash, 178    or reuse of custom models and custom cloning voices.''')179    gui_group = parser.add_argument_group('**** The following option are for gradio/gui mode only', 'Optional')180    gui_group.add_argument(options[2], action='store_true', help='''Enable a public shareable Gradio link.''')181    headless_group = parser.add_argument_group('**** The following options are for --headless mode only')182    headless_group.add_argument(options[3], action='store_true', help='''Run the script in headless mode''')183    headless_group.add_argument(options[4], type=str, help='''Path to the ebook file for conversion. Cannot be used when --ebooks_dir is present.''')184    headless_group.add_argument(options[5], type=str, help=f'''Relative or absolute path of the directory containing the files to convert. 185    Cannot be used when --ebook is present.''')186    headless_group.add_argument(options[6], type=str, default=default_language_code, help=f'''Language of the e-book. Default language is set 187    in ./lib/lang.py sed as default if not present. All compatible language codes are in ./lib/lang.py''')188    headless_optional_group = parser.add_argument_group('optional parameters')189    headless_optional_group.add_argument(options[7], type=str, default=None, help='''(Optional) Path to the voice cloning file for TTS engine. 190    Uses the default voice if not present.''')191    headless_optional_group.add_argument(options[8], type=str, default=default_device, choices=device_list, help=f'''(Optional) Pprocessor unit type for the conversion. 192    Default is set in ./lib/conf.py if not present. Fall back to CPU if GPU not available.''')193    headless_optional_group.add_argument(options[9], type=str, default=None, choices=tts_engine_list_keys+tts_engine_list_values, help=f'''(Optional) Preferred TTS engine (available are: {tts_engine_list_keys+tts_engine_list_values}.194    Default depends on the selected language. The tts engine should be compatible with the chosen language''')195    headless_optional_group.add_argument(options[10], type=str, default=None, help=f'''(Optional) Path to the custom model zip file cntaining mandatory model files. 196    Please refer to ./lib/models.py''')197    headless_optional_group.add_argument(options[11], type=str, default=default_fine_tuned, help='''(Optional) Fine tuned model path. Default is builtin model.''')198    headless_optional_group.add_argument(options[12], type=str, default=default_output_format, help=f'''(Optional) Output audio format. Default is set in ./lib/conf.py''')199    headless_optional_group.add_argument(options[13], type=float, default=None, help=f"""(xtts only, optional) Temperature for the model. 200    Default to config.json model. Higher temperatures lead to more creative outputs.""")201    headless_optional_group.add_argument(options[14], type=float, default=None, help=f"""(xtts only, optional) A length penalty applied to the autoregressive decoder. 202    Default to config.json model. Not applied to custom models.""")203    headless_optional_group.add_argument(options[15], type=int, default=None, help=f"""(xtts only, optional) Controls how many alternative sequences the model explores. Must be equal or greater than length penalty. 204    Default to config.json model.""")205    headless_optional_group.add_argument(options[16], type=float, default=None, help=f"""(xtts only, optional) A penalty that prevents the autoregressive decoder from repeating itself. 206    Default to config.json model.""")207    headless_optional_group.add_argument(options[17], type=int, default=None, help=f"""(xtts only, optional) Top-k sampling. 208    Lower values mean more likely outputs and increased audio generation speed. 209    Default to config.json model.""")210    headless_optional_group.add_argument(options[18], type=float, default=None, help=f"""(xtts only, optional) Top-p sampling. 211    Lower values mean more likely outputs and increased audio generation speed. Default to config.json model.""")212    headless_optional_group.add_argument(options[19], type=float, default=None, help=f"""(xtts only, optional) Speed factor for the speech generation. 213    Default to config.json model.""")214    headless_optional_group.add_argument(options[20], action='store_true', help=f"""(xtts only, optional) Enable TTS text splitting. This option is known to not be very efficient. 215    Default to config.json model.""")216    headless_optional_group.add_argument(options[21], type=float, default=None, help=f"""(bark only, optional) Text Temperature for the model. 217    Default to {default_engine_settings[TTS_ENGINES['BARK']]['text_temp']}. Higher temperatures lead to more creative outputs.""")218    headless_optional_group.add_argument(options[22], type=float, default=None, help=f"""(bark only, optional) Waveform Temperature for the model. 219    Default to {default_engine_settings[TTS_ENGINES['BARK']]['waveform_temp']}. Higher temperatures lead to more creative outputs.""")220    headless_optional_group.add_argument(options[23], type=str, help=f'''(Optional) Path to the output directory. Default is set in ./lib/conf.py''')221    headless_optional_group.add_argument(options[24], action='version', version=f'ebook2audiobook version {prog_version}', help='''Show the version of the script and exit''')222    headless_optional_group.add_argument(options[25], action='store_true', help=argparse.SUPPRESS)223    224    for arg in sys.argv:225        if arg.startswith('--') and arg not in options:226            error = f'Error: Unrecognized option "{arg}"'227            print(error)228            sys.exit(1)229 230    args = vars(parser.parse_args())231 232    if not 'help' in args:233        if not check_virtual_env(args['script_mode']):234            sys.exit(1)235 236        if not check_python_version():237            sys.exit(1)238 239        # Check if the port is already in use to prevent multiple launches240        if not args['headless'] and is_port_in_use(interface_port):241            error = f'Error: Port {interface_port} is already in use. The web interface may already be running.'242            print(error)243            sys.exit(1)244 245        args['script_mode'] = args['script_mode'] if args['script_mode'] else NATIVE246        args['session'] = 'ba800d22-ee51-11ef-ac34-d4ae52cfd9ce' if args['workflow'] else args['session'] if args['session'] else None247        args['share'] =  args['share'] if args['share'] else False248        args['ebook_list'] = None249 250        print(f"v{prog_version} {args['script_mode']} mode")251 252        if args['script_mode'] == NATIVE:253            check_pkg = check_and_install_requirements(requirements_file)254            if check_pkg:255                if not check_dictionary():256                    sys.exit(1)257            else:258                error = 'Some packages could not be installed'259                print(error)260                sys.exit(1)261 262        from lib.functions import SessionContext, convert_ebook_batch, convert_ebook, web_interface263        ctx = SessionContext()264        # Conditions based on the --headless flag265        if args['headless']:266            args['is_gui_process'] = False267            args['audiobooks_dir'] = os.path.abspath(args['output_dir']) if args['output_dir'] else audiobooks_cli_dir268            args['device'] = 'cuda' if args['device'] == 'gpu' else args['device']269            args['tts_engine'] = TTS_ENGINES[args['tts_engine']] if args['tts_engine'] in TTS_ENGINES.keys() else args['tts_engine'] if args['tts_engine'] in TTS_ENGINES.values() else None270            args['output_split'] = default_output_split271            args['output_split_hours'] = default_output_split_hours272            # Condition to stop if both --ebook and --ebooks_dir are provided273            if args['ebook'] and args['ebooks_dir']:274                error = 'Error: You cannot specify both --ebook and --ebooks_dir in headless mode.'275                print(error)276                sys.exit(1)277            # convert in absolute path voice, custom_model if any278            if args['voice']:279                if os.path.exists(args['voice']):280                    args['voice'] = os.path.abspath(args['voice'])281            if args['custom_model']:282                if os.path.exists(args['custom_model']):283                    args['custom_model'] = os.path.abspath(args['custom_model'])284            if not os.path.exists(args['audiobooks_dir']):285                error = 'Error: --output_dir path does not exist.'286                print(error)287                sys.exit(1)                288            if args['ebooks_dir']:289                args['ebooks_dir'] = os.path.abspath(args['ebooks_dir'])290                if not os.path.exists(args['ebooks_dir']):291                    error = f'Error: The provided --ebooks_dir "{args["ebooks_dir"]}" does not exist.'292                    print(error)293                    sys.exit(1)                   294                args['ebook_list'] = []295                for file in os.listdir(args['ebooks_dir']):296                    if any(file.endswith(ext) for ext in ebook_formats):297                        full_path = os.path.abspath(os.path.join(args['ebooks_dir'], file))298                        args['ebook_list'].append(full_path)299                progress_status, passed = convert_ebook_batch(args, ctx)300                if passed is False:301                    error = f'Conversion failed: {progress_status}'302                    print(error)303                    sys.exit(1)304            elif args['ebook']:305                args['ebook'] = os.path.abspath(args['ebook'])306                if not os.path.exists(args['ebook']):307                    error = f'Error: The provided --ebook "{args["ebook"]}" does not exist.'308                    print(error)309                    sys.exit(1) 310                progress_status, passed = convert_ebook(args, ctx)311                if passed is False:312                    error = f'Conversion failed: {progress_status}'313                    print(error)314                    sys.exit(1)315            else:316                error = 'Error: In headless mode, you must specify either an ebook file using --ebook or an ebook directory using --ebooks_dir.'317                print(error)318                sys.exit(1)       319        else:320            args['is_gui_process'] = True321            passed_arguments = sys.argv[1:]322            allowed_arguments = {'--share', '--script_mode'}323            passed_args_set = {arg for arg in passed_arguments if arg.startswith('--')}324            if passed_args_set.issubset(allowed_arguments):325                 web_interface(args, ctx)326            else:327                error = 'Error: In non-headless mode, no option or only --share can be passed'328                print(error)329                sys.exit(1)330if __name__ == '__main__':331    main()332