项目文件夹

文件
2026-07-13 13:03:09 +08:00

504 行
32 KiB
Python

import argparse, json, socket, shutil, multiprocessing, sys, uuid, copy, warnings
from pathlib import Path
from lib.conf import *
from lib.conf_lang import default_language_code, language_mapping, install_info
from lib.conf_models import TTS_ENGINES, default_fine_tuned, default_engine_settings
warnings.filterwarnings('ignore', category=SyntaxWarning)
warnings.filterwarnings('ignore', category=UserWarning, module='jieba._compat')
def init_multiprocessing():
try:
multiprocessing.set_start_method('spawn')
except RuntimeError:
pass
def check_virtual_env(script_mode:str)->bool:
current_version=sys.version_info[:2] # (major, minor)
search_python_env = str(os.path.basename(sys.prefix))
if search_python_env == 'python_env' or script_mode == FULL_DOCKER or current_version >= min_python_version and current_version <= max_python_version:
return True
error=f'''***********
Wrong launch! ebook2audiobook must run in its own virtual environment!
NOTE: If you are running a Docker so you are probably using an old version of ebook2audiobook.
To solve this issue go to download the new version at https://github.com/DrewThomasson/ebook2audiobook
If the directory python_env does not exist in the ebook2audiobook root directory,
run your command with "./ebook2audiobook.command" for Linux and Mac or "ebook2audiobook.cmd" for Windows
to install it all automatically.
{install_info}
***********'''
print(error)
return False
def check_python_version(script_mode:str)->bool:
if script_mode == BUILD_DOCKER:
return True
current_version = sys.version_info[:2] # (major, minor)
if current_version < min_python_version or current_version > max_python_version:
error = f'''***********
Wrong launch: Your OS Python version is not compatible! (current: {current_version[0]}.{current_version[1]})
In order to install and/or use ebook2audiobook correctly you must delete completly the folder python_env
and run "./ebook2audiobook.command" for Linux and Mac or "ebook2audiobook.cmd" for Windows.
{install_info}
***********'''
print(error)
return False
else:
return True
def is_running_in_container()->bool:
return os.environ.get("IN_DOCKER") == "1"
def is_port_in_use(port:int)->bool:
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
return s.connect_ex(('0.0.0.0',port))==0
def kill_previous_instances(script_name: str):
current_pid = os.getpid()
this_script_path = os.path.realpath(script_name)
import psutil
for proc in psutil.process_iter(['pid', 'cmdline']):
try:
cmdline = proc.info['cmdline']
if not cmdline:
continue
# unify case and absolute paths for comparison
joined_cmd = ' '.join(cmdline).lower()
if this_script_path.lower().endswith(script_name.lower()) and \
(script_name.lower() in joined_cmd) and \
proc.info['pid'] != current_pid:
print(f"[WARN] Found running instance PID={proc.info['pid']} -> killing it.")
proc.kill()
proc.wait(timeout=3)
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
def register_dlls()->str|bool:
candidates = [
Path(os.environ['USERPROFILE']) / 'scoop' / 'apps' / 'ffmpeg-shared' / 'current' / 'bin',
Path(os.environ.get('PROGRAMFILES', r'C:\Program Files')) / 'ffmpeg' / 'bin',
Path(os.environ.get('LOCALAPPDATA', '')) / 'Microsoft' / 'WinGet' / 'Links',
]
found = shutil.which('ffmpeg')
if found:
candidates.append(Path(found).parent)
for p in candidates:
if p and p.is_dir() and any(p.glob('avcodec-*.dll')):
os.add_dll_directory(str(p))
return str(p)
return False
def main()->None:
wsl_cmd = ''
wsl_extra = ''
if os.environ.get('DOCKER_IN_WSL', '0') == '1' and os.environ.get('DOCKER_DESKTOP', '0') == '0' and os.environ.get('PODMAN_DESKTOP', '0') == '0':
wsl_cmd = 'wsl --user root --'
if wsl_cmd:
wsl_extra = f'DEVICE_TAG=cu128'
else:
wsl_extra = f'DEVICE_TAG=cu128 &&'
# 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=f'''
Example usage:
Windows native mode:
Gradio/GUI:
ebook2audiobook.cmd
Headless mode:
ebook2audiobook.cmd --headless --ebook '/path/to/file' --language eng
Linux/Mac natvie mode:
Gradio/GUI:
./ebook2audiobook.command
Headless mode:
./ebook2audiobook.command --headless --ebook '/path/to/file' --language eng
Docker build image:
Windows:
Docker:
ebook2audiobook.cmd --script_mode build_docker
Docker Compose:
ebook2audiobook.cmd --script_mode build_docker --docker_mode compose
Podman Compose:
ebook2audiobook.cmd --script_mode build_docker --docker_mode podman
Linux/Mac
Docker:
./ebook2audiobook.command --script_mode build_docker
Docker Compose
./ebook2audiobook.command --script_mode build_docker --docker_mode compose
Podman Compose:
./ebook2audiobook.command --script_mode build_docker --docker_mode podman
Docker run image:
Gradio/GUI:
CPU:
{wsl_cmd} docker run -v "./ebooks:/app/ebooks" -v "./audiobooks:/app/audiobooks" -v "./models:/app/models" -v "./voices:/app/voices" --rm -it -p 7860:7860 athomasson2/ebook2audiobook:cpu
CUDA:
{wsl_cmd} docker run -v "./ebooks:/app/ebooks" -v "./audiobooks:/app/audiobooks" -v "./models:/app/models" -v "./voices:/app/voices" --gpus all --rm -it -p 7860:7860 athomasson2/ebook2audiobook:cu[118/122/124/126 etc..]
ROCM:
{wsl_cmd} docker run -v "./ebooks:/app/ebooks" -v "./audiobooks:/app/audiobooks" -v "./models:/app/models" -v "./voices:/app/voices" --device=/dev/kfd --device=/dev/dri --rm -it -p 7860:7860 athomasson2/ebook2audiobook:rocm[6.0/6.1/6.4 etc..]
XPU:
{wsl_cmd} docker run -v "./ebooks:/app/ebooks" -v "./audiobooks:/app/audiobooks" -v "./models:/app/models" -v "./voices:/app/voices" --device=/dev/dri --rm -it -p 7860:7860 athomasson2/ebook2audiobook:xpu
JETSON:
{wsl_cmd} docker run -v "./ebooks:/app/ebooks" -v "./audiobooks:/app/audiobooks" -v "./models:/app/models" -v "./voices:/app/voices" --runtime nvidia --rm -it -p 7860:7860 athomasson2/ebook2audiobook:jetson[51/60/61 etc...]
Headless mode:
CPU:
{wsl_cmd} docker run -v "./ebooks:/app/ebooks" -v "./audiobooks:/app/audiobooks" -v "./models:/app/models" -v "./voices:/app/voices" -v "/my/real/ebooks/folder/absolute/path:/app/another_ebook_folder" --rm -it -p 7860:7860 ebook2audiobook:cpu --headless --ebook "/app/another_ebook_folder/myfile.pdf" [--voice /app/my/voicepath/voice.mp3 etc..]
CUDA:
{wsl_cmd} docker run -v "./ebooks:/app/ebooks" -v "./audiobooks:/app/audiobooks" -v "./models:/app/models" -v "./voices:/app/voices" -v "/my/real/ebooks/folder/absolute/path:/app/another_ebook_folder" --gpus all --rm -it -p 7860:7860 ebook2audiobook:cu[118/122/124/126 etc..] --headless --ebook "/app/another_ebook_folder/myfile.pdf" [--voice /app/my/voicepath/voice.mp3 etc..]
ROCM:
{wsl_cmd} docker run -v "./ebooks:/app/ebooks" -v "./audiobooks:/app/audiobooks" -v "./models:/app/models" -v "./voices:/app/voices" -v "/my/real/ebooks/folder/absolute/path:/app/another_ebook_folder" --device=/dev/kfd --device=/dev/dri --rm -it -p 7860:7860 ebook2audiobook:rocm[6.0/6.1/6.4 etc.] --headless --ebook "/app/another_ebook_folder/myfile.pdf" [--voice /app/my/voicepath/voice.mp3 etc..]
XPU:
{wsl_cmd} docker run -v "./ebooks:/app/ebooks" -v "./audiobooks:/app/audiobooks" -v "./models:/app/models" -v "./voices:/app/voices" -v "/my/real/ebooks/folder/absolute/path:/app/another_ebook_folder" --device=/dev/dri --rm -it -p 7860:7860 ebook2audiobook:xpu --headless --ebook "/app/another_ebook_folder/myfile.pdf" [--voice /app/my/voicepath/voice.mp3 etc..]
JETSON:
{wsl_cmd} docker run -v "./ebooks:/app/ebooks" -v "./audiobooks:/app/audiobooks" -v "./models:/app/models" -v "./voices:/app/voices" -v "/my/real/ebooks/folder/absolute/path:/app/another_ebook_folder" --runtime nvidia --rm -it -p 7860:7860 ebook2audiobook:jetson[51/60/61 etc.] --headless --ebook "/app/another_ebook_folder/myfile.pdf" [--voice /app/my/voicepath/voice.mp3 etc..]
Docker Compose (i.e. cuda 12.8:
Run Gradio GUI:
{wsl_cmd} {wsl_extra} docker compose --profile gpu up --no-log-prefix
Run Headless mode:
{wsl_cmd} {wsl_extra} docker compose --profile gpu run --rm ebook2audiobook --headless --ebook "/app/ebooks/myfile.pdf" --voice /app/voices/eng/adult/female/some_voice.wav etc..
Podman Compose (i.e. cuda 12.8:
Run Gradio GUI:
{wsl_cmd} {wsl_extra} podman-compose -f podman-compose.yml --profile gpu up
Run Headless mode:
{wsl_cmd} {wsl_extra} podman-compose -f podman-compose.yml --profile gpu run --rm ebook2audiobook-gpu --headless --ebook "/app/ebooks/myfile.pdf" --voice /app/voices/eng/adult/female/some_voice.wav etc..
SML tags available:
[break] — silence (random range **0.3–0.6 sec.**)
[pause] — silence (random range **1.0–1.6 sec.**)
[pause:N] — fixed pause (**N sec.**)
[voice:/path/to/voice/file]...[/voice] — switch voice from default or selected voice from GUI/CLI
''',
formatter_class=argparse.RawTextHelpFormatter
)
tts_engine_list_keys = [*TTS_ENGINES]
tts_engine_list_values = list(TTS_ENGINES.values())
all_group = parser.add_argument_group('**** The following options are for container only', 'Optional')
all_group.add_argument(cli_options[0], type=str, help='Mandatory to build a container. The only value is: build_docker.')
all_group.add_argument(cli_options[1], type=str, help='Optional. The only values are: podman and compose. without this option standard docker buildx is used.')
parser.add_argument(cli_options[2], type=str, help='Session to resume the conversion in case of interruption, crash, or reuse of custom models and custom cloning voices.')
gui_group = parser.add_argument_group('**** The following option are for gradio/gui mode only', 'Optional')
gui_group.add_argument(cli_options[3], action='store_true', help='''Enable a public shareable Gradio link.''')
headless_group = parser.add_argument_group('**** The following cli_options are for --headless mode only')
headless_group.add_argument(cli_options[4], action='store_true', help='''Run the script in headless mode''')
headless_group.add_argument(cli_options[5], type=str, help='''Path to the ebook file for conversion. Cannot be used when --ebooks_dir or --text is present.''')
headless_group.add_argument(cli_options[6], type=str, help=f'''Relative or absolute path of the directory containing the files to convert.
Cannot be used when --ebook or --text is present.''')
headless_group.add_argument(cli_options[7], type=str, help='''Raw text for conversion. Cannot be used when --ebook or --ebooks_dir is present.''')
headless_group.add_argument(cli_options[8], type=str, default=default_language_code, help=f'''Language of the e-book. Default language is set
in ./lib/lang.py sed as default if not present. All compatible language codes are in ./lib/lang.py''')
headless_optional_group = parser.add_argument_group('optional parameters')
headless_optional_group.add_argument(cli_options[9], type=str, default=None, metavar='ISO3', help='''(Optional) Translate ebook to a target language (ISO 639-3 code, e.g. eng, fra, deu) before TTS synthesis.
Uses argostranslate. The target language becomes the effective TTS language for the run.
A copy of the source ebook is made with the _<iso3> suffix so translated and non-translated
outputs stay isolated (independent process folder, audio chunks, and final file).''')
headless_optional_group.add_argument(cli_options[10], type=str, default=None, help='''(Optional) Path to the voice cloning file for TTS engine.
Uses the default voice if not present.''')
headless_optional_group.add_argument(cli_options[11], type=str, default=None, help='''(Optional, --ebooks_dir only) Path to a JSON file mapping ebook path -> voice path.
Each entry overrides --voice for that specific ebook. Missing/null entries fall back to --voice.
Keys may be absolute paths or basenames. Example:
{"book1.epub": "/voices/eng/adult/female/alice.wav", "/abs/path/book2.epub": null}''')
headless_optional_group.add_argument(cli_options[12], type=str, default=default_device, choices=list(devices.keys())+[k.lower() for k in devices.keys()], help=f'''(Optional) Processor unit type for the conversion.
Default is set in ./lib/conf.py if not present. Fall back to CPU if CUDA or MPS is not available.''')
headless_optional_group.add_argument(cli_options[13], type=str, default=TTS_ENGINES['XTTS'], 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}.
Default depends on the selected language. The tts engine should be compatible with the chosen language''')
headless_optional_group.add_argument(cli_options[14], type=str, default=None, help=f'''(Optional) Path to the custom model zip file cntaining mandatory model files.
Please refer to ./lib/models.py''')
headless_optional_group.add_argument(cli_options[15], type=str, default=default_fine_tuned, help='''(Optional) Fine tuned model path. Default is builtin model.''')
headless_optional_group.add_argument(cli_options[16], type=str, default=default_output_format, help=f'''(Optional) Output audio format. Default is {default_output_format} set in ./lib/conf.py''')
headless_optional_group.add_argument(cli_options[17], type=str, default=default_output_channel, help=f'''(Optional) Output audio channel. Default is {default_output_channel} set in ./lib/conf.py''')
headless_optional_group.add_argument(cli_options[18], type=float, default=default_engine_settings[TTS_ENGINES['XTTS']]['temperature'], help=f"""(xtts only, optional) Temperature for the model.
Default to config.json model. Higher temperatures lead to more creative outputs.""")
headless_optional_group.add_argument(cli_options[19], type=float, default=default_engine_settings[TTS_ENGINES['XTTS']]['length_penalty'], help=f"""(xtts only, optional) A length penalty applied to the autoregressive decoder.
Default to config.json model. Not applied to custom models.""")
headless_optional_group.add_argument(cli_options[20], type=int, default=default_engine_settings[TTS_ENGINES['XTTS']]['num_beams'], help=f"""(xtts only, optional) Controls how many alternative sequences the model explores. Must be equal or greater than length penalty.
Default to config.json model.""")
headless_optional_group.add_argument(cli_options[21], type=float, default=default_engine_settings[TTS_ENGINES['XTTS']]['repetition_penalty'], help=f"""(xtts only, optional) A penalty that prevents the autoregressive decoder from repeating itself.
Default to config.json model.""")
headless_optional_group.add_argument(cli_options[22], type=int, default=default_engine_settings[TTS_ENGINES['XTTS']]['top_k'], help=f"""(xtts only, optional) Top-k sampling.
Lower values mean more likely outputs and increased audio generation speed.
Default to config.json model.""")
headless_optional_group.add_argument(cli_options[23], type=float, default=default_engine_settings[TTS_ENGINES['XTTS']]['top_p'], help=f"""(xtts only, optional) Top-p sampling.
Lower values mean more likely outputs and increased audio generation speed. Default to config.json model.""")
headless_optional_group.add_argument(cli_options[24], type=float, default=default_engine_settings[TTS_ENGINES['XTTS']]['speed'], help=f"""(xtts only, optional) Speed factor for the speech generation.
Default to config.json model.""")
headless_optional_group.add_argument(cli_options[25], action='store_true', help=f"""(xtts only, optional) Enable TTS text splitting. This option is known to not be very efficient.
Default to config.json model.""")
headless_optional_group.add_argument(cli_options[26], type=float, default=default_engine_settings[TTS_ENGINES['BARK']]['text_temp'], help=f"""(bark only, optional) Text Temperature for the model.
Default to config.json model.""")
headless_optional_group.add_argument(cli_options[27], type=float, default=default_engine_settings[TTS_ENGINES['BARK']]['waveform_temp'], help=f"""(bark only, optional) Waveform Temperature for the model.
Default to config.json model.""")
headless_optional_group.add_argument(cli_options[28], type=str, help=f'''(Optional) Path to the output directory. Default is set in ./lib/conf.py''')
headless_optional_group.add_argument(cli_options[29], action='version', version=f'ebook2audiobook version {prog_version}', help='''Show the version of the script and exit''')
headless_optional_group.add_argument(cli_options[30], action='store_true', help=argparse.SUPPRESS)
headless_optional_group.add_argument(cli_options[31], action='store_true', help=argparse.SUPPRESS)
for arg in sys.argv:
if arg.startswith('--') and arg not in cli_options:
error = f'Error: Unrecognized option "{arg}"'
print(error)
sys.exit(1)
args = vars(parser.parse_args())
if not 'help' in args:
args['script_mode'] = args['script_mode'] if args['script_mode'] else NATIVE
args['share'] = args['share'] if args['share'] else False
args['ebook_mode'] = 'single'
args['ebook_list'] = None
if args['script_mode'] == FULL_DOCKER and not is_running_in_container():
error = f'{FULL_DOCKER} is only an internal option for the docker itself. Use {BUILD_DOCKER} if you need to build a docker image.'
print(error)
sys.exit(1)
elif (not check_virtual_env(args['script_mode'])) or (not check_python_version(args['script_mode'])):
sys.exit(1)
elif args.get('headless', False) and args.get('share', False):
error = '--share option is only allowed in non-headless mode.'
print(error)
sys.exit(1)
elif not args.get('headless', False) and is_port_in_use(interface_port):
# Check if the port is already in use to prevent multiple launches
error = f'Error: Port {interface_port} is already in use. The web interface may already be running.'
print(error)
sys.exit(1)
print(f"v{prog_version} {args['script_mode']} mode")
from lib.classes.device_installer import DeviceInstaller
manager = DeviceInstaller()
device_info_str = manager.check_device_info(args['script_mode'])
if args['script_mode'] == NATIVE:
if manager.install_device_packages(device_info_str) == 1:
error = f'Error: Could not installed device packages!'
print(error)
sys.exit(1)
result = manager.install_python_packages()
if result == 1:
sys.exit(1)
elif args['script_mode'] == FULL_DOCKER:
if manager.check_voices() == 1:
error = f'Error: Could not download voices!'
print(error)
sys.exit(1)
if DEVICE_SYSTEM == systems['WINDOWS'] and not register_dlls():
error = 'WARNING: shared DLLs not found. aborting…'
print(error)
sys.exit(1)
import lib.core as c
c.context = c.SessionContext() if c.context is None else c.context
c.context_tracker = c.SessionTracker() if c.context_tracker is None else c.context_tracker
c.active_sessions = set() if c.active_sessions is None else c.active_sessions
error = ''
if args.get('headless', False):
args['id'] = args['workflow'] if args['workflow'] else args['session'] if args['session'] else str(uuid.uuid4())
if args['id'] == workflow_id or not args['session']:
session = c.context.set_session(args['id'])
else:
session_dir = os.path.join(tmp_dir, f"proc-{args['id']}")
session = c.context.get_session(args['id'])
if not os.path.exists(session_dir) and not session or (session and not session.get('id', False)):
error = 'Session expired or does not exist!'
print(error)
sys.exit(1)
session = c.context.set_session(args['id'])
if not c.context_tracker.start_session(args['id']):
error = 'Session could not start!'
print(error)
sys.exit(1)
args['is_gui_process'] = False
args['blocks_preview'] = False
args['device'] = devices.get(args['device'].upper(), {}).get('proc') or devices['CPU']['proc']
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 None
args['output_split'] = default_output_split
args['output_split_hours'] = default_output_split_hours
args['xtts_temperature'] = args['temperature']
args['xtts_length_penalty'] = args['length_penalty']
args['xtts_num_beams'] = args['num_beams']
args['xtts_repetition_penalty'] = args['repetition_penalty']
args['xtts_top_k'] = args['top_k']
args['xtts_top_p'] = args['top_p']
args['xtts_speed'] = args['speed']
args['xtts_enable_text_splitting'] = False
args['bark_text_temp'] = args['text_temp']
args['bark_waveform_temp'] = args['waveform_temp']
args['translate_enabled'] = False
_user_translate_raw = args.get('translate')
args['translate'] = None
args['translate_iso1'] = None
if _user_translate_raw:
from iso639 import Lang
tgt = str(_user_translate_raw).strip().lower()
try:
if len(tgt) in (2, 3):
ld = Lang(tgt)
if ld:
tgt = ld.pt3
except Exception:
pass
if not tgt or tgt not in language_mapping.keys():
error = f"Error: --translate target '{_user_translate_raw}' is not a supported language."
print(error)
sys.exit(1)
if tgt == args.get('language'):
print(f"[translate] target equals source ({tgt}), translation skipped.")
else:
try:
tgt_iso1 = Lang(tgt).pt1
except Exception:
tgt_iso1 = None
if not tgt_iso1:
error = f"Error: --translate target '{tgt}' has no iso639-1 mapping."
print(error)
sys.exit(1)
args['translate_enabled'] = True
args['translate'] = tgt
args['translate_iso1'] = tgt_iso1
# -------------------------------------------------------------------------
specified_input = sum(
args.get(k, None) is not None
for k in ('ebook', 'ebooks_dir', 'text')
)
if specified_input > 1:
error = 'Error: You can only specify one of --ebook, --ebooks_dir, or --text in headless mode.'
else:
if args.get('voice'):
if os.path.exists(args['voice']):
args['voice'] = os.path.abspath(args['voice'])
if args.get('custom_model', None) is not None:
if os.path.exists(args['custom_model']):
args['custom_model'] = os.path.abspath(args['custom_model'])
if args.get('output_dir', None) is not None and not os.path.exists(args['output_dir']):
error = 'Error: --output_dir path does not exist.'
elif args.get('ebooks_dir', None) is not None:
args['ebook_mode'] = 'directory'
args['ebooks_dir'] = os.path.abspath(args['ebooks_dir'])
if not os.path.exists(args['ebooks_dir']):
error = f"Error: The provided --ebooks_dir {args['ebooks_dir']} does not exist."
else:
# --- voice_map: load the optional per-file override map ---
voice_map:dict = {}
if args.get('voice_map'):
voice_map_path = os.path.abspath(args['voice_map'])
if not os.path.exists(voice_map_path):
error = f'Error: The provided --voice_map {voice_map_path} does not exist.'
else:
try:
with open(voice_map_path, 'r', encoding='utf-8') as f:
raw = json.load(f)
if not isinstance(raw, dict):
error = 'Error: --voice_map JSON must be an object {ebook_path: voice_path}.'
else:
voice_map = {}
for k, v in raw.items():
normalized_key = os.path.abspath(k) if os.path.isabs(k) else k
voice_map[normalized_key] = os.path.abspath(v) if v else None
except Exception as e:
error = f'Error: Failed to parse --voice_map: {e}'
if not error:
# Persist the map onto the session so resolve_voice() can read it.
c.context.sessions[args['id']]['voice_map'] = voice_map
default_voice = args.get('voice')
# Filter and sort upfront: ebook_list becomes the authoritative to-process set.
# sorted() gives reproducible ordering across Linux/macOS/Windows.
all_entries = sorted(os.listdir(args['ebooks_dir']))
args['ebook_list'] = []
for name in all_entries:
abs_path = os.path.abspath(os.path.join(args['ebooks_dir'], name))
if not os.path.isfile(abs_path):
continue
if not any(name.endswith(ext) for ext in ebook_formats):
print(f'{name} skipped (unsupported format)')
continue
args['ebook_list'].append(abs_path)
if not args['ebook_list']:
error = 'Error: No supported ebook files found in --ebooks_dir.'
else:
ebook_list = copy.deepcopy(args['ebook_list'])
for file in ebook_list:
c.context.sessions[args['id']]['status'] = c.status_tags['READY']
c.reset_ebook_session(args['id'], force=True, filter_keys=False)
args['ebook_src'] = file
# Per-file voice resolution: abs-path override -> basename override -> default
override = voice_map.get(file) or voice_map.get(os.path.basename(file))
if override and not os.path.exists(override):
print(f'--voice_map: override for {Path(file).name} ({override}) not found, falling back to --voice')
override = None
args['voice'] = override or default_voice
progress_status, passed = c.convert_ebook(args)
if passed:
args['ebook_list'].remove(file)
else:
error = progress_status
break
elif args.get('ebook', None) is not None:
args['ebook_mode'] = 'single'
args['ebook_src'] = os.path.abspath(args['ebook'])
if not os.path.exists(args['ebook_src']):
error = f"Error: The provided --ebook {args['ebook_src']} does not exist."
else:
progress_status, passed = c.convert_ebook(args)
c.context.sessions[args['id']]['status'] = c.status_tags['READY']
c.reset_ebook_session(args['id'], force=True, filter_keys=False)
if not passed:
error = progress_status
elif args.get('text', None) is not None:
args['ebook_mode'] = 'text'
args['ebook_textarea'] = args['text'].strip()
if not args['ebook_textarea']:
error = f'Error: The --text is empty.'
elif len(args['ebook_textarea']) > max_ebook_textarea_length:
error = f'Error: --text input exceeds {max_ebook_textarea_length} characters.'
else:
progress_status, passed = c.convert_ebook(args)
c.context.sessions[args['id']]['status'] = c.status_tags['READY']
c.reset_ebook_session(args['id'], force=True, filter_keys=False)
if not passed:
error = progress_status
else:
error = 'Error: In headless mode, you must specify either an ebook file using --ebook, ebook directory using --ebooks_dir or a raw text using --text.'
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):
try:
from lib.gradio import build_interface
c.progress_bar = c.gr.Progress(track_tqdm=False)
app = build_interface(args)
if app is not None:
app.queue(
default_concurrency_limit=interface_concurrency_limit
).launch(
debug=bool(int(os.environ.get('GRADIO_DEBUG', '0'))),
show_error=debug_mode, favicon_path='./favicon.ico',
server_name=interface_host,
server_port=interface_port,
share= args['share'],
max_file_size=max_upload_size
)
except OSError as e:
error = f'Connection error: {e}'
c.exception_alert(None, error)
except socket.error as e:
error = f'Socket error: {e}'
c.exception_alert(None, error)
except KeyboardInterrupt:
error = 'Server interrupted by user. Shutting down...'
c.exception_alert(None, error)
except Exception as e:
error = f'An unexpected error occurred: {e}'
c.exception_alert(None, error)
else:
error = 'Error: In GUI mode, no option or only --share can be passed'
if error:
print(error)
sys.exit(1)
if __name__ == '__main__':
init_multiprocessing()
main()