|
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] |
|
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: |
|
|
|
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 |
|
|
|
|
|
lang_list_str = ', '.join(list(language_mapping.keys())) |
|
|
|
|
|
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', |
|
|
|
|
|
'--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() |
|
|
|
|
|
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 |
|
|
|
|
|
if args.headless: |
|
args.is_gui_process = False |
|
args.audiobooks_dir = audiobooks_cli_dir |
|
|
|
|
|
if args.ebook and args.ebooks_dir: |
|
print('Error: You cannot specify both --ebook and --ebooks_dir in headless mode.') |
|
sys.exit(1) |
|
|
|
|
|
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: |
|
|
|
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): |
|
|
|
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() |
|
|