diff --git a/script.py b/script.py index c4bbf0f..e954e0e 100644 --- a/script.py +++ b/script.py @@ -3,11 +3,9 @@ import re import sys import json import time -import shutil -import random import atexit import signal -import logging +import inspect import requests import platform import subprocess @@ -16,17 +14,318 @@ import soundfile as sf from pathlib import Path from datetime import datetime, timedelta from requests.exceptions import RequestException, ConnectionError +""" +Note: The following function names are reserved for TGWUI integration. +When running under text-generation-webui, these functions will be imported from +system/TGWUI Extension/script.py when in TGWUI's Python environment/extensions dir. + +Reserved names: +- output_modifier +- input_modifier +- state_modifier +- ui +- history_modifier +- remove_tts_from_history +- toggle_text_in_history +""" +TGWUI_AVAILABLE = False +def output_modifier(string, state): + """Modify chat output (required for TGWUI)""" + from system.TGWUI_Extension.script import output_modifier as tgwui_output_modifier + return tgwui_output_modifier(string, state) +def input_modifier(string, state): + """Modify chat input (required for TGWUI)""" + from system.TGWUI_Extension.script import input_modifier as tgwui_input_modifier + return tgwui_input_modifier(string, state) +def state_modifier(state): + """Modify chat state (required for TGWUI)""" + from system.TGWUI_Extension.script import state_modifier as tgwui_state_modifier + return tgwui_state_modifier(state) +def ui(): + """Create extension UI (required for TGWUI)""" + try: + from system.TGWUI_Extension.script import ui as tgwui_ui + return tgwui_ui() + except ImportError: + import gradio as gr + # Return empty interface if not in TGWUI + return gr.Blocks() +def history_modifier(history): + """Modify chat history (required for TGWUI)""" + from system.TGWUI_Extension.script import history_modifier as tgwui_history_modifier + return tgwui_history_modifier(history) +def remove_tts_from_history(history): + """Remove TTS from history (required for TGWUI)""" + from system.TGWUI_Extension.script import remove_tts_from_history as tgwui_remove_tts + return tgwui_remove_tts(history) +def toggle_text_in_history(history): + """Toggle text in history (required for TGWUI)""" + from system.TGWUI_Extension.script import toggle_text_in_history as tgwui_toggle_text + return tgwui_toggle_text(history) +try: + from modules import chat, shared, ui_chat + TGWUI_AVAILABLE = True +except ImportError: + class DummyShared: + processing_message = "" + class DummyState: + def __init__(self): + self.mode = 'chat' # Add default mode + shared = DummyShared() + +# pylint: disable=all + +######################### +# Central config loader # +######################### +# Confguration file management for confignew.json +try: + from .config import AlltalkConfig, AlltalkTTSEnginesConfig, AlltalkNewEnginesConfig # TGWUI import +except ImportError: + from config import AlltalkConfig, AlltalkTTSEnginesConfig, AlltalkNewEnginesConfig # Standalone import + +def initialize_configs(): + """Initialize all configuration instances""" + config = AlltalkConfig.get_instance() + tts_engines_config = AlltalkTTSEnginesConfig.get_instance() + new_engines_config = AlltalkNewEnginesConfig.get_instance() + return config, tts_engines_config, new_engines_config + +# Load in configs +config, tts_engines_config, new_engines_config = initialize_configs() +config.save() # Force the config file to save in case it was missing new any settings + +########################## +# Central print function # +########################## +def print_message(message, message_type="standard", component="TTS"): + """Centralized print function for AllTalk messages + Args: + message (str): The message to print + message_type (str): Type of message (standard/warning/error/debug_*/debug) + component (str): Component identifier (TTS/ENG/GEN/API/etc.) + """ + # ANSI color codes + BLUE = "\033[94m" + MAGENTA = "\033[95m" + YELLOW = "\033[93m" + RED = "\033[91m" + GREEN = "\033[92m" + RESET = "\033[0m" + + prefix = f"[{config.branding}{component}] " + + if message_type.startswith("debug_"): + debug_flag = getattr(config.debugging, message_type, False) + if not debug_flag: + return + + if message_type == "debug_func" and "Function entry:" in message: + message_parts = message.split("Function entry:", 1) + print(f"{prefix}{BLUE}Debug{RESET} {YELLOW}{message_type}{RESET} Function entry:{GREEN}{message_parts[1]}{RESET}") + else: + print(f"{prefix}{BLUE}Debug{RESET} {YELLOW}{message_type}{RESET} {message}") + + elif message_type == "debug": + print(f"{prefix}{BLUE}Debug{RESET} {message}") + + elif message_type == "warning": + print(f"{prefix}{YELLOW}Warning{RESET} {message}") + + elif message_type == "error": + print(f"{prefix}{RED}Error{RESET} {message}") + + else: + print(f"{prefix}{message}") + +def debug_func_entry(): + """Print debug message for function entry if debug_func is enabled""" + if config.debugging.debug_func: + current_func = inspect.currentframe().f_back.f_code.co_name + print_message(f"Function entry: {current_func}", "debug_func") + +########################### +# Central config updaters # +########################### +def update_settings_at(delete_output_wavs, gradio_interface, gradio_port_number, output_folder, + api_port_number, gr_debug_tts, transcode_audio_format, generate_help_page, + voice2rvc_page, tts_generator_page, tts_engines_settings_page, + alltalk_documentation_page, api_documentation_page): + """Update AllTalk main settings using the centralized config system""" + debug_func_entry() + try: + # Get the current config instance + config = AlltalkConfig.get_instance() + + # Update main settings + config.delete_output_wavs = delete_output_wavs + config.gradio_interface = gradio_interface == "Enabled" + config.output_folder = output_folder + config.api_def.api_port_number = api_port_number + config.gradio_port_number = gradio_port_number + config.transcode_audio_format = transcode_audio_format + + # Update debugging options + for key in vars(config.debugging): + if not key.startswith('_'): # Skip private attributes + setattr(config.debugging, key, key in gr_debug_tts) + + # Update gradio pages settings + config.gradio_pages.Generate_Help_page = generate_help_page + config.gradio_pages.Voice2RVC_page = voice2rvc_page + config.gradio_pages.TTS_Generator_page = tts_generator_page + config.gradio_pages.TTS_Engines_Settings_page = tts_engines_settings_page + config.gradio_pages.alltalk_documentation_page = alltalk_documentation_page + config.gradio_pages.api_documentation_page = api_documentation_page + + # Save the updated configuration + config.save() + # Tell tts_server.py to update + get_alltalk_settings() + + print_message("Default Settings Saved") + return "Settings updated successfully!" + except Exception as e: + print_message(f"Failed to save configuration data: {str(e)}", message_type="error") + return f"Error updating settings: {str(e)}" + +def update_settings_api(api_length_stripping, api_legacy_ip_address, api_allowed_filter, + api_max_characters, api_use_legacy_api, api_text_filtering, + api_narrator_enabled, api_text_not_inside, api_language, + api_output_file_name, api_output_file_timestamp, api_autoplay, + api_autoplay_volume): + """Update API settings using the centralized config system""" + debug_func_entry() + try: + # Get the current config instance + config = AlltalkConfig.get_instance() + + # Update API settings + config.api_def.api_length_stripping = api_length_stripping + config.api_def.api_legacy_ip_address = api_legacy_ip_address + config.api_def.api_allowed_filter = api_allowed_filter + config.api_def.api_max_characters = api_max_characters + config.api_def.api_use_legacy_api = api_use_legacy_api == "AllTalk v1 API (Legacy)" + config.api_def.api_text_filtering = api_text_filtering + config.api_def.api_narrator_enabled = api_narrator_enabled + config.api_def.api_text_not_inside = api_text_not_inside + config.api_def.api_language = api_language + config.api_def.api_output_file_name = api_output_file_name + config.api_def.api_output_file_timestamp = api_output_file_timestamp == "Timestamp files" + config.api_def.api_autoplay = api_autoplay == "Play remotely" + config.api_def.api_autoplay_volume = api_autoplay_volume + + # Save the updated configuration + config.save() + # Tell tts_server.py to update + get_alltalk_settings() + + print_message("API Settings Saved") + return "Default API settings updated successfully!" + except Exception as e: + print_message(f"Failed to save configuration data: {str(e)}", message_type="error") + return f"Error updating settings: {str(e)}" + +def update_settings_tgwui(activate, autoplay, show_text, language, narrator_enabled): + """Update Text-gen-webui settings using the centralized config system""" + debug_func_entry() + try: + # Get the current config instance + config = AlltalkConfig.get_instance() + + # Update TGWUI settings + config.tgwui.tgwui_activate_tts = activate == "Enabled" + config.tgwui.tgwui_autoplay_tts = autoplay == "Enabled" + config.tgwui.tgwui_show_text = show_text == "Enabled" + config.tgwui.tgwui_language = language + config.tgwui.tgwui_narrator_enabled = narrator_enabled + + # Save the updated configuration + config.save() + + print_message("Default Text-gen-webui Settings Saved") + return "Settings updated successfully!" + except Exception as e: + print_message(f"Failed to save configuration data: {str(e)}", message_type="error") + return f"Error updating settings: {str(e)}" + +def update_rvc_settings(rvc_enabled, rvc_char_model_file, rvc_narr_model_file, split_audio, + autotune, pitch, filter_radius, index_rate, rms_mix_rate, protect, + hop_length, f0method, embedder_model, training_data_size, progress=None): + """Update RVC settings using the centralized config system""" + debug_func_entry() + + try: + # Get the current config instance + config = AlltalkConfig.get_instance() + + # Update RVC settings + config.rvc_settings.rvc_enabled = rvc_enabled + config.rvc_settings.rvc_char_model_file = rvc_char_model_file + config.rvc_settings.rvc_narr_model_file = rvc_narr_model_file + config.rvc_settings.split_audio = split_audio + config.rvc_settings.autotune = autotune + config.rvc_settings.pitch = pitch + config.rvc_settings.filter_radius = filter_radius + config.rvc_settings.index_rate = index_rate + config.rvc_settings.rms_mix_rate = rms_mix_rate + config.rvc_settings.protect = protect + config.rvc_settings.hop_length = hop_length + config.rvc_settings.f0method = f0method + config.rvc_settings.embedder_model = embedder_model + config.rvc_settings.training_data_size = training_data_size + + def ensure_directory_exists(directory): + if not os.path.exists(directory): + os.makedirs(directory) + + def load_file_urls(json_path): + with open(json_path, 'r') as f: + return json.load(f) + + def download_file(url, dest_path): + response = requests.get(url, stream=True) + response.raise_for_status() + with open(dest_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + if rvc_enabled: + base_dir = os.path.join(this_dir, "models", "rvc_base") + rvc_voices_dir = os.path.join(this_dir, "models", "rvc_voices") + ensure_directory_exists(base_dir) + ensure_directory_exists(rvc_voices_dir) + json_path = os.path.join(this_dir, "system", "tts_engines", "rvc_files.json") + file_urls = load_file_urls(json_path) + for idx, file in enumerate(file_urls): + if not os.path.exists(os.path.join(base_dir, file)): + progress(idx / len(file_urls), desc=f"Downloading Required RVC Files: {file}...") + print(f"[{branding}TTS] Downloading {file}...") # Print statement for terminal + download_file(file_urls[file], os.path.join(base_dir, file)) + result = "RVC Base Files downloaded successfully." if len(file_urls) > 0 else "All files are present." + + # Save the updated configuration + config.save() + # Tell tts_server.py to update + get_alltalk_settings() + + return "RVC settings updated successfully!" + except Exception as e: + print_message(f"Failed to save configuration data: {str(e)}", message_type="error") + return f"Error updating settings: {str(e)}" + ########################################################################### # START-UP # Silence Character Normaliser when it checks the Ready Status # ########################################################################### import warnings warnings.filterwarnings('ignore', message='Trying to detect encoding from a tiny portion') + ########################################### # START-UP # AllTalk allowed startup time # ########################################### startup_wait_time = 240 -# You can change the above setting to a larger number to allow AllTAlk more time to start up. +# You can change the above setting to a larger number to allow AllTAlk more time to start up. # The default setting is 240 seconds (4 minutes). If its taking longer though, you may have a # Very old system or system issue. @@ -34,22 +333,15 @@ startup_wait_time = 240 # START-UP # Load confignew.json into params # ############################################## this_dir = Path(__file__).parent.resolve() - -def load_config(file_path): - with open(file_path, "r") as config_file: - config = json.load(config_file) - return config - -config_file_path = this_dir / "confignew.json" -params = load_config(config_file_path) -branding = params['branding'] +config = AlltalkConfig.get_instance() +branding = config.branding github_site = "erew123" github_repository = "alltalk_tts" github_branch = "alltalkbeta" current_folder = os.path.basename(os.getcwd()) -output_folder = this_dir / params["output_folder"] -delete_output_wavs_setting = params["delete_output_wavs"] -gradio_enabled = params["gradio_interface"] +output_folder = config.get_output_directory() +delete_output_wavs_setting = config.delete_output_wavs +gradio_enabled = config.gradio_interface script_path = this_dir / "tts_server.py" tunnel_url = None running_on_google_colab = False @@ -58,173 +350,12 @@ running_in_docker = False ############################################ # START-UP # Display initial splash screen # ############################################ -print(f"[{branding}TTS]\033[94m _ _ _ \033[1;35m_____ _ _ \033[0m _____ _____ ____ ") -print(f"[{branding}TTS]\033[94m / \ | | |\033[1;35m_ _|_ _| | | __ \033[0m |_ _|_ _/ ___| ") -print(f"[{branding}TTS]\033[94m / _ \ | | |\033[1;35m | |/ _` | | |/ / \033[0m | | | | \___ \ ") -print(f"[{branding}TTS]\033[94m / ___ \| | |\033[1;35m | | (_| | | < \033[0m | | | | ___) |") -print(f"[{branding}TTS]\033[94m /_/ \_\_|_|\033[1;35m |_|\__,_|_|_|\_\ \033[0m |_| |_| |____/ ") -print(f"[{branding}TTS]") - - -########################################################### -# START-UP # Check for updates needed to the config file # -########################################################### -update_config_path = this_dir / "system" / "config" / "at_configupdate.json" -downgrade_config_path = this_dir / "system" / "config" / "at_configdowngrade.json" -def changes_needed(main_config, update_config, downgrade_config): - """Check if there are any changes to be made to the main configuration.""" - for key in downgrade_config.keys(): - if key in main_config: - return True - for key, value in update_config.items(): - if key not in main_config: - return True - return False - -def update_config(config_file_path, update_config_path, downgrade_config_path): - try: - with open(config_file_path, 'r') as file: - main_config = json.load(file) - with open(update_config_path, 'r') as file: - update_config = json.load(file) - with open(downgrade_config_path, 'r') as file: - downgrade_config = json.load(file) - - # Determine if changes are needed - if changes_needed(main_config, update_config, downgrade_config): - # Backup with timestamp to avoid overwriting - timestamp = datetime.now().strftime("%Y%m%d%H%M%S") - backup_path = config_file_path.with_suffix(f".{timestamp}.bak") - logging.info(f"Creating backup of the main config to {backup_path}") - shutil.copy(config_file_path, backup_path) - - # Proceed with updates and downgrades - for key, value in update_config.items(): - if key not in main_config: - main_config[key] = value - for key in downgrade_config.keys(): - if key in main_config: - del main_config[key] - - # Save the updated configuration - with open(config_file_path, 'w') as file: - json.dump(main_config, file, indent=4) - - print(f"[{branding}TTS] \033[92mConfig file update: \033[91mUpdates applied\033[0m") - else: - print(f"[{branding}TTS] \033[92mConfig file update: \033[93mNo Updates required\033[0m") - - except Exception as e: - print(f"[{branding}TTS] \033[92mConfig file update: \033[91mError updating\033[0m") - -# Update the configuration -update_config(config_file_path, update_config_path, downgrade_config_path) - -########################################################################################### -# START-UP # Check for updates needed to the TTS engines list when a new engine is added # -########################################################################################### -tts_engines_path = this_dir / "system" / "tts_engines" / "tts_engines.json" -new_engines_path = this_dir / "system" / "tts_engines" / "new_engines.json" -tts_engines_backup_path = tts_engines_path.with_suffix(".backup") # Backup file path - -# Function to safely load JSON with error handling -def safe_load_json(file_path, backup_path=None): - try: - # Attempt to open and load the JSON file - with open(file_path, 'r') as f: - return json.load(f) - except FileNotFoundError: - # If the file is not found, handle it by either restoring from a backup or creating a default - print(f"[{branding}TTS] File not found: {file_path}") - - if backup_path and os.path.exists(backup_path): - # Load the backup if it exists - print(f"[{branding}TTS] Restoring from backup: {backup_path}") - with open(backup_path, 'r') as f: - data = json.load(f) - # Save the backup data to the original path - with open(file_path, 'w') as f: - json.dump(data, f, indent=4) - print(f"[{branding}TTS] Restored {file_path} from backup.") - return data - else: - # If no backup is available, create a new default JSON structure - print(f"[{branding}TTS] No backup found, creating a new default tts_engine JSON structure.") - default_data = { - "engine_loaded": "piper", - "selected_model": "piper", - "engines_available": [ - { - "name": "parler", - "selected_model": "parler - parler_tts_mini_v1" - }, - { - "name": "piper", - "selected_model": "piper" - }, - { - "name": "vits", - "selected_model": "vits - tts_models--en--vctk--vits" - }, - { - "name": "xtts", - "selected_model": "xtts - xttsv2_2.0.3" - } - ] - } - with open(file_path, 'w') as f: - json.dump(default_data, f, indent=4) - print(f"[{branding}TTS] Created new default file at: {file_path}") - return default_data - except json.JSONDecodeError as e: - # Handle JSON corruption - print(f"[{branding}TTS] JSON decoding error in file {file_path}: {e}") - if backup_path and os.path.exists(backup_path): - print(f"[{branding}TTS] Restoring from backup due to corrupted file: {backup_path}") - with open(backup_path, 'r') as f: - data = json.load(f) - # Save the backup data to the original path - with open(file_path, 'w') as f: - json.dump(data, f, indent=4) - print(f"[{branding}TTS] Restored {file_path} from backup.") - return data - else: - raise Exception(f"File {file_path} is corrupted and no backup is available.") - -# Load tts_engines.json with retry if corrupted -tts_engines_data = safe_load_json(tts_engines_path, tts_engines_backup_path) - -# Load new_engines.json -with open(new_engines_path, 'r') as f: - new_engines_data = json.load(f) - -# Extract the list of current and new engines -current_engines = {engine['name'] for engine in tts_engines_data['engines_available']} -new_engines = new_engines_data['engines_available'] - -# Iterate over the new engines to see if they need to be added -for engine in new_engines: - engine_name = engine['name'] - if engine_name not in current_engines: - # Check if the directory for the new engine exists - engine_dir = this_dir / "system" / "tts_engines" / engine_name - if engine_dir.is_dir(): - # Add the new engine to the list - tts_engines_data['engines_available'].append(engine) - print(f"[{branding}TTS] \033[92mNew TTS Engine : \033[91mAdded {engine_name}\033[0m") - - # Backup the current tts_engines.json before saving the updated file - shutil.copy(tts_engines_path, tts_engines_backup_path) - - # Save the updated tts_engines.json - with open(tts_engines_path, 'w') as f: - json.dump(tts_engines_data, f, indent=4) - print(f"{tts_engines_path} updated and saved successfully.") - -#################################################### -# START-UP # Re-load from confignew.json to params # -#################################################### -params = load_config(config_file_path) +print_message("\033[94m _ _ _ \033[1;35m_____ _ _ \033[0m _____ _____ ____ ") # pylint: disable=line-too-long +print_message("\033[94m / \ | | |\033[1;35m_ _|_ _| | | __ \033[0m |_ _|_ _/ ___| ") # pylint: disable=line-too-long +print_message("\033[94m / _ \ | | |\033[1;35m | |/ _` | | |/ / \033[0m | | | | \___ \ ") # pylint: disable=line-too-long +print_message("\033[94m / ___ \| | |\033[1;35m | | (_| | | < \033[0m | | | | ___) |") # pylint: disable=line-too-long +print_message("\033[94m/_/ \_\_|_|\033[1;35m |_|\__,_|_|_|\_\ \033[0m |_| |_| |____/ ") # pylint: disable=line-too-long +print_message("") ############################################################################# # START-UP # Check current folder name has dashes '-' in it and error if so # @@ -235,17 +366,18 @@ this_script_dir = this_script_path.parent # Get the current folder name this_current_folder = this_script_dir.name if "-" in this_current_folder: - print(f"[{branding}TTS]") - print(f"[{branding}TTS] \033[91mWarning\033[0m The current folder name contains a dash ('\033[93m-\033[0m') and this causes errors/issues. Please ensure") - print(f"[{branding}TTS] \033[91mWarning\033[0m the folder name does not have a dash e.g. rename ('\033[93malltalk_tts-main\033[0m') to ('\033[93malltalk_tts\033[0m').") - print(f"[{branding}TTS]") - print(f"[{branding}TTS] \033[92mCurrent folder:\033[0m {this_current_folder}") + print_message("") + print_message("The current folder name contains a dash ('\033[93m-\033[0m') and this causes errors/issues. Please ensure", message_type="warning") + print_message("the folder name does not have a dash e.g. rename ('\033[93malltalk_tts-main\033[0m') to ('\033[93malltalk_tts\033[0m').", message_type="warning") + print_message("") + print_message("\033[92mCurrent folder:\033[0m {this_current_folder}", message_type="warning") sys.exit(1) ############################################## # START-UP # Check if we are on Google Colab # ############################################## def check_google_colab(): + debug_func_entry() try: import google.colab return True @@ -263,18 +395,19 @@ try: from modules.logging_colors import logger from modules.ui import create_refresh_button from modules.utils import gradio - print(f"[{branding}TTS] \033[92mStart-up Mode : \033[93mText-gen-webui mode\033[0m") + print_message("\033[92mStart-up Mode : \033[93mText-gen-webui mode\033[0m") running_in_standalone = False running_in_tgwui = True except ModuleNotFoundError: running_in_standalone = True running_in_tgwui = False - print(f"[{branding}TTS] \033[92mStart-up Mode : \033[93mStandalone mode\033[0m") + print_message("\033[92mStart-up Mode : \033[93mStandalone mode\033[0m") ###################################################### # START-UP # Check if this is a first time start-up # ###################################################### def run_firsttime_script(): + debug_func_entry() try: if running_on_google_colab: script_path = '/content/alltalk_tts/system/config/firstrun.py' @@ -283,13 +416,13 @@ def run_firsttime_script(): elif running_in_tgwui: script_path = os.path.join(this_dir, 'system', 'config', 'firstrun_tgwui.py') else: - script_path = os.path.join(this_dir, 'system', 'config', 'firstrun.py') # Default to Standalone if nothing matched. + script_path = os.path.join(this_dir, 'system', 'config', 'firstrun.py') subprocess.run([sys.executable, script_path], check=True) except subprocess.CalledProcessError as e: - print(f"Error occurred while running the script: {e}") + print_message("Error occurred while running the script: " + str(e), message_type="error") except Exception as e: - print(f"An unexpected error occurred: {e}") + print_message("An unexpected error occurred: " + str(e), message_type="error") # Call the function to run the startup script run_firsttime_script() @@ -298,8 +431,9 @@ run_firsttime_script() # START-UP # Delete files in outputs folder if configured # ########################################################### def delete_old_files(folder_path, days_to_keep): + debug_func_entry() current_time = datetime.now() - print(f"[{branding}TTS] \033[92mWAV file deletion :\033[93m", delete_output_wavs_setting,"\033[0m") + print_message("\033[92mWAV file deletion :\033[93m", delete_output_wavs_setting,"\033[0m") for file_name in os.listdir(folder_path): file_path = os.path.join(folder_path, file_name) if os.path.isfile(file_path): @@ -310,25 +444,33 @@ def delete_old_files(folder_path, days_to_keep): # Check and perform file deletion if delete_output_wavs_setting.lower() == "disabled": - print(f"[{branding}TTS] \033[92mWAV file deletion :\033[93m Disabled\033[0m") + print_message("\033[92mWAV file deletion :\033[93m Disabled\033[0m") else: try: days_to_keep = int(delete_output_wavs_setting.split()[0]) delete_old_files(output_folder, days_to_keep) except ValueError: - print(f"[{branding}TTS] \033[92mWAV file deletion :\033[93m Invalid setting for deleting old wav files. Please use 'Disabled' or 'X Days' format\033[0m") + print_message("\033[92mWAV file deletion :\033[93m Invalid setting for deleting old wav files. Please use 'Disabled' or 'X Days' format\033[0m") ##################################################################### # START-UP # Check Githubs last update and output into splashscreen # ##################################################################### -def ordinal(n): - return "%d%s" % (n, "th" if 4 <= n % 100 <= 20 else {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")) - def format_datetime(iso_str): + """Formats an ISO datetime string into a human-readable format with ordinal day numbers. + + Example: Converts "2024-03-19T10:30:00Z" to "19th March 2024 at 10:30" + """ + debug_func_entry() + def _ordinal(n): + """Helper function to convert numbers to ordinal form (1st, 2nd, 3rd, etc)""" + debug_func_entry() + return "%d%s" % (n, "th" if 4 <= n % 100 <= 20 else {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")) + dt = datetime.strptime(iso_str, "%Y-%m-%dT%H:%M:%SZ") - return dt.strftime(f"{ordinal(dt.day)} %B %Y at %H:%M") + return dt.strftime(f"{_ordinal(dt.day)} %B %Y at %H:%M") def fetch_latest_commit_sha_and_date(owner, repo, branch): + debug_func_entry() # Modified URL to include the branch url = f"https://api.github.com/repos/{owner}/{repo}/commits/{branch}" try: @@ -339,13 +481,14 @@ def fetch_latest_commit_sha_and_date(owner, repo, branch): latest_commit_date = commit_data['commit']['committer']['date'] return latest_commit_sha, latest_commit_date else: - print(f"[{branding}TTS] \033[92mGithub updated :\033[91m Failed to fetch the latest commit from branch {branch} due to an unexpected response from GitHub") + print_message(f"\033[92mGithub updated :\033[91m Failed to fetch the latest commit from branch {branch} due to an unexpected response from GitHub") return None, None except ConnectionError: - print(f"[{branding}TTS] \033[92mGithub updated :\033[91m Could not reach GitHub to check for updates\033[0m") + print_message("\033[92mGithub updated :\033[91m Could not reach GitHub to check for updates\033[0m") return None, None def read_or_initialize_sha(file_path, owner, repo, branch): + debug_func_entry() if os.path.exists(file_path): with open(file_path, 'r') as file: data = json.load(file) @@ -360,6 +503,7 @@ def read_or_initialize_sha(file_path, owner, repo, branch): return None def update_sha_file(file_path, new_sha): + debug_func_entry() with open(file_path, 'w') as file: json.dump({"last_known_commit_sha": new_sha}, file) @@ -375,17 +519,19 @@ latest_commit_sha, latest_commit_date = fetch_latest_commit_sha_and_date(github_ formatted_date = format_datetime(latest_commit_date) if latest_commit_date else "an unknown date" if latest_commit_sha and latest_commit_sha != last_known_commit_sha: - print(f"[{branding}TTS] \033[92mGithub updated :\033[93m {formatted_date} \033[92mBranch:\033[93m {github_branch}\033[0m") + print_message(f"\033[92mGithub updated :\033[93m {formatted_date} \033[92mBranch:\033[93m {github_branch}\033[0m") # Update the file with the new SHA update_sha_file(sha_file_path, latest_commit_sha) elif latest_commit_sha == last_known_commit_sha: - print(f"[{branding}TTS] \033[92mGithub updated :\033[93m {formatted_date} \033[92mBranch:\033[93m {github_branch}\033[0m") + print_message(f"\033[92mGithub updated :\033[93m {formatted_date} \033[92mBranch:\033[93m {github_branch}\033[0m") ################################################## -# START-UP # Configure the subprocess hanlder #### +# START-UP # Configure the subprocess handler #### ################################################## def signal_handler(sig, frame): - print(f"[{branding}Shutdown] \033[94mReceived Ctrl+C, terminating subprocess. Kill your Python processes if this fails to exit.\033[92m") + debug_func_entry() + config.save() + print_message("\033[94mReceived Ctrl+C, terminating subprocess. Kill your Python processes if this fails to exit.\033[92m") if process.poll() is None: process.terminate() process.wait() # Wait for the subprocess to finish @@ -401,7 +547,7 @@ try: data = json.load(f) tunnel_url_1, tunnel_url_2 = data.get('google_ip_address', [None, None]) except FileNotFoundError: - print("Could not find IP address") + print_message("Could not find IP address") tunnel_url_1, tunnel_url_2 = None, None except ImportError: running_on_google_colab = False @@ -411,28 +557,28 @@ signal.signal(signal.SIGINT, signal_handler) # Check if we're running in docker if os.path.isfile("/.dockerenv") and 'google.colab' not in sys.modules: - print(f"[{branding}TTS]") - print(f"[{branding}TTS] \033[94mRunning in Docker environment. Please note:\033[0m") - print(f"[{branding}TTS] Docker environments have various considerations:") - print(f"[{branding}TTS] AllTalk v2 is running 2x web servers. One webserver is the API address that exernal TTS") - print(f"[{branding}TTS] generation requests are sent to. The other webserver is the Gradio management interface.") - print(f"[{branding}TTS] If you want to access and use both API calls and the Gradio interface, in a docker style") - print(f"[{branding}TTS] environment, you will need to somehow make these accessable, depending on your scenario:") - print(f"[{branding}TTS] 1. A Local Area Network (LAN) scenario:") - print(f"[{branding}TTS] - Ensure you've exposed the Gradio and API ports correctly.") - print(f"[{branding}TTS] - The application should work as expected.") - print(f"[{branding}TTS] 2. Internet/Remotely accessed scenario:") - print(f"[{branding}TTS] - You'll need to set up a secure tunnel or VPN to the host server.") - print(f"[{branding}TTS] - If you want just API requests, you can map just the API and control AllTalk via API calls.") - print(f"[{branding}TTS] - If you want both API and Gradio interfaces you need to make both accessible.") - print(f"[{branding}TTS] 3. Default Addresses:") - print(f"[{branding}TTS] - Internal API address: http://localhost:{params['api_def']['api_port_number']}") - print(f"[{branding}TTS] - Internal Gradio address: http://localhost:{params['gradio_port_number']}") - print(f"[{branding}TTS] Tunnel Setup:") - print(f"[{branding}TTS] - If using a tunnel, you'll need to provide the API external URL/Address in Gradio.") - print(f"[{branding}TTS] - Look for 'API URL/Address' option in the Gradio interface `Generate` page.") + print_message("") + print_message("\033[94mRunning in Docker environment. Please note:\033[0m") + print_message("Docker environments have various considerations:") + print_message("AllTalk v2 is running 2x web servers. One webserver is the API address that exernal TTS") + print_message("generation requests are sent to. The other webserver is the Gradio management interface.") + print_message("If you want to access and use both API calls and the Gradio interface, in a docker style") + print_message("environment, you will need to somehow make these accessable, depending on your scenario:") + print_message(" 1. A Local Area Network (LAN) scenario:") + print_message(" - Ensure you've exposed the Gradio and API ports correctly.") + print_message(" - The application should work as expected.") + print_message("2. Internet/Remotely accessed scenario:") + print_message(" - You'll need to set up a secure tunnel or VPN to the host server.") + print_message(" - If you want just API requests, you can map just the API and control AllTalk via API calls.") + print_message(" - If you want both API and Gradio interfaces you need to make both accessible.") + print_message(" 3. Default Addresses:") + print_message(f" - Internal API address: http://localhost:{config.api_def.api_port_number}") + print_message(f" - Internal Gradio address: http://localhost:{config.gradio_port_number}") + print_message(" Tunnel Setup:") + print_message(" - If using a tunnel, you'll need to provide the API external URL/Address in Gradio.") + print_message(" - Look for 'API URL/Address' option in the Gradio interface `Generate` page.") running_in_docker = True - docker_url = f"http://localhost:{params['api_def']['api_port_number']}" + docker_url = f"http://localhost:{config.api_def.api_port_number}" else: running_in_docker = False @@ -442,16 +588,16 @@ process = subprocess.Popen([sys.executable, script_path]) # Check if the subprocess has started successfully if process.poll() is None: if running_on_google_colab: - print(f"[{branding}TTS]") - print(f"[{branding}TTS] \033[94mGoogle Colab Detected\033[00m") - print(f"[{branding}TTS]") + print_message("") + print_message("\033[94mGoogle Colab Detected\033[00m") + print_message("") else: - print(f"[{branding}TTS] \033[91mWarning\033[0m TTS Subprocess Webserver failing to start process") - print(f"[{branding}TTS] \033[91mWarning\033[0m It could be that you have something on port:", params["port_number"]) - print(f"[{branding}TTS] \033[91mWarning\033[0m Or you have not started in a Python environment with all the necessary bits installed") - print(f"[{branding}TTS] \033[91mWarning\033[0m Check you are starting Text-generation-webui with either the start_xxxxx file or the Python environment with cmd_xxxxx file.") - print(f"[{branding}TTS] \033[91mWarning\033[0m xxxxx is the type of OS you are on e.g. windows, linux or mac.") - print(f"[{branding}TTS] \033[91mWarning\033[0m Alternatively, you could check no other Python processes are running that shouldn't be e.g. Restart your computer is the simple way.") + print_message("TTS Subprocess Webserver failing to start process", message_type="warning") + print_message(f"It could be that you have something on port: {config.port_number}", message_type="warning") + print_message("Or you have not started in a Python environment with all the necessary bits installed", message_type="warning") + print_message("Check you are starting Text-generation-webui with either the start_xxxxx file or the Python environment with cmd_xxxxx file.", message_type="warning") + print_message("xxxxx is the type of OS you are on e.g. windows, linux or mac.", message_type="warning") + print_message("Alternatively, you could check no other Python processes are running that shouldn't be e.g. Restart your computer is the simple way.", message_type="warning") # Cleanly kill off this script, but allow text-generation-webui to keep running, albeit without this alltalk_tts sys.exit(1) @@ -465,7 +611,7 @@ time.sleep(initial_delay) start_time = time.time() warning_displayed = False -url = f"http://localhost:{params['api_def']['api_port_number']}/api/ready" +url = f"http://localhost:{config.api_def.api_port_number}/api/ready" while time.time() - start_time < timeout: try: response = requests.get(url) @@ -476,80 +622,83 @@ while time.time() - start_time < timeout: pass if not warning_displayed and time.time() - start_time >= warning_delay: - print(f"[{branding}TTS] \033[91mWarning\033[0m TTS Engine has NOT started up yet. Will keep trying for {timeout} seconds maximum. Please wait.") - print(f"[{branding}TTS] \033[91mWarning\033[0m Mechanical hard drives and a slow PCI BUS are examples of things that can affect load times.") - print(f"[{branding}TTS] \033[91mWarning\033[0m Some TTS engines index their AI TTS models on loading, which can be slow on CPU or old systems.") - print(f"[{branding}TTS] \033[91mWarning\033[0m Using one of the other TTS engines on slower systems can help ease this issue.") + print_message("TTS Engine has NOT started up yet. Will keep trying for " + str(timeout) + " seconds maximum. Please wait.", message_type="warning") + print_message("Mechanical hard drives and a slow PCI BUS are examples of things that can affect load times.", message_type="warning") + print_message("Some TTS engines index their AI TTS models on loading, which can be slow on CPU or old systems.", message_type="warning") + print_message("Using one of the other TTS engines on slower systems can help ease this issue.", message_type="warning") warning_displayed = True - + time.sleep(1) else: - print(f"[{branding}TTS]") - print(f"[{branding}TTS] Startup timed out. Full help available here \033[92mhttps://github.com/erew123/alltalk_tts#-help-with-problems\033[0m") - print(f"[{branding}TTS] On older systems, you may wish to open and edit \033[94mscript.py\033[0m with a text editor and change the") - print(f"[{branding}TTS] \033[94mstartup_wait_time = 240\033[0m setting to something like \033[94mstartup_wait_time = 460\033[0m as this will allow") - print(f"[{branding}TTS] AllTalk more time (6 mins) to try load the model into your VRAM. Otherwise, please visit the GitHub for") - print(f"[{branding}TTS] a list of other possible troubleshooting options.") + print_message("") + print_message("Startup timed out. Full help available here https://github.com/erew123/alltalk_tts#-help-with-problems") + print_message("On older systems, you may wish to open and edit script.py with a text editor and change the") + print_message("startup_wait_time = 240 setting to something like startup_wait_time = 460 as this will allow") + print_message("AllTalk more time (6 mins) to try load the model into your VRAM. Otherwise, please visit the GitHub for") + print_message("a list of other possible troubleshooting options.") # Cleanly kill off this script, but allow text-generation-webui to keep running, albeit without this alltalk_tts sys.exit(1) if running_on_google_colab: - print(f"[{branding}TTS]") - print(f"[{branding}TTS] \033[94mAPI Address :\033[00m",f"\033[92m{tunnel_url_1}\033[00m") - print(f"[{branding}TTS] \033[94mGradio Light:\033[00m",f"\033[92m{tunnel_url_2}\033[00m") - print(f"[{branding}TTS] \033[94mGradio Dark :\033[00m",f"\033[92m{tunnel_url_2}?__theme=dark\033[00m") - print(f"[{branding}TTS]") + print_message("") + print_message("\033[94mAPI Address :\033[00m \033[92m" + tunnel_url_1 + "\033[00m") + print_message("\033[94mGradio Light:\033[00m \033[92m" + tunnel_url_2 + "\033[00m") + print_message("\033[94mGradio Dark :\033[00m \033[92m" + tunnel_url_2 + "?__theme=dark\033[00m") + print_message("") else: - print(f"[{branding}TTS]") - print(f"[{branding}TTS] \033[94mAPI Address :\033[00m",f"\033[92m127.0.0.1:{params['api_def']['api_port_number']}\033[00m") - print(f"[{branding}TTS] \033[94mGradio Light:\033[00m",f"\033[92mhttp://127.0.0.1:{params['gradio_port_number']}\033[00m") - print(f"[{branding}TTS] \033[94mGradio Dark :\033[00m \033[92mhttp://127.0.0.1:{params['gradio_port_number']}?__theme=dark\033[00m") - print(f"[{branding}TTS]") - -print(f"[{branding}TTS] \033[94mAllTalk WIKI:\033[00m",f"\033[92mhttps://github.com/erew123/alltalk_tts/wiki\033[00m") -print(f"[{branding}TTS] \033[94mErrors Help :\033[00m",f"\033[92mhttps://github.com/erew123/alltalk_tts/wiki/Error-Messages-List\033[00m") -print(f"[{branding}TTS]") + print_message("") + print_message("\033[94mAPI Address :\033[00m \033[92m127.0.0.1:" + str(config.api_def.api_port_number) + "\033[00m") + print_message("\033[94mGradio Light:\033[00m \033[92mhttp://127.0.0.1:" + str(config.gradio_port_number) + "\033[00m") + print_message("\033[94mGradio Dark :\033[00m \033[92mhttp://127.0.0.1:" + str(config.gradio_port_number) + "?__theme=dark\033[00m") + print_message("") + +print_message("\033[94mAllTalk WIKI:\033[00m \033[92mhttps://github.com/erew123/alltalk_tts/wiki\033[00m") +print_message("\033[94mErrors Help :\033[00m \033[92mhttps://github.com/erew123/alltalk_tts/wiki/Error-Messages-List\033[00m") +print_message("") ######################################### # START-UP # Espeak-ng check on Windows # ######################################### def check_espeak_ng(): + debug_func_entry() if platform.system() == "Windows": try: result = subprocess.run(['espeak-ng', '--version'], capture_output=True, text=True, check=True) return except (subprocess.CalledProcessError, FileNotFoundError) as e: # If the command returns an error, print the error message - print(f"[{branding}TTS]") - print(f"[{branding}TTS] Espeak-ng for Windows\033[91m WAS NOT FOUND. \033[0mYou can install\033[0m") - print(f"[{branding}TTS] \033[0mit from this location \033[93m\\alltalk_tts\\system\espeak-ng\\\033[0m") - print(f"[{branding}TTS] Then close this command prompt window and open a new") - print(f"[{branding}TTS] command prompt, before starting {branding}again.") + print_message("") + print_message("Espeak-ng for Windows\033[91m WAS NOT FOUND. \033[0mYou can install\033[0m", message_type="warning") + print_message("\033[0mit from this location \033[93m\\alltalk_tts\\system\espeak-ng\\\033[0m", message_type="warning") + print_message("Then close this command prompt window and open a new", message_type="warning") + print_message("command prompt, before re-starting.", message_type="warning") elif platform.system() == "Darwin": # macOS try: result = subprocess.run(['espeak-ng', '--version'], capture_output=True, text=True, check=True) return except FileNotFoundError: - print(f"[{branding}TTS]") - print(f"[{branding}TTS] Espeak-ng for macOS\033[91m WAS NOT FOUND. \033[0mYou can install\033[0m") - print(f"[{branding}TTS] \033[0mit using Homebrew: \033[93mbrew install espeak-ng\033[0m") + print_message("") + print_message("Espeak-ng for macOS\033[91m WAS NOT FOUND. \033[0mYou can install\033[0m", message_type="warning") + print_message("\033[0mit using Homebrew: \033[93mbrew install espeak-ng\033[0m", message_type="warning") else: # Linux try: result = subprocess.run(['espeak-ng', '--version'], capture_output=True, text=True, check=True) return except FileNotFoundError: - print(f"[{branding}TTS]") - print(f"[{branding}TTS] Espeak-ng for Linux\033[91m WAS NOT FOUND. \033[0mYou can install\033[0m") - print(f"[{branding}TTS] \033[0mit using your package manager, e.g., \033[93mapt-get install espeak-ng\033[0m") - print(f"[{branding}TTS]") - print(f"[{branding}TTS]") + print_message("") + print_message("Espeak-ng for Linux\033[91m WAS NOT FOUND. \033[0mYou can install\033[0m", message_type="warning") + print_message("\033[0mit using your package manager, e.g., \033[93mapt-get install espeak-ng\033[0m", message_type="warning") + print_message("") + print_message("") check_espeak_ng() + #################################### # START-UP # Subprecess management # #################################### def start_subprocess(): + debug_func_entry() global process if process is None or process.poll() is not None: process = subprocess.Popen([sys.executable, script_path]) @@ -558,6 +707,7 @@ def start_subprocess(): return "Subprocess is already running." def stop_subprocess(): + debug_func_entry() global process if process is not None: process.terminate() @@ -568,20 +718,21 @@ def stop_subprocess(): return "Subprocess is not running." def restart_subprocess(): + debug_func_entry() stop_subprocess() - print(f"[{branding}ENG]") - print(f"[{branding}ENG] \033[94mSwapping TTS Engine. Please wait.\033[00m") - print(f"[{branding}ENG]") + print_message("") + print_message("\033[94mSwapping TTS Engine. Please wait.\033[00m") + print_message("") return start_subprocess() def check_subprocess_status(): + debug_func_entry() global process if process is None or process.poll() is not None: return "Subprocess is not running." else: return "Subprocess is running." - ################################################################### # START-UP # Register the termination code to be executed at exit # ################################################################### @@ -592,7 +743,7 @@ atexit.register(lambda: process.terminate() if process.poll() is None else None) ########################## # Load the language options from languages with open(os.path.join(this_dir, "system", "config", "languages.json"), "r") as f: - languages_list = json.load(f) + languages_list = json.load(f) current_model_loaded = None #deepspeed_installed = True tgwui_lovram = False @@ -601,7 +752,7 @@ tgwui_deepspeed = False process_lock = threading.Lock() # Pull the values for IP address, port and protocol for communication with the AllTalk renote server alltalk_protocol = "http://" -alltalk_ip_port = f"127.0.0.1:{params['api_def']['api_port_number']}" +alltalk_ip_port = f"127.0.0.1:{config.api_def.api_port_number}" # Pull the connection timeout value for communication requests with the AllTalk remote server connection_timeout = 10 # Create a few base global variables that are required @@ -615,674 +766,203 @@ tts_model_loaded = None # Pull all the settings from the AllTalk Server # ################################################# def get_alltalk_settings(): - global current_model_loaded, models_available, engines_available, current_engine_loaded + """Retrieves current settings from the AllTalk server including voices, models, and configuration. + Returns a dictionary of settings with default values if server is unreachable. + """ + debug_func_entry() + # Determine the appropriate base URL based on environment if running_on_google_colab: - voices_url = f"{tunnel_url_1}/api/voices" - rvcvoices_url = f"{tunnel_url_1}/api/rvcvoices" - settings_url = f"{tunnel_url_1}/api/currentsettings" + base_url = tunnel_url_1 elif running_in_docker: - voices_url = f"{docker_url}/api/voices" - rvcvoices_url = f"{docker_url}/api/rvcvoices" - settings_url = f"{docker_url}/api/currentsettings" + base_url = docker_url else: - voices_url = f"{alltalk_protocol}{alltalk_ip_port}/api/voices" - rvcvoices_url = f"{alltalk_protocol}{alltalk_ip_port}/api/rvcvoices" - settings_url = f"{alltalk_protocol}{alltalk_ip_port}/api/currentsettings" + base_url = f"{alltalk_protocol}{alltalk_ip_port}" + + # Define endpoints + endpoints = { + "voices": f"{base_url}/api/voices", + "rvcvoices": f"{base_url}/api/rvcvoices", + "settings": f"{base_url}/api/currentsettings" + } + + # Default settings in case of failure + default_settings = { + "voices": ["Please Refresh Settings"], + "rvcvoices": ["Please Refresh Settings"], + "engines_available": ["Please Refresh Settings"], + "current_engine_loaded": ["Please Refresh Settings"], + "models_available": ["Please Refresh Settings"], + "current_model_loaded": "Please Refresh Settings", + "manufacturer_name": "", + "deepspeed_capable": False, + "deepspeed_available": False, + "deepspeed_enabled": False, + "generationspeed_capable": False, + "generationspeed_set": 1.0, + "lowvram_capable": False, + "lowvram_enabled": False, + "pitch_capable": False, + "pitch_set": 0, + "repetitionpenalty_capable": False, + "repetitionpenalty_set": 10.0, + "streaming_capable": False, + "temperature_capable": False, + "temperature_set": 0.75, + "ttsengines_installed": False, + "languages_capable": False, + "multivoice_capable": False, + "multimodel_capable": False, + } try: - voices_response = requests.get(voices_url, timeout=connection_timeout) - rvcvoices_response = requests.get(rvcvoices_url, timeout=connection_timeout) - settings_response = requests.get(settings_url, timeout=connection_timeout) - - if voices_response.status_code == 200 and settings_response.status_code == 200: - voices_data = voices_response.json() - rvcvoices_data = rvcvoices_response.json() - settings_data = settings_response.json() - - engines_available = sorted(settings_data["engines_available"]) - current_engine_loaded = settings_data["current_engine_loaded"] - models_available = sorted([model["name"] for model in settings_data["models_available"]]) - current_model_loaded = settings_data["current_model_loaded"] - - return { - "voices": sorted(voices_data["voices"], key=lambda s: s.lower()), - "rvcvoices": (rvcvoices_data["rvcvoices"]), - "engines_available": engines_available, - "current_engine_loaded": current_engine_loaded, - "models_available": models_available, - "current_model_loaded": current_model_loaded, - "manufacturer_name": settings_data.get("manufacturer_name", ""), - "deepspeed_capable": settings_data.get("deepspeed_capable", False), - "deepspeed_available": settings_data.get("deepspeed_available", False), - "deepspeed_enabled": settings_data.get("deepspeed_enabled", False), - "generationspeed_capable": settings_data.get("generationspeed_capable", False), - "generationspeed_set": settings_data.get("generationspeed_set", 1.0), - "lowvram_capable": settings_data.get("lowvram_capable", False), - "lowvram_enabled": settings_data.get("lowvram_enabled", False), - "pitch_capable": settings_data.get("pitch_capable", False), - "pitch_set": settings_data.get("pitch_enabled", False), - "repetitionpenalty_capable": settings_data.get("repetitionpenalty_capable", False), - "repetitionpenalty_set": settings_data.get("repetitionpenalty_set", 10.0), - "streaming_capable": settings_data.get("streaming_capable", False), - "temperature_capable": settings_data.get("temperature_capable", False), - "temperature_set": settings_data.get("temperature_set", 0.75), - "ttsengines_installed": settings_data.get("ttsengines_installed", False), - "languages_capable": settings_data.get("languages_capable", False), - "multivoice_capable": settings_data.get("multivoice_capable", False), - "multimodel_capable": settings_data.get("multimodel_capable", False), + # Make all requests with a single session for efficiency + with requests.Session() as session: + responses = { + name: session.get(url, timeout=connection_timeout) + for name, url in endpoints.items() } - else: - print(f"[{branding}Server] \033[91mWarning\033[0m Failed to retrieve {branding}settings from API.") - if voices_response.status_code != 200: - print(f"[{branding}Server] \033[91mWarning\033[0m Failed to retrieve voices from API. Status code:\n{voices_response.status_code}") - if settings_response.status_code != 200: - print(f"[{branding}Server] \033[91mWarning\033[0m Failed to retrieve current settings from API. Status code:\n{settings_response.status_code}") - return { - "voices": ["Please Refresh Settings"], - "rvcvoices": ["Please Refresh Settings"], - "engines_available": ["Please Refresh Settings"], - "current_engine_loaded": ["Please Refresh Settings"], - "models_available": ["Please Refresh Settings"], - "current_model_loaded": "Please Refresh Settings", - "manufacturer_name": "", - "deepspeed_capable": False, - "deepspeed_available": False, - "deepspeed_enabled": False, - "generationspeed_capable": False, - "generationspeed_set": 1.0, - "lowvram_capable": False, - "lowvram_enabled": False, - "pitch_capable": False, - "pitch_set": 0, - "repetitionpenalty_capable": False, - "repetitionpenalty_set": 10.0, - "streaming_capable": False, - "temperature_capable": False, - "temperature_set": 0.75, - "ttsengines_installed": False, - "languages_capable": False, - "multivoice_capable": False, - "multimodel_capable": False, - } - except (RequestException, ConnectionError) as e: - print(f"[{branding}Server] \033[91mWarning\033[0m Unable to connect to the {branding}server. Status code:\n{str(e)}") + + # Check if all responses are successful + if not all(r.status_code == 200 for r in responses.values()): + failed_endpoints = [name for name, r in responses.items() + if r.status_code != 200] + print_message(f"Failed to retrieve data from: {', '.join(failed_endpoints)}", + message_type="warning", + component="Server") + return default_settings + + # Parse responses + voices_data = responses["voices"].json() + rvcvoices_data = responses["rvcvoices"].json() + settings_data = responses["settings"].json() + + # Update global variables + global engines_available, current_engine_loaded, models_available, current_model_loaded + engines_available = sorted(settings_data["engines_available"]) + current_engine_loaded = settings_data["current_engine_loaded"] + models_available = sorted([model["name"] for model in settings_data["models_available"]]) + current_model_loaded = settings_data["current_model_loaded"] + + # Construct and return settings dictionary return { - "voices": ["Please Refresh Settings"], - "rvcvoices": ["Please Refresh Settings"], - "engines_available": ["Please Refresh Settings"], - "current_engine_loaded": ["Please Refresh Settings"], - "models_available": ["Please Refresh Settings"], - "current_model_loaded": "Please Refresh Settings", - "manufacturer_name": "", - "deepspeed_capable": False, - "deepspeed_available": False, - "deepspeed_enabled": False, - "generationspeed_capable": False, - "generationspeed_set": 1.0, - "lowvram_capable": False, - "lowvram_enabled": False, - "pitch_capable": False, - "pitch_set": 0, - "repetitionpenalty_capable": False, - "repetitionpenalty_set": 10.0, - "streaming_capable": False, - "temperature_capable": False, - "temperature_set": 0.75, - "ttsengines_installed": False, - "languages_capable": False, - "multivoice_capable": False, - "multimodel_capable": False, + "voices": sorted(voices_data["voices"], key=lambda s: s.lower()), + "rvcvoices": rvcvoices_data["rvcvoices"], + "engines_available": engines_available, + "current_engine_loaded": current_engine_loaded, + "models_available": models_available, + "current_model_loaded": current_model_loaded, + **{k: settings_data.get(k, default_settings[k]) + for k in default_settings if k not in ["voices", "rvcvoices", "engines_available", + "current_engine_loaded", "models_available", + "current_model_loaded"]} } -at_settings = get_alltalk_settings() # Pull all the current settings from the AllTalk server, if its online. + except (RequestException, ConnectionError) as e: + print_message(f"Unable to connect to the {branding} server: {str(e)}", + message_type="warning", + component="Server") + return default_settings + +# Pull all the current settings from the AllTalk server, if its online. +at_settings = get_alltalk_settings() ############################# #### TTS STOP GENERATION #### ############################# def stop_generate_tts(): + debug_func_entry() if running_on_google_colab: api_url = f"{tunnel_url_1}/api/stop-generation" elif running_in_docker: - api_url = f"{docker_url}/api/stop-generation" + api_url = f"{docker_url}/api/stop-generation" else: api_url = f"{alltalk_protocol}{alltalk_ip_port}/api/stop-generation" try: - response = requests.put(api_url, timeout=connection_timeout) + response = requests.put(api_url, timeout=connection_timeout) if response.status_code == 200: return response.json()["message"] else: - print(f"[{branding}TTS] \033[91mWarning\033[0m Failed to stop generation. Status code:\n{response.status_code}") + print_message(f"Failed to stop generation. Status code:\n{response.status_code}", message_type="warning") return {"message": "Failed to stop generation"} except (RequestException, ConnectionError) as e: - print(f"[{branding}TTS] \033[91mWarning\033[0m Unable to connect to the {branding}server. Status code:\n{str(e)}") + print_message(f"Unable to connect to the {branding} server. Status code:\n{str(e)}", message_type="warning") return {"message": "Failed to stop generation"} -##################################### -#### MODEL LOADING AND UNLOADING #### -##################################### -# MODEL - Swap model based on Gradio selection -def send_reload_request(value_sent): +def send_api_request(endpoint, payload=None, headers=None, params=None): + """Base function for making API requests to the AllTalk server. + + Args: + endpoint (str): API endpoint (e.g., '/api/reload') + payload (dict, optional): POST data + headers (dict, optional): Request headers + params (dict, optional): URL parameters + + Returns: + dict: Response data or error status + """ + debug_func_entry() try: + # Construct base URL based on environment if running_on_google_colab: - url = f"{tunnel_url_1}/api/reload" + base_url = f"{tunnel_url_1}{endpoint}" elif running_in_docker: - url = f"{docker_url}/api/reload" + base_url = f"{docker_url}{endpoint}" else: - url = f"{alltalk_protocol}{alltalk_ip_port}/api/reload" - payload = {"tts_method": value_sent} - response = requests.post(url, params=payload) - response.raise_for_status() # Raises an HTTPError for bad responses - except requests.exceptions.RequestException as e: - # Handle the HTTP request error - print(f"[{branding}TTS] \033[91mWarning\033[0m Error during request to webserver process: Status code:\n{e}") - return {"status": "error", "message": str(e)} + base_url = f"{alltalk_protocol}{alltalk_ip_port}{endpoint}" -############################################################################################################# -# _____ _ _ _ ____ _ _ # -# |_ _|____ _| |_ __ _ ___ _ __ __ _____| |__ _ _(_) / ___|_ __ __ _ __| (_) ___ # -# | |/ _ \ \/ / __|____ / _` |/ _ \ '_ \ ____\ \ /\ / / _ \ '_ \| | | | | | | _| '__/ _` |/ _` | |/ _ \ # -# | | __/> <| ||_____| (_| | __/ | | |_____\ V V / __/ |_) | |_| | | | |_| | | | (_| | (_| | | (_) | # -# |_|\___/_/\_\\__| \__, |\___|_| |_| \_/\_/ \___|_.__/ \__,_|_| \____|_| \__,_|\__,_|_|\___/ # -# |___/ # -############################################################################################################# - -#################################################################################################### -# TGWUI # Saves all settings back to the config file for the REMOTE version of the TGWUI extension # -#################################################################################################### -def tgwui_save_settings(): - # Read the existing settings from the file - try: - with open(config_file_path, "r") as file: - existing_settings = json.load(file) - except FileNotFoundError: - existing_settings = {} - # Update the specific settings you want to change - existing_settings.setdefault("tgwui", {}) - existing_settings["tgwui"]["tgwui_activate_tts"] = params["tgwui"]['tgwui_activate_tts'] - existing_settings["tgwui"]["tgwui_autoplay_tts"] = params["tgwui"]['tgwui_autoplay_tts'] - existing_settings["tgwui"]["tgwui_narrator_enabled"] = params["tgwui"]['tgwui_narrator_enabled'] - existing_settings["tgwui"]["tgwui_non_quoted_text_is"] = params["tgwui"]["tgwui_non_quoted_text_is"] - existing_settings["tgwui"]["tgwui_language"] = params["tgwui"]['tgwui_language'] - existing_settings["tgwui"]["tgwui_temperature_set"] = params["tgwui"]['tgwui_temperature_set'] - existing_settings["tgwui"]["tgwui_repetitionpenalty_set"] = params["tgwui"]['tgwui_repetitionpenalty_set'] - existing_settings["tgwui"]["tgwui_generationspeed_set"] = params["tgwui"]['tgwui_generationspeed_set'] - existing_settings["tgwui"]["tgwui_pitch_set"] = params["tgwui"]['tgwui_pitch_set'] - existing_settings["tgwui"]["tgwui_narrator_voice"] = params["tgwui"]['tgwui_narrator_voice'] - existing_settings["tgwui"]["tgwui_show_text"] = params["tgwui"]['tgwui_show_text'] - existing_settings["tgwui"]["tgwui_character_voice"] = params["tgwui"]['tgwui_character_voice'] - existing_settings["tgwui"]["tgwui_rvc_char_voice"] = params["tgwui"]['tgwui_rvc_char_voice'] - existing_settings["tgwui"]["tgwui_rvc_narr_voice"] = params["tgwui"]['tgwui_rvc_narr_voice'] - # Save the updated settings back to the file - with open(config_file_path, "w") as file: - json.dump(existing_settings, file, indent=4) + response = requests.post( + base_url, + json=payload, + headers=headers or {"Content-Type": "application/json"}, + params=params + ) + response.raise_for_status() -##################################################### -# TGWUI - Low VRAM change request to enable/disable # -##################################################### -# LOW VRAM - Gradio Checkbox handling -def send_lowvram_request(value_sent): - try: - params["tgwui"]["tts_model_loaded"] = False - if value_sent: - audio_path = this_dir / "lowvramenabled.wav" - else: - audio_path = this_dir / "lowvramdisabled.wav" - url = f"{alltalk_protocol}{alltalk_ip_port}/api/lowvramsetting?new_low_vram_value={value_sent}" - headers = {"Content-Type": "application/json"} - response = requests.post(url, headers=headers) - response.raise_for_status() # Raises an HTTPError for bad responses - json_response = response.json() - # Check if the low VRAM request was successful - if json_response.get("status") == "lowvram-success": - # Update any relevant variables or perform other actions on success - params["tgwui"]["tts_model_loaded"] = True - return f'' - except requests.exceptions.RequestException as e: - # Handle the HTTP request error - print(f"[{branding}Server] \033[91mWarning\033[0m Error during request to webserver process: Status code:\n{e}") - return {"status": "error", "message": str(e)} - -######################### -# TGWUI - DeepSpeed #### -######################### -def send_deepspeed_request(value_sent): - try: - params["tgwui"]["tts_model_loaded"] = False - if value_sent: - audio_path = this_dir / "deepspeedenabled.wav" - else: - audio_path = this_dir / "deepspeeddisabled.wav" - url = f"{alltalk_protocol}{alltalk_ip_port}/api/deepspeed?new_deepspeed_value={value_sent}" - headers = {"Content-Type": "application/json"} - response = requests.post(url, headers=headers) - response.raise_for_status() # Raises an HTTPError for bad responses - json_response = response.json() - # Check if the deepspeed request was successful - if json_response.get("status") == "deepspeed-success": - # Update any relevant variables or perform other actions on success - params["tgwui"]["tts_model_loaded"] = True - return f'' - except requests.exceptions.RequestException as e: - # Handle the HTTP request error - print(f"[{branding}Server] \033[91mWarning\033[0m Error during request to webserver process: Status code:\n{e}") - return {"status": "error", "message": str(e)} - -################################### -# TGWUI - TTS STANDARD GENERATION # -################################### -def send_and_generate(gen_text, gen_character_voice, gen_rvccharacter_voice, gen_narrator_voice, gen_rvcnarrator_voice, gen_narrator_activated, gen_textnotinisde, gen_repetition, gen_language, gen_filter, gen_speed, gen_pitch, gen_autoplay, gen_autoplay_vol, gen_file_name, gen_temperature, gen_filetimestamp, gen_stream, gen_stopcurrentgen): - api_url = f"{alltalk_protocol}{alltalk_ip_port}/api/tts-generate" - if gen_text == "": - print(f"[{branding}TTS] No Text was sent to generate as TTS") - return None, str("No Text was sent to generate as TTS") - if gen_stopcurrentgen: - stop_generate_tts() # Call for the current generation to stop - tgwui_save_settings() # Save the current TGWUI settings to the config file - if gen_stream == "true": - api_url = f"{alltalk_protocol}{alltalk_ip_port}/api/tts-generate-streaming" - encoded_text = requests.utils.quote(gen_text) - streaming_url = f"{api_url}?text={encoded_text}&voice={gen_character_voice}&language={gen_language}&output_file={gen_file_name}" - return streaming_url, str("TTS Streaming Audio Generated") - else: - data = { - "text_input": gen_text, - "text_filtering": gen_filter, - "character_voice_gen": gen_character_voice, - "rvccharacter_voice_gen": gen_rvccharacter_voice, - "narrator_enabled": str(gen_narrator_activated).lower(), - "narrator_voice_gen": gen_narrator_voice, - "rvcnarrator_voice_gen": gen_rvcnarrator_voice, - "text_not_inside": gen_textnotinisde, - "language": gen_language, - "output_file_name": str(gen_file_name), - "output_file_timestamp": str(gen_filetimestamp).lower(), - "autoplay": str(gen_autoplay).lower(), - "autoplay_volume": str(gen_autoplay_vol), - "speed": str(gen_speed), - "pitch": str(gen_pitch), - "temperature": str(gen_temperature), - "repetition_penalty": str(gen_repetition), + return { + "status": "success", + "response": response.json() if response.content else None, + "raw_response": response } - # print(f"Debug: Generate request param:", data) if params["debug_tts"] else None - try: - response = requests.post(api_url, data=data) - response.raise_for_status() # Raise an exception for 4xx or 5xx status codes - result = response.json() - if gen_autoplay == "true": - return None, str("TTS Audio Generated (Played remotely)") - else: - if params["api_def"]["api_use_legacy_api"]: - return result['output_file_url'], str("TTS Audio Generated") - else: - # Prepend the URL and PORT to the output_file_url - output_file_url = f"{alltalk_protocol}{alltalk_ip_port}{result['output_file_url']}" - return output_file_url, str("TTS Audio Generated") - except (RequestException, ConnectionError) as e: - print(f"[{branding}TTS] \033[91mWarning\033[0m Error occurred during the API request: Status code:\n{str(e)}") - return None, str("Error occurred during the API request") + except requests.exceptions.RequestException as e: + print_message(f"Error during request to webserver process: Status code:\n{e}", + message_type="warning") + return {"status": "error", "message": str(e)} -#################################################################### -# TGWUI # Text-generation-webui sends text for TTS generation here # -#################################################################### -def output_modifier(string, state): - if not params["tgwui"]["tgwui_activate_tts"]: - return string - # Strip out Images - img_info = "" - cleaned_text, img_info = tgwui_extract_and_remove_images(string) - if cleaned_text is None: - return - # Get current settings - language_code = languages_list.get(params["tgwui"]["tgwui_language"]) - character_voice = params["tgwui"]["tgwui_character_voice"] - rvc_character_voice = params["tgwui"]["tgwui_rvc_char_voice"] - narrator_voice = params["tgwui"]["tgwui_narrator_voice"] - rvc_narrator_voice = params["tgwui"]["tgwui_rvc_narr_voice"] - narrator_enabled = params["tgwui"]["tgwui_narrator_enabled"] - text_not_inside = params["tgwui"]["tgwui_non_quoted_text_is"] - repetition_policy = params["tgwui"]["tgwui_repetitionpenalty_set"] - text_filtering = "html" - speed = params["tgwui"]["tgwui_generationspeed_set"] - pitch = params["tgwui"]["tgwui_pitch_set"] - autoplay = False # This is never used in TGWUI so a base False setting is sent - autoplay_volume = 0.8 # This is never used in TGWUI so a base False setting is sent - temperature = params["tgwui"]["tgwui_temperature_set"] - # Lock and process TTS request - if process_lock.acquire(blocking=False): - try: - if "character_menu" in state: - output_file = state["character_menu"] - else: - output_file = str("TTSOUT_") - generate_response, status_message = send_and_generate(cleaned_text, character_voice, rvc_character_voice, narrator_voice, rvc_narrator_voice, narrator_enabled, text_not_inside, repetition_policy, language_code, text_filtering, speed, pitch, autoplay, autoplay_volume, output_file, temperature, True, False, False) - if status_message == "TTS Audio Generated": - # Handle Gradio and playback - autoplay = "autoplay" if params["tgwui"]["tgwui_autoplay_tts"] else "" - string = f'' - if params["tgwui"]["tgwui_show_text"]: - string += tgwui_reinsert_images(cleaned_text, img_info) - shared.processing_message = "*Is typing...*" - return string - else: - print(f"[{branding}Server] \033[91mWarning\033[0m Audio generation failed. Status code:\n{status_message}") - finally: - # Always release the lock, whether an exception occurs or not - process_lock.release() - else: - # The lock is already acquired - print(f"[{branding}Model] \033[91mWarning\033[0m Audio generation is already in progress. Please wait.") - return +def send_reload_request(value_sent): + """Sends a request to reload the TTS model. -########################################################################################################### -# TGWUI # Strips out images from the TGWUI string if needed and re-inserts the image after TTS generation # -########################################################################################################### -img_pattern = r']*src\s*=\s*["\'][^"\'>]+["\'][^>]*>' - -def tgwui_extract_and_remove_images(text): + Args: + value_sent: The TTS method/model to load """ - Extracts all image data from the text and removes it for clean TTS processing. - Returns the cleaned text and the extracted image data. + debug_func_entry() + params = {"tts_method": value_sent} + return send_api_request("/api/reload", params=params) + +def send_lowvram_request(value_sent): + """Sends a request to change the low VRAM setting and returns audio feedback. + + Args: + value_sent (bool): Whether to enable low VRAM mode + + Returns: + str: HTML audio element or error message """ - img_matches = re.findall(img_pattern, text) - img_info = "\n".join(img_matches) # Store extracted image data - cleaned_text = re.sub(img_pattern, '', text) # Remove images from text - return cleaned_text, img_info - -def tgwui_reinsert_images(text, img_info): - """ - Reinserts the previously extracted image data back into the text. - """ - if img_info: # Check if there are images to reinsert - text += f"\n\n{img_info}" - return text - -################################################################ -# TGWUI # Used to generate a preview voice sample within TGWUI # -################################################################ -def random_sentence(): - with open(this_dir / "system" / "config" / "harvard_sentences.txt") as f: - return random.choice(list(f)).rstrip() - -def voice_preview(string): - if not params["tgwui"]["tgwui_activate_tts"]: - return string - language_code = languages_list.get(params["tgwui"]["tgwui_language"]) - if not string: - string = random_sentence() - generate_response, status_message = send_and_generate(string, params["tgwui"]["tgwui_character_voice"], params["tgwui"]["tgwui_rvc_char_voice"], params["tgwui"]["tgwui_narrator_voice"], params["tgwui"]["tgwui_rvc_narr_voice"], False, "character", params["tgwui"]["tgwui_repetitionpenalty_set"], language_code, "standard", params["tgwui"]["tgwui_generationspeed_set"], params["tgwui"]["tgwui_pitch_set"], False, 0.8, "previewvoice", params["tgwui"]["tgwui_temperature_set"], False, False, False) - if status_message == "TTS Audio Generated": - # Handle Gradio and playback - autoplay = "autoplay" if params["tgwui"]["tgwui_autoplay_tts"] else "" - return f'' - else: - # Handle the case where audio generation was not successful - return f"[{branding}Server] Audio generation failed. Status code:\n{status_message}" - -################################################################### -# TGWUI # Used to inform TGWUI that TTS is disabled/not activated # -################################################################### -def state_modifier(state): - if not params["tgwui"]["tgwui_activate_tts"]: - return state - state["stream"] = False - return state - -################################################################### -# TGWUI # Sends message to TGWUI interface during TTS generation # -################################################################### -def input_modifier(string, state): - if not params["tgwui"]["tgwui_activate_tts"]: - return string - shared.processing_message = "*Is recording a voice message...*" - return string - -######################################################################## -# TGWUI # Used to delete historic TTS audios from TGWUI chat interface # -######################################################################## -def remove_tts_from_history(history): - for i, entry in enumerate(history["internal"]): - history["visible"][i] = [history["visible"][i][0], entry[1]] - return history - -def toggle_text_in_history(history): - for i, entry in enumerate(history["visible"]): - visible_reply = entry[1] - if visible_reply.startswith("')[0]}\n\n{reply}", - ] - else: - history["visible"][i] = [ - history["visible"][i][0], - f"{visible_reply.split('')[0]}", - ] - return history - -def history_modifier(history): - # Remove autoplay from the last reply - if len(history["internal"]) > 0: - history["visible"][-1] = [ - history["visible"][-1][0], - history["visible"][-1][1].replace("controls autoplay>", "controls>"), - ] - return history - -########################################################### -# TGWUI # Update IP address and Port from TGWUI interface # -########################################################### -def tgwui_update_alltalk_ip_port(value_sent): - global alltalk_ip_port - alltalk_ip_port = value_sent - -########################################################### -# TGWUI # Update http/https protocol from TGWUI interface # -########################################################### -# Update Protocol from Gradio -def tgwui_update_alltalk_protocol(value_sent): - global alltalk_protocol - alltalk_protocol = value_sent - -########################################################################################################################### -# TGWUI # Debounces model updates when tgwui_update_dropdowns is called, preventing an update from causing a model reload # -########################################################################################################################### -def tgwui_handle_ttsmodel_dropdown_change(model_name): - if not getattr(tgwui_handle_ttsmodel_dropdown_change, "skip_reload", False): - send_reload_request(model_name) - tgwui_handle_ttsmodel_dropdown_change.skip_reload = False - -################################################################################################## -# TGWUI # Pulls the current AllTalk Server settings & updates gradio when Refresh button pressed # -################################################################################################## -def tgwui_update_dropdowns(): - global at_settings - at_settings = get_alltalk_settings() # Pull all the current settings from the AllTalk server, if its online. - current_voices = at_settings["voices"] - rvccurrent_voices = at_settings["rvcvoices"] - rvccurrent_character_voice = params["tgwui"]["tgwui_rvc_char_voice"] - rvccurrent_narrator_voice = params["tgwui"]["tgwui_rvc_char_voice"] - current_models_available = sorted(at_settings["models_available"]) - current_model_loaded = at_settings["current_model_loaded"] - current_character_voice = params["tgwui"]["tgwui_character_voice"] - current_narrator_voice = params["tgwui"]["tgwui_narrator_voice"] - current_lowvram_capable = at_settings["lowvram_capable"] - current_lowvram_enabled = at_settings["lowvram_enabled"] - current_temperature_capable = at_settings["temperature_capable"] - current_repetitionpenalty_capable = at_settings["repetitionpenalty_capable"] - current_generationspeed_capable = at_settings["generationspeed_capable"] - current_pitch_capable = at_settings["pitch_capable"] - current_deepspeed_capable = at_settings["deepspeed_capable"] - current_deepspeed_enabled = at_settings["deepspeed_enabled"] - current_non_quoted_text_is = params.get("tgwui")["tgwui_non_quoted_text_is"] # Use the correct parameter path - current_languages_capable = at_settings["languages_capable"] - if at_settings["languages_capable"]: - language_label = "Languages" - else: - language_label = "Model not multi language" - if current_character_voice not in current_voices: - current_character_voice = current_voices[0] if current_voices else "" - params["tgwui"]["tgwui_character_voice"] = current_character_voice - if current_narrator_voice not in current_voices: - current_narrator_voice = current_voices[0] if current_voices else "" - params["tgwui"]["tgwui_narrator_voice"] = current_narrator_voice - if rvccurrent_character_voice not in rvccurrent_voices: - rvccurrent_character_voice = rvccurrent_voices[0] if rvccurrent_voices else "" - params["tgwui"]["tgwui_rvc_char_voice"] = rvccurrent_character_voice - if rvccurrent_narrator_voice not in rvccurrent_voices: - rvccurrent_narrator_voice = rvccurrent_voices[0] if rvccurrent_voices else "" - params["tgwui"]["tgwui_rvc_char_voice"] = rvccurrent_narrator_voice - tgwui_handle_ttsmodel_dropdown_change.skip_reload = True # Debounce tgwui_tts_dropdown_gr and stop it sending a model reload when it is updated. - - return ( - gr.Checkbox(interactive=current_lowvram_capable, value=current_lowvram_enabled), - gr.Checkbox(interactive=current_deepspeed_capable, value=current_deepspeed_enabled), - gr.Dropdown(choices=current_voices, value=current_character_voice), - gr.Dropdown(choices=rvccurrent_voices, value=rvccurrent_character_voice, interactive=True), - gr.Dropdown(choices=current_voices, value=current_narrator_voice), - gr.Dropdown(choices=rvccurrent_voices, value=rvccurrent_narrator_voice, interactive=True), - gr.Dropdown(choices=current_models_available, value=current_model_loaded), - gr.Dropdown(interactive=current_temperature_capable), - gr.Dropdown(interactive=current_repetitionpenalty_capable), - gr.Dropdown(interactive=current_languages_capable, label=language_label), - gr.Dropdown(interactive=current_generationspeed_capable), - gr.Dropdown(interactive=current_pitch_capable), - gr.Dropdown(value=current_non_quoted_text_is), + debug_func_entry() + config.tgwui.tts_model_loaded = False + + response = send_api_request( + "/api/lowvramsetting", + params={"new_low_vram_value": value_sent} ) - -############################################################### -# TGWUI # Gradio interface & layout for Text-generation-webui # -############################################################### -def ui(): - global at_settings - alltalk_settings = get_alltalk_settings() # Pull all the current settings from the AllTalk server, if its online. - with gr.Accordion(params["branding"] + " TTS (Text-gen-webui Remote)"): - tgwui_available_voices_gr = alltalk_settings["voices"] - tgwui_rvc_available_voices_gr = alltalk_settings["rvcvoices"] - # Activate alltalk_tts, Enable autoplay, Show text - with gr.Row(): - def tgwui_on_load(): - tgwui_update_dropdowns() - app.load(tgwui_on_load, inputs=None, outputs=None) - tgwui_activate_tts_gr = gr.Checkbox(value=params["tgwui"]["tgwui_activate_tts"], label="Enable TGWUI TTS") - tgwui_autoplay_gr = gr.Checkbox(value=params["tgwui"]["tgwui_autoplay_tts"], label="Autoplay TTS Generated") - tgwui_show_text_gr = gr.Checkbox(value=params["tgwui"]["tgwui_show_text"], label="Show Text in chat") - - # Low vram enable, Deepspeed enable, Link - with gr.Row(): - tgwui_lowvram_enabled_gr = gr.Checkbox(value=alltalk_settings["lowvram_enabled"] if alltalk_settings["lowvram_capable"] else False, label="Enable Low VRAM Mode", interactive=alltalk_settings["lowvram_capable"]) - tgwui_lowvram_enabled_play_gr = gr.HTML(visible=False) - tgwui_deepspeed_enabled_gr = gr.Checkbox(value=params["tgwui"]["tgwui_deepspeed_enabled"], label="Enable DeepSpeed", interactive=alltalk_settings["deepspeed_capable"],) - tgwui_deepspeed_enabled_play_gr = gr.HTML(visible=False) - tgwui_empty_space_gr = gr.HTML(f"

AllTalk Server & Documentation Link") - - # Model, Language, Character voice - with gr.Row(): - tgwui_tts_dropdown_gr = gr.Dropdown(choices=models_available, label="TTS Engine/Model", value=current_model_loaded,) - tgwui_language_gr = gr.Dropdown(languages_list.keys(), label="Languages" if alltalk_settings["languages_capable"] else "Model not multi language", interactive=alltalk_settings["languages_capable"], value=params["tgwui"]["tgwui_language"]) - tgwui_narrator_enabled_gr = gr.Dropdown(choices=[("Enabled", "true"), ("Disabled", "false"), ("Silent", "silent")], label="Narrator Enable", value="true" if params.get("tgwui_narrator_enabled") == "true" else ("silent" if params.get("tgwui_narrator_enabled") == "silent" else "false")) - - # Narrator - with gr.Row(): - tgwui_available_voices_gr = alltalk_settings["voices"] - tgwui_default_voice_gr = params["tgwui"]["tgwui_character_voice"] - if tgwui_default_voice_gr not in tgwui_available_voices_gr: - tgwui_default_voice_gr = tgwui_available_voices_gr[0] if tgwui_available_voices_gr else "" - tgwui_character_voice_gr = gr.Dropdown(choices=tgwui_available_voices_gr, label="Character Voice", value=tgwui_default_voice_gr, allow_custom_value=True,) - tgwui_narr_voice_gr = params["tgwui"]["tgwui_narrator_voice"] - if tgwui_narr_voice_gr not in tgwui_available_voices_gr: - tgwui_narr_voice_gr = tgwui_available_voices_gr[0] if tgwui_available_voices_gr else "" - tgwui_narrator_voice_gr = gr.Dropdown(choices=tgwui_available_voices_gr, label="Narrator Voice", value=tgwui_narr_voice_gr, allow_custom_value=True,) - tgwui_non_quoted_text_is_gr = gr.Dropdown(choices=[("Character", "character"), ("Narrator", "narrator"), ("Silent", "silent")], label='Narrator unmarked text is', value=params.get("tgwui_non_quoted_text_is", "character")) - - # RVC voices - with gr.Row(): - tgwui_rvc_default_voice_gr = params["tgwui"]["tgwui_rvc_char_voice"] - tgwui_rvc_narrator_voice_gr = params["tgwui"]["tgwui_rvc_narr_voice"] - if tgwui_rvc_default_voice_gr not in tgwui_rvc_available_voices_gr: - tgwui_rvc_default_voice_gr = tgwui_rvc_available_voices_gr[0] if tgwui_rvc_available_voices_gr else "" - tgwui_rvc_char_voice_gr = gr.Dropdown(choices=tgwui_rvc_available_voices_gr, label="RVC Character Voice", value=tgwui_rvc_default_voice_gr, allow_custom_value=True,) - if tgwui_rvc_narrator_voice_gr not in tgwui_rvc_available_voices_gr: - tgwui_rvc_narrator_voice_gr = tgwui_rvc_available_voices_gr[0] if tgwui_rvc_available_voices_gr else "" - tgwui_rvc_narr_voice_gr = gr.Dropdown(choices=tgwui_rvc_available_voices_gr, label="RVC Narrator Voice", value=tgwui_rvc_narrator_voice_gr, allow_custom_value=True,) - - # Temperature, Repetition Penalty, Speed, pitch - with gr.Row(): - tgwui_temperature_set_gr = gr.Slider(minimum=0.05, maximum=1, step=0.05, label="Temperature", value=params["tgwui"]["tgwui_temperature_set"], interactive=alltalk_settings["temperature_capable"]) - tgwui_repetitionpenalty_set_gr = gr.Slider(minimum=0.5, maximum=20, step=0.5, label="Repetition Penalty", value=params["tgwui"]["tgwui_repetitionpenalty_set"], interactive=alltalk_settings["repetitionpenalty_capable"]) - with gr.Row(): - tgwui_generationspeed_set_gr = gr.Slider(minimum=0.30, maximum=2.00, step=0.10, label="TTS Speed", value=params["tgwui"]["tgwui_generationspeed_set"], interactive=alltalk_settings["generationspeed_capable"]) - tgwui_pitch_set_gr = gr.Slider(minimum=-10, maximum=10, step=1, label="Pitch", value=params["tgwui"]["tgwui_pitch_set"], interactive=alltalk_settings["pitch_capable"]) - - # Preview speech - with gr.Row(): - tgwui_preview_text_gr = gr.Text(show_label=False, placeholder="Preview text", elem_id="silero_preview_text", scale=2,) - tgwui_preview_play_gr = gr.Button("Generate Preview", scale=1) - tgwui_preview_audio_gr = gr.HTML(visible=False) - - with gr.Row(): - tgwui_protocol_gr = gr.Dropdown(choices=["http://", "https://"], label="AllTalk Server Protocol", value=alltalk_protocol) - tgwui_protocol_gr.change(tgwui_update_alltalk_protocol, tgwui_protocol_gr, None) - tgwui_ip_address_port_gr = gr.Textbox(label="AllTalk Server IP:Port", value=alltalk_ip_port) - tgwui_ip_address_port_gr.change(tgwui_update_alltalk_ip_port, tgwui_ip_address_port_gr, None) - tgwui_refresh_settings_gr = gr.Button("Refresh settings & voices") - tgwui_refresh_settings_gr.click(tgwui_update_dropdowns, None, [tgwui_lowvram_enabled_gr, tgwui_deepspeed_enabled_gr, tgwui_character_voice_gr, tgwui_rvc_char_voice_gr, tgwui_narrator_voice_gr, tgwui_rvc_narr_voice_gr, tgwui_tts_dropdown_gr, tgwui_temperature_set_gr, tgwui_repetitionpenalty_set_gr, tgwui_language_gr, tgwui_generationspeed_set_gr, tgwui_pitch_set_gr, tgwui_non_quoted_text_is_gr]) - - with gr.Row(): - tgwui_convert_gr = gr.Button("Remove old TTS audio and leave only message texts") - tgwui_convert_cancel_gr = gr.Button("Cancel", visible=False) - tgwui_convert_confirm_gr = gr.Button("Confirm (cannot be undone)", variant="stop", visible=False) - tgwui_stop_generation_gr = gr.Button("Stop current TTS generation") - tgwui_stop_generation_gr.click(stop_generate_tts, None, None,) - - # Convert history with confirmation - convert_arr = [tgwui_convert_confirm_gr, tgwui_convert_gr, tgwui_convert_cancel_gr] - tgwui_convert_gr.click(lambda: [gr.update(visible=True), gr.update(visible=False), gr.update(visible=True),], None, convert_arr,) - tgwui_convert_confirm_gr.click(lambda: [ gr.update(visible=False), gr.update(visible=True), gr.update(visible=False),], None, convert_arr, - ).then(remove_tts_from_history, gradio("history"), gradio("history") - ).then(chat.save_history, gradio("history", "unique_id", "character_menu", "mode"), None, - ).then(chat.redraw_html, gradio(ui_chat.reload_arr), gradio("display")) - tgwui_convert_cancel_gr.click(lambda: [gr.update(visible=False), gr.update(visible=True), gr.update(visible=False),], None, convert_arr,) - - # Toggle message text in history - tgwui_show_text_gr.change(lambda x: params["tgwui"].update({"tgwui_show_text": x}), tgwui_show_text_gr, None - ).then(toggle_text_in_history, gradio("history"), gradio("history") - ).then(chat.save_history, gradio("history", "unique_id", "character_menu", "mode"), None, - ).then(chat.redraw_html, gradio(ui_chat.reload_arr), gradio("display")) - - # Event functions to update the parameters in the backend - tgwui_activate_tts_gr.change(lambda x: params["tgwui"].update({"tgwui_activate_tts": x}), tgwui_activate_tts_gr, None) - tgwui_autoplay_gr.change(lambda x: params["tgwui"].update({"tgwui_autoplay_tts": x}), tgwui_autoplay_gr, None) - tgwui_lowvram_enabled_gr.change(lambda x: params["tgwui"].update({"tgwui_lowvram_enabled": x}), tgwui_lowvram_enabled_gr, None) - tgwui_lowvram_enabled_gr.change(lambda x: send_lowvram_request(x), tgwui_lowvram_enabled_gr, tgwui_lowvram_enabled_play_gr, None) - - # Trigger the send_reload_request function when the dropdown value changes - tgwui_tts_dropdown_gr.change(tgwui_handle_ttsmodel_dropdown_change, tgwui_tts_dropdown_gr, None) - - tgwui_deepspeed_enabled_gr.change(lambda x: params["tgwui"].update({"tgwui_deepspeed_enabled": x}), tgwui_deepspeed_enabled_gr, None) - tgwui_deepspeed_enabled_gr.change(send_deepspeed_request, tgwui_deepspeed_enabled_gr, tgwui_deepspeed_enabled_play_gr, None) - tgwui_character_voice_gr.change(lambda x: params["tgwui"].update({"tgwui_character_voice": x}), tgwui_character_voice_gr, None) - tgwui_language_gr.change(lambda x: params["tgwui"].update({"tgwui_language": x}), tgwui_language_gr, None) - - # TSS Settings - tgwui_temperature_set_gr.change(lambda x: params["tgwui"].update({"tgwui_temperature_set": x}), tgwui_temperature_set_gr, None) - tgwui_repetitionpenalty_set_gr.change(lambda x: params["tgwui"].update({"tgwui_repetitionpenalty_set": x}), tgwui_repetitionpenalty_set_gr, None) - tgwui_generationspeed_set_gr.change(lambda x: params["tgwui"].update({"tgwui_generationspeed_set": x}), tgwui_generationspeed_set_gr, None) - tgwui_pitch_set_gr.change(lambda x: params["tgwui"].update({"tgwui_pitch_set": x}), tgwui_pitch_set_gr, None) - - # Narrator selection actions - tgwui_narrator_enabled_gr.change(lambda x: params["tgwui"].update({"tgwui_narrator_enabled": x}), tgwui_narrator_enabled_gr, None) - tgwui_non_quoted_text_is_gr.change(lambda x: params["tgwui"].update({"tgwui_non_quoted_text_is": x}), tgwui_non_quoted_text_is_gr, None) - tgwui_narrator_voice_gr.change(lambda x: params["tgwui"].update({"tgwui_narrator_voice": x}), tgwui_narrator_voice_gr, None) - # RVC selection actions - tgwui_rvc_char_voice_gr.change(lambda x: params["tgwui"].update({"tgwui_rvc_char_voice": x}), tgwui_rvc_char_voice_gr, None) - tgwui_rvc_narr_voice_gr.change(lambda x: params["tgwui"].update({"tgwui_rvc_narr_voice": x}), tgwui_rvc_narr_voice_gr, None) - - # Play preview - tgwui_preview_text_gr.submit(voice_preview, tgwui_preview_text_gr, tgwui_preview_audio_gr) - tgwui_preview_play_gr.click(voice_preview, tgwui_preview_text_gr, tgwui_preview_audio_gr) + if response["status"] == "success": + json_response = response["response"] + if json_response.get("status") == "lowvram-success": + config.tgwui.tts_model_loaded = True + audio_path = this_dir / ("lowvramenabled.wav" if value_sent else "lowvramdisabled.wav") + return f'' + + return response ################################################################## # _ _ _ _____ _ _ ____ _ _ # @@ -1299,7 +979,7 @@ if gradio_enabled == True: # Get the directory of the current script script_dir = Path(__file__).resolve().parent my_current_url = "null" - at_default_voice_gr = params["tgwui"]["tgwui_character_voice"] + at_default_voice_gr = config.tgwui.tgwui_character_voice #################################################### # Dynamically import the Themes builder for Gradio # #################################################### @@ -1328,7 +1008,8 @@ if gradio_enabled == True: # Dynamically import modules and set path based on being a standalone or part of TGWUI # ######################################################################################## # Function to get TTS engines data - def get_tts_engines_data(): + def get_tts_engines_data(): #TODO: we should be pulling this from our central function now? + debug_func_entry() global engines_available, engine_loaded, selected_model tts_engines_file = os.path.join(this_dir, "system", "tts_engines", "tts_engines.json") with open(tts_engines_file, "r") as f: @@ -1342,13 +1023,12 @@ if gradio_enabled == True: if __name__ == "__main__" or "text-generation-webui" not in this_dir.parts: # Standalone execution base_package = None # No base package needed for absolute imports - # print("standalone execution", __name__) from system.gradio_pages.alltalk_documentation import alltalk_documentation from system.gradio_pages.alltalk_generation_help import alltalk_generation_help from system.gradio_pages.alltalk_about import alltalk_about from system.gradio_pages.alltalk_diskspace import get_disk_interface from system.gradio_pages.api_documentation import api_documentation - if params["firstrun_splash"]: + if config.firstrun_splash: from system.gradio_pages.alltalk_welcome import alltalk_welcome else: # Running within text-generation-webui @@ -1358,14 +1038,14 @@ if gradio_enabled == True: from .system.gradio_pages.alltalk_about import alltalk_about from .system.gradio_pages.alltalk_diskspace import get_disk_interface from .system.gradio_pages.api_documentation import api_documentation - if params["firstrun_splash"]: + if config.firstrun_splash: from .system.gradio_pages.alltalk_welcome import alltalk_welcome - # print("Running within text-generation-webui", __name__) current_folder_name = this_dir.name base_package = f"extensions.{current_folder_name}" # Function to dynamically import a module from a given path def dynamic_import(module_name, package=None): + debug_func_entry() try: if package: module = importlib.import_module(module_name, package=package) @@ -1373,14 +1053,14 @@ if gradio_enabled == True: module = importlib.import_module(module_name) return module except ModuleNotFoundError as e: - print(f"Module not found: {module_name} - {e}") + print_message(f"Module not found: {module_name} - {e}", message_type="error") return None except Exception as e: - print(f"Error importing {module_name}: {e}") + print_message(f"Error importing {module_name}: {e}", message_type="error") return None # Call the function to populate engines_available - get_tts_engines_data() + get_tts_engines_data() #TODO: we should be pulling this from our central function now? # Dynamically import modules and load JSON data for each available engine for engine_name in engines_available: @@ -1395,64 +1075,34 @@ if gradio_enabled == True: json_file_path = os.path.join(this_dir, "system", "tts_engines", engine_name, "model_settings.json") with open(json_file_path, "r") as f: globals()[f"{engine_name}_model_config_data"] = json.load(f) - + ########################################### # Finishing Dynamically importing Modules # ########################################### def confirm(message): + debug_func_entry() + '''Need to test if this is a dead function''' return gr.Interface.fn("confirmation", f"""""") - def save_config_data(): - try: - # Save the updated JSON data to confignew.json - with open(os.path.join(this_dir, "confignew.json"), "w") as f: - json.dump(params, f, indent=4) - reload_config() - except Exception as e: - error_message = "Failed to save config data." - print(f"[{branding}TTS] \033[91mError:\033[0m {error_message}") - print(f"[{branding}TTS] \033[91mError Details:\033[0m {str(e)}") - - def reload_config(): - # Send a GET request to the /reload_config endpoint - if running_on_google_colab: - reload_config_url = f"{tunnel_url_1}/api/reload_config" - elif running_in_docker: - reload_config_url = f"{docker_url}/api/reload_config" - else: - reload_config_url = f"http://localhost:{params['api_def']['api_port_number']}/api/reload_config" - try: - response = requests.get(reload_config_url) - if response.status_code == 200: - print(f"[{branding}TTS] Reloaded config") - else: - print(f"[{branding}TTS] Failed to reload config file") - except ConnectionError as e: - error_message = "Connection to the server has been lost. Please reload the webpage." - print(f"[{branding}TTS] \033[91mError:\033[0m {error_message}") - except Exception as e: - error_message = "An error occurred while reloading the config file." - print(f"[{branding}TTS] \033[91mError:\033[0m {error_message}") - print(f"[{branding}TTS] \033[91mError Details:\033[0m {str(e)}") - ########################################################################################## # Pulls the current AllTalk Server settings & updates gradio when Refresh button pressed # ########################################################################################## def at_update_dropdowns(): + debug_func_entry() global at_settings at_settings = get_alltalk_settings() # Pull all the current settings from the AllTalk server, if its online. engines_available = at_settings["engines_available"] current_engine_loaded = at_settings["current_engine_loaded"] current_voices = at_settings["voices"] - current_character_voice = params["tgwui"]["tgwui_character_voice"] - current_narrator_voice = params["tgwui"]["tgwui_narrator_voice"] + current_character_voice = config.tgwui.tgwui_character_voice + current_narrator_voice = config.tgwui.tgwui_narrator_voice rvccurrent_voices = at_settings["rvcvoices"] - rvccurrent_character_voice = params["rvc_settings"]["rvc_char_model_file"] - rvccurrent_narrator_voice = params["rvc_settings"]["rvc_narr_model_file"] + rvccurrent_character_voice = config.rvc_settings.rvc_char_model_file + rvccurrent_narrator_voice = config.rvc_settings.rvc_narr_model_file current_temperature_capable = at_settings["temperature_capable"] current_repetitionpenalty_capable = at_settings["repetitionpenalty_capable"] current_languages_capable = at_settings["languages_capable"] @@ -1488,42 +1138,28 @@ if gradio_enabled == True: gr.Dropdown(interactive=current_languages_capable, label=language_label), gr.Dropdown(choices=models_available, value=current_model_loaded), gr.Dropdown(choices=engines_available, value=current_engine_loaded), - ) - + ) + ###################################################################################### # Sends request to reload the current TTS engine & set the default TTS engine loaded # ###################################################################################### def set_engine_loaded(engine_name): - global engine_loaded, selected_model, tts_engines_data, models_available, tts_engines_path, tts_engines_backup_path, at_update_dropdowns + debug_func_entry() + global models_available, at_update_dropdowns - # Load the JSON with a retry mechanism - safe_load_json(tts_engines_path, tts_engines_backup_path) + # Use the config system to update the engine + tts_engines_config = AlltalkTTSEnginesConfig.get_instance() - # Iterate and update engines - for engine in tts_engines_data["engines_available"]: - if engine["name"] == engine_name: - tts_engines_data["engine_loaded"] = engine_name - tts_engines_data["selected_model"] = engine["selected_model"] + # Update the engine and selected model + for engine in tts_engines_config.engines_available: + if engine.name == engine_name: + tts_engines_config.engine_loaded = engine_name + tts_engines_config.selected_model = engine.selected_model break - # Backup the existing file before writing - shutil.copy(tts_engines_path, tts_engines_backup_path) + # Save changes + tts_engines_config.save() - # Write the updated JSON data safely - with open(tts_engines_path, "w") as f: - try: - json.dump(tts_engines_data, f, indent=4) - except Exception as e: - print(f"Error writing to {tts_engines_path}: {e}") - # Restore the backup if writing failed - shutil.copy(tts_engines_backup_path, tts_engines_path) - raise Exception("Failed to save tts_engines.json, restored from backup.") - - engine_loaded = engine_name - selected_model = tts_engines_data["selected_model"] - - # Add a brief delay to ensure file is fully written - time.sleep(0.1) # 100ms delay # Restart the subprocess restart_subprocess() @@ -1538,36 +1174,41 @@ if gradio_enabled == True: break except requests.exceptions.RequestException: pass - + retries += 1 if retries == max_retries: raise Exception("Failed to connect to the TTS engine after multiple retries.") - + time.sleep(retry_delay) - + + # Get updated available models models_available = at_settings["models_available"] # Update the dropdowns directly - print(f"[{branding}ENG]") - print(f"[{branding}ENG] {branding}Server Ready") - gen_stream_value, gen_char_choices, rvcgen_char_choices, gen_narr_choices, rvcgen_narr_choices, gen_speed_interactive, gen_pitch_interactive, gen_temperature_interactive, gen_repetition_interactive, gen_lang_interactive, model_choices_gr_interactive, engine_choices_interactive = at_update_dropdowns() + print_message("") + print_message("Server Ready") + + # Update UI elements + return_values = at_update_dropdowns() + + return ( + "TTS Engine changed successfully!", # Output message + *return_values # Unpack all the UI update values + ) - return ("TTS Engine changed successfully!", # This is your output message - gen_stream_value, gen_char_choices, rvcgen_char_choices, gen_narr_choices, rvcgen_narr_choices, gen_speed_interactive, gen_pitch_interactive, gen_temperature_interactive, gen_repetition_interactive, gen_lang_interactive, model_choices_gr_interactive, engine_choices_interactive - ) - ############################### # Sends voice2rvc request off # ############################### def voice2rvc(audio, rvc_voice, rvc_pitch, rvc_f0method): + debug_func_entry() # Save the uploaded or recorded audio to a file input_tts_path = this_dir / "outputs" / "voice2rvcInput.wav" if rvc_voice == "Disabled": - print(f"[{branding}ENG] Voice2RVC Convert: No RVC voice was selected") + print_message("Voice2RVC Convert: No RVC voice was selected") return if audio == None: - print(f"[{branding}ENG] Voice2RVC Convert: No recorded audio was provided") - return + print_message("Voice2RVC Convert: No recorded audio was provided") + return if isinstance(audio, tuple): sample_rate, audio_data = audio # Save the numpy array as a wav file @@ -1575,13 +1216,13 @@ if gradio_enabled == True: else: # It's a file path os.rename(audio, input_tts_path) - + # Define the output path for the processed audio output_rvc_path = this_dir / "outputs" / "voice2rvcOutput.wav" if running_on_google_colab: url = f"{tunnel_url_1}/api/voice2rvc" elif running_in_docker: - url = f"{docker_url}/api/voice2rvc" + url = f"{docker_url}/api/voice2rvc" else: url = f"{alltalk_protocol}{alltalk_ip_port}/api/voice2rvc" @@ -1602,23 +1243,24 @@ if gradio_enabled == True: result_path = None else: result_path = None - - return result_path - + + return result_path + ################################################################################################## # Sends request to reload the current TTS engine model & set the default TTS engine model loaded # ################################################################################################## def change_model_loaded(engine_name, selected_model): + debug_func_entry() global at_update_dropdowns try: - print(f"[{branding}ENG]") - print(f"[{branding}ENG] \033[94mChanging model loaded. Please wait.\033[00m") - print(f"[{branding}ENG]") + print_message("") + print_message("\033[94mChanging model loaded. Please wait.\033[00m") + print_message("") if running_on_google_colab: url = f"{tunnel_url_1}/api/reload" elif running_in_docker: - url = f"{docker_url}/api/reload" - else: + url = f"{docker_url}/api/reload" + else: url = f"{alltalk_protocol}{alltalk_ip_port}/api/reload" payload = {"tts_method": selected_model} response = requests.post(url, params=payload) @@ -1634,134 +1276,29 @@ if gradio_enabled == True: break with open(tts_engines_file, "w") as f: json.dump(tts_engines_data, f) - print(f"[{branding}ENG]") - print(f"[{branding}ENG] {branding}Server Ready") - gen_stream_value, gen_char_choices, rvcgen_char_choices, gen_narr_choices, rvcgen_narr_choices, gen_speed_interactive, gen_pitch_interactive, gen_temperature_interactive, gen_repetition_interactive, gen_lang_interactive, model_choices_gr_interactive, engine_choices_interactive = at_update_dropdowns() + print_message("") + print_message("Server Ready") + gen_stream_value, gen_char_choices, rvcgen_char_choices, gen_narr_choices, rvcgen_narr_choices, gen_speed_interactive, gen_pitch_interactive, gen_temperature_interactive, gen_repetition_interactive, gen_lang_interactive, model_choices_gr_interactive, engine_choices_interactive = at_update_dropdowns() return ("TTS Model changed successfully!", # This is your output message gen_stream_value, gen_char_choices, rvcgen_char_choices, gen_narr_choices, rvcgen_narr_choices, gen_speed_interactive, gen_pitch_interactive, gen_temperature_interactive, gen_repetition_interactive, gen_lang_interactive, model_choices_gr_interactive, engine_choices_interactive ) except requests.exceptions.RequestException as e: # Handle the HTTP request error - print(f"[{branding}TTS] \033[91mWarning\033[0m Error during request to webserver process: Status code:\n{e}") + print_message(f"Error during request to webserver process: Status code:\n{e}", message_type="warning") return {"status": "error", "message": str(e)} - def update_settings_tg(activate, autoplay, show_text, language, narrator_enabled): - # Update the config_data dictionary with the new values - params["activate"] = activate == "Enabled" - params["autoplay"] = autoplay == "Enabled" - params["show_text"] = show_text == "Enabled" - params["language"] = language - params["narrator_enabled"] = narrator_enabled - print(f"[{branding}TTS] Default Text-gen-webui Settings Saved") - save_config_data() - return "Settings updated successfully!" - - debugging_options = params['debugging'] - debugging_choices = list(debugging_options.keys()) - default_values = [key for key, value in debugging_options.items() if value] - - def update_settings_at(delete_output_wavs, gradio_interface, gradio_port_number, output_folder, api_port_number, gr_debug_tts, transcode_audio_format, generate_help_page, voice2rvc_page, tts_generator_page, tts_engines_settings_page, alltalk_documentation_page, api_documentation_page): - # Update the config_data dictionary with the new values - params["delete_output_wavs"] = delete_output_wavs - params["gradio_interface"] = gradio_interface == "Enabled" - params["output_folder"] = output_folder - params['api_def']['api_port_number'] = api_port_number - params["gradio_port_number"] = gradio_port_number - for key in debugging_options.keys(): - params['debugging'][key] = key in gr_debug_tts - params["transcode_audio_format"] = transcode_audio_format - - # Update gradio_pages settings - params["gradio_pages"] = { - "Generate_Help_page": generate_help_page, - "Voice2RVC_page": voice2rvc_page, - "TTS_Generator_page": tts_generator_page, - "TTS_Engines_Settings_page": tts_engines_settings_page, - "alltalk_documentation_page": alltalk_documentation_page, - "api_documentation_page": api_documentation_page - } - - output_message = "Settings updated successfully!" - print(f"[{branding}TTS] Default Settings Saved") - save_config_data() - return output_message - - def update_settings_api(api_length_stripping, api_legacy_ip_address, api_allowed_filter, api_max_characters, api_use_legacy_api, api_text_filtering, api_narrator_enabled, api_text_not_inside, api_language, api_output_file_name, api_output_file_timestamp, api_autoplay, api_autoplay_volume): - # Update the params dictionary with the new values - params["api_def"]["api_length_stripping"] = api_length_stripping - params["api_def"]["api_allowed_filter"] = api_allowed_filter - params["api_def"]["api_max_characters"] = api_max_characters - params["api_def"]["api_use_legacy_api"] = api_use_legacy_api == "AllTalk v1 API (Legacy)" - params["api_def"]["api_legacy_ip_address"] = api_legacy_ip_address - params["api_def"]["api_text_filtering"] = api_text_filtering - params["api_def"]["api_narrator_enabled"] = api_narrator_enabled - params["api_def"]["api_text_not_inside"] = api_text_not_inside - params["api_def"]["api_language"] = api_language - params["api_def"]["api_output_file_name"] = api_output_file_name - params["api_def"]["api_output_file_timestamp"] = api_output_file_timestamp == "Timestamp files" - params["api_def"]["api_autoplay"] = api_autoplay == "Play remotely" - params["api_def"]["api_autoplay_volume"] = api_autoplay_volume - output_message = "Default API settings updated successfully!" - print(f"[{branding}TTS] API Settings Saved") - save_config_data() - return output_message - - def ensure_directory_exists(directory): - if not os.path.exists(directory): - os.makedirs(directory) - - def load_file_urls(json_path): - with open(json_path, 'r') as f: - return json.load(f) - - def download_file(url, dest_path): - response = requests.get(url, stream=True) - response.raise_for_status() - with open(dest_path, 'wb') as f: - for chunk in response.iter_content(chunk_size=8192): - f.write(chunk) - - def update_rvc_settings(rvc_enabled, rvc_char_model_file, rvc_narr_model_file, split_audio, autotune, pitch, - filter_radius, index_rate, rms_mix_rate, protect, hop_length, f0method, embedder_model, training_data_size, progress=gr.Progress(track_tqdm=True)): - params["rvc_settings"]["rvc_enabled"] = rvc_enabled - params["rvc_settings"]["rvc_char_model_file"] = rvc_char_model_file - params["rvc_settings"]["rvc_narr_model_file"] = rvc_narr_model_file - params["rvc_settings"]["split_audio"] = split_audio - params["rvc_settings"]["autotune"] = autotune - params["rvc_settings"]["pitch"] = pitch - params["rvc_settings"]["filter_radius"] = filter_radius - params["rvc_settings"]["index_rate"] = index_rate - params["rvc_settings"]["rms_mix_rate"] = rms_mix_rate - params["rvc_settings"]["protect"] = protect - params["rvc_settings"]["hop_length"] = hop_length - params["rvc_settings"]["f0method"] = f0method - params["rvc_settings"]["embedder_model"] = embedder_model - params["rvc_settings"]["training_data_size"] = training_data_size - if rvc_enabled: - base_dir = os.path.join(this_dir, "models", "rvc_base") - rvc_voices_dir = os.path.join(this_dir, "models", "rvc_voices") - ensure_directory_exists(base_dir) - ensure_directory_exists(rvc_voices_dir) - json_path = os.path.join(this_dir, "system", "tts_engines", "rvc_files.json") - file_urls = load_file_urls(json_path) - for idx, file in enumerate(file_urls): - if not os.path.exists(os.path.join(base_dir, file)): - progress(idx / len(file_urls), desc=f"Downloading Required Files: {file}...") - print(f"[{branding}TTS] Downloading {file}...") # Print statement for terminal - download_file(file_urls[file], os.path.join(base_dir, file)) - result = "Files downloaded successfully." if len(file_urls) > 0 else "All files are present." - print(f"[{branding}TTS] {result}") - print(f"[{branding}TTS] RVC Settings Saved") - save_config_data() - return "RVC settings updated successfully!" + debugging_options = config.debugging + debugging_choices = list(vars(debugging_options).keys()) + default_values = [key for key, value in vars(debugging_options).items() if value] def generate_tts(gen_text, gen_char, rvcgen_char, rvcgen_char_pitch, gen_narr, rvcgen_narr, rvcgen_narr_pitch, gen_narren, gen_textni, gen_repetition, gen_lang, gen_filter, gen_speed, gen_pitch, gen_autopl, gen_autoplvol, gen_filen, gen_temperature, gen_filetime, gen_stream, gen_stopcurrentgen): + debug_func_entry() if running_on_google_colab: api_url = f"{tunnel_url_1}/api/tts-generate" else: api_url = f"http://{my_current_url}/api/tts-generate" if gen_text == "": - print(f"[{branding}TTS] No Text was sent to generate as TTS") + print_message("No Text was sent to generate as TTS") return None, str("No Text was sent to generate as TTS") if gen_stopcurrentgen: stop_generate_tts() @@ -1769,7 +1306,7 @@ if gradio_enabled == True: if running_on_google_colab: api_url = f"{tunnel_url_1}/api/tts-generate-streaming" elif running_in_docker: - api_url = f"{docker_url}/api/tts-generate-streaming" + api_url = f"{docker_url}/api/tts-generate-streaming" else: api_url = f"http://{my_current_url}/api/tts-generate-streaming" encoded_text = requests.utils.quote(gen_text) @@ -1797,19 +1334,22 @@ if gradio_enabled == True: "temperature": str(gen_temperature), "repetition_penalty": str(gen_repetition), } - #print(f"Debug: Generate request param:", data) if params["debug_tts"] else None + print_message("\033[94mDebug of data > def generate_tts > script.py\033[0m", message_type="debug_tts_variables") + for key, value in data.items(): + print_message(f"{key}: {value}", message_type="debug_tts_variables") + print_message(f"API Url being used: {api_url}", message_type="debug_tts_variables") max_retries = 180 #3 minutes retry_delay = 1 # seconds retries = 0 while retries < max_retries: try: response = requests.post(api_url, data=data, timeout=60) - response.raise_for_status() # Raise an exception for 4xx or 5xx status codes + response.raise_for_status() result = response.json() if gen_autopl == "true": return None, str("TTS Audio Generated (Played remotely)") else: - if params["api_def"]["api_use_legacy_api"]: + if config.api_def.api_use_legacy_api: return result['output_file_url'], str("TTS Audio Generated") else: # Set the protocol type @@ -1818,21 +1358,35 @@ if gradio_enabled == True: if running_on_google_colab: output_file_url = f"{tunnel_url_1}{result['output_file_url']}" elif running_in_docker: - output_file_url = f"{docker_url}{result['output_file_url']}" + output_file_url = f"{docker_url}{result['output_file_url']}" else: output_file_url = f"{protocol}{my_current_url}{result['output_file_url']}" return output_file_url, str("TTS Audio Generated") + except requests.exceptions.Timeout: + retries += 1 + if retries == max_retries: + error_message = "Request timed out after maximum retries" + print_message(f"Error: {error_message}", message_type="error") + return None, str(error_message) + except json.JSONDecodeError as e: + retries += 1 + if retries == max_retries: + error_message = "Failed to parse API response" + print_message(f"Error: {error_message}", message_type="error") + print_message(f"Error Details: {str(e)}", message_type="error") + print_message(f"Raw response: {response.content}", message_type="error") + return None, str(error_message) except requests.exceptions.RequestException as e: retries += 1 if retries == max_retries: - error_message = "An error occurred. Please see console output." - print(f"[{branding}TTS] \033[91mError:\033[0m {error_message}") - print(f"[{branding}TTS] \033[91mError Details:\033[0m {str(e)}") - # Handle the error or return an appropriate error message + error_message = "An error occurred while communicating with the API" + print_message(f"Error: {error_message}", message_type="error") + print_message(f"Error Details: {str(e)}", message_type="error") return None, str(error_message) time.sleep(retry_delay) def alltalk_gradio(): + debug_func_entry() global languages_list, at_settings # Get the URL IP or domain name def get_domain_name(request: gr.Request): @@ -1842,15 +1396,15 @@ if gradio_enabled == True: return None elif running_in_docker: my_current_url = docker_url - return None + return None if request: host = request.headers.get("host", "Unknown") my_current_url = host.split(":")[0] # Split the host by ":" and take the first part - my_current_url = my_current_url + ":" + str(params['api_def']['api_port_number']) + my_current_url = my_current_url + ":" + str(config.api_def.api_port_number) return None else: return "Unable to retrieve the domain name." - + # Get the list of languages from languages languages = list(languages_list.keys()) with gr.Blocks(theme=selected_theme, title="AllTalk", analytics_enabled=False) as app: @@ -1869,17 +1423,17 @@ if gradio_enabled == True: // localStorage.setItem('darkMode', 'enabled'); } }""", show_api=False) - if params.get("firstrun_splash", True): + if config.firstrun_splash: alltalk_welcome() with gr.Tab("Generate TTS"): with gr.Tab("Generate"): with gr.Row(): gen_text = gr.Textbox(label="Text Input", lines=10) - if running_in_docker: + if running_in_docker: with gr.Row(): - with gr.Accordion("Docker IP/URL for API Address updater", open=False): + with gr.Accordion("Docker IP/URL for API Address updater", open=False): with gr.Row(): - docker_url = f"http://localhost:{params['api_def']['api_port_number']}" + docker_url = f"http://localhost:{config.api_def.api_port_number}" docker_upd = gr.Textbox(label="Docker IP/URL for API Address", value=docker_url, show_label=False) update_docker_btn = gr.Button("Update Docker IP/URL API Address") def update_docker_address(new_url): @@ -1904,33 +1458,51 @@ if gradio_enabled == True: at_available_voices_gr = at_settings["voices"] rvcat_available_voices_gr = at_settings["rvcvoices"] with gr.Row(): - at_default_voice_gr = params["tgwui"]["tgwui_character_voice"] + at_default_voice_gr = config.tgwui.tgwui_character_voice if at_default_voice_gr not in at_available_voices_gr: at_default_voice_gr = at_available_voices_gr[0] if at_available_voices_gr else "" gen_char = gr.Dropdown(choices=at_available_voices_gr, label="Character Voice", value=at_default_voice_gr, allow_custom_value=True,) - rvcat_default_voice_gr = params["rvc_settings"]["rvc_char_model_file"] + rvcat_default_voice_gr = config.rvc_settings.rvc_char_model_file if rvcat_default_voice_gr not in rvcat_available_voices_gr: rvcat_default_voice_gr = rvcat_available_voices_gr[0] if rvcat_available_voices_gr else "" - rvcgen_char = gr.Dropdown(choices=rvcat_available_voices_gr, label="RVC Character Voice", value=rvcat_default_voice_gr, allow_custom_value=True,) - at_narrator_voice_gr = params["tgwui"]["tgwui_narrator_voice"] + rvcgen_char = gr.Dropdown(choices=rvcat_available_voices_gr, label="RVC Character Voice", value=rvcat_default_voice_gr, allow_custom_value=True,) + at_narrator_voice_gr = config.tgwui.tgwui_narrator_voice if at_narrator_voice_gr not in at_available_voices_gr: at_narrator_voice_gr = at_available_voices_gr[0] if at_available_voices_gr else "" - gen_narr = gr.Dropdown(choices=at_available_voices_gr, label="Narrator Voice", value=at_narrator_voice_gr, allow_custom_value=True,) - rvcat_narrator_voice_gr = params["rvc_settings"]["rvc_narr_model_file"] + gen_narr = gr.Dropdown(choices=at_available_voices_gr, label="Narrator Voice", value=at_narrator_voice_gr, allow_custom_value=True,) + rvcat_narrator_voice_gr = config.rvc_settings.rvc_narr_model_file if rvcat_narrator_voice_gr not in rvcat_available_voices_gr: rvcat_narrator_voice_gr = rvcat_available_voices_gr[0] if rvcat_available_voices_gr else "" rvcgen_narr = gr.Dropdown(choices=rvcat_available_voices_gr, label="RVC Narrator Voice", value=rvcat_narrator_voice_gr, allow_custom_value=True,) - with gr.Group(): + with gr.Group(): with gr.Row(): - rvcat_default_pitch_gr = gr.Slider(minimum=-24, maximum=24, step=1, label="RVC Character Pitch", info="Corrects the Character input TTS pitch to match the desired RVC voice output pitch.", value=params["rvc_settings"].get("pitch", 0), interactive=True, visible=False) - rvcat_narrator_pitch_gr = gr.Slider(minimum=-24, maximum=24, step=1, label="RVC Narrator Pitch", info="Corrects the Narrator input TTS pitch to match the desired RVC voice output pitch.", value=params["rvc_settings"].get("pitch", 0), interactive=True, visible=False) + rvcat_default_pitch_gr = gr.Slider( + minimum=-24, + maximum=24, + step=1, + label="RVC Character Pitch", + info="Corrects the Character input TTS pitch to match the desired RVC voice output pitch.", + value=config.rvc_settings.pitch, + interactive=True, + visible=False + ) + rvcat_narrator_pitch_gr = gr.Slider( + minimum=-24, + maximum=24, + step=1, + label="RVC Narrator Pitch", + info="Corrects the Narrator input TTS pitch to match the desired RVC voice output pitch.", + value=config.rvc_settings.pitch, + interactive=True, + visible=False + ) def update_visibility(char_voice, narr_voice): is_visible = char_voice != "Disabled" or narr_voice != "Disabled" return gr.update(visible=is_visible), gr.update(visible=is_visible) rvcgen_char.change(fn=update_visibility, inputs=[rvcgen_char, rvcgen_narr], outputs=[rvcat_default_pitch_gr, rvcat_narrator_pitch_gr]) rvcgen_narr.change(fn=update_visibility, inputs=[rvcgen_char, rvcgen_narr], outputs=[rvcat_default_pitch_gr, rvcat_narrator_pitch_gr]) - with gr.Accordion("Advanced Engine/Model Settings", open=False): + with gr.Accordion("Advanced Engine/Model Settings", open=False): with gr.Row(): #Get the current URL from the page domain_name_output = gr.Textbox(label="Domain Name", visible=False) @@ -1944,23 +1516,29 @@ if gradio_enabled == True: gen_choices = [("Standard", "false")] #Continue on with the Gradio interface gen_stream = gr.Dropdown(choices=gen_choices, label="Generation Mode", value="false") - gen_lang = gr.Dropdown(value=params['api_def']['api_language'], choices=["ar", "zh", "cs", "nl", "en", "fr", "de", "hi", "hu", "it", "ja", "ko", "pl", "pt", "ru", "es", "tr"], label="Languages" if at_settings["languages_capable"] else "Model not multi language", interactive=at_settings["languages_capable"]) - gen_narren = gr.Dropdown(choices=[("Enabled", "true"), ("Disabled", "false"), ("Enabled (Silent)", "silent")], label="Narrator Enabled/Disabled", value="true" if params['api_def']['api_narrator_enabled'] == "true" else ("silent" if params['api_def']['api_narrator_enabled'] == "silent" else "false")) - gen_textni = gr.Dropdown(choices=[("Character", "character"), ("Narrator", "narrator"), ("Silent", "silent")], label="Narrator Text-not-inside", value=params['api_def']['api_text_not_inside']) - gen_stopcurrentgen = gr.Dropdown(choices={("Stop", "true"), ("Dont stop", "false")}, label="Auto-Stop current generation", value="true") + gen_lang = gr.Dropdown(value=config.api_def.api_language, choices=["ar", "zh", "cs", "nl", "en", "fr", "de", "hi", "hu", "it", "ja", "ko", "pl", "pt", "ru", "es", "tr"], label="Languages" if at_settings["languages_capable"] else "Model not multi language", interactive=at_settings["languages_capable"], allow_custom_value=True) + gen_narren = gr.Dropdown(choices=[("Enabled", "true"), ("Disabled", "false"), ("Enabled (Silent)", "silent")], label="Narrator Enabled/Disabled", value="true" if config.api_def.api_narrator_enabled == "true" else ("silent" if config.api_def.api_narrator_enabled == "silent" else "false"), allow_custom_value=True) + gen_textni = gr.Dropdown(choices=[("Character", "character"), ("Narrator", "narrator"), ("Silent", "silent")], label="Narrator Text-not-inside", value=config.api_def.api_text_not_inside) + gen_stopcurrentgen = gr.Dropdown(choices={("Stop", "true"), ("Dont stop", "false")}, label="Auto-Stop current generation", value="true") + with gr.Row(): + gen_filter = gr.Dropdown(value=config.api_def.api_text_filtering, label="Text filtering", choices=["none", "standard", "html"]) + gen_filetime = gr.Dropdown( + choices=[("Timestamp files", "true"), ("Dont Timestamp (Over-write)", "false")], + label="Include Timestamp", + value="true" if config.api_def.api_output_file_timestamp else "false", + allow_custom_value=True + ) + gen_autopl = gr.Dropdown(choices={("Play locally", "false"), ("Play remotely", "true")}, label="Play Locally or Remotely", value="true" if config.api_def.api_autoplay else "false", allow_custom_value=True) + gen_autoplvol = gr.Dropdown(choices=[str(i / 10) for i in range(11)], value=str(config.api_def.api_autoplay_volume), label="Remote play volume", allow_custom_value=True) + gen_filen = gr.Textbox(value=config.api_def.api_output_file_name, label="Output File Name") with gr.Row(): - gen_filter = gr.Dropdown(value=params['api_def']['api_text_filtering'], label="Text filtering", choices=["none", "standard", "html"]) - gen_filetime = gr.Dropdown(choices={("Timestamp files", "true"), ("Dont Timestamp (Over-write)", "false")}, label="Include Timestamp", value="true" if params['api_def']['api_output_file_timestamp'] else "false") - gen_autopl = gr.Dropdown(choices={("Play locally", "false"), ("Play remotely", "true")}, label="Play Locally or Remotely", value="true" if params['api_def']['api_autoplay'] else "false") - gen_autoplvol = gr.Dropdown(choices=[str(i / 10) for i in range(11)], value=str(params['api_def']['api_autoplay_volume']), label="Remote play volume", allow_custom_value=True) - gen_filen = gr.Textbox(value=params['api_def']['api_output_file_name'], label="Output File Name") - with gr.Row(): gen_speed = gr.Slider(minimum=0.25, maximum=2.00, step=0.25, label="Speed", value="1.00", interactive=at_settings["generationspeed_capable"]) gen_pitch = gr.Slider(minimum=-10, maximum=10, step=1, label="Pitch", value="1", interactive=at_settings["pitch_capable"]) gen_temperature = gr.Slider(minimum=0.05, maximum=1.0, step=0.05, label="Temperature", value=0.75, interactive=at_settings["temperature_capable"]) - gen_repetition = gr.Slider(minimum=1.0, maximum=20.0, step=1.0, label="Repetition Penalty", value=10, interactive=at_settings["repetitionpenalty_capable"]) + gen_repetition = gr.Slider(minimum=1.0, maximum=20.0, step=1.0, label="Repetition Penalty", value=10, interactive=at_settings["repetitionpenalty_capable"]) #Toggle narrator selection on Streaming select def update_narren_and_autopl(gen_stream): + debug_func_entry() if gen_stream == "true": return "false", "false" else: @@ -1968,7 +1546,7 @@ if gradio_enabled == True: gen_stream.change(update_narren_and_autopl, inputs=[gen_stream], outputs=[gen_narren, gen_autopl]) with gr.Row(): output_audio = gr.Audio(show_label=False, label="Generated Audio", autoplay=True, scale=3) - output_message = gr.Textbox( label="TTS Result", lines=5, scale=1) + output_message = gr.Textbox( label="TTS Result", lines=5, scale=1) with gr.Row(): dark_mode_btn = gr.Button("Light/Dark Mode", variant="primary") refresh_button = gr.Button("Refresh Server Settings", elem_id="refresh_button") @@ -1977,7 +1555,7 @@ if gradio_enabled == True: model_btn_gr.click(fn=change_model_loaded, inputs=[engine_choices, model_choices_gr], outputs=[output_message, gen_stream, gen_char, rvcgen_char, gen_narr, rvcgen_narr, gen_speed, gen_pitch, gen_temperature, gen_repetition, gen_lang, model_choices_gr, engine_choices]) engine_btn.click(fn=set_engine_loaded, inputs=[engine_choices], outputs=[output_message, gen_stream, gen_char, rvcgen_char, gen_narr, rvcgen_narr, gen_speed, gen_pitch, gen_temperature, gen_repetition, gen_lang, model_choices_gr, engine_choices]) - + dark_mode_btn.click(None, None, None, js="""() => { if (document.querySelectorAll('.dark').length) { @@ -1992,82 +1570,98 @@ if gradio_enabled == True: stop_button.click(stop_generate_tts, inputs=[], outputs=[output_message]) submit_button.click(generate_tts, inputs=[gen_text, gen_char, rvcgen_char, rvcat_default_pitch_gr, gen_narr, rvcgen_narr, rvcat_narrator_pitch_gr, gen_narren, gen_textni, gen_repetition, gen_lang, gen_filter, gen_speed, gen_pitch, gen_autopl, gen_autoplvol, gen_filen, gen_temperature, gen_filetime, gen_stream, gen_stopcurrentgen], outputs=[output_audio, output_message]) - if params.get("gradio_pages", {}).get("Generate_Help_page", True): + if config.gradio_pages.Generate_Help_page: with gr.Tab("Generate Help"): help_content = alltalk_generation_help() gr.Markdown(help_content) - - if params.get("gradio_pages", {}).get("Voice2RVC_page", True): + + if config.gradio_pages.Voice2RVC_page: with gr.Tab("Voice2RVC"): gr.Markdown("""Voice2RVC allows you to convert your spoken audio files into synthesized speech using advanced RVC (Retrieval-based Voice Conversion) models. You can either record your own speech or upload a pre-recorded audio file for processing. The tool offers features trim your input audio and undo changes if necessary. Simply record or upload your audio, select an RVC voice model, and submit it for processing. Once completed, you can download your synthesized speech.""") with gr.Row(): audio_input = gr.Audio(sources=["microphone", "upload"], type="numpy", label="Record audio or Upload a spoken audio file") - if running_in_docker: + if running_in_docker: with gr.Row(): - with gr.Accordion("Docker IP/URL for API Address updater", open=False): + with gr.Accordion("Docker IP/URL for API Address updater", open=False): with gr.Row(): - docker_url = f"http://localhost:{params['api_def']['api_port_number']}" + docker_url = f"http://localhost:{config.api_def.api_port_number}" docker_upd = gr.Textbox(label="Docker IP/URL for API Address", value=docker_url, show_label=False) update_docker_btn = gr.Button("Update Docker IP/URL API Address") - def update_docker_address(new_url): - global docker_url # Need this to modify the global variable - docker_url = new_url - return f"Docker API address updated to: {new_url}" update_docker_btn.click( fn=update_docker_address, inputs=[docker_upd], outputs=[gr.Text(label="Status")] - ) + ) with gr.Row(): rvc_voices_dropdown = gr.Dropdown(choices=at_settings["rvcvoices"], label="Select RVC Voice to generate as", value=at_settings["rvcvoices"][0], scale=1) rvc_pitch_slider = gr.Slider(minimum=-24, maximum=24, step=1, label="RVC Pitch", info="Depending on the pitch of your input audio, you will need to adjust this accordingly to change the pitch for the output voice. The higher the value, the higher the pitch.", value=0, interactive=True, scale=2) with gr.Row(): - rvc_f0method = gr.Radio(label="Pitch Extraction Algorithm", info="Select the algorithm to be used for extracting the pitch (F0) during audio conversion. The default algorithm is rmvpe, which is generally recommended for most cases due to its balance of accuracy and performance.", choices=["crepe", "crepe-tiny", "dio", "fcpe", "harvest", "hybrid[rmvpe+fcpe]", "pm", "rmvpe"], value=params["rvc_settings"].get("f0method", "rmvpe"), interactive=True, scale=1) + rvc_f0method = gr.Radio(label="Pitch Extraction Algorithm", info="Select the algorithm to be used for extracting the pitch (F0) during audio conversion. The default algorithm is rmvpe, which is generally recommended for most cases due to its balance of accuracy and performance.", choices=["crepe", "crepe-tiny", "dio", "fcpe", "harvest", "hybrid[rmvpe+fcpe]", "pm", "rmvpe"], value=config.rvc_settings.f0method, interactive=True, scale=1) submit_button = gr.Button("Submit to RVC", scale=0) audio_output = gr.Audio(label="Converted Audio") - + submit_button.click(fn=voice2rvc, inputs=[audio_input, rvc_voices_dropdown, rvc_pitch_slider, rvc_f0method], outputs=audio_output) - - if params.get("gradio_pages", {}).get("TTS_Generator_page", True): + + if config.gradio_pages.TTS_Generator_page: with gr.Tab("TTS Generator"): gr.Markdown("""With the TTS Generator you can create incredibly long audio e.g. entire books. Yet to be migrated into Gradio""") gr.Markdown("""Please find it on the web address http://127.0.0.1:7851/static/tts_generator/tts_generator.html (Assuming you have not changed your IP Address)""") - + with gr.Tab("Global Settings"): with gr.Tab("AllTalk Settings"): with gr.Row(): - delete_output_wavs = gr.Dropdown(value=params["delete_output_wavs"], label="Del WAV's older than", choices=["Disabled", "1 Day", "2 Days", "3 Days", "4 Days", "5 Days", "6 Days", "7 Days", "14 Days", "21 Days", "28 Days"]) - api_port_number = gr.Number(value=int(params['api_def']['api_port_number']), label="API Port Number", precision=0) - gradio_port_number = gr.Number(value=int(params["gradio_port_number"]), label="Gradio Port Number", precision=0) - output_folder = gr.Textbox(value=params["output_folder"], label=f"Output Folder name (sub {branding})") + delete_output_wavs = gr.Dropdown(value=config.delete_output_wavs, label="Del WAV's older than", choices=["Disabled", "1 Day", "2 Days", "3 Days", "4 Days", "5 Days", "6 Days", "7 Days", "14 Days", "21 Days", "28 Days"]) + api_port_number = gr.Number( + value=config.api_def.api_port_number, + label="API Port Number", + precision=0 + ) + gradio_port_number = gr.Number( + value=config.gradio_port_number, + label="Gradio Port Number", + precision=0 + ) + output_folder = gr.Textbox( + value=config.output_folder, + label=f"Output Folder name (sub {config.branding})" + ) with gr.Row(): - transcode_audio_format = gr.Dropdown(choices={"Disabled": "disabled", "aac": "aac", "flac": "flac", "mp3": "mp3", "opus": "opus", "wav": "wav"}, label="Audio Transcoding", value=params["transcode_audio_format"]) + transcode_audio_format = gr.Dropdown(choices={"Disabled": "disabled", "aac": "aac", "flac": "flac", "mp3": "mp3", "opus": "opus", "wav": "wav"}, label="Audio Transcoding", value=config.transcode_audio_format) with gr.Row(): - themes_select = gr.Dropdown(loadThemes.get_list(), value=loadThemes.read_json(), label="Gradio Theme Selection", visible=True,) - themes_select.change(fn=loadThemes.select_theme, inputs=[themes_select], outputs=[gr.Textbox(label="Gradio Selection Result")],) + themes_select = gr.Dropdown(loadThemes.get_list(), value=loadThemes.read_json(), label="Gradio Theme Selection", visible=True) + def update_theme_selection(theme_name): + config = AlltalkConfig.get_instance() + config.theme.clazz = theme_name + config.save() # Save the updated configuration + return f"Theme '{theme_name}' has been selected and saved." + themes_select.change( + fn=update_theme_selection, + inputs=[themes_select], + outputs=[gr.Textbox(label="Gradio Selection Result")] + ) with gr.Row(): with gr.Column(): gr_debug_tts = gr.CheckboxGroup(choices=debugging_choices, label="Debugging Options list", value=default_values) with gr.Column(): - gradio_interface = gr.Dropdown(choices={"Enabled": "true", "Disabled": "false"}, label="Gradio Interface", value="Enabled" if params["gradio_interface"] else "Disabled", info="**WARNING**: This will disable the AllTalk Gradio interface from loading. To re-enable the interface, go to the API address in a web browser and enable it there. http://127.0.0.1:7851/") + gradio_interface = gr.Dropdown(choices={"Enabled": "true", "Disabled": "false"}, label="Gradio Interface", value="Enabled" if config.gradio_interface else "Disabled", info="**WARNING**: This will disable the AllTalk Gradio interface from loading. To re-enable the interface, go to the API address in a web browser and enable it there. http://127.0.0.1:7851/", allow_custom_value=True) gr.Markdown("### Disable Gradio Interface Tabs") # Adds a title - gr.Markdown("Use the checkboxes below to enable or disable individual interface tabs or components.") + gr.Markdown("Use the checkboxes below to enable or disable individual interface tabs or components.") with gr.Group(): with gr.Row(): - generate_help_page = gr.Checkbox(label="Generate Help", value=params.get("gradio_pages", {}).get("Generate_Help_page", True)) - voice2rvc_page = gr.Checkbox(label="Voice2RVC", value=params.get("gradio_pages", {}).get("Voice2RVC_page", True)) - tts_generator_page = gr.Checkbox(label="TTS Generator", value=params.get("gradio_pages", {}).get("TTS_Generator_page", True)) - tts_engines_settings_page = gr.Checkbox(label="TTS Engines Settings", value=params.get("gradio_pages", {}).get("TTS_Engines_Settings_page", True)) - alltalk_documentation_page = gr.Checkbox(label="AllTalk Documentation", value=params.get("gradio_pages", {}).get("alltalk_documentation_page", True)) - api_documentation_page = gr.Checkbox(label="API Documentation", value=params.get("gradio_pages", {}).get("api_documentation_page", True)) - + generate_help_page = gr.Checkbox(label="Generate Help", value=config.gradio_pages.Generate_Help_page) + voice2rvc_page = gr.Checkbox(label="Voice2RVC", value=config.gradio_pages.Voice2RVC_page) + tts_generator_page = gr.Checkbox(label="TTS Generator", value=config.gradio_pages.TTS_Generator_page) + tts_engines_settings_page = gr.Checkbox(label="TTS Engines Settings", value=config.gradio_pages.TTS_Engines_Settings_page) + alltalk_documentation_page = gr.Checkbox(label="AllTalk Documentation", value=config.gradio_pages.alltalk_documentation_page) + api_documentation_page = gr.Checkbox(label="API Documentation", value=config.gradio_pages.api_documentation_page) + with gr.Row(): submit_button = gr.Button("Update Settings") output_message = gr.Textbox(label="Output Message", interactive=False, show_label=False) # Update the function to include these new settings submit_button.click( - update_settings_at, + update_settings_at, inputs=[delete_output_wavs, gradio_interface, gradio_port_number, output_folder, api_port_number, gr_debug_tts, transcode_audio_format, generate_help_page, voice2rvc_page, tts_generator_page, tts_engines_settings_page, alltalk_documentation_page, api_documentation_page], outputs=output_message ) @@ -2076,53 +1670,51 @@ if gradio_enabled == True: gr.Markdown("""##   API Version Settings""") with gr.Group(): with gr.Row(): - api_use_legacy_api = gr.Dropdown(choices=["AllTalk v2 API", "AllTalk v1 API (Legacy)"], label=f"{branding} API version", value="AllTalk v1 API (Legacy)" if params['api_def']['api_use_legacy_api'] else "AllTalk v2 API", scale=1) + api_use_legacy_api = gr.Dropdown(choices=["AllTalk v2 API", "AllTalk v1 API (Legacy)"], label=f"{branding} API version", value=config.api_def.api_use_legacy_api, scale=1, allow_custom_value=True) gr.Textbox(value="Determines the API version to use. The legacy API includes the full URL (protocol, IP address, and port) in the output responses, while the new API returns only the relative path of the output file. Default: AllTalk v2 API", interactive=False, show_label=False, lines=2, scale=4) with gr.Row(): - api_legacy_ip_address = gr.Textbox(value=params['api_def']['api_legacy_ip_address'], label="AllTalk v1 API IP address") - gr.Textbox(value="Specifies the IP address to be included in the output responses when using the legacy API. Default: 127.0.0.1", interactive=False,show_label=False, lines=2, scale=4) + api_legacy_ip_address = gr.Textbox(value=config.api_def.api_legacy_ip_address, label="AllTalk v1 API IP address") + gr.Textbox(value="Specifies the IP address to be included in the output responses when using the legacy API. Default: 127.0.0.1", interactive=False,show_label=False, lines=2, scale=4) gr.Markdown("""##   API Default Settings""") - with gr.Group(): + with gr.Group(): with gr.Row(): - api_length_stripping = gr.Slider(minimum=1, maximum=20, step=1, value=int(params['api_def']['api_length_stripping']), label="Strip sentences shorter than", scale=1) + api_length_stripping = gr.Slider(minimum=1, maximum=20, step=1, value=int(config.api_def.api_length_stripping), label="Strip sentences shorter than", scale=1) gr.Textbox(value="Defines the minimum length of a sentence (in characters) that will be processed for text-to-speech. Sentences shorter than the X characters value will be filtered out by the Narrator to remove unwanted text characters. Default: 3", interactive=False,show_label=False, lines=2, scale=4) with gr.Row(): - api_max_characters = gr.Slider(minimum=50, maximum=10000, step=50, value=int(params['api_def']['api_max_characters']), label="Maximum amount of characters") + api_max_characters = gr.Slider(minimum=50, maximum=10000, step=50, value=int(config.api_def.api_max_characters), label="Maximum amount of characters") gr.Textbox(value="Sets the maximum number of characters allowed in a single text-to-speech generation request. Requests exceeding this limit will be rejected. Default: 2000", interactive=False,show_label=False, lines=2, scale=4) with gr.Row(): - api_text_filtering = gr.Dropdown(value=params['api_def']['api_text_filtering'], label="Text filtering", choices=["none", "standard", "html"]) + api_text_filtering = gr.Dropdown(value=config.api_def.api_text_filtering, label="Text filtering", choices=["none", "standard", "html"]) gr.Textbox(value="Determines the text filtering method applied to the input text before processing. Available options are 'none' (no filtering), 'standard' (basic filtering), and 'html' (HTML-specific filtering). Default: Standard", interactive=False,show_label=False, lines=2, scale=4) with gr.Row(): - api_language = gr.Dropdown(value=params['api_def']['api_language'], label="Language", choices=["ar", "zh", "cs", "nl", "en", "fr", "de", "hi", "hu", "it", "ja", "ko", "pl", "pt", "ru", "es", "tr"]) + api_language = gr.Dropdown(value=config.api_def.api_language, label="Language", choices=["ar", "zh", "cs", "nl", "en", "fr", "de", "hi", "hu", "it", "ja", "ko", "pl", "pt", "ru", "es", "tr"]) gr.Textbox(value="Sets the default language for text-to-speech if no language is explicitly provided in the request. Default: en", interactive=False,show_label=False, lines=2, scale=4) with gr.Row(): api_narrator_enabled = gr.Dropdown( choices={("Enabled", "true"), ("Disabled", "false"), ("Enabled (Silent)", "silent")}, label="Narrator Enabled/Disable/Silent", - value=("true" if params['api_def']['api_narrator_enabled'] == "true" - else "false" if params['api_def']['api_narrator_enabled'] == "false" - else "silent") + value=config.api_def.api_narrator_enabled ) gr.Textbox(value="Determines whether the narrator functionality is enabled by default when not explicitly specified in the request. Please note, if you set `Enabled` or `Enabled (silent)` as the APi defaults, then all text will go into the narrator function unless `disabled` is sent as part of the TTS generation request, possibly resulting in silenced TTS. Default: Disabled", interactive=False,show_label=False, lines=2, scale=4) with gr.Row(): - api_text_not_inside = gr.Dropdown(choices={"character", "narrator", "silent"}, label="Narrator Text-not-inside", value="character" if params['api_def']['api_text_not_inside'] else "narrator") + api_text_not_inside = gr.Dropdown(choices={"character", "narrator", "silent"}, label="Narrator Text-not-inside", value=config.api_def.api_text_not_inside) gr.Textbox(value="Defines how narrated text is split and processed when not explicitly specified in the request. The available options are 'character' (text is associated with the character) and 'narrator' (text is associated with the narrator). Default: Narrator", interactive=False,show_label=False, lines=2, scale=4) with gr.Row(): - api_output_file_name = gr.Textbox(value=params['api_def']['api_output_file_name'], label="Output file name") + api_output_file_name = gr.Textbox(value=config.api_def.api_output_file_name, label="Output file name") gr.Textbox(value="Specifies the default name for the output file when no filename is provided in the request. Default: myoutputfile", interactive=False,show_label=False, lines=2, scale=4) with gr.Row(): - api_output_file_timestamp = gr.Dropdown(choices={"Timestamp files", "Dont Timestamp (Over-write)"}, label="Include Timestamp", value="Timestamp files" if params['api_def']['api_output_file_timestamp'] else "Dont Timestamp (Over-write)") + api_output_file_timestamp = gr.Dropdown(choices={"Timestamp files", "Dont Timestamp (Over-write)"}, label="Include Timestamp", value="Timestamp files" if config.api_def.api_output_file_timestamp else "Dont Timestamp (Over-write)", allow_custom_value=True) gr.Textbox(value="Determines whether a unique identifier (UUID) timestamp is appended to the generated text-to-speech output file. When enabled, each output file will have a unique timestamp, preventing overwriting of files. When disabled, files with the same name will be overwritten. Default: Timestamp files", interactive=False,show_label=False, lines=2, scale=4) with gr.Row(): - api_autoplay = gr.Dropdown(choices={"Play locally", "Play remotely"}, label="Play Locally or Remotely", value="Play remotely" if params['api_def']['api_autoplay'] else "Play locally") + api_autoplay = gr.Dropdown(choices={"Play locally", "Play remotely"}, label="Play Locally or Remotely", value="Play remotely" if config.api_def.api_autoplay else "Play locally", allow_custom_value=True) gr.Textbox(value="Specifies whether the generated audio should be played locally on the client-side or remotely on the server-side console/terminal. Default: Play locally", interactive=False,show_label=False, lines=2, scale=4) with gr.Row(): - api_autoplay_volume = gr.Slider(minimum=0.1, maximum=0.9, step=0.1, label="Remote play volume", value=float(params['api_def']['api_autoplay_volume'])) + api_autoplay_volume = gr.Slider(minimum=0.1, maximum=0.9, step=0.1, label="Remote play volume", value=float(config.api_def.api_autoplay_volume)) gr.Textbox(value="Adjusts the volume level for audio playback when the 'Play Remotely' option is selected. The value ranges from 0.1 (lowest) to 0.9 (highest). Default: 0.9", interactive=False,show_label=False, lines=2, scale=4) gr.Markdown("""##   API Allowed Text Filtering/Passthrough Settings""") with gr.Row(): - api_allowed_filter = gr.Textbox(value=params['api_def']['api_allowed_filter'], label="Allowed text filter", show_label=False, lines=6, scale=1) - gr.Textbox(value="""Defines the set of characters and Unicode ranges that are permitted to be processed by the AllTalk TTS system. This filter ensures that only valid and supported characters are passed to the TTS engine or AI model for generation. The allowed characters include ASCII letters and digits, punctuation, whitespace, and various Unicode ranges for different languages and scripts. + api_allowed_filter = gr.Textbox(value=config.api_def.api_allowed_filter, label="Allowed text filter", show_label=False, lines=6, scale=1) + gr.Textbox(value="""Defines the set of characters and Unicode ranges that are permitted to be processed by the AllTalk TTS system. This filter ensures that only valid and supported characters are passed to the TTS engine or AI model for generation. The allowed characters include ASCII letters and digits, punctuation, whitespace, and various Unicode ranges for different languages and scripts. a-zA-Z0-9: ASCII letters and digits .,;:!?: Punctuation characters @@ -2153,16 +1745,17 @@ if gradio_enabled == True: """, interactive=False,show_label=False, lines=32, scale=1) with gr.Row(): submit_button = gr.Button("Update Settings") - output_message = gr.Textbox(label="Output Message", interactive=False, show_label=False) + output_message = gr.Textbox(label="Output Message", interactive=False, show_label=False) submit_button.click(update_settings_api, inputs=[api_length_stripping, api_legacy_ip_address, api_allowed_filter, api_max_characters, api_use_legacy_api, api_text_filtering, api_narrator_enabled, api_text_not_inside, api_language, api_output_file_name, api_output_file_timestamp, api_autoplay, api_autoplay_volume], outputs=output_message) def rvc_update_dropdowns(): + debug_func_entry() global at_settings at_settings = get_alltalk_settings() # Pull all the current settings from the AllTalk server, if its online. current_voices = at_settings["rvcvoices"] - current_char = params["rvc_settings"]["rvc_char_model_file"] - current_narr = params["rvc_settings"]["rvc_narr_model_file"] + current_char = config.rvc_settings.rvc_char_model_file + current_narr = config.rvc_settings.rvc_narr_model_file if current_char not in current_voices: current_char = current_voices[0] if current_voices else "" if current_narr not in current_voices: @@ -2174,6 +1767,7 @@ if gradio_enabled == True: def gr_update_rvc_settings(rvc_enabled, rvc_char_model_file, rvc_narr_model_file, split_audio, autotune, pitch, filter_radius, index_rate, rms_mix_rate, protect, hop_length, f0method, embedder_model, training_data_size): + debug_func_entry() progress = gr.Progress(track_tqdm=True) return update_rvc_settings(rvc_enabled, rvc_char_model_file, rvc_narr_model_file, split_audio, autotune, pitch, filter_radius, index_rate, rms_mix_rate, protect, hop_length, f0method, embedder_model, training_data_size, progress) @@ -2183,95 +1777,99 @@ if gradio_enabled == True: with gr.Column(scale=3): with gr.Row(): rvc_model_file_available = at_settings["rvcvoices"] - rvc_char_model_file_default = params["rvc_settings"]["rvc_char_model_file"] + rvc_char_model_file_default = config.rvc_settings.rvc_char_model_file if rvc_char_model_file_default not in rvc_model_file_available: rvc_char_model_file_default = rvc_model_file_available[0] if rvc_model_file_available else "" rvc_char_model_file_gr = gr.Dropdown(choices=rvc_model_file_available, label="Default Character Voice Model", info="Select the Character voice model used for conversion.", value=rvc_char_model_file_default, allow_custom_value=True,) - rvc_narr_model_file_default = params["rvc_settings"]["rvc_narr_model_file"] + rvc_narr_model_file_default = config.rvc_settings.rvc_narr_model_file if rvc_narr_model_file_default not in rvc_model_file_available: rvc_narr_model_file_default = rvc_model_file_available[0] if rvc_model_file_available else "" - rvc_narr_model_file_gr = gr.Dropdown(choices=rvc_model_file_available, label="Default Narrator Voice Model", info="Select the Narrator voice model used for conversion.", value=rvc_narr_model_file_default, allow_custom_value=True,) + rvc_narr_model_file_gr = gr.Dropdown(choices=rvc_model_file_available, label="Default Narrator Voice Model", info="Select the Narrator voice model used for conversion.", value=rvc_narr_model_file_default, allow_custom_value=True,) rvc_refresh_button = gr.Button("Refresh Model Choices") with gr.Column(scale=0): - rvc_enabled = gr.Checkbox(label="Enable RVC", info="RVC (Real-Time Voice Cloning) enhances TTS by replicating voice characteristics for characters or narrators, adding depth to synthesized speech.", value=params["rvc_settings"].get("rvc_enabled", False), interactive=True) + rvc_enabled = gr.Checkbox(label="Enable RVC", info="RVC (Real-Time Voice Cloning) enhances TTS by replicating voice characteristics for characters or narrators, adding depth to synthesized speech.", value=config.rvc_settings.rvc_enabled, interactive=True) with gr.Row(equal_height=True): with gr.Column(): - pitch = gr.Slider(minimum=-24, maximum=24, step=1, label="Pitch", info="Set the pitch of the audio, the higher the value, the higher the pitch.", value=params["rvc_settings"].get("pitch", 0), interactive=True,) + pitch = gr.Slider(minimum=-24, maximum=24, step=1, label="Pitch", info="Set the pitch of the audio, the higher the value, the higher the pitch.", value=config.rvc_settings.pitch, interactive=True) with gr.Column(): - hop_length = gr.Slider(minimum=1, maximum=512, step=1, label="Hop Length", info="Smaller hop lengths require more time for inference but tend to yield higher pitch accuracy.", value=params["rvc_settings"].get("hop_length", 128), interactive=True,) - with gr.Row(equal_height=True): - with gr.Column(): - training_data_size = gr.Slider(minimum=10000, maximum=100000, step=5000, label="Training Data Size", info="Determines the number of training data points used to train the FAISS index. Increasing the size may improve the quality but can also increase computation time.", value=params["rvc_settings"].get("training_data_size", 25000), interactive=True) - with gr.Column(): - index_rate = gr.Slider(minimum=0, maximum=1, label="Index Influence Ratio", info="Sets the influence exerted by the index file on the final output. A higher value increases the impact of the index, potentially enhancing detail but also increasing the risk of artifacts.", value=params["rvc_settings"].get("index_rate", 0.75), interactive=True) - with gr.Row(equal_height=True): - with gr.Column(): - rms_mix_rate = gr.Slider(minimum=0, maximum=1, label="Volume Envelope", info="Substitute or blend with the volume envelope of the output. The closer the ratio is to 1, the more the output envelope is employed.", value=params["rvc_settings"].get("rms_mix_rate", 1), interactive=True,) + hop_length = gr.Slider(minimum=1, maximum=512, step=1, label="Hop Length", info="Smaller hop lengths require more time for inference but tend to yield higher pitch accuracy.", value=config.rvc_settings.hop_length, interactive=True,) + with gr.Row(equal_height=True): with gr.Column(): - protect = gr.Slider(minimum=0, maximum=0.5, label="Protect Voiceless Consonants/Breath sounds", info="Prevents sound artifacts. Higher values (up to 0.5) provide stronger protection but may affect indexing.", value=params["rvc_settings"].get("protect", 0.5), interactive=True,) - with gr.Column(): - filter_radius = gr.Slider(minimum=0, maximum=7, label="Filter Radius", info="If the number is greater than or equal to three, employing median filtering on the collected tone results has the potential to decrease respiration.", value=params["rvc_settings"].get("filter_radius", 3), step=1, interactive=True,) + training_data_size = gr.Slider(minimum=10000, maximum=100000, step=5000, label="Training Data Size", info="Determines the number of training data points used to train the FAISS index. Increasing the size may improve the quality but can also increase computation time.", value=config.rvc_settings.training_data_size, interactive=True) + with gr.Column(): + index_rate = gr.Slider(minimum=0, maximum=1, label="Index Influence Ratio", info="Sets the influence exerted by the index file on the final output. A higher value increases the impact of the index, potentially enhancing detail but also increasing the risk of artifacts.", value=config.rvc_settings.index_rate, interactive=True) + with gr.Row(equal_height=True): + with gr.Column(): + rms_mix_rate = gr.Slider(minimum=0, maximum=1, label="Volume Envelope", info="Substitute or blend with the volume envelope of the output. The closer the ratio is to 1, the more the output envelope is employed.", value=config.rvc_settings.rms_mix_rate, interactive=True,) + with gr.Column(): + protect = gr.Slider(minimum=0, maximum=0.5, label="Protect Voiceless Consonants/Breath sounds", info="Prevents sound artifacts. Higher values (up to 0.5) provide stronger protection but may affect indexing.", value=config.rvc_settings.protect, interactive=True,) + with gr.Column(): + filter_radius = gr.Slider(minimum=0, maximum=7, label="Filter Radius", info="If the number is greater than or equal to three, employing median filtering on the collected tone results has the potential to decrease respiration.", value=config.rvc_settings.filter_radius, step=1, interactive=True,) with gr.Row(): with gr.Column(): with gr.Row(): - embedder_model = gr.Radio(label="Embedder Model", info="Model used for learning speaker embedding.", choices=["hubert", "contentvec"], value=params["rvc_settings"].get("embedder_model", "hubert"), interactive=True,) - with gr.Row(): - split_audio = gr.Checkbox(label="Split Audio", info="Split the audio into chunks for inference to obtain better results in some cases.", value=params["rvc_settings"].get("split_audio", False), interactive=True,) - autotune = gr.Checkbox(label="Autotune", info="Apply a soft autotune to your inferences, recommended for singing conversions.", value=params["rvc_settings"].get("autotune", False), interactive=True,) + embedder_model = gr.Radio(label="Embedder Model", info="Model used for learning speaker embedding.", choices=["hubert", "contentvec"], value=config.rvc_settings.embedder_model, interactive=True,) + with gr.Row(): + split_audio = gr.Checkbox(label="Split Audio", info="Split the audio into chunks for inference to obtain better results in some cases.", value=config.rvc_settings.split_audio, interactive=True,) + autotune = gr.Checkbox(label="Autotune", info="Apply a soft autotune to your inferences, recommended for singing conversions.", value=config.rvc_settings.autotune, interactive=True,) with gr.Column(): - f0method = gr.Radio(label="Pitch Extraction Algorithm", info="Select the algorithm to be used for extracting the pitch (F0) during audio conversion. The default algorithm is rmvpe, which is generally recommended for most cases due to its balance of accuracy and performance.", choices=["crepe", "crepe-tiny", "dio", "fcpe", "harvest", "hybrid[rmvpe+fcpe]", "pm", "rmvpe"], value=params["rvc_settings"].get("f0method", "rmvpe"), interactive=True,) + f0method = gr.Radio(label="Pitch Extraction Algorithm", info="Select the algorithm to be used for extracting the pitch (F0) during audio conversion. The default algorithm is rmvpe, which is generally recommended for most cases due to its balance of accuracy and performance.", choices=["crepe", "crepe-tiny", "dio", "fcpe", "harvest", "hybrid[rmvpe+fcpe]", "pm", "rmvpe"], value=config.rvc_settings.f0method, interactive=True,) with gr.Row(): update_button = gr.Button("Update RVC Settings") update_output = gr.Textbox(label="Update Status", show_label=False) rvc_refresh_button.click(rvc_update_dropdowns, None, [rvc_char_model_file_gr, rvc_narr_model_file_gr]) update_button.click(fn=gr_update_rvc_settings, inputs=[rvc_enabled, rvc_char_model_file_gr, rvc_narr_model_file_gr, split_audio, autotune, pitch, filter_radius, index_rate, rms_mix_rate, protect, hop_length, f0method, embedder_model, training_data_size], outputs=[update_output]) - + with gr.Tab("Text-generation-webui Settings"): with gr.Row(): - activate = gr.Dropdown(choices={"Enabled": "true", "Disabled": "false"}, label="Activate TTS", value="Enabled" if params["tgwui"]["tgwui_activate_tts"] else "Disabled") - autoplay = gr.Dropdown(choices={"Enabled": "true", "Disabled": "false"}, label="Autoplay TTS", value="Enabled" if params["tgwui"]["tgwui_autoplay_tts"] else "Disabled") - show_text = gr.Dropdown(choices={"Enabled": "true", "Disabled": "false"}, label="Show Text", value="Enabled" if params["tgwui"]["tgwui_show_text"] else "Disabled") - narrator_enabled = gr.Dropdown(choices=[("Enabled", "true"), ("Disabled", "false"), ("Enabled (Silent)", "silent")], label="Narrator enabled", value="true" if params["tgwui"]["tgwui_narrator_enabled"] == "true" else ("silent" if params["tgwui"]["tgwui_narrator_enabled"] == "silent" else "false")) - language = gr.Dropdown(value=params["tgwui"]["tgwui_language"], label="Default Language", choices=languages_list) + activate = gr.Dropdown(choices={"Enabled": "true", "Disabled": "false"}, label="Activate TTS", value="Enabled" if config.tgwui.tgwui_activate_tts else "Disabled", allow_custom_value=True) + autoplay = gr.Dropdown(choices={"Enabled": "true", "Disabled": "false"}, label="Autoplay TTS", value="Enabled" if config.tgwui.tgwui_autoplay_tts else "Disabled", allow_custom_value=True) + show_text = gr.Dropdown(choices={"Enabled": "true", "Disabled": "false"}, label="Show Text", value="Enabled" if config.tgwui.tgwui_show_text else "Disabled", allow_custom_value=True) + narrator_enabled = gr.Dropdown(choices=[("Enabled", "true"), ("Disabled", "false"), ("Enabled (Silent)", "silent")], label="Narrator enabled", value="true" if config.tgwui.tgwui_narrator_enabled == "true" else ("silent" if config.tgwui.tgwui_narrator_enabled == "silent" else "false"), allow_custom_value=True) + language = gr.Dropdown(value=config.tgwui.tgwui_language, label="Default Language", choices=languages_list, allow_custom_value=True) with gr.Row(): submit_button = gr.Button("Update Settings") output_message = gr.Textbox(label="Output Message", interactive=False, show_label=False) - - submit_button.click(update_settings_tg, inputs=[activate, autoplay, show_text, language, narrator_enabled], outputs=output_message) + + submit_button.click(update_settings_tgwui, inputs=[activate, autoplay, show_text, language, narrator_enabled], outputs=output_message) disk_space_page = get_disk_interface() disk_space_page() - - if params.get("gradio_pages", {}).get("TTS_Engines_Settings_page", True): + + if config.gradio_pages.TTS_Engines_Settings_page: with gr.Tab("TTS Engines Settings"): - with gr.Tabs(): - for engine_name in engines_available: - with gr.Tab(f"{engine_name.capitalize()} TTS"): - gr.Markdown(f"###   {engine_name.capitalize()} TTS") - globals()[f"{engine_name}_at_gradio_settings_page"](globals()[f"{engine_name}_model_config_data"]) - if params.get("gradio_pages", {}).get("alltalk_documentation_page", True): + with gr.Tabs(): + for engine_name in engines_available: + with gr.Tab(f"{engine_name.capitalize()} TTS"): + gr.Markdown(f"###   {engine_name.capitalize()} TTS") + globals()[f"{engine_name}_at_gradio_settings_page"](globals()[f"{engine_name}_model_config_data"]) + + if config.gradio_pages.alltalk_documentation_page: alltalk_documentation() - if params.get("gradio_pages", {}).get("api_documentation_page", True): + + if config.gradio_pages.api_documentation_page: api_documentation() + with gr.Tab("About this project"): alltalk_about() + return app if __name__ == "__main__": app = alltalk_gradio().queue() - app.launch(server_name="0.0.0.0", server_port=params['gradio_port_number'], prevent_thread_lock=True, quiet=True) + app.launch(server_name="0.0.0.0", server_port=config.gradio_port_number, prevent_thread_lock=True, quiet=True) if not running_in_standalone: app = alltalk_gradio().queue() - app.launch(server_name="0.0.0.0", server_port=params['gradio_port_number'], prevent_thread_lock=True, quiet=True) + app.launch(server_name="0.0.0.0", server_port=config.gradio_port_number, prevent_thread_lock=True, quiet=True) ######################################### # START-UP # Final Splash before Gradio # ######################################### -print(f"[{branding}TTS] Please use \033[91mCtrl+C\033[0m when exiting AllTalk otherwise a") -print(f"[{branding}TTS] subprocess may continue running in the background.") -print(f"[{branding}TTS]") -print(f"[{branding}TTS] {branding}Server Ready") +print_message("Please use \033[91mCtrl+C\033[0m when exiting AllTalk otherwise a") +print_message("subprocess may continue running in the background.") +print_message("") +print_message("Server Ready") ############################################################################################### # START-UP # Loop to keep the script from exiting out if its being run as a standalone script # diff --git a/test_server.py b/test_server.py new file mode 100644 index 0000000..e3d2d19 --- /dev/null +++ b/test_server.py @@ -0,0 +1,1184 @@ +import asyncio +import logging +import platform +import subprocess +import sys +import time +from datetime import datetime +from typing import Dict, List, Optional, Tuple +import aiohttp + +# pylint: disable=all + +class AllTalkTester: + def __init__(self): + self.banner = """ + _ _ _ _____ _ _ _____ _____ ____ + / \ | | |_ _|_ _| | | __ |_ _|_ _/ ___| + / _ \ | | | | |/ _` | | |/ / | | | | \___ \ + / ___ \| | | | | (_| | | < | | | | ___) | + /_/ \_\_|_| |_|\__,_|_|_|\_\ |_| |_| |____/ + + ╔════════════════════════════════════╗ + ║ Server Testing Suite v1.1 ║ + ╚════════════════════════════════════╝ + """ + self.config = { + "host": "127.0.0.1", + "port": 7851, + "log_file": "alltalk_test_results.log", + "timeout": 30, # Default timeout for tests + "retry_count": 3, # Number of retries for failed tests + "retry_delay": 5, # Delay between retries in seconds + } + self.narrator_test_cases = { + "narrator_simple": { + "text": "*The old wizard gazed at the stars.* \"What mysteries do they hold?\" *he wondered aloud.*", + "expect_files": 3 + }, + "mixed_complex": { + "text": "*The wind howled through the trees.* \"I don't like this place,\" *Sarah whispered.* \"Neither do I,\" *John replied, his voice trembling.*", + "expect_files": 5 + }, + "silent_test": { + "text": "*Silent narrator text.* \"Speaking character.\" Plain text here.", + "narrator_enabled": "silent", + "text_not_inside": "silent", + "expect_files": 1 + } + } + + self.filtering_test_cases = { + "text": "Test with special chars: @#$% and special é character and HTML tags bold", + "filters": ["none", "standard", "html"] + } + + self.volume_test_cases = [ + {"volume": 0.5, "should_succeed": True}, + {"volume": 1.5, "should_succeed": False} + ] + + self.error_test_cases = [ + {"case": "invalid_voice", "voice": "nonexistent.wav", "expect_error": 500}, + {"case": "bad_language", "language": "xx", "expect_error": 400}, + {"case": "missing_text", "text": "", "expect_error": 422} + ] + self.test_results = [] + self.setup_logging() + + def setup_logging(self): + """Configure logging to both file and console""" + self.log_file = datetime.now().strftime('alltalk_test_%Y%m%d_%H%M%S.log') + + # Configure logging with UTF-8 encoding + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(self.log_file, encoding='utf-8'), + logging.StreamHandler(sys.stdout) + ] + ) + self.logger = logging.getLogger(__name__) + self.logger.info("AllTalk Testing Suite Started") + + async def setup(self): + """Initial setup and configuration""" + print(self.banner) + self.logger.info("Starting test suite setup") + + try: + self.host = input(f"Enter server IP/URL [{self.config['host']}]: ") or self.config['host'] + self.port = input(f"Enter port [{self.config['port']}]: ") or self.config['port'] + self.base_url = f"http://{self.host}:{self.port}" + + self.logger.info(f"Configuration set - Host: {self.host}, Port: {self.port}") + return True + except Exception as e: + self.logger.error(f"Setup failed: {str(e)}") + return False + + async def menu(self): + """Display and handle test selection menu""" + while True: + print("\nTest Options:") + print("1. Run All Tests (takes 30 seconds)") + print("2. Network Connectivity Tests") + print("3. Core API Tests") + print("4. TTS Generation Tests") + print("5. Error Case Tests") + print("6. Individual Test Selection") + print("7. Exit") + + try: + choice = input("\nSelect option: ") + + if choice == "1": + await self.run_all_tests() + elif choice == "2": + await self.run_network_tests() + elif choice == "3": + await self.run_core_api_tests() + elif choice == "4": + await self.run_tts_tests() + elif choice == "5": + await self._test_error_cases() + elif choice == "6": + await self.individual_test_menu() + elif choice == "7": + self.logger.info("Test suite shutting down") + break + else: + print("Invalid option. Please try again.") + + except Exception as e: + self.logger.error(f"Menu operation failed: {str(e)}") + + async def individual_test_menu(self): + """Sub-menu for selecting individual tests""" + while True: + print("\nSelect Individual Test:") + print("1. Text Filtering Tests") + print("2. Narrator Mode Tests") + print("3. Autoplay Tests") + print("4. Additional Language Test") + print("5. OpenAI Compatibility Tests") + print("6. Return to Main Menu") + + try: + choice = input("\nSelect option: ") + + if choice == "1": + await self._test_text_filtering() + elif choice == "2": + await self._test_narrator_modes() + elif choice == "3": + await self._test_autoplay() + elif choice == "4": + await self._test_additional_language() + elif choice == "5": + await self._test_openai_tts(self._generate_test_texts()["openai"]) + elif choice == "6": + break + else: + print("Invalid option. Please try again.") + + except Exception as e: + self.logger.error(f"Individual test selection failed: {str(e)}") + + def log_result(self, category: str, test_name: str, status: str, details: str = ""): + """Log a test result to both the results list and logger""" + result = { + "timestamp": datetime.now().isoformat(), + "category": category, + "test": test_name, + "status": status, + "details": details + } + self.test_results.append(result) + + if status == "PASS": + self.logger.info(f"{category} - {test_name}: {status}") + elif status == "WARNING": + self.logger.warning(f"{category} - {test_name}: {status} - {details}") + else: + self.logger.error(f"{category} - {test_name}: {status} - {details}") + + async def run_network_tests(self): + """Perform network connectivity tests""" + self.logger.info("Starting network connectivity tests") + + # Ping test + await self._run_ping_test() + + # Basic connectivity test + await self._test_server_connection() + + async def _run_ping_test(self): + """Perform ping test to server""" + try: + param = '-n' if platform.system().lower() == 'windows' else '-c' + command = ['ping', param, '4', self.host] + + self.logger.info(f"Running ping test to {self.host}") + output = subprocess.check_output(command, stderr=subprocess.STDOUT).decode() + + if "unreachable" in output.lower() or "timed out" in output.lower(): + self.log_result("Network", "Ping Test", "WARNING", + "Host reachable but some packets lost") + else: + self.log_result("Network", "Ping Test", "PASS", + "Host responding to ping") + + except subprocess.CalledProcessError as e: + self.log_result("Network", "Ping Test", "WARNING", + f"Ping failed but continuing with tests: {str(e)}") + except Exception as e: + self.log_result("Network", "Ping Test", "ERROR", + f"Unexpected error during ping test: {str(e)}") + + async def _test_server_connection(self): + """Test basic server connectivity using /api/ready endpoint""" + self.logger.info("Testing server API connectivity") + + async with aiohttp.ClientSession() as session: + try: + start_time = time.time() + retries = 0 + + while retries < self.config['retry_count']: + try: + async with session.get( + f"{self.base_url}/api/ready", + timeout=aiohttp.ClientTimeout(total=5) + ) as response: + + if response.status == 200: + content = await response.text() + if content == "Ready": + elapsed = time.time() - start_time + self.log_result( + "Network", + "Server Connection", + "PASS", + f"Server ready. Response time: {elapsed:.2f}s" + ) + return True + else: + self.log_result( + "Network", + "Server Connection", + "WARNING", + f"Server responded but not ready: {content}" + ) + else: + self.log_result( + "Network", + "Server Connection", + "WARNING", + f"Unexpected status code: {response.status}" + ) + + except aiohttp.ClientError as e: + self.logger.warning(f"Connection attempt {retries + 1} failed: {str(e)}") + + retries += 1 + if retries < self.config['retry_count']: + await asyncio.sleep(self.config['retry_delay']) + + self.log_result( + "Network", + "Server Connection", + "ERROR", + f"Server not responding after {retries} attempts" + ) + return False + + except Exception as e: + self.log_result( + "Network", + "Server Connection", + "ERROR", + f"Unexpected error testing server connection: {str(e)}" + ) + return False + + async def run_core_api_tests(self): + """Run tests for core API functionality""" + self.logger.info("Starting core API tests") + + # Test core endpoints + settings = await self._test_current_settings() + if not settings: + self.logger.error("Failed to get current settings, skipping dependent tests") + return + + await self._test_voices() + await self._test_rvc_voices() + await self._test_root_endpoint() + + async def _test_current_settings(self) -> Optional[dict]: + """Test /api/currentsettings endpoint""" + self.logger.info("Testing current settings endpoint") + + async with aiohttp.ClientSession() as session: + try: + async with session.get( + f"{self.base_url}/api/currentsettings", + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + if response.status == 200: + settings = await response.json() + + # Validate essential settings exist + required_keys = [ + 'current_engine_loaded', + 'current_model_loaded', + 'streaming_capable', + 'multivoice_capable' + ] + + missing_keys = [key for key in required_keys if key not in settings] + + if missing_keys: + self.log_result( + "Core API", + "Current Settings", + "WARNING", + f"Missing required settings: {', '.join(missing_keys)}" + ) + else: + self.log_result( + "Core API", + "Current Settings", + "PASS", + f"Engine: {settings['current_engine_loaded']}, Model: {settings['current_model_loaded']}" + ) + + return settings + else: + self.log_result( + "Core API", + "Current Settings", + "ERROR", + f"Unexpected status code: {response.status}" + ) + return None + + except Exception as e: + self.log_result( + "Core API", + "Current Settings", + "ERROR", + f"Failed to get current settings: {str(e)}" + ) + return None + + async def _test_voices(self): + """Test /api/voices endpoint""" + self.logger.info("Testing voices endpoint") + + async with aiohttp.ClientSession() as session: + try: + async with session.get( + f"{self.base_url}/api/voices", + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + if response.status == 200: + data = await response.json() + voices = data.get('voices', []) + + if voices: + self.log_result( + "Core API", + "Voices", + "PASS", + f"Found {len(voices)} voices" + ) + # Store voices for later use in TTS tests + self.available_voices = voices + else: + self.log_result( + "Core API", + "Voices", + "WARNING", + "No voices found" + ) + else: + self.log_result( + "Core API", + "Voices", + "ERROR", + f"Unexpected status code: {response.status}" + ) + + except Exception as e: + self.log_result( + "Core API", + "Voices", + "ERROR", + f"Failed to get voices: {str(e)}" + ) + + async def _test_rvc_voices(self): + """Test /api/rvcvoices endpoint""" + self.logger.info("Testing RVC voices endpoint") + + async with aiohttp.ClientSession() as session: + try: + async with session.get( + f"{self.base_url}/api/rvcvoices", + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + if response.status == 200: + data = await response.json() + rvc_voices = data.get('rvcvoices', []) + + if "Disabled" in rvc_voices and len(rvc_voices) == 1: + self.log_result( + "Core API", + "RVC Voices", + "PASS", + "RVC is disabled (expected behavior)" + ) + elif rvc_voices: + self.log_result( + "Core API", + "RVC Voices", + "PASS", + f"Found {len(rvc_voices)} RVC voices" + ) + # Store RVC voices for later use + self.available_rvc_voices = rvc_voices + else: + self.log_result( + "Core API", + "RVC Voices", + "WARNING", + "No RVC voices found" + ) + else: + self.log_result( + "Core API", + "RVC Voices", + "ERROR", + f"Unexpected status code: {response.status}" + ) + + except Exception as e: + self.log_result( + "Core API", + "RVC Voices", + "ERROR", + f"Failed to get RVC voices: {str(e)}" + ) + + async def _test_root_endpoint(self): + """Test root endpoint /""" + self.logger.info("Testing root endpoint") + + async with aiohttp.ClientSession() as session: + try: + async with session.get( + self.base_url, + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + if response.status == 200: + content = await response.text() + if content: + self.log_result( + "Core API", + "Root Endpoint", + "PASS", + "Root endpoint returning content" + ) + else: + self.log_result( + "Core API", + "Root Endpoint", + "WARNING", + "Root endpoint returned empty content" + ) + else: + self.log_result( + "Core API", + "Root Endpoint", + "ERROR", + f"Unexpected status code: {response.status}" + ) + + except Exception as e: + self.log_result( + "Core API", + "Root Endpoint", + "ERROR", + f"Failed to access root endpoint: {str(e)}" + ) + + def _generate_test_texts(self) -> dict: + """Generate various test texts for different TTS scenarios""" + return { + "simple": "This is a simple test of the TTS system.", + "narrator": """*The narrator speaks first.* "Then a character speaks," *and the narrator returns.*""", + "mixed": """*It was a dark and stormy night.* "What's that noise?" *asked Sarah, her voice trembling.* "Just the wind," *replied John confidently.*""", + "openai": "Testing the OpenAI compatible endpoint with this text.", + "long": "This is a longer text that will be used to test the streaming capabilities of the TTS system. It includes multiple sentences to ensure we have enough content to properly test the streaming functionality." + } + + async def _select_test_voice(self) -> Optional[str]: + """Select a random voice from available voices""" + if not hasattr(self, 'available_voices') or not self.available_voices: + self.log_result( + "TTS", + "Voice Selection", + "ERROR", + "No voices available for testing" + ) + return None + return self.available_voices[0] # Use first available voice for consistency + + async def run_tts_tests(self): + """Run comprehensive TTS generation tests""" + self.logger.info("Starting TTS generation tests") + + test_voice = await self._select_test_voice() + if not test_voice: + return + + # Standard TTS test with text filtering variations + await self._test_text_filtering() + + # Narrator mode tests - now checks for available voices + voices = await self._select_two_voices() + if voices: + await self._test_narrator_modes() + + # Autoplay tests + await self._test_autoplay() + + # Additional language test + await self._test_additional_language() + + # Error case testing + await self._test_error_cases() + + # OpenAI compatibility tests + await self._test_openai_tts(self._generate_test_texts()["openai"]) + + async def _test_standard_tts(self, text: str, voice: str): + """Test standard TTS generation""" + self.logger.info("Testing standard TTS generation") + + async with aiohttp.ClientSession() as session: + try: + data = { + "text_input": text, + "character_voice_gen": voice, + "language": "en", + "output_file_name": f"test_standard_{int(time.time())}", + "text_filtering": "standard" + } + + async with session.post( + f"{self.base_url}/api/tts-generate", + data=data, + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + if response.status == 200: + result = await response.json() + if result.get("status") == "generate-success": + self.log_result( + "TTS", + "Standard Generation", + "PASS", + f"Output file: {result.get('output_file_path')}" + ) + # Store successful generation for later tests + self.last_generated_file = result.get('output_file_path') + else: + self.log_result( + "TTS", + "Standard Generation", + "ERROR", + f"Generation failed: {result}" + ) + else: + self.log_result( + "TTS", + "Standard Generation", + "ERROR", + f"Unexpected status code: {response.status}" + ) + + except Exception as e: + self.log_result( + "TTS", + "Standard Generation", + "ERROR", + f"Failed to generate TTS: {str(e)}" + ) + + async def _test_narrator_tts(self, text: str, voice: str): + """Test narrator mode TTS generation""" + self.logger.info("Testing narrator mode TTS generation") + + async with aiohttp.ClientSession() as session: + try: + data = { + "text_input": text, + "character_voice_gen": voice, + "narrator_voice_gen": voice, # Using same voice for simplicity + "narrator_enabled": "true", + "language": "en", + "output_file_name": f"test_narrator_{int(time.time())}", + "text_filtering": "standard", + "text_not_inside": "narrator" + } + + async with session.post( + f"{self.base_url}/api/tts-generate", + data=data, + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + if response.status == 200: + result = await response.json() + if result.get("status") == "generate-success": + self.log_result( + "TTS", + "Narrator Mode", + "PASS", + f"Output file: {result.get('output_file_path')}" + ) + # Store successful generation for later tests + self.last_narrator_file = result.get('output_file_path') + else: + self.log_result( + "TTS", + "Narrator Mode", + "ERROR", + f"Generation failed: {result}" + ) + else: + self.log_result( + "TTS", + "Narrator Mode", + "ERROR", + f"Unexpected status code: {response.status}" + ) + + except Exception as e: + self.log_result( + "TTS", + "Narrator Mode", + "ERROR", + f"Failed to generate narrator TTS: {str(e)}" + ) + + async def _test_streaming_tts(self, text: str, voice: str): + """Test streaming TTS generation""" + self.logger.info("Testing streaming TTS generation") + + async with aiohttp.ClientSession() as session: + try: + params = { + "text": text, + "voice": voice, + "language": "en", + "output_file": f"test_streaming_{int(time.time())}" + } + + async with session.get( + f"{self.base_url}/api/tts-generate-streaming", + params=params, + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + if response.status == 200: + # Test streaming by reading chunks + chunks_received = 0 + async for chunk in response.content.iter_any(): + chunks_received += 1 + + if chunks_received > 0: + self.log_result( + "TTS", + "Streaming Generation", + "PASS", + f"Received {chunks_received} chunks" + ) + else: + self.log_result( + "TTS", + "Streaming Generation", + "WARNING", + "Stream succeeded but no chunks received" + ) + elif response.status == 400: + error_data = await response.json() + if "streaming not supported" in error_data.get("error", "").lower(): + self.log_result( + "TTS", + "Streaming Generation", + "PASS", + "Correctly reported streaming not supported" + ) + else: + self.log_result( + "TTS", + "Streaming Generation", + "ERROR", + f"Bad request: {error_data}" + ) + else: + self.log_result( + "TTS", + "Streaming Generation", + "ERROR", + f"Unexpected status code: {response.status}" + ) + + except Exception as e: + self.log_result( + "TTS", + "Streaming Generation", + "ERROR", + f"Failed to test streaming: {str(e)}" + ) + + async def _test_openai_tts(self, text: str): + """Test OpenAI compatible endpoint""" + self.logger.info("Testing OpenAI compatible endpoint") + self.logger.info(f"Testing OpenAI generation with text: {text}") + + test_voices = ["alloy", "echo", "fable", "nova", "onyx", "shimmer"] + test_formats = ["wav", "mp3", "opus", "aac"] + + for voice in test_voices[:2]: # Test first two voices only + for format in test_formats[:2]: # Test first two formats only + await self._test_openai_single_case(text, voice, format) + + async def _test_openai_single_case(self, text: str, voice: str, format: str): + """Test single case for OpenAI endpoint""" + self.logger.info(f"Testing OpenAI voice: {voice}, format: {format}") + self.logger.info(f"Testing OpenAI generation with text: {text}") + + payload = { + "model": "tts-1", # Model name doesn't matter + "input": text, + "voice": voice, + "response_format": format, + "speed": 1.0 + } + + async with aiohttp.ClientSession() as session: + try: + async with session.post( + f"{self.base_url}/v1/audio/speech", + json=payload, + headers={"Content-Type": "application/json"}, + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + if response.status == 200: + # Check if we received audio data + content_type = response.headers.get('Content-Type', '') + if content_type.startswith('audio/'): + data = await response.read() + if len(data) > 0: + self.log_result( + "TTS", + f"OpenAI Generation ({voice}, {format})", + "PASS", + f"Generated audio size: {len(data)} bytes" + ) + else: + self.log_result( + "TTS", + f"OpenAI Generation ({voice}, {format})", + "WARNING", + "Response successful but no audio data" + ) + else: + self.log_result( + "TTS", + f"OpenAI Generation ({voice}, {format})", + "ERROR", + f"Unexpected content type: {content_type}" + ) + else: + error_data = await response.json() + self.log_result( + "TTS", + f"OpenAI Generation ({voice}, {format})", + "ERROR", + f"Failed with status {response.status}: {error_data}" + ) + + except Exception as e: + self.log_result( + "TTS", + f"OpenAI Generation ({voice}, {format})", + "ERROR", + f"Failed to test OpenAI generation: {str(e)}" + ) + + async def _print_test_summary(self): + """Print summary of all test results""" + total = len(self.test_results) + passed = sum(1 for r in self.test_results if r['status'] == 'PASS') + warnings = sum(1 for r in self.test_results if r['status'] == 'WARNING') + errors = sum(1 for r in self.test_results if r['status'] == 'ERROR') + + summary = f""" + Test Summary + ============ + Total Tests: {total} + Passed: {passed} + Warnings: {warnings} + Errors: {errors} + """ + + print(summary) + self.logger.info(summary) + + async def _select_two_voices(self) -> Optional[Tuple[str, str]]: + """Select two different voices for narrator testing.""" + if not hasattr(self, 'available_voices') or len(self.available_voices) < 2: + self.log_result( + "TTS", + "Voice Selection", + "SKIP", + "Need at least 2 voices for narrator testing" + ) + return None + return self.available_voices[0], self.available_voices[1] + + async def _test_narrator_modes(self): + """Test various narrator mode combinations""" + self.logger.info("Testing narrator mode combinations") + + voices = await self._select_two_voices() + if not voices: + return + + character_voice, narrator_voice = voices + self.logger.info(f"Using character voice: {character_voice}, narrator voice: {narrator_voice}") + + for test_name, case in self.narrator_test_cases.items(): + self.logger.info(f"Running narrator test case: {test_name}") + + data = { + "text_input": case["text"], + "character_voice_gen": character_voice, + "narrator_voice_gen": narrator_voice, + "narrator_enabled": case.get("narrator_enabled", "true"), + "text_not_inside": case.get("text_not_inside", "narrator"), + "language": "en", + "output_file_name": f"test_narrator_{test_name}_{int(time.time())}", + "output_file_timestamp": True + } + + async with aiohttp.ClientSession() as session: + try: + async with session.post( + f"{self.base_url}/api/tts-generate", + data=data, + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + if response.status == 200: + result = await response.json() + if result.get("status") == "generate-success": + self.log_result( + "TTS", + f"Narrator Test {test_name}", + "PASS", + f"Output file: {result.get('output_file_path')}" + ) + else: + self.log_result( + "TTS", + f"Narrator Test {test_name}", + "ERROR", + f"Generation failed: {result}" + ) + except Exception as e: + self.log_result( + "TTS", + f"Narrator Test {test_name}", + "ERROR", + f"Test failed: {str(e)}" + ) + + async def _test_text_filtering(self): + """Test different text filtering options""" + self.logger.info("Testing text filtering options") + + test_voice = await self._select_test_voice() + if not test_voice: + return + + test_text = self.filtering_test_cases["text"] + + for filter_type in self.filtering_test_cases["filters"]: + self.logger.info(f"Testing filter type: {filter_type}") + self.logger.info(f"Testing with text: {test_text}") + + data = { + "text_input": test_text, + "character_voice_gen": test_voice, + "text_filtering": filter_type, + "language": "en", + "output_file_name": f"test_filter_{filter_type}_{int(time.time())}", + "output_file_timestamp": True + } + + async with aiohttp.ClientSession() as session: + try: + async with session.post( + f"{self.base_url}/api/tts-generate", + data=data, + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + if response.status == 200: + result = await response.json() + if result.get("status") == "generate-success": + self.log_result( + "TTS", + f"Text Filter {filter_type}", + "PASS", + f"Generated with {filter_type} filtering" + ) + else: + self.log_result( + "TTS", + f"Text Filter {filter_type}", + "ERROR", + f"Generation failed: {result}" + ) + except Exception as e: + self.log_result( + "TTS", + f"Text Filter {filter_type}", + "ERROR", + f"Test failed: {str(e)}" + ) + + async def _test_autoplay(self): + """Test autoplay functionality with different volumes""" + self.logger.info("Testing autoplay functionality") + + try: + test_voice = await self._select_test_voice() + if not test_voice: + return + + for test_case in self.volume_test_cases: + try: + volume = test_case["volume"] + self.logger.info(f"Testing autoplay with volume: {volume}") + + data = { + "text_input": "Testing autoplay functionality with different volume levels.", + "character_voice_gen": test_voice, + "language": "en", + "output_file_name": f"test_autoplay_{int(time.time())}", + "autoplay": True, + "autoplay_volume": volume + } + + async with aiohttp.ClientSession() as session: + try: + async with session.post( + f"{self.base_url}/api/tts-generate", + data=data, + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + if response.status == 200: + result = await response.json() + expected_success = test_case["should_succeed"] + + if result.get("status") == "generate-success" and expected_success: + self.log_result( + "TTS", + f"Autoplay Volume {volume}", + "PASS", + "Audio should be playing" + ) + elif result.get("status") != "generate-success" and not expected_success: + self.log_result( + "TTS", + f"Autoplay Volume {volume}", + "PASS", + "Correctly rejected invalid volume" + ) + else: + self.log_result( + "TTS", + f"Autoplay Volume {volume}", + "ERROR", + f"Unexpected result: {result}" + ) + except Exception as e: + self.log_result( + "TTS", + f"Autoplay Volume {volume}", + "ERROR", + f"Test failed: {str(e)}" + ) + + except asyncio.CancelledError: + self.logger.warning("Autoplay test cancelled - cleaning up") + raise + except Exception as e: + self.log_result( + "TTS", + f"Autoplay Volume {volume}", + "ERROR", + f"Test failed: {str(e)}" + ) + + except asyncio.CancelledError: + self.logger.warning("Autoplay tests cancelled") + raise + except Exception as e: + self.logger.error(f"Autoplay testing failed: {str(e)}") + + async def _test_error_cases(self): + """Test various error cases to ensure proper error handling""" + self.logger.info("Testing error cases") + + test_voice = await self._select_test_voice() + if not test_voice: + return + + for case in self.error_test_cases: + self.logger.info(f"Testing error case: {case['case']}") + + # Build base data with known good values + data = { + "text_input": "Test text", + "character_voice_gen": test_voice, + "language": "en", + "output_file_name": f"test_error_{int(time.time())}" + } + + # Modify data based on error case + if case['case'] == "invalid_voice": + data["character_voice_gen"] = case["voice"] + elif case['case'] == "bad_language": + data["language"] = case["language"] + elif case['case'] == "missing_text": + data["text_input"] = case["text"] + + async with aiohttp.ClientSession() as session: + try: + async with session.post( + f"{self.base_url}/api/tts-generate", + data=data, + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + response_data = await response.json() + + if response.status == case["expect_error"]: + # Check if error message is user-friendly + error_msg = response_data.get("error", "") + if error_msg and len(error_msg) < 200 and not any(x in error_msg.lower() for x in ["traceback", "exception", "stack"]): + self.log_result( + "Error Cases", + f"Error Test {case['case']}", + "PASS", + f"Received expected error: {error_msg}" + ) + else: + self.log_result( + "Error Cases", + f"Error Test {case['case']}", + "WARNING", + "Error received but message might not be user-friendly" + ) + else: + self.log_result( + "Error Cases", + f"Error Test {case['case']}", + "ERROR", + f"Expected status {case['expect_error']}, got {response.status}" + ) + except Exception as e: + self.log_result( + "Error Cases", + f"Error Test {case['case']}", + "ERROR", + f"Test failed: {str(e)}" + ) + + async def _test_additional_language(self): + """Test TTS generation with an additional language if supported""" + self.logger.info("Testing additional language support") + + # Check if multiple languages are supported + if not hasattr(self, 'current_settings') or not self.current_settings.get('languages_capable'): + self.log_result( + "TTS", + "Additional Language", + "SKIP", + "Multiple languages not supported by current engine" + ) + return + + test_voice = await self._select_test_voice() + if not test_voice: + return + + test_language = "es" # Spanish as test language + test_text = "Prueba de texto en español" + + data = { + "text_input": test_text, + "character_voice_gen": test_voice, + "language": test_language, + "output_file_name": f"test_lang_{int(time.time())}", + "output_file_timestamp": True + } + + async with aiohttp.ClientSession() as session: + try: + async with session.post( + f"{self.base_url}/api/tts-generate", + data=data, + timeout=aiohttp.ClientTimeout(total=self.config['timeout']) + ) as response: + if response.status == 200: + result = await response.json() + if result.get("status") == "generate-success": + self.log_result( + "TTS", + "Additional Language", + "PASS", + f"Generated {test_language} audio" + ) + else: + self.log_result( + "TTS", + "Additional Language", + "ERROR", + f"Generation failed: {result}" + ) + else: + self.log_result( + "TTS", + "Additional Language", + "ERROR", + f"Unexpected status code: {response.status}" + ) + except Exception as e: + self.log_result( + "TTS", + "Additional Language", + "ERROR", + f"Test failed: {str(e)}" + ) + + + async def run_all_tests(self): + """Run all available tests in sequence""" + self.logger.info("Starting complete test suite") + + # Network tests + await self.run_network_tests() + + # Core API tests + await self.run_core_api_tests() + + # TTS tests + await self.run_tts_tests() + + # Print summary + await self._print_test_summary() + +async def main(): + tester = AllTalkTester() + if await tester.setup(): + await tester.menu() + else: + print("Setup failed. Exiting.") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/tts_server.py b/tts_server.py index 5fefe03..7d0f3df 100644 --- a/tts_server.py +++ b/tts_server.py @@ -2,6 +2,7 @@ import os import sys import json import time +import inspect import argparse import librosa import logging @@ -29,7 +30,7 @@ import uuid import hashlib import numpy as np import soundfile as sf -from typing import Union, Dict, List, Optional +from typing import Union, Dict, List, Optional, Tuple from pydantic import BaseModel, ValidationError, Field, field_validator ######################################################################################## # START-UP # Silence RVC warning about torch.nn.utils.weight_norm even though not used # @@ -65,62 +66,117 @@ def after_config_load(): load_config() +# pylint: disable=all + +########################## +# Central print function # +########################## +def print_message(message, message_type="standard", component="TTS"): + """Centralized print function for AllTalk messages + Args: + message (str): The message to print + message_type (str): Type of message (standard/warning/error/debug_*/debug) + component (str): Component identifier (TTS/ENG/GEN/API/etc.) + """ + # ANSI color codes + BLUE = "\033[94m" + MAGENTA = "\033[95m" + YELLOW = "\033[93m" + RED = "\033[91m" + GREEN = "\033[92m" + RESET = "\033[0m" + + prefix = f"[{config.branding}{component}] " + + if message_type.startswith("debug_"): + debug_flag = getattr(config.debugging, message_type, False) + if not debug_flag: + return + + if message_type == "debug_func" and "Function entry:" in message: + message_parts = message.split("Function entry:", 1) + print(f"{prefix}{BLUE}Debug{RESET} {YELLOW}{message_type}{RESET} Function entry:{GREEN}{message_parts[1]}{RESET}") + else: + print(f"{prefix}{BLUE}Debug{RESET} {YELLOW}{message_type}{RESET} {message}") + + elif message_type == "debug": + print(f"{prefix}{BLUE}Debug{RESET} {message}") + + elif message_type == "warning": + print(f"{prefix}{YELLOW}Warning{RESET} {message}") + + elif message_type == "error": + print(f"{prefix}{RED}Error{RESET} {message}") + + else: + print(f"{prefix}{message}") + +def debug_func_entry(): + """Print debug message for function entry if debug_func is enabled""" + if config.debugging.debug_func: + current_func = inspect.currentframe().f_back.f_code.co_name + print_message(f"Function entry: {current_func}", "debug_func") + #################### # Check for FFMPEG # #################### def check_ffmpeg(this_dir): + """Verify FFmpeg availability in the system.""" + debug_func_entry() + message = "" + if sys.platform == "win32": - # Check for ffmpeg.exe in the specified directory on Windows ffmpeg_path = os.path.join(this_dir, "system", "win_ffmpeg", "ffmpeg.exe") if os.path.exists(ffmpeg_path): - return True + return True, "FFmpeg found in Windows directory" + message = "FFmpeg not found in Windows directory" else: - # Check for FFmpeg on Linux and macOS using the command line try: subprocess.run(['ffmpeg', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) - return True + return True, "FFmpeg found in system PATH" except (subprocess.CalledProcessError, FileNotFoundError): - pass - return False + message = "FFmpeg not found in system PATH" + + print_message(message, "warning") + return False, message # Check if FFmpeg is installed ffmpeg_installed = check_ffmpeg(this_dir) if not ffmpeg_installed: - print(f"[{config.branding}ENG] \033[92mTranscoding :\033[94m ffmpeg not found\033[0m") - print(f"[{config.branding}ENG] FFmpeg is not installed. Transcoding will be disabled.") - print(f"[{config.branding}ENG] Please install FFmpeg on your system.") + print_message("\033[92mTranscoding :\033[91m ffmpeg not found\033[0m", component="ENG") + print_message("FFmpeg is not installed. Transcoding will be disabled.", "warning", "ENG") + print_message("Please install FFmpeg on your system.", component="ENG") if sys.platform == "win32": - print(f"[{config.branding}ENG] \033[92mTranscoding :\033[94m ffmpeg not found\033[0m") - print(f"[{config.branding}ENG] Installation instructions for Windows:") - print(f"[{config.branding}ENG] Copy the 'ffmpeg.exe' file to '{os.path.join(this_dir, 'system', 'win_ffmpeg')}'") + print_message("\033[92mTranscoding :\033[91m ffmpeg not found\033[0m", component="ENG") + print_message("Installation instructions for Windows:", component="ENG") + print_message(f"Copy the 'ffmpeg.exe' file to '{os.path.join(this_dir, 'system', 'win_ffmpeg')}'", component="ENG") else: - print(f"[{config.branding}ENG] \033[92mTranscoding :\033[94m ffmpeg not found\033[0m") - print(f"[{config.branding}ENG] Installation instructions:") - print(f"[{config.branding}ENG] Linux (Debian-based systems): Run 'sudo apt-get install ffmpeg' in the terminal.") - print(f"[{config.branding}ENG] macOS: Run 'brew install ffmpeg' in the terminal (requires Homebrew).") - # You can choose to exit the script or continue without FFmpeg - # exit(1) + print_message("\033[92mTranscoding :\033[91m ffmpeg not found\033[0m", component="ENG") + print_message("Installation instructions:", component="ENG") + print_message("Linux (Debian-based systems): Run 'sudo apt-get install ffmpeg' in the terminal.", component="ENG") + print_message("macOS: Run 'brew install ffmpeg' in the terminal (requires Homebrew).", component="ENG") if ffmpeg_installed: from ffmpeg.asyncio import FFmpeg - print(f"[{config.branding}ENG] \033[92mTranscoding :\033[93m ffmpeg found\033[0m") + print_message("\033[92mTranscoding :\033[93m ffmpeg found\033[0m", component="ENG") ################################ # Check for portaudio on Linux # ################################ try: - import sounddevice as sd - sounddevice_installed=True + import sounddevice as sd + sounddevice_installed=True except OSError: - print(f"[{config.branding}Startup] \033[91mInfo\033[0m PortAudio library not found. If you wish to play TTS in standalone mode through the API suite") - print(f"[{config.branding}Startup] \033[91mInfo\033[0m please install PortAudio. This will not affect any other features or use of Alltalk.") - print(f"[{config.branding}Startup] \033[91mInfo\033[0m If you don't know what the API suite is, then this message is nothing to worry about.") + print_message("The PortAudio library is not installed. To enable audio playback for TTS in the terminal or console,", "warning") + print_message("please install PortAudio. This will not impact other features of Alltalk.", "warning") + print_message("You can still play audio through web browsers without PortAudio.", "warning") + print_message("Installing PortAudio is optional and not strictly required.") sounddevice_installed=False if sys.platform.startswith('linux'): - print(f"[{config.branding}Startup] \033[91mInfo\033[0m On Linux, you can use the following command to install PortAudio:") - print(f"[{config.branding}Startup] \033[91mInfo\033[0m sudo apt-get install portaudio19-dev") + print_message("On Linux, you can use the following command to install PortAudio:", "warning") + print_message("sudo apt-get install portaudio19-dev", "warning") ####################################################################### # Attempt to import the ModelLoader class for the selected TTS engine # @@ -138,12 +194,13 @@ else: ########################################## @asynccontextmanager async def startup_shutdown(no_actual_value_it_demanded_something_be_here): + """Initialize model engine and handle graceful shutdown. This is a context manager.""" + debug_func_entry() try: await model_engine.setup() except FileNotFoundError as e: - print(f"Error during setup: {e}. Continuing without the TTS model.") + print_message(f"Error during setup: {e}. Continuing without the TTS model.", "error") yield - # Shutdown logic ############################### # Setup FastAPI with Lifespan # @@ -164,22 +221,32 @@ app.add_middleware( model_currently_changing = False @app.route("/api/reload", methods=["POST"]) async def apifunction_reload(request: Request): + """Handle API request to change TTS model. Manages model change state and validates requested model.""" + debug_func_entry() + global model_currently_changing - if model_currently_changing == True: + if model_currently_changing: + print_message("Model change already in progress", "debug_api", "API") return Response(content=json.dumps({"status": "model-is currently changing"}), media_type="application/json") - model_currently_changing == True + + model_currently_changing = True # Fixed assignment operator requested_model = request.query_params.get("tts_method") + if requested_model not in model_engine.available_models: - model_currently_changing == False + model_currently_changing = False # Fixed assignment operator + print_message(f"Invalid TTS method requested: {requested_model}", "error", "API") return {"status": "error", "message": "Invalid TTS method specified"} - # Call the handle_tts_method_change method from the imported class + + print_message(f"Attempting to change model to: {requested_model}", "debug_api", "API") success = await model_engine.handle_tts_method_change(requested_model) + + model_currently_changing = False # Fixed assignment operator if success: model_engine.current_model_loaded = requested_model - model_currently_changing == False + print_message(f"Model successfully changed to: {requested_model}", "debug_api", "API") return Response(content=json.dumps({"status": "model-success"}), media_type="application/json") else: - model_currently_changing == False + print_message(f"Failed to change model to: {requested_model}", "error", "API") return Response(content=json.dumps({"status": "model-failure"}), media_type="application/json") #################################### @@ -188,41 +255,48 @@ async def apifunction_reload(request: Request): import asyncio uvicorn_server = None def restart_self(): - """Restart the current script.""" - #print(f"[{config.branding}ENG] Restarting subprocess...") + """Restart the current Python process.""" + debug_func_entry() + print_message("Restarting subprocess...", component="ENG") os.execv(sys.executable, ['python'] + sys.argv) async def handle_restart(): + """Handle graceful shutdown of uvicorn server before restart.""" + debug_func_entry() global uvicorn_server + if uvicorn_server: - print(f"[{config.branding}ENG] Stopping uvicorn server...") + print_message("Stopping uvicorn server...", component="ENG") uvicorn_server.should_exit = True uvicorn_server.force_exit = True - # Check if the server has stopped + while not uvicorn_server.is_stopped: - await asyncio.sleep(0.1) # Check every 100ms if server is stopped + await asyncio.sleep(0.1) + restart_self() @app.post("/api/enginereload") async def apifunction_reload(request: Request): + """Handle API request to change TTS engine. Validates engine, updates config, and triggers restart.""" + debug_func_entry() + requested_engine = request.query_params.get("engine") if not tts_engines_config.is_valid_engine(requested_engine): + print_message(f"Invalid engine requested: {requested_engine}", "error", "API") return {"status": "error", "message": "Invalid TTS engine specified"} - print(f"[{config.branding}ENG]") - print(f"[{config.branding}ENG] \033[94mChanging model loaded. Please wait.\033[00m") - print(f"[{config.branding}ENG]") - - # Update the tts_engines_data with the requested engine: + print_message("", component="ENG") + print_message("\033[94mChanging model loaded. Please wait.\033[00m", component="ENG") + print_message("", component="ENG") + try: tts_engines_config.change_engine(requested_engine).save() + print_message(f"Engine configuration updated to: {requested_engine}", "debug_api", "API") finally: - # Reload just to be sure the right config is used (e.g. in case of an exception): tts_engines_config.reload() - - # Start the restart process in the background + print_message("Configuration reloaded", "debug_api", "API") + asyncio.create_task(handle_restart()) - return Response(content=json.dumps({"status": "engine-success"}), media_type="application/json") @@ -232,17 +306,24 @@ async def apifunction_reload(request: Request): # When this endpoint it called it will set tts_stop_generation in the model_engine to True, which can be used to interrupt the current TTS generation. @app.put("/api/stop-generation") async def apifunction_stop_generation(): + """Handle request to stop ongoing TTS generation.""" + debug_func_entry() if model_engine.tts_generating_lock and not model_engine.tts_stop_generation: model_engine.tts_stop_generation = True + print_message("Stopping TTS generation", "debug_api", "API") return {"message": "Cancelling current TTS generation"} ############################# # API Endpoint - /api/audio # ############################# -# Gives web access to the output files as downloads @app.get("/audio/{filename}") async def apifunction_get_audio(filename: str): + """Serve audio file from output directory.""" + debug_func_entry() audio_path = this_dir / config.get_output_directory() / filename + if not audio_path.is_file(): + print_message(f"Audio file not found: {filename}", "error", "API") + raise HTTPException(status_code=404, detail="File not found") return FileResponse(audio_path) ################################## @@ -250,13 +331,16 @@ async def apifunction_get_audio(filename: str): ################################## @app.get("/audiocache/{filename}") async def apifunction_get_audio(filename: str): - audio_path = Path({config.get_output_directory()}) / filename + """Serve cached audio file with caching headers.""" + debug_func_entry() + audio_path = Path(config.get_output_directory()) / filename if not audio_path.is_file(): + print_message(f"Cached audio file not found: {filename}", "error", "API") raise HTTPException(status_code=404, detail="File not found") + response = FileResponse(path=audio_path, media_type='audio/wav', filename=filename) - # Set caching headers - response.headers["Cache-Control"] = "public, max-age=604800" # Cache for one week - response.headers["ETag"] = str(audio_path.stat().st_mtime) # Use the file's last modified time as a simple ETag + response.headers["Cache-Control"] = "public, max-age=604800" + response.headers["ETag"] = str(audio_path.stat().st_mtime) return response ############################## @@ -264,121 +348,168 @@ async def apifunction_get_audio(filename: str): ############################## @app.get("/api/voices") async def apifunction_get_voices(): - if not model_engine.multivoice_capable: - return {"status": "error", "message": f"The currently loaded TTS engine '{model_engine.engine_loaded}' does not support multiple voices."} + """Get available voices for current TTS engine.""" + debug_func_entry() - available_voices = model_engine.voices_file_list() - return {"status": "success", "voices": available_voices} + try: + if not model_engine.multivoice_capable: + print_message(f"Engine '{model_engine.engine_loaded}' does not support multiple voices", "warning", "API") + return {"status": "error", "message": f"The currently loaded TTS engine '{model_engine.engine_loaded}' does not support multiple voices."} + + available_voices = model_engine.voices_file_list() + print_message(f"Successfully retrieved {len(available_voices)} available voices", "debug_api", "API") + return {"status": "success", "voices": available_voices} + except Exception as e: + print_message(f"Error retrieving voices: {str(e)}", "error", "API") + return JSONResponse(content={"status": "error", "message": str(e)}, status_code=500) ################################# # API Endpoint - /api/rvcvoices # ################################# @app.get("/api/rvcvoices") -async def apifunction_get_voices(): - load_config() - if not config.rvc_settings.rvc_enabled: - return {"status": "success", "rvcvoices": ["Disabled"]} - # Define the directory path - directory = os.path.join(this_dir, "models", "rvc_voices") - # Check if the directory exists - if not os.path.exists(directory): - return {"status": "success", "rvcvoices": ["Disabled"]} - # List all .pth files recursively in the directory - pth_files = [] - for root, dirs, files in os.walk(directory): - for file in files: - if file.endswith(".pth"): - pth_files.append(os.path.relpath(os.path.join(root, file), start=directory)) - # If no .pth files found, return "Disabled" - if not pth_files: - return {"status": "success", "rvcvoices": ["Disabled"]} - # Sort the list alphabetically - pth_files.sort() - # Add "Disabled" at the beginning of the list - pth_files.insert(0, "Disabled") - return {"status": "success", "rvcvoices": pth_files} +async def apifunction_get_rvcvoices(): + """Get available RVC voice models.""" + debug_func_entry() + + try: + load_config() + if not config.rvc_settings.rvc_enabled: + return {"status": "success", "rvcvoices": ["Disabled"]} + + directory = os.path.join(this_dir, "models", "rvc_voices") + if not os.path.exists(directory): + print_message("RVC voices directory not found", "warning", "API") + return {"status": "success", "rvcvoices": ["Disabled"]} + + if not os.access(directory, os.R_OK): + print_message("No read permission for RVC voices directory", "error", "API") + return JSONResponse(content={"status": "error", "message": "Cannot access RVC voices directory"}, status_code=500) + + pth_files = [] + try: + for root, dirs, files in os.walk(directory): + for file in files: + if file.endswith(".pth"): + pth_files.append(os.path.relpath(os.path.join(root, file), start=directory)) + except OSError as e: + print_message(f"Error scanning RVC directory: {str(e)}", "error", "API") + return JSONResponse(content={"status": "error", "message": str(e)}, status_code=500) + + pth_files = ["Disabled"] + sorted(pth_files) if pth_files else ["Disabled"] + return {"status": "success", "rvcvoices": pth_files} + + except Exception as e: + print_message(f"Error processing RVC voices: {str(e)}", "error", "API") + return JSONResponse(content={"status": "error", "message": str(e)}, status_code=500) ##################################### # API Endpoint - /api/reload_config # ##################################### @app.get("/api/reload_config") async def apifunction_reload_config(): - load_config(True) - return Response("Config file reloaded successfully") + """Reload configuration settings.""" + debug_func_entry() + + try: + load_config(True) + print_message("Configuration reloaded successfully", "debug_api", "API") + return Response("Config file reloaded successfully") + except Exception as e: + print_message(f"Error reloading config: {str(e)}", "error", "API") + return JSONResponse(content={"status": "error", "message": str(e)}, status_code=500) ############################# # API Endpoint - /api/ready # ############################# @app.get("/api/ready") async def apifunction_ready(): - if model_engine.setup_has_run: - return Response("Ready") - else: - return Response("Unloaded") + """Check if model engine setup has completed.""" + debug_func_entry() + status = "Ready" if model_engine.setup_has_run else "Unloaded" + print_message(f"Engine status: {status}", "debug_api", "API") + return Response(status) ####################################### # API Endpoint - /api/currentsettings # ####################################### @app.get('/api/currentsettings') def apifunction_get_current_settings(): - settings = { - "engines_available": tts_engines_config.get_engine_names_available(), # Lists the currently available TTS engines - "current_engine_loaded": model_engine.engine_loaded, # Lists the currently loaded TTS engine - "models_available": [{"name": name} for name in model_engine.available_models.keys()], # Lists the currently available models that can be loaded on this TTS engine. - "current_model_loaded": model_engine.current_model_loaded, # Lists the currently loaded model - "manufacturer_name": model_engine.manufacturer_name, # The name of the person/company/group who made the current TTS engine - "audio_format": model_engine.audio_format, # The default audio format the the currently loaded TTS engine generates in - "deepspeed_capable": model_engine.deepspeed_capable, # Is the TTS engine capable of supporting DeepSpeed - "deepspeed_available": model_engine.deepspeed_available, # Was DeepSpeed detected as being available for use - "deepspeed_enabled": model_engine.deepspeed_enabled, # Is DeepSpeed current enabled or disabled - "generationspeed_capable": model_engine.generationspeed_capable, # Does the TTS engine support changing speed of generated TTS - "generationspeed_set": model_engine.generationspeed_set, # What is the currently set TTS speed - "lowvram_capable": model_engine.lowvram_capable, # Does the TTS engine support moving the model between VRAM and System Ram - "lowvram_enabled": model_engine.lowvram_enabled, # What is the currently enabled/disabled state of low vram - "pitch_capable": model_engine.pitch_capable, # Does the TTS engine support pitch on tts generation - "pitch_set": model_engine.pitch_set, # What is the currently set value for pitch - "repetitionpenalty_capable": model_engine.repetitionpenalty_capable, # Does the TTS engine support differnt repitition penalties - "repetitionpenalty_set": model_engine.repetitionpenalty_set, # What is the currently set repetition penalty - "streaming_capable": model_engine.streaming_capable, # Does the model support streaming TTS generation - "temperature_capable": model_engine.temperature_capable, # Does the TTS engine support model temperature settings - "temperature_set": model_engine.temperature_set, # What is the currently set temperature - "ttsengines_installed": model_engine.engine_installed, # - "languages_capable": model_engine.languages_capable, # Does the TTS engine support multi languages - "multivoice_capable": model_engine.multivoice_capable, # Does the TTS engine support multi voice models - "multimodel_capable": model_engine.multimodel_capable, # Does the engine support loading many different models - } - return settings + """Get comprehensive dictionary of current engine settings and capabilities.""" + debug_func_entry() + load_config() + try: + settings = { + "engines_available": tts_engines_config.get_engine_names_available(), + "current_engine_loaded": model_engine.engine_loaded, + "models_available": [{"name": name} for name in model_engine.available_models.keys()], + "current_model_loaded": model_engine.current_model_loaded, + "manufacturer_name": model_engine.manufacturer_name, + "audio_format": model_engine.audio_format, + "deepspeed_capable": model_engine.deepspeed_capable, + "deepspeed_available": model_engine.deepspeed_available, + "deepspeed_enabled": model_engine.deepspeed_enabled, + "generationspeed_capable": model_engine.generationspeed_capable, + "generationspeed_set": model_engine.generationspeed_set, + "lowvram_capable": model_engine.lowvram_capable, + "lowvram_enabled": model_engine.lowvram_enabled, + "pitch_capable": model_engine.pitch_capable, + "pitch_set": model_engine.pitch_set, + "repetitionpenalty_capable": model_engine.repetitionpenalty_capable, + "repetitionpenalty_set": model_engine.repetitionpenalty_set, + "streaming_capable": model_engine.streaming_capable, + "temperature_capable": model_engine.temperature_capable, + "temperature_set": model_engine.temperature_set, + "ttsengines_installed": model_engine.engine_installed, + "languages_capable": model_engine.languages_capable, + "multivoice_capable": model_engine.multivoice_capable, + "multimodel_capable": model_engine.multimodel_capable, + } + print_message("Current settings retrieved successfully", "debug_api", "API") + return settings + except Exception as e: + print_message(f"Error retrieving settings: {str(e)}", "error", "API") + return JSONResponse(content={"status": "error", "message": str(e)}, status_code=500) ###################################### # API Endpoint - /api/lowvramsetting # ###################################### @app.post("/api/lowvramsetting") async def apifunction_low_vram(request: Request, new_low_vram_value: bool): + """Handle LowVRAM mode toggle. Updates model device allocation between VRAM and system RAM.""" + debug_func_entry() + if not model_engine.lowvram_capable: + print_message(f"Engine '{model_engine.engine_loaded}' does not support lowvram", "error", "API") return {"status": "error", "message": f"The currently loaded TTS engine '{model_engine.engine_loaded}' does not support lowvram."} + try: if new_low_vram_value is None: raise ValueError("Missing 'low_vram' parameter") + if model_engine.lowvram_enabled == new_low_vram_value: - return Response(content=json.dumps({"status": "success", "message": f"[{config.branding}Model] LowVRAM is already {'enabled' if new_low_vram_value else 'disabled'}.",})) + msg = f"LowVRAM is already {'enabled' if new_low_vram_value else 'disabled'}" + print_message(msg, "debug_api", "API") + return Response(content=json.dumps({"status": "success", "message": msg})) + model_engine.lowvram_enabled = new_low_vram_value await model_engine.unload_model() if model_engine.cuda_is_available: if model_engine.lowvram_enabled: model_engine.device = "cpu" - print(f"[{config.branding}Engine] \033[94mLowVRAM Enabled.\033[0m Model will move between \033[93mVRAM(cuda) <> System RAM(cpu)\033[0m") + print_message("\033[94mLowVRAM Enabled.\033[0m Model will move between \033[93mVRAM(cuda) <> System RAM(cpu)\033[0m", component="ENG") else: model_engine.device = "cuda" - print(f"[{config.branding}Engine] \033[94mLowVRAM Disabled.\033[0m Model will stay in \033[93mVRAM(cuda)\033[0m") + print_message("\033[94mLowVRAM Disabled.\033[0m Model will stay in \033[93mVRAM(cuda)\033[0m", component="ENG") await model_engine.setup() else: - # Handle the case where CUDA is not available - print(f"[{config.branding}Engine] \033[91mError:\033[0m Nvidia CUDA is not available on this system. Unable to use LowVRAM mode.") + print_message("Nvidia CUDA is not available on this system. Unable to use LowVRAM mode.", "error", "ENG") model_engine.lowvram_enabled = False return Response(content=json.dumps({"status": "lowvram-success"})) + except Exception as e: + print_message(f"Error setting LowVRAM mode: {str(e)}", "error", "API") return Response(content=json.dumps({"status": "error", "message": str(e)})) ################################# @@ -386,54 +517,79 @@ async def apifunction_low_vram(request: Request, new_low_vram_value: bool): ################################# @app.post("/api/deepspeed") async def deepspeed(request: Request, new_deepspeed_value: bool): + """Handle DeepSpeed mode toggle. Verifies capability and handles model changes.""" + debug_func_entry() + if not model_engine.deepspeed_capable or not model_engine.deepspeed_available: + print_message("DeepSpeed not supported or available", "error", "API") return {"status": "error", "message": f"The currently loaded TTS engine '{model_engine.engine_loaded}' does not support DeepSpeed or DeepSpeed is not available on this system."} + try: if new_deepspeed_value is None: raise ValueError("Missing 'deepspeed' parameter") + if model_engine.deepspeed_enabled == new_deepspeed_value: - return Response( - content=json.dumps({"status": "success", "message": f"DeepSpeed is already {'enabled' if new_deepspeed_value else 'disabled'}.",})) + msg = f"DeepSpeed is already {'enabled' if new_deepspeed_value else 'disabled'}" + print_message(msg, "debug_api", "API") + return Response(content=json.dumps({"status": "success", "message": msg})) + model_engine.deepspeed_enabled = new_deepspeed_value await model_engine.handle_deepspeed_change(new_deepspeed_value) + print_message(f"DeepSpeed {'enabled' if new_deepspeed_value else 'disabled'}", "debug_api", "API") return Response(content=json.dumps({"status": "deepspeed-success"})) + except Exception as e: + print_message(f"Error setting DeepSpeed mode: {str(e)}", "error", "API") return Response(content=json.dumps({"status": "error", "message": str(e)})) ################################# # API Endpoint - /api/voice2rvc # ################################# @app.post("/api/voice2rvc") -async def voice2rvc(input_tts_path: str = Form(...), output_rvc_path: str = Form(...), pth_name: str = Form(...), pitch: str = Form(...), method: str = Form(...)): +async def voice2rvc(input_tts_path: str = Form(...), output_rvc_path: str = Form(...), + pth_name: str = Form(...), pitch: str = Form(...), method: str = Form(...)): + """Handle voice conversion using RVC. Processes input audio through specified RVC model.""" + debug_func_entry() + try: - # Handle "Disabled" case if pth_name.lower() in ["disabled", "disable"]: - print(f"[{config.branding}TTS] \033[94mVoice2RVC Convert: No voice was specified or the name was Disabled\033[0m") + print_message("\033[94mVoice2RVC Convert: No voice was specified or the name was Disabled\033[0m") return {"status": "error", "message": "No voice was specified or the name was Disabled"} + input_tts_path = Path(input_tts_path) - output_rvc_path = Path(output_rvc_path) - # Check if input file exists + output_rvc_path = Path(output_rvc_path) + if not input_tts_path.is_file(): + print_message(f"Input file not found: {input_tts_path}", "error") raise HTTPException(status_code=400, detail=f"Input file {input_tts_path} does not exist.") - # Define the model path based on the selected model name + pth_path = this_dir / "models" / "rvc_voices" / pth_name - # Check if model file exists if not pth_path.is_file(): + print_message(f"RVC model file not found: {pth_path}", "error") raise HTTPException(status_code=400, detail=f"Model file {pth_path} does not exist.") - # Run the RVC conversion + + print_message(f"Starting RVC conversion with model: {pth_name}", "debug_rvc") result_path = run_voice2rvc(input_tts_path, output_rvc_path, pth_path, pitch, method) + if result_path: + print_message("RVC conversion completed successfully", "debug_rvc") return {"status": "success", "output_path": str(result_path)} else: + print_message("RVC conversion failed", "error") raise HTTPException(status_code=500, detail="RVC conversion failed.") + except Exception as e: - print(f"Error during Voice2RVC conversion: {e}") + print_message(f"Error during Voice2RVC conversion: {e}", "error") raise HTTPException(status_code=500, detail=str(e)) -# Define the run_rvc function def run_voice2rvc(input_tts_path, output_rvc_path, pth_path, pitch, method): - print(f"[{config.branding}GEN] \033[94mVoice2RVC Convert: Started\033[0m") + """Run RVC conversion on input audio using specified model and parameters.""" + debug_func_entry() + + print_message("\033[94mVoice2RVC Convert: Started\033[0m", component="GEN") generate_start_time = time.time() + + # Get settings from config f0up_key = pitch filter_radius = config.rvc_settings.filter_radius index_rate = config.rvc_settings.index_rate @@ -445,119 +601,138 @@ def run_voice2rvc(input_tts_path, output_rvc_path, pth_path, pitch, method): f0autotune = config.rvc_settings.autotune embedder_model = config.rvc_settings.embedder_model training_data_size = config.rvc_settings.training_data_size - # Convert path variables to strings input_tts_path = str(input_tts_path) pth_path = str(pth_path) - # Check if the model file exists + if not os.path.isfile(pth_path): - print(f"Model file {pth_path} does not exist. Exiting.") + print_message(f"Model file {pth_path} does not exist. Exiting.", "error", "GEN") return - # Get the directory of the model file + model_dir = os.path.dirname(pth_path) - # Find all .index files in the model directory index_files = [file for file in os.listdir(model_dir) if file.endswith(".index")] - if len(index_files) == 1: - index_path = str(os.path.join(model_dir, index_files[0])) - else: - index_path = "" - # Call the infer_pipeline function + index_path = str(os.path.join(model_dir, index_files[0])) if len(index_files) == 1 else "" + + print_message(f"Using RVC model: {os.path.basename(pth_path)}", "debug_rvc", "GEN") + print_message(f"Using index file: {os.path.basename(index_path) if index_path else 'None'}", "debug_rvc", "GEN") + infer_pipeline(f0up_key, filter_radius, index_rate, rms_mix_rate, protect, hop_length, f0method, - input_tts_path, output_rvc_path, pth_path, index_path, split_audio, f0autotune, embedder_model, training_data_size, config.debugging.debug_rvc) - generate_end_time = time.time() - generate_elapsed_time = generate_end_time - generate_start_time - print(f"[{config.branding}GEN] \033[94mVoice2RVC Convert: \033[91m{generate_elapsed_time:.2f} seconds.\033[0m") + input_tts_path, output_rvc_path, pth_path, index_path, split_audio, f0autotune, + embedder_model, training_data_size, config.debugging.debug_rvc) + + generate_elapsed_time = time.time() - generate_start_time + print_message(f"\033[94mVoice2RVC Convert: \033[91m{generate_elapsed_time:.2f} seconds.\033[0m", component="GEN") + return output_rvc_path ################################## # Transcode between file formats # ################################## async def transcode_audio(input_file, output_format): - print(f"[{config.branding}Debug] *************************************************",) if config.debugging.debug_transcode else None - print(f"[{config.branding}Debug] transcode_audio function called (debug_transcode)",) if config.debugging.debug_transcode else None - print(f"[{config.branding}Debug] *************************************************",) if config.debugging.debug_transcode else None - print(f"[{config.branding}Debug] Input file : {input_file}") if config.debugging.debug_transcode else None - print(f"[{config.branding}Debug] Output format : {output_format}") if config.debugging.debug_transcode else None + """Transcode audio files between different formats using FFmpeg. Supports wav, mp3, flac, aac, and opus formats.""" + debug_func_entry() + + print_message("*************************************************", "debug_transcode") + print_message("transcode_audio function called (debug_transcode)", "debug_transcode") + print_message("*************************************************", "debug_transcode") + print_message(f"Input file : {input_file}", "debug_transcode") + print_message(f"Output format : {output_format}", "debug_transcode") + if output_format == "Disabled": - print(f"[{config.branding}Debug] Transcode format is set to Disabled so skipping transcode.") if config.debugging.debug_transcode else None + print_message("Transcode format is set to Disabled so skipping transcode.", "debug_transcode") return input_file + if not ffmpeg_installed: - print(f"[{config.branding}TTS] FFmpeg is not installed. Format conversion is not possible.") + print_message("FFmpeg is not installed. Format conversion is not possible.", "error") raise Exception("FFmpeg is not installed. Format conversion is not possible.") - # Get the file extension of the input file + input_extension = os.path.splitext(input_file)[1][1:].lower() - print(f"[{config.branding}Debug] Input file extension: {input_extension}") if config.debugging.debug_transcode else None - # Check if the input extension matches the requested output format + print_message(f"Input file extension: {input_extension}", "debug_transcode") + if input_extension == output_format.lower(): - print(f"[{config.branding}Debug] Input file is already in the requested format: {output_format}") if config.debugging.debug_transcode else None + print_message(f"Input file is already in the requested format: {output_format}", "debug_transcode") return input_file + output_file = os.path.splitext(input_file)[0] + f".{output_format}" - print(f"[{config.branding}Debug] Output file: {output_file}") if config.debugging.debug_transcode else None - ffmpeg_path = "ffmpeg" # Default path for Linux and macOS - if sys.platform == "win32": - ffmpeg_path = os.path.join(this_dir, "system", "win_ffmpeg", "ffmpeg.exe") - ffmpeg = (FFmpeg(ffmpeg_path).option("y").input(input_file).output(output_file)) - print(f"[{config.branding}Debug] Transcoding to {output_format}") if config.debugging.debug_transcode else None + print_message(f"Output file: {output_file}", "debug_transcode") + + ffmpeg_path = os.path.join(this_dir, "system", "win_ffmpeg", "ffmpeg.exe") if sys.platform == "win32" else "ffmpeg" + ffmpeg = FFmpeg(ffmpeg_path).option("y").input(input_file).output(output_file) + + print_message(f"Transcoding to {output_format}", "debug_transcode") + + # Configure format-specific options if output_format == "opus": - print(f"[{config.branding}Debug] Configuring Opus options") if config.debugging.debug_transcode else None - ffmpeg.output(output_file, {"codec:a": "libopus", "b:a": "128k", "vbr": "on", "compression_level": 10, "frame_duration": 60, "application": "voip"}) + print_message("Configuring Opus options", "debug_transcode") + ffmpeg.output(output_file, { + "codec:a": "libopus", + "b:a": "128k", + "vbr": "on", + "compression_level": 10, + "frame_duration": 60, + "application": "voip" + }) elif output_format == "aac": - print(f"[{config.branding}Debug] Configuring AAC options") if config.debugging.debug_transcode else None + print_message("Configuring AAC options", "debug_transcode") ffmpeg.output(output_file, {"codec:a": "aac", "b:a": "192k"}) elif output_format == "flac": - print(f"[{config.branding}Debug] Configuring FLAC options") if config.debugging.debug_transcode else None + print_message("Configuring FLAC options", "debug_transcode") ffmpeg.output(output_file, {"codec:a": "flac", "compression_level": 8}) elif output_format == "wav": - print(f"[{config.branding}Debug] Configuring WAV options") if config.debugging.debug_transcode else None + print_message("Configuring WAV options", "debug_transcode") ffmpeg.output(output_file, {"codec:a": "pcm_s16le"}) elif output_format == "mp3": - print(f"[{config.branding}Debug] Configuring MP3 options") if config.debugging.debug_transcode else None + print_message("Configuring MP3 options", "debug_transcode") ffmpeg.output(output_file, {"codec:a": "libmp3lame", "b:a": "192k"}) else: - print(f"[{config.branding}TTS] Unsupported output format: {output_format}") + print_message(f"Unsupported output format: {output_format}", "error") raise ValueError(f"Unsupported output format: {output_format}") + try: - print(f"[{config.branding}Debug] Starting transcoding process") if config.debugging.debug_transcode else None + print_message("Starting transcoding process", "debug_transcode") await ffmpeg.execute() - print(f"[{config.branding}Debug] Transcoding completed successfully") if config.debugging.debug_transcode else None + print_message("Transcoding completed successfully", "debug_transcode") except Exception as e: - print(f"[{config.branding}TTS] Error occurred during transcoding: {str(e)}") + print_message(f"Error occurred during transcoding: {str(e)}", "error") raise - print(f"[{config.branding}Debug] Deleting original input file") if config.debugging.debug_transcode else None + + print_message("Deleting original input file", "debug_transcode") os.remove(input_file) - print(f"[{config.branding}Debug] Transcoding process completed") if config.debugging.debug_transcode else None - print(f"[{config.branding}Debug] Transcoded file: {output_file}") if config.debugging.debug_transcode else None + + print_message("Transcoding process completed", "debug_transcode") + print_message(f"Transcoded file: {output_file}", "debug_transcode") return output_file ############################## # Central Transcode function # ############################## async def transcode_audio_if_necessary(output_file, model_audio_format, output_audio_format): - if config.debugging.debug_transcode: - print(f"[{config.branding}Debug] model_engine.audio_format is:", model_audio_format) - print(f"[{config.branding}Debug] audio format is:", output_audio_format) - print(f"[{config.branding}Debug] Entering the transcode condition") + """Transcode audio to requested format if needed and generate appropriate URLs.""" + debug_func_entry() + + print_message(f"model_engine.audio_format is: {model_audio_format}", "debug_transcode") + print_message(f"audio format is: {output_audio_format}", "debug_transcode") + print_message("Entering the transcode condition", "debug_transcode") + try: - if config.debugging.debug_transcode: - print(f"[{config.branding}Debug] Calling transcode_audio function") + print_message("Calling transcode_audio function", "debug_transcode") output_file = await transcode_audio(output_file, output_audio_format) - if config.debugging.debug_transcode: - print(f"[{config.branding}Debug] Transcode completed successfully") + print_message("Transcode completed successfully", "debug_transcode") except Exception as e: - print(f"[{config.branding}Debug] Error occurred during transcoding:", str(e)) + print_message(f"Error occurred during transcoding: {str(e)}", "error") raise - if config.debugging.debug_transcode: - print(f"[{config.branding}Debug] Transcode condition completed") - # Update the output file path and URLs based on the transcoded file - if config.debugging.debug_tts: - print(f"[{config.branding}Debug] Updating output file paths and URLs") + + print_message("Transcode condition completed", "debug_transcode") + print_message("Updating output file paths and URLs", "debug_tts") + + # Generate appropriate URLs based on API configuration if config.api_def.api_use_legacy_api: output_file_url = f'http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}/audio/{os.path.basename(output_file)}' output_cache_url = f'http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}/audiocache/{os.path.basename(output_file)}' else: output_file_url = f'/audio/{os.path.basename(output_file)}' output_cache_url = f'/audiocache/{os.path.basename(output_file)}' - if config.debugging.debug_tts: - print(f"[{config.branding}Debug] Output file paths and URLs updated") + + print_message("Output file paths and URLs updated", "debug_tts") return output_file, output_file_url, output_cache_url ############################## @@ -565,56 +740,77 @@ async def transcode_audio_if_necessary(output_file, model_audio_format, output_a ############################## @app.get("/api/tts-generate-streaming", response_class=StreamingResponse) async def apifunction_generate_streaming(text: str, voice: str, language: str, output_file: str): - if model_engine.streaming_capable == False: - print(f"[{config.branding}GEN] The selected TTS Engine does not support streaming. To use streaming, please select a TTS") - print(f"[{config.branding}GEN] Engine that has streaming capability. You can find the streaming support information for") - print(f"[{config.branding}GEN] each TTS Engine in the 'Engine Information' section of the Gradio interface.") + """Handle streaming TTS generation via GET request.""" + debug_func_entry() + + if not model_engine.streaming_capable: + print_message("The selected TTS Engine does not support streaming. To use streaming, please select a TTS", "warning", "GEN") + print_message("Engine that has streaming capability. You can find the streaming support information for", "warning", "GEN") + print_message("each TTS Engine in its 'Engine Information' section of the Gradio interface.", "warning", "GEN") + try: output_file_path = f'{this_dir / config.get_output_directory() / output_file}.{model_engine.audio_format}' - stream = await generate_audio(text, voice, language, model_engine.temperature_set, model_engine.repetitionpenalty_set, 1.0, 1.0, output_file_path, streaming=True) + print_message(f"Starting streaming TTS generation for: {text[:50]}{'...' if len(text) > 50 else ''}") + stream = await generate_audio(text, voice, language, model_engine.temperature_set, + model_engine.repetitionpenalty_set, 1.0, 1.0, + output_file_path, streaming=True) return StreamingResponse(stream, media_type="audio/wav") except Exception as e: - print(f"An error occurred: {e}") + print_message(f"An error occurred: {e}", "error", "GEN") return JSONResponse(content={"error": "An error occurred"}, status_code=500) @app.post("/api/tts-generate-streaming", response_class=JSONResponse) -async def tts_generate_streaming(request: Request, text: str = Form(...), voice: str = Form(...), language: str = Form(...), output_file: str = Form(...)): - if model_engine.streaming_capable == False: - print(f"[{config.branding}GEN] The selected TTS Engine does not support streaming. To use streaming, please select a TTS") - print(f"[{config.branding}GEN] Engine that has streaming capability. You can find the streaming support information for") - print(f"[{config.branding}GEN] each TTS Engine in the 'Engine Information' section of the Gradio interface.") +async def tts_generate_streaming(request: Request, text: str = Form(...), voice: str = Form(...), + language: str = Form(...), output_file: str = Form(...)): + """Handle streaming TTS generation via POST request.""" + debug_func_entry() + + if not model_engine.streaming_capable: + print_message("The selected TTS Engine does not support streaming. To use streaming, please select a TTS", "warning", "GEN") + print_message("Engine that has streaming capability. You can find the streaming support information for", "warning", "GEN") + print_message("each TTS Engine in its 'Engine Information' section of the Gradio interface.", "warning", "GEN") + try: output_file_path = f'{this_dir / config.get_output_directory() / output_file}.{model_engine.audio_format}' - await generate_audio(text, voice, language, model_engine.temperature_set, model_engine.repetitionpenalty_set, "1.0", "1.0", output_file_path, streaming=False) + print_message(f"Starting TTS generation for file: {os.path.basename(output_file)}") + await generate_audio(text, voice, language, model_engine.temperature_set, + model_engine.repetitionpenalty_set, "1.0", "1.0", + output_file_path, streaming=False) return JSONResponse(content={"output_file_path": str(output_file)}, status_code=200) except Exception as e: - print(f"An error occurred: {e}") + print_message(f"An error occurred: {e}", "error", "GEN") return JSONResponse(content={"error": "An error occurred"}, status_code=500) ################################### # Central generate_audio function # ################################### async def generate_audio(text, voice, language, temperature, repetition_penalty, speed, pitch, output_file, streaming=False): - if model_engine.streaming_capable == False and streaming==True: - print(f"[{config.branding}GEN] The selected TTS Engine does not support streaming. To use streaming, please select a TTS") - print(f"[{config.branding}GEN] Engine that has streaming capability. You can find the streaming support information for") - print(f"[{config.branding}GEN] each TTS Engine in the 'Engine Information' section of the Gradio interface.") - # Get the async generator from the internal function + """Generate TTS audio with specified parameters. Supports streaming and non-streaming modes.""" + debug_func_entry() + + if not model_engine.streaming_capable and streaming: + print_message("The selected TTS Engine does not support streaming. To use streaming, please select a TTS", "warning", "GEN") + print_message("Engine that has streaming capability. You can find the streaming support information for", "warning", "GEN") + print_message("each TTS Engine in the 'Engine Information' section of the Gradio interface.", "warning", "GEN") + raise ValueError("Streaming not supported by current TTS engine") + response = model_engine.generate_tts(text, voice, language, temperature, repetition_penalty, speed, pitch, output_file, streaming) - # If streaming, then return the generator as-is, otherwise just exhaust it and return + if streaming: - # Return an async generator that yields from the response async def stream_response(): - async for chunk in response: - yield chunk + try: + async for chunk in response: + yield chunk + except Exception as e: + print_message(f"Error during streaming audio generation: {str(e)}", "error", "GEN") + raise return stream_response() else: - # Exhaust the generator and handle any errors try: async for _ in response: pass except Exception as e: - print(f"{config.branding}[GEN] Error during audio generation: {str(e)}") + print_message(f"Error during audio generation: {str(e)}", "error", "GEN") raise ########################### @@ -627,43 +823,45 @@ async def apifunction_preview_voice( rvccharacter_voice_gen: Optional[str] = Form(None), rvccharacter_pitch: Optional[float] = Form(None) ): + """Generate preview audio for selected voice with optional RVC processing.""" + debug_func_entry() + try: - # Hardcoded settings language = "en" - output_file_name = "api_preview_voice" - # Clean the voice filename for inclusion in the text + output_file_name = "api_preview_voice" + clean_voice_filename = re.sub(r'\.wav$', '', voice.replace(' ', '_')) clean_voice_filename = re.sub(r'[^a-zA-Z0-9]', ' ', clean_voice_filename) - # Generate the audio text text = f"Hello, this is a preview of voice {clean_voice_filename}." - # Set default values for new parameters if not provided + rvccharacter_voice_gen = rvccharacter_voice_gen or "Disabled" rvccharacter_pitch = rvccharacter_pitch if rvccharacter_pitch is not None else 0 - # Generate the audio + + print_message(f"Generating preview for voice: {clean_voice_filename}", "debug_tts", "GEN") output_file_path = this_dir / config.get_output_directory() / f'{output_file_name}.{model_engine.audio_format}' + await generate_audio( - text, - voice, - language, + text, voice, language, model_engine.temperature_set, model_engine.repetitionpenalty_set, - 1.0, - 0, - output_file_path, + 1.0, 0, output_file_path, streaming=False - ) + ) + if config.rvc_settings.rvc_enabled: if rvccharacter_voice_gen.lower() in ["disabled", "disable"]: - print(f"[{config.branding}Debug] PREVIEW VOICE - Pass rvccharacter_voice_gen") if config.debugging.debug_tts else None - pass # Skip RVC processing for character part + print_message("def apifunction_preview_voice RVC processing skipped", "debug_tts") else: - print(f"[{config.branding}Debug] PREVIEW VOICE - send to rvc") if config.debugging.debug_tts else None + print_message(f"def apifunction_preview_voice processing with RVC: {rvccharacter_voice_gen}", "debug_tts") rvccharacter_voice_gen = this_dir / "models" / "rvc_voices" / rvccharacter_voice_gen pth_path = rvccharacter_voice_gen if rvccharacter_voice_gen else config.rvc_settings.rvc_char_model_file - run_rvc(output_file_path, pth_path, rvccharacter_pitch, infer_pipeline) - # Generate the URL + + if not os.path.exists(pth_path): + raise FileNotFoundError(f"RVC model not found: {pth_path}") + + run_rvc(output_file_path, pth_path, rvccharacter_pitch, infer_pipeline) + output_file_url = f'/audio/{output_file_name}.{model_engine.audio_format}' - # Return the response with both local file path and URL return JSONResponse( content={ "status": "generate-success", @@ -672,14 +870,19 @@ async def apifunction_preview_voice( }, status_code=200, ) + + except FileNotFoundError as e: + print_message(str(e), "error", "GEN") + return JSONResponse(content={"error": str(e)}, status_code=404) except Exception as e: - print(f"An error occurred: {e}") - return JSONResponse(content={"error": "An error occurred"}, status_code=500) + print_message(f"Error generating preview: {str(e)}", "error", "GEN") + return JSONResponse(content={"error": str(e)}, status_code=500) ################################################################## # API Endpoint - OpenAI Speech API compatable endpoint Validator # ################################################################## class OpenAIInput(BaseModel): + """Validates input parameters for OpenAI TTS generation.""" model: str = Field(..., description="The TTS model to use. Currently ignored.") input: str = Field(..., max_length=4096, description="The text to generate audio for.") voice: str = Field(..., description="The voice to use when generating the audio.") @@ -688,16 +891,22 @@ class OpenAIInput(BaseModel): @field_validator('voice') def validate_voice(cls, value): + """Validate that the requested voice is supported by OpenAI TTS.""" supported_voices = ["alloy", "echo", "fable", "nova", "onyx", "shimmer"] if value not in supported_voices: + print_message(f"Invalid OpenAI voice requested: {value}", "debug_openai", "TTS") raise ValueError(f'Voice must be one of {supported_voices}') + print_message(f"OpenAI voice validated: {value}", "debug_openai", "TTS") return value class OpenAIGenerator: @staticmethod def validate_input(json_data: dict) -> Union[None, str]: + """Validate OpenAI TTS request parameters.""" + debug_func_entry() try: OpenAIInput(**json_data) + print_message("OpenAI input validation successful", "debug_openai", "TTS") return None except ValidationError as e: errors = [] @@ -706,30 +915,37 @@ class OpenAIGenerator: message = err['msg'] description = OpenAIInput.model_fields[field].field_info.description errors.append(f"Error in field '{field}': {message}. Description: {description}") - return ', '.join(errors) + error_msg = ', '.join(errors) + print_message(f"OpenAI input validation failed: {error_msg}", "error", "TTS") + return error_msg ######################################################################### # API Endpoint - OpenAI Speech API compatable endpoint /v1/audio/speech # ######################################################################### @app.post("/v1/audio/speech", response_class=JSONResponse) async def openai_tts_generate(request: Request): + """Handle OpenAI-compatible TTS generation requests.""" + debug_func_entry() + try: json_data = await request.json() - print(f"[{config.branding}Debug] Received JSON data: {json_data}") if config.debugging.debug_openai else None + print_message(f"Received JSON data: {json_data}", "debug_openai", "TTS") + validation_error = OpenAIGenerator.validate_input(json_data) if validation_error: - print(f"Validation error: {validation_error}") if config.debugging.debug_openai else None + print_message(f"Validation error: {validation_error}", "debug_openai", "TTS") return JSONResponse(content={"error": validation_error}, status_code=400) - model = json_data["model"] # Currently ignored + input_text = json_data["input"] voice = json_data["voice"] response_format = json_data.get("response_format", "wav").lower() speed = json_data.get("speed", 1.0) - print(f"[{config.branding}Debug] Input text: {input_text}") if config.debugging.debug_openai else None - print(f"[{config.branding}Debug] Voice: {voice}") if config.debugging.debug_openai else None - print(f"[{config.branding}Debug] Speed: {speed}") if config.debugging.debug_openai else None - cleaned_string = html.unescape(standard_filtering(input_text)) - # Map the OpenAI voice to the corresponding internal voice + + print_message(f"Input text: {input_text}", "debug_openai", "TTS") + print_message(f"Voice: {voice}", "debug_openai", "TTS") + print_message(f"Speed: {speed}", "debug_openai", "TTS") + + cleaned_string = html.unescape(standard_filtering(input_text)) voice_mapping = { "alloy": model_engine.openai_alloy, "echo": model_engine.openai_echo, @@ -738,104 +954,116 @@ async def openai_tts_generate(request: Request): "onyx": model_engine.openai_onyx, "shimmer": model_engine.openai_shimmer } + mapped_voice = voice_mapping.get(voice) if not mapped_voice: - print(f"Unsupported voice: {voice}") + print_message(f"Unsupported voice: {voice}", "error", "TTS") return JSONResponse(content={"error": "Unsupported voice"}, status_code=400) - print(f"[{config.branding}Debug]Mapped voice: {mapped_voice}") if config.debugging.debug_openai else None - # Generate the audio - # Generate a unique filename + + print_message(f"Mapped voice: {mapped_voice}", "debug_openai", "TTS") + unique_id = uuid.uuid4() timestamp = int(time.time()) output_file_path = f'{this_dir / config.get_output_directory() / f"openai_output_{unique_id}_{timestamp}.{model_engine.audio_format}"}' + + if config.debugging.debug_fullttstext: + print_message(cleaned_string, component="TTS") + else: + print_message(f"{cleaned_string[:90]}{'...' if len(cleaned_string) > 90 else ''}", component="TTS") + await generate_audio(cleaned_string, mapped_voice, "en", model_engine.temperature_set, - model_engine.repetitionpenalty_set, speed, model_engine.pitch_set, - output_file_path, streaming=False) - print(f"[{config.branding}Debug] Audio generated at: {output_file_path}") if config.debugging.debug_openai else None + model_engine.repetitionpenalty_set, speed, model_engine.pitch_set, + output_file_path, streaming=False) + + print_message(f"Audio generated at: {output_file_path}", "debug_openai", "TTS") + if config.rvc_settings.rvc_enabled: if config.rvc_settings.rvc_char_model_file.lower() in ["disabled", "disable"]: - print(f"[{config.branding}Debug] Pass rvccharacter_voice_gen") if config.debugging.debug_openai else None - pass # Skip RVC processing for character part + print_message("Pass rvccharacter_voice_gen", "debug_openai", "TTS") else: - print(f"[{config.branding}Debug] send to rvc") if config.debugging.debug_openai else None + print_message("send to rvc", "debug_openai", "TTS") pth_path = this_dir / "models" / "rvc_voices" / config.rvc_settings.rvc_char_model_file pitch = config.rvc_settings.pitch run_rvc(output_file_path, pth_path, pitch, infer_pipeline) - # Transcode the audio to the requested format + transcoded_file_path = await transcode_for_openai(output_file_path, response_format) - print(f"[{config.branding}Debug] Audio transcoded to: {transcoded_file_path}") if config.debugging.debug_openai else None - # Return the audio file - return FileResponse(transcoded_file_path, media_type=f"audio/{response_format}", filename=f"output.{response_format}") + print_message(f"Audio transcoded to: {transcoded_file_path}", "debug_openai", "TTS") + + return FileResponse(transcoded_file_path, media_type=f"audio/{response_format}", + filename=f"output.{response_format}") + except Exception as e: - print(f"[{config.branding}Debug] An error occurred: {str(e)}") if config.debugging.debug_openai else None + print_message(f"An error occurred: {str(e)}", "error", "TTS") return JSONResponse(content={"error": "An error occurred"}, status_code=500) ########################################################################### # API Endpoint - OpenAI Speech API compatable endpoint Transcode Function # ########################################################################### async def transcode_for_openai(input_file, output_format): - print(f"[{config.branding}Debug] ************************************") if config.debugging.debug_openai else None - print(f"[{config.branding}Debug] transcode_for_openai function called") if config.debugging.debug_openai else None - print(f"[{config.branding}Debug] ************************************") if config.debugging.debug_openai else None - print(f"[{config.branding}Debug] Input file : {input_file}") if config.debugging.debug_openai else None - print(f"[{config.branding}Debug] Output format : {output_format}") if config.debugging.debug_openai else None + """Transcode audio files for OpenAI API compatibility. Handles additional formats like ogg and m4a.""" + debug_func_entry() + + print_message("************************************", "debug_openai", "TTS") + print_message("transcode_for_openai function called", "debug_openai", "TTS") + print_message("************************************", "debug_openai", "TTS") + print_message(f"Input file : {input_file}", "debug_openai", "TTS") + print_message(f"Output format : {output_format}", "debug_openai", "TTS") + if not ffmpeg_installed: - print(f"[{config.branding}TTS] FFmpeg is not installed. Format conversion is not possible.") + print_message("FFmpeg is not installed. Format conversion is not possible.", "error") raise Exception("FFmpeg is not installed. Format conversion is not possible.") - # Get the file extension of the input file + input_extension = os.path.splitext(input_file)[1][1:].lower() - print(f"[{config.branding}Debug] Input file extension: {input_extension}") if config.debugging.debug_openai else None - # Check if the input extension matches the requested output format + print_message(f"Input file extension: {input_extension}", "debug_openai", "TTS") + if input_extension == output_format.lower(): - print(f"[{config.branding}Debug] Input file is already in the requested format: {output_format}") if config.debugging.debug_openai else None + print_message(f"Input file is already in the requested format: {output_format}", "debug_openai", "TTS") return input_file + output_file = os.path.splitext(input_file)[0] + f".{output_format}" - print(f"[{config.branding}Debug] Output file: {output_file}") if config.debugging.debug_openai else None - ffmpeg_path = "ffmpeg" # Default path for Linux and macOS - if sys.platform == "win32": - ffmpeg_path = os.path.join(this_dir, "system", "win_ffmpeg", "ffmpeg.exe") - ffmpeg = (FFmpeg(ffmpeg_path).option("y").input(input_file).output(output_file)) - print(f"[{config.branding}Debug] Transcoding to {output_format}") if config.debugging.debug_openai else None - if output_format == "opus": - print(f"[{config.branding}Debug] Configuring Opus options") if config.debugging.debug_openai else None - ffmpeg.output(output_file, {"codec:a": "libopus", "b:a": "128k", "vbr": "on", "compression_level": 10, "frame_duration": 60, "application": "voip"}) - elif output_format == "aac": - print(f"[{config.branding}Debug] Configuring AAC options") if config.debugging.debug_openai else None - ffmpeg.output(output_file, {"codec:a": "aac", "b:a": "192k"}) - elif output_format == "flac": - print(f"[{config.branding}Debug] Configuring FLAC options") if config.debugging.debug_openai else None - ffmpeg.output(output_file, {"codec:a": "flac", "compression_level": 8}) - elif output_format == "wav": - print(f"[{config.branding}Debug] Configuring WAV options") if config.debugging.debug_openai else None - ffmpeg.output(output_file, {"codec:a": "pcm_s16le"}) - elif output_format == "mp3": - print(f"[{config.branding}Debug] Configuring MP3 options") if config.debugging.debug_openai else None - ffmpeg.output(output_file, {"codec:a": "libmp3lame", "b:a": "192k"}) - elif output_format in ["ogg", "m4a"]: - if output_format == "ogg": - print(f"[{config.branding}Debug] Configuring OGG options") if config.debugging.debug_openai else None - ffmpeg.output(output_file, {"codec:a": "libvorbis"}) - elif output_format == "m4a": - print(f"[{config.branding}Debug] Configuring M4A options") if config.debugging.debug_openai else None - ffmpeg.output(output_file, {"codec:a": "aac", "b:a": "192k"}) + print_message(f"Output file: {output_file}", "debug_openai", "TTS") + + ffmpeg_path = os.path.join(this_dir, "system", "win_ffmpeg", "ffmpeg.exe") if sys.platform == "win32" else "ffmpeg" + ffmpeg = FFmpeg(ffmpeg_path).option("y").input(input_file).output(output_file) + + print_message(f"Transcoding to {output_format}", "debug_openai", "TTS") + + codec_settings = { + "opus": {"codec:a": "libopus", "b:a": "128k", "vbr": "on", "compression_level": 10, + "frame_duration": 60, "application": "voip"}, + "aac": {"codec:a": "aac", "b:a": "192k"}, + "flac": {"codec:a": "flac", "compression_level": 8}, + "wav": {"codec:a": "pcm_s16le"}, + "mp3": {"codec:a": "libmp3lame", "b:a": "192k"}, + "ogg": {"codec:a": "libvorbis"}, + "m4a": {"codec:a": "aac", "b:a": "192k"} + } + + if output_format in codec_settings: + print_message(f"Configuring {output_format.upper()} options", "debug_openai", "TTS") + ffmpeg.output(output_file, codec_settings[output_format]) else: - print(f"[{config.branding}TTS] Unsupported output format: {output_format}") + print_message(f"Unsupported output format: {output_format}", "error", "TTS") raise ValueError(f"Unsupported output format: {output_format}") + try: - print(f"[{config.branding}Debug] Starting transcoding process") if config.debugging.debug_openai else None + print_message("Starting transcoding process", "debug_openai", "TTS") await ffmpeg.execute() - print(f"[{config.branding}Debug] Transcoding completed successfully") if config.debugging.debug_openai else None + print_message("Transcoding completed successfully", "debug_openai", "TTS") except Exception as e: - print(f"[{config.branding}TTS] Error occurred during transcoding: {str(e)}") + print_message(f"Error occurred during transcoding: {str(e)}", "error", "TTS") raise - print(f"[{config.branding}Debug] Transcoding process completed") if config.debugging.debug_openai else None - print(f"[{config.branding}Debug] Transcoded file: {output_file}") if config.debugging.debug_openai else None + + print_message("Transcoding process completed", "debug_openai", "TTS") + print_message(f"Transcoded file: {output_file}", "debug_openai", "TTS") return output_file + ###################################################################################### # API Endpoint - OpenAI Speech API compatable endpoint change engine voices Function # ###################################################################################### class VoiceMappings(BaseModel): + """OpenAI to engine voice mapping configuration.""" alloy: str echo: str fable: str @@ -845,15 +1073,20 @@ class VoiceMappings(BaseModel): @app.put("/api/openai-voicemap") async def update_openai_voice_mappings(mappings: VoiceMappings): - # Update in-memory versions (wont display in Gradio without a restart as gradio caches) - model_engine.openai_alloy = mappings.alloy - model_engine.openai_echo = mappings.echo - model_engine.openai_fable = mappings.fable - model_engine.openai_nova = mappings.nova - model_engine.openai_onyx = mappings.onyx - model_engine.openai_shimmer = mappings.shimmer - # Update model_settings.json file for the currently loaded TTS engine + """Update OpenAI voice mappings in memory and settings file.""" + debug_func_entry() + try: + # Update in-memory mappings + print_message("Updating in-memory voice mappings", "debug_openai", "TTS") + model_engine.openai_alloy = mappings.alloy + model_engine.openai_echo = mappings.echo + model_engine.openai_fable = mappings.fable + model_engine.openai_nova = mappings.nova + model_engine.openai_onyx = mappings.onyx + model_engine.openai_shimmer = mappings.shimmer + + # Update settings file settings_file = this_dir / "system" / "tts_engines" / model_engine.engine_loaded / "model_settings.json" with open(settings_file, "r+") as f: settings = json.load(f) @@ -861,251 +1094,289 @@ async def update_openai_voice_mappings(mappings: VoiceMappings): f.seek(0) json.dump(settings, f, indent=4) f.truncate() + + print_message("Voice mappings updated successfully", "debug_openai", "TTS") + return {"message": "OpenAI voice mappings updated successfully"} + except Exception as e: + print_message(f"Failed to update voice mappings: {str(e)}", "error", "TTS") raise HTTPException(status_code=500, detail=f"Failed to update model settings file: {str(e)}") - return {"message": "OpenAI voice mappings updated successfully"} ####################### # Play at the console # ####################### def play_audio(file_path, volume): - print(f"[{config.branding}GEN] \033[94m File path is: {file_path}") if config.debugging.debug_tts else None - print(f"[{config.branding}GEN] \033[94m Volume is: {volume}") if config.debugging.debug_tts else None - normalized_file_path = os.path.normpath(file_path) - print(f"[{config.branding}GEN] \033[94m Normalized file path is: {normalized_file_path}") if config.debugging.debug_tts else None - directory = os.path.dirname(normalized_file_path) - if os.path.isdir(directory): - print(f"[{config.branding}GEN] \033[94m Directory contents: {os.listdir(directory)}") if config.debugging.debug_tts else None - else: - print(f"[{config.branding}GEN] \033[94mError: Directory does not exist: {directory}") if config.debugging.debug_tts else None - if not os.path.isfile(normalized_file_path): - print(f"[{config.branding}GEN] \033[94mError: File does not exist: {normalized_file_path}\033[0m") if config.debugging.debug_tts else None - return - # Check for AAC format - if normalized_file_path.lower().endswith('.aac'): - print(f"[{config.branding}GEN] \033[94mPlay Audio : \033[0mAAC format files cannot be played at the console. Please choose another format") - return + """Play audio file with specified volume through system speakers at the console/terminal window""" + debug_func_entry() + try: - print(f"[{config.branding}GEN] \033[94mPlay Audio : \033[0mPlaying audio at console") + print_message(f"\033[94mFile path is: {file_path}", "debug_tts", "GEN") + print_message(f"\033[94mVolume is: {volume}", "debug_tts", "GEN") + + normalized_file_path = os.path.normpath(file_path) + print_message(f"\033[94mNormalized file path is: {normalized_file_path}", "debug_tts", "GEN") + + directory = os.path.dirname(normalized_file_path) + if not os.path.isdir(directory): + print_message(f"\033[94mError: Directory does not exist: {directory}", "error", "GEN") + return + + print_message(f"\033[94mDirectory contents: {os.listdir(directory)}", "debug_tts", "GEN") + + if not os.path.isfile(normalized_file_path): + print_message(f"\033[94mError: File does not exist: {normalized_file_path}\033[0m", "error", "GEN") + return + + if normalized_file_path.lower().endswith('.aac'): + print_message("\033[94mPlay Audio : \033[0mAAC format files cannot be played at the console. Please choose another format", "error", "GEN") + return + + print_message("\033[94mPlay Audio : \033[0mPlaying audio at console", "debug_tts", "GEN") data, fs = sf.read(normalized_file_path) sd.play(volume * data, fs) sd.wait() + except Exception as e: - print(f"[{config.branding}GEN] \033[94mError playing audio file: {e}\033[0m") + print_message(f"\033[94mError playing audio file: {e}\033[0m", "error", "GEN") + class Request(BaseModel): - # Define the structure of the 'Request' class if needed + """Define the structure of the 'Request' class if needed""" pass - class TTSGenerator: @staticmethod def validate_json_input(json_data: Union[Dict, str]) -> Union[None, str]: + """Validate TTS generation input parameters.""" + debug_func_entry() + try: if isinstance(json_data, str): json_data = json.loads(json_data) JSONInput(**json_data) - return None # JSON is valid + print_message("JSON input validation successful", "debug_tts", "GEN") + return None except ValidationError as e: + print_message(f"JSON validation failed: {str(e)}", "error", "GEN") return str(e) ################################################################# # /api/tts-generate Generation API Endpoint Narration Filtering # ################################################################# def process_text(text): - # Normalize HTML encoded quotes + """Parse text into narrator, character, and ambiguous speech segments. Handles mixed formatting and nested quotes.""" + debug_func_entry() + + # Normalize and clean text text = html.unescape(text) - # Replace ellipsis with a single dot text = re.sub(r'\.{3,}', '.', text) - # Pattern to identify combined narrator and character speech + + # Pattern for identifying speech segments combined_pattern = r'(\*[^*"]+\*|"[^"*]+")' - # List to hold parts of speech along with their type ordered_parts = [] - # Track the start of the next segment start = 0 - # Find all matches + + # Process text segments for match in re.finditer(combined_pattern, text): - # Add the text before the match, if any, as ambiguous + # Handle pre-match ambiguous text if start < match.start(): ambiguous_text = text[start:match.start()].strip() if ambiguous_text: ordered_parts.append(('ambiguous', ambiguous_text)) - # Add the matched part as either narrator or character + print_message(f"Added ambiguous segment: {ambiguous_text}", "debug_tts", "GEN") + + # Process matched segment matched_text = match.group(0) if matched_text.startswith('*') and matched_text.endswith('*'): - ordered_parts.append(('narrator', matched_text.strip('*').strip())) + text_part = matched_text.strip('*').strip() + ordered_parts.append(('narrator', text_part)) + print_message(f"Added narrator segment: {text_part}", "debug_tts", "GEN") elif matched_text.startswith('"') and matched_text.endswith('"'): - ordered_parts.append(('character', matched_text.strip('"').strip())) + text_part = matched_text.strip('"').strip() + ordered_parts.append(('character', text_part)) + print_message(f"Added character segment: {text_part}", "debug_tts", "GEN") else: - # In case of mixed or improperly formatted parts + text_part = matched_text.strip('*').strip('"') if '*' in matched_text: - ordered_parts.append(('narrator', matched_text.strip('*').strip('"'))) + ordered_parts.append(('narrator', text_part)) + print_message(f"Added mixed narrator segment: {text_part}", "debug_tts", "GEN") else: - ordered_parts.append(('character', matched_text.strip('"').strip('*'))) - # Update the start of the next segment + ordered_parts.append(('character', text_part)) + print_message(f"Added mixed character segment: {text_part}", "debug_tts", "GEN") + start = match.end() - # Add any remaining text after the last match as ambiguous + + # Handle remaining text if start < len(text): ambiguous_text = text[start:].strip() if ambiguous_text: ordered_parts.append(('ambiguous', ambiguous_text)) + print_message(f"Added final ambiguous segment: {ambiguous_text}", "debug_tts", "GEN") + return ordered_parts def standard_filtering(text_input): + """Remove special characters and normalize text formatting.""" + debug_func_entry() + text_output = (text_input - .replace("***", "") - .replace("**", "") - .replace("*", "") - .replace("\n\n", "\n") - .replace("'", "'") - ) + .replace("***", "") + .replace("**", "") + .replace("*", "") + .replace("\n\n", "\n") + .replace("'", "'") + ) + + print_message(f"Filtered text: {text_output[:50]}{'...' if len(text_output) > 50 else ''}", + "debug_tts", "GEN") return text_output ################################################################# # /api/tts-generate Generation API Endpoint Narration Combining # ################################################################# def combine(output_file_timestamp, output_file_name, audio_files, target_sample_rate=44100): - audio = np.array([]) + """Combine multiple audio files into one, with optional resampling and timestamping.""" + debug_func_entry() + + audio = np.array([]) try: for audio_file in audio_files: normalized_audio_file = os.path.normpath(audio_file) - print(f"[{config.branding}Debug] Processing file: {normalized_audio_file}") if config.debugging.debug_concat else None - # Check if the file exists + print_message(f"Processing file: {normalized_audio_file}", "debug_concat", "TTS") + if not os.path.isfile(normalized_audio_file): - print(f"[{config.branding}Debug] Error: File does not exist: {normalized_audio_file}") if config.debugging.debug_concat else None - return None, None - # Read the audio file + print_message(f"Error: File does not exist: {normalized_audio_file}", "error", "TTS") + return None, None, None + audio_data, current_sample_rate = sf.read(normalized_audio_file) - print(f"[{config.branding}Debug] Read file: {normalized_audio_file}, Sample rate: {current_sample_rate}, Data shape: {audio_data.shape}") if config.debugging.debug_concat else None - # Resample if necessary + print_message(f"Read file: {normalized_audio_file}, Sample rate: {current_sample_rate}, Data shape: {audio_data.shape}", + "debug_concat", "TTS") + if current_sample_rate != target_sample_rate: - print(f"[{config.branding}Debug] Resampling file from {current_sample_rate} to {target_sample_rate} Hz") if config.debugging.debug_concat else None + print_message(f"Resampling file from {current_sample_rate} to {target_sample_rate} Hz", "debug_concat", "TTS") audio_data = librosa.resample(audio_data, orig_sr=current_sample_rate, target_sr=target_sample_rate) - # Concatenate audio data - if audio.size == 0: - audio = audio_data - else: - audio = np.concatenate((audio, audio_data)) + + audio = audio_data if audio.size == 0 else np.concatenate((audio, audio_data)) + + # Prepare output paths if output_file_timestamp: timestamp = int(time.time()) - output_file_path = os.path.join(this_dir / "outputs" / f'{output_file_name}_{timestamp}_combined.wav') - sf.write(output_file_path, audio, target_sample_rate) - # Legacy API or New API return - if config.api_def.api_use_legacy_api: - output_file_url = f"http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}/audio/{output_file_name}_{timestamp}_combined.wav" - output_cache_url = f"http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}/audiocache/{output_file_name}_{timestamp}_combined.wav" - else: - output_file_url = f'/audio/{output_file_name}_{timestamp}_combined.wav' - output_cache_url = f'/audiocache/{output_file_name}_{timestamp}_combined.wav' + filename = f'{output_file_name}_{timestamp}_combined.wav' else: - output_file_path = os.path.join(this_dir / "outputs" / f'{output_file_name}_combined.wav') - sf.write(output_file_path, audio, target_sample_rate) - # Legacy API or New API return - if config.api_def.api_use_legacy_api: - output_file_url = f"http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}/audio/{output_file_name}_combined.wav" - output_cache_url = f"http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}/audiocache/{output_file_name}_combined.wav" - else: - output_file_url = f'/audio/{output_file_name}_combined.wav' - output_cache_url = f'/audiocache/{output_file_name}_combined.wav' - print(f"[{config.branding}Debug] Output file changed to:", output_file_path) if config.debugging.debug_concat else None - print(f"[{config.branding}Debug] Output file changed to:", output_file_url) if config.debugging.debug_concat else None - print(f"[{config.branding}Debug] Output file changed to:", output_cache_url) if config.debugging.debug_concat else None + filename = f'{output_file_name}_combined.wav' + + output_file_path = os.path.join(this_dir / "outputs" / filename) + sf.write(output_file_path, audio, target_sample_rate) + + # Generate URLs based on API configuration + base_url = f"http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}" if config.api_def.api_use_legacy_api else "" + output_file_url = f"{base_url}/audio/{filename}" + output_cache_url = f"{base_url}/audiocache/{filename}" + + print_message(f"Output file path: {output_file_path}", "debug_concat", "TTS") + print_message(f"Output file URL: {output_file_url}", "debug_concat", "TTS") + print_message(f"Output cache URL: {output_cache_url}", "debug_concat", "TTS") + + return output_file_path, output_file_url, output_cache_url + except Exception as e: - print(f"Error occurred: {e}") - return output_file_path, output_file_url, output_cache_url + print_message(f"Error combining audio files: {str(e)}", "error", "TTS") + return None, None, None ################################## # Central RVC Generation request # ################################## def run_rvc(input_tts_path, pth_path, pitch, infer_pipeline): + """Run RVC (Retrieval-based Voice Conversion) processing on input audio.""" + debug_func_entry() + generate_start_time = time.time() - f0up_key = pitch - filter_radius = config.rvc_settings.filter_radius - index_rate = config.rvc_settings.index_rate - rms_mix_rate = config.rvc_settings.rms_mix_rate - protect = config.rvc_settings.protect - hop_length = config.rvc_settings.hop_length - f0method = config.rvc_settings.f0method - split_audio = config.rvc_settings.split_audio - f0autotune = config.rvc_settings.autotune - embedder_model = config.rvc_settings.embedder_model - training_data_size = config.rvc_settings.training_data_size - # Convert path variables to strings + + # Config settings + settings = { + "f0up_key": pitch, + "filter_radius": config.rvc_settings.filter_radius, + "index_rate": config.rvc_settings.index_rate, + "rms_mix_rate": config.rvc_settings.rms_mix_rate, + "protect": config.rvc_settings.protect, + "hop_length": config.rvc_settings.hop_length, + "f0method": config.rvc_settings.f0method, + "split_audio": config.rvc_settings.split_audio, + "f0autotune": config.rvc_settings.autotune, + "embedder_model": config.rvc_settings.embedder_model, + "training_data_size": config.rvc_settings.training_data_size, + } + input_tts_path = str(input_tts_path) pth_path = str(pth_path) - # Check if the model file exists + output_rvc_path = input_tts_path + if not os.path.isfile(pth_path): - print(f"Model file {pth_path} does not exist. Exiting.") - return - # Get the directory of the model file + print_message(f"Model file {pth_path} does not exist. Exiting.", "error", "GEN") + return + model_dir = os.path.dirname(pth_path) - # Get the filename of pth_path pth_filename = os.path.basename(pth_path) - # Find all .index files in the model directory index_files = [file for file in os.listdir(model_dir) if file.endswith(".index")] + if len(index_files) == 1: index_path = str(os.path.join(model_dir, index_files[0])) - # Get the filename of index_path - index_filename = os.path.basename(index_path) - index_filename_print = index_filename - index_size_print = training_data_size - elif len(index_files) > 1: - print(f"[{config.branding}GEN] \033[94mRVC Convert :\033[0m Multiple RVC index files found in the models folder where \033[93m{pth_filename}\033[0m is") - print(f"[{config.branding}GEN] \033[94mRVC Convert :\033[0m located. Unable to determine which index to use. Continuing without an index file.") - index_path = "" - index_filename = None - index_filename_print = "None used" - index_size_print = "N/A" + index_filename_print = os.path.basename(index_path) + index_size_print = settings["training_data_size"] else: + if len(index_files) > 1: + print_message(f"RVC Convert: Multiple RVC index files found in the models folder where {pth_filename} is", "warning", "GEN") + print_message("RVC Convert: located. Unable to determine which index to use. Continuing without an index file.", "warning", "GEN") index_path = "" - index_filename = None index_filename_print = "None used" index_size_print = "N/A" - # Set the output path to be the same as the input path - output_rvc_path = input_tts_path + if config.debugging.debug_rvc: - debug_lines = [ - f"[{config.branding}Debug] ************************************", - f"[{config.branding}Debug] run_rvc function called (debug_rvc)", - f"[{config.branding}Debug] ***********************************", - f"[{config.branding}Debug] f0up_key : {f0up_key}", - f"[{config.branding}Debug] filter_radius : {filter_radius}", - f"[{config.branding}Debug] index_rate : {index_rate}", - f"[{config.branding}Debug] rms_mix_rate : {rms_mix_rate}", - f"[{config.branding}Debug] protect : {protect}", - f"[{config.branding}Debug] hop_length : {hop_length}", - f"[{config.branding}Debug] f0method : {f0method}", - f"[{config.branding}Debug] input_tts_path : {input_tts_path}", - f"[{config.branding}Debug] output_rvc_path : {output_rvc_path}", - f"[{config.branding}Debug] pth_path : {pth_path}", - f"[{config.branding}Debug] index_path : {index_path}", - f"[{config.branding}Debug] split_audio : {split_audio}", - f"[{config.branding}Debug] f0autotune : {f0autotune}", - f"[{config.branding}Debug] embedder_model : {embedder_model}", - f"[{config.branding}Debug] training_data_size: {training_data_size}" - ] - debug_info = "\n".join(debug_lines) - print(debug_info) - # Call the infer_pipeline function - infer_pipeline(f0up_key, filter_radius, index_rate, rms_mix_rate, protect, hop_length, f0method, - input_tts_path, output_rvc_path, pth_path, index_path, split_audio, f0autotune, embedder_model, training_data_size, config.debugging.debug_rvc) - generate_end_time = time.time() - generate_elapsed_time = generate_end_time - generate_start_time - print(f"[{config.branding}GEN] \033[94mRVC Convert : \033[94mModel: \033[93m{pth_filename} \033[94mIndex: \033[93m{index_filename_print}\033[0m") - print(f"[{config.branding}GEN] \033[94mRVC Convert : \033[93m{generate_elapsed_time:.2f} seconds. \033[94mMethod: \033[93m{f0method} \033[94mIndex size used \033[93m{index_size_print}\033[0m") + print_message("RVC Pipeline Configuration:", "debug_rvc", "GEN") + for key, value in settings.items(): + print_message(f"{key}: {value}", "debug_rvc", "GEN") + print_message(f"input_tts_path: {input_tts_path}", "debug_rvc", "GEN") + print_message(f"output_rvc_path: {output_rvc_path}", "debug_rvc", "GEN") + print_message(f"pth_path: {pth_path}", "debug_rvc", "GEN") + print_message(f"index_path: {index_path}", "debug_rvc", "GEN") + + infer_pipeline(settings["f0up_key"], settings["filter_radius"], settings["index_rate"], + settings["rms_mix_rate"], settings["protect"], settings["hop_length"], + settings["f0method"], input_tts_path, output_rvc_path, pth_path, index_path, + settings["split_audio"], settings["f0autotune"], settings["embedder_model"], + settings["training_data_size"], config.debugging.debug_rvc) + + generate_elapsed_time = time.time() - generate_start_time + print_message(f"RVC Convert: Model: {pth_filename} Index: {index_filename_print}", "debug_rvc", "GEN") + print_message(f"RVC Convert: {generate_elapsed_time:.2f} seconds. Method: {settings['f0method']} Index size used {index_size_print}", + "debug_rvc", "GEN") return #################################################################### # /api/tts-generate Generation API Endpoint Narration RVC handling # #################################################################### def process_rvc_narrator(part_type, voice_gen, pitch, default_model_file, output_file, infer_pipeline): + """Process RVC voice conversion for narrator segments.""" + debug_func_entry() + if voice_gen.lower() in ["disabled", "disable"]: + print_message(f"RVC processing skipped for {part_type} segment - voice disabled", "debug_rvc", "GEN") return - else: + + try: voice_gen_path = this_dir / "models" / "rvc_voices" / voice_gen pth_path = voice_gen_path if voice_gen else default_model_file + + if not os.path.exists(pth_path): + print_message(f"RVC model not found: {pth_path}", "error", "GEN") + return + + print_message(f"Processing {part_type} segment with RVC model: {os.path.basename(pth_path)}", "debug_rvc", "GEN") run_rvc(output_file, pth_path, pitch, infer_pipeline) - + + except Exception as e: + print_message(f"Error processing RVC for {part_type} segment: {str(e)}", "error", "GEN") + ############################################################# # /api/tts-generate Generation API Endpoint JSON validation # ############################################################# @@ -1132,38 +1403,78 @@ class JSONInput(BaseModel): @classmethod def validate_autoplay_volume(cls, value): - if not (0.1 <= value <= 1.0): - raise ValueError("Autoplay volume must be between 0.1 and 1.0") - return value + """Validate autoplay volume is within acceptable range (0.1-1.0).""" + debug_func_entry() + try: + if not (0.1 <= value <= 1.0): + print_message(f"Invalid autoplay volume: {value}", "error", "GEN") + raise ValueError("Autoplay volume must be between 0.1 and 1.0") + print_message(f"Autoplay volume validated: {value}", "debug_tts", "GEN") + return value + except Exception as e: + print_message(f"Error validating autoplay volume: {str(e)}", "error", "GEN") + raise + @classmethod def validate_rvccharacter_voice_gen(cls, v): - if v.lower() == "disabled": + """Validate RVC character voice file format (folder/file.pth or 'Disabled').""" + debug_func_entry() + try: + if v.lower() == "disabled": + print_message("RVC character voice disabled", "debug_rvc", "GEN") + return v + pattern = re.compile(r'^.*\.(pth)$') + if not pattern.match(v): + print_message(f"Invalid RVC character voice format: {v}", "error", "GEN") + raise ValueError("rvccharacter_voice_gen needs to be the name of a valid pth file in the 'folder\\file.pth' format or 'Disabled'.") + print_message(f"RVC character voice validated: {v}", "debug_rvc", "GEN") return v - pattern = re.compile(r'^.*\.(pth)$') - if not pattern.match(v): - raise ValueError("rvccharacter_voice_gen needs to be the name of a valid pth file in the 'folder\\file.pth' format or 'Disabled'.") - return v + except Exception as e: + print_message(f"Error validating RVC character voice: {str(e)}", "error", "GEN") + raise + @classmethod def validate_rvnarrator_voice_gen(cls, v): - if v.lower() == "disabled": + """Validate RVC narrator voice file format (folder/file.pth or 'Disabled').""" + debug_func_entry() + try: + if v.lower() == "disabled": + print_message("RVC narrator voice disabled", "debug_rvc", "GEN") + return v + pattern = re.compile(r'^.*\.(pth)$') + if not pattern.match(v): + print_message(f"Invalid RVC narrator voice format: {v}", "error", "GEN") + raise ValueError("rvcnarrator_voice_gen needs to be the name of a valid pth file in the 'folder\\file.pth' format or 'Disabled'.") + print_message(f"RVC narrator voice validated: {v}", "debug_rvc", "GEN") return v - pattern = re.compile(r'^.*\.(pth)$') - if not pattern.match(v): - raise ValueError("rvcnarrator_voice_gen needs to be the name of a valid pth file in the 'folder\\file.pth' format or 'Disabled'.") - return v - + except Exception as e: + print_message(f"Error validating RVC narrator voice: {str(e)}", "error", "GEN") + raise + + @classmethod def validate_pitches(cls, value): + """Validate pitch value is within acceptable range (-24 to 24).""" + debug_func_entry() try: num_value = float(value) + if not -24 <= num_value <= 24: + print_message(f"Invalid pitch value: {value}", "error", "GEN") + raise ValueError("Pitch needs to be a number between -24 and 24.") + print_message(f"Pitch validated: {value}", "debug_rvc", "GEN") + return value except ValueError: + print_message(f"Invalid pitch format: {value}", "error", "GEN") raise ValueError("Pitch must be a number or a string representing a number.") - if not -24 <= num_value <= 24: - raise ValueError("Pitch needs to be a number between -24 and 24.") - return value + except Exception as e: + print_message(f"Error validating pitch: {str(e)}", "error", "GEN") + raise def validate_json_input(json_input_data): + """Validate JSON input against JSONInput model schema.""" + debug_func_entry() try: JSONInput(**json_input_data) + return None except ValidationError as e: error_messages = [] for error in e.errors(): @@ -1171,54 +1482,345 @@ def validate_json_input(json_input_data): description = JSONInput.__fields__[field].description error_messages.append(f"{field}: {description}") error_message = "\n".join(error_messages) - print(f"[{config.branding}API] \033[91mError with API request:\033[0m", error_message) + print_message(f"Error with API request: {error_message}", "error", "API") return error_message - return None -############################################################### -# /api/tts-generate Generation API Endpoint pull fresh values # -############################################################### -def get_api_text_filtering(): - return config.api_def.api_text_filtering -def get_api_narrator_enabled(): - return config.api_def.api_narrator_enabled -def get_api_text_not_inside(): - return config.api_def.api_text_not_inside -def get_api_language(): - return config.api_def.api_language -def get_api_output_file_name(): - return config.api_def.api_output_file_name -def get_api_output_file_timestamp(): - return config.api_def.api_output_file_timestamp -def get_api_autoplay(): - return config.api_def.api_autoplay -def get_api_autoplay_volume(): - return config.api_def.api_autoplay_volume -def get_params_speed(): - return model_engine.generationspeed_set -def get_params_temperature(): - return model_engine.temperature_set -def get_params_repetition(): - value = model_engine.repetitionpenalty_set - return float(str(value).replace(',', '.')) -def get_params_pitch(): - return model_engine.pitch_set -def get_character_voice_gen(): - return model_engine.def_character_voice -def get_rvccharacter_voice_gen(): - return config.rvc_settings.rvc_char_model_file -def get_rvccharacter_pitch(): - return config.rvc_settings.pitch -def get_narrator_voice_gen(): - return model_engine.def_narrator_voice -def get_rvcnarrator_voice_gen(): - return config.rvc_settings.rvc_narr_model_file -def get_rvcnarrator_pitch(): - return config.rvc_settings.pitch +# API Configuration Getters +def get_api_text_filtering(): """Get text filtering setting."""; return config.api_def.api_text_filtering +def get_api_narrator_enabled(): """Get narrator mode status."""; return config.api_def.api_narrator_enabled +def get_api_text_not_inside(): """Get text parsing mode."""; return config.api_def.api_text_not_inside +def get_api_language(): """Get API language setting."""; return config.api_def.api_language +def get_api_output_file_name(): """Get output filename template."""; return config.api_def.api_output_file_name +def get_api_output_file_timestamp(): """Get file timestamp setting."""; return config.api_def.api_output_file_timestamp +def get_api_autoplay(): """Get autoplay status."""; return config.api_def.api_autoplay +def get_api_autoplay_volume(): """Get autoplay volume level."""; return config.api_def.api_autoplay_volume + +# Engine Parameter Getters +def get_params_speed(): """Get speech generation speed."""; return model_engine.generationspeed_set +def get_params_temperature(): """Get model temperature."""; return model_engine.temperature_set +def get_params_repetition(): """Get repetition penalty."""; return float(str(model_engine.repetitionpenalty_set).replace(',', '.')) +def get_params_pitch(): """Get voice pitch setting."""; return model_engine.pitch_set +def get_character_voice_gen(): """Get default character voice."""; return model_engine.def_character_voice +def get_narrator_voice_gen(): """Get default narrator voice."""; return model_engine.def_narrator_voice + +# RVC Settings Getters +def get_rvccharacter_voice_gen(): """Get RVC character model."""; return config.rvc_settings.rvc_char_model_file +def get_rvccharacter_pitch(): """Get RVC character pitch."""; return config.rvc_settings.pitch +def get_rvcnarrator_voice_gen(): """Get RVC narrator model."""; return config.rvc_settings.rvc_narr_model_file +def get_rvcnarrator_pitch(): """Get RVC narrator pitch."""; return config.rvc_settings.pitch ############################################# # /api/tts-generate Generation API Endpoint # ############################################# +async def tts_validate_and_prepare_input( + text_input: str, + request_params: dict, + default_params: dict +) -> Tuple[dict, Union[None, str]]: + """Validate TTS input parameters and prepare settings.""" + debug_func_entry() + + if config.debugging.debug_tts or config.debugging.debug_tts_variables: + print_message("\033[94mPre-validation parameter check > tts_validate_and_prepare_input > tts_server.py\033[0m", "debug_tts_variables", "GEN") + print_message("API Configuration:", "debug_tts_variables", "GEN") + for key, value in { + "max_characters": config.api_def.api_max_characters, + "length_stripping": config.api_def.api_length_stripping, + "legacy_api": config.api_def.api_use_legacy_api, + "legacy_api_ip": config.api_def.api_legacy_ip_address, + "allowed_filter": config.api_def.api_allowed_filter + }.items(): + print_message(f"{key}: {value}", "debug_tts_variables", "GEN") + + print_message("\033[94mIncoming TTS Variables > tts_validate_and_prepare_input > tts_server.py\033[0m", "debug_tts_variables", "GEN") + for key, value in request_params.items(): + print_message(f"{key}: {value}", "debug_tts_variables", "GEN") + + # Merge request parameters with defaults + params = {} + for key, default_value in default_params.items(): + params[key] = request_params.get(key) if request_params.get(key) is not None else default_value + + # Validate merged parameters + json_input_data = { + "text_input": text_input, + **params + } + + validation_result = validate_json_input(json_input_data) + + if config.debugging.debug_tts or config.debugging.debug_tts_variables: + print_message("\033[94mPost Validation Variables > tts_validate_and_prepare_input > tts_server.py\033[0m", "debug_tts_variables", "GEN") + for key, value in params.items(): + print_message(f"{key}: {value}", "debug_tts_variables", "GEN") + + return params, validation_result + +async def tts_handle_output_paths(output_file_name: str, timestamp: bool = True) -> Tuple[Path, str, str]: + """Generate output paths and URLs for TTS files.""" + debug_func_entry() + + if timestamp: + hash_object = hashlib.sha256(str(uuid.uuid4()).encode()) + short_uuid = hash_object.hexdigest()[:5] + timestamp_str = str(int(time.time())) + filename = f'{output_file_name}_{timestamp_str}{short_uuid}.{model_engine.audio_format}' + else: + filename = f"{output_file_name}.{model_engine.audio_format}" + + output_path = this_dir / config.get_output_directory() / filename + + if config.api_def.api_use_legacy_api: + base_url = f'http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}' + file_url = f'{base_url}/audio/{filename}' + cache_url = f'{base_url}/audiocache/{filename}' + else: + file_url = f'/audio/{filename}' + cache_url = f'/audiocache/{filename}' + + print_message(f"Generated output path: {output_path}", "debug_tts", "GEN") + print_message(f"Generated URLs: {file_url}, {cache_url}", "debug_tts", "GEN") + + return output_path, file_url, cache_url + +def tts_clean_text(text: str, filtering_type: str) -> str: + """Clean and filter input text based on specified filtering type.""" + debug_func_entry() + + print_message(f"Cleaning text with filter type: {filtering_type}", "debug_tts", "GEN") + + if filtering_type == "html": + cleaned = html.unescape(standard_filtering(text)) + elif filtering_type == "standard": + cleaned = text + else: + print_message("No filtering applied", "debug_tts", "GEN") + return text + + # Common cleaning steps + cleaned = re.sub(r'([!?.])\1+', r'\1', cleaned) + cleaned = re.sub(rf'{config.api_def.api_allowed_filter}', '', cleaned) + cleaned = re.sub(r'\n+', ' ', cleaned) + + print_message(f"Cleaned text: {cleaned}", "debug_tts", "GEN") + + return cleaned + +async def tts_process_narrator_mode(params: dict, text_input: str) -> Tuple[Path, str, str]: + """Process text with narrator mode, handling different voice types.""" + debug_func_entry() + + if config.debugging.debug_fullttstext: + print_message(text_input, component="TTS") + else: + print_message(f"{text_input[:90]}{'...' if len(text_input) > 90 else ''}", component="TTS") + + if params['narrator_enabled'].lower() == "silent" and params['text_not_inside'].lower() == "silent": + print_message("Both Narrator & Text-not-inside are set to silent. If you get no TTS, this is why.", "warning", "GEN") + + if model_engine.lowvram_enabled and model_engine.device == "cpu": + await model_engine.handle_lowvram_change() + + model_engine.tts_narrator_generatingtts = True + print_message("Processing with narrator mode", "debug_tts", "GEN") + + processed_parts = process_text(text_input) + audio_files = [] + + for part_type, part in processed_parts: + if len(part.strip()) <= int(config.api_def.api_length_stripping): + continue + + voice_to_use = tts_get_voice_for_part(part_type, params, part) + if not voice_to_use: + continue + + output_file = await tts_generate_part(part, voice_to_use, params) + if output_file: + await tts_apply_rvc(output_file, part_type, params) + audio_files.append(output_file) + + model_engine.tts_narrator_generatingtts = False + + if audio_files: + print_message("\033[92mNarrated TTS generation complete\033[0m", component="GEN") + return await tts_finalize_output(audio_files, params) + + print_message("\033[92mNarrated TTS generation complete\033[0m", component="GEN") + return await tts_finalize_output(audio_files, params) + +def tts_get_voice_for_part(part_type: str, params: dict, part: str) -> Optional[str]: + """Determine appropriate voice for text part.""" + preview = part[:50] + ('...' if len(part) > 50 else '') if not config.debugging.debug_fullttstext else part + + if part_type == 'narrator': + if params['narrator_enabled'].lower() == "silent": + print_message(f"\033[95mNarrator Silent:\033[0m {preview}", component="GEN") + return None + voice = params['narrator_voice_gen'] + print_message(f"\033[92mNarrator:\033[0m {preview}", component="GEN") + + elif part_type == 'character': + voice = params['character_voice_gen'] + print_message(f"\033[96mCharacter:\033[0m {preview}", component="GEN") + + else: + if params['text_not_inside'] == "silent": + print_message(f"\033[95mText-not-inside Silent:\033[0m {preview}", component="GEN") + return None + + voice = params['character_voice_gen'] if params['text_not_inside'] == "character" else params['narrator_voice_gen'] + voice_type = "\033[96mCharacter (Text-not-inside)\033[0m" if params['text_not_inside'] == "character" else "\033[92mNarrator (Text-not-inside)\033[0m" + print_message(f"{voice_type}: {preview}", component="GEN") + + return voice + +async def tts_generate_part(part: str, voice: str, params: dict) -> Optional[Path]: + """Generate audio for text part.""" + cleaned_part = tts_clean_text(part, params['text_filtering']) + output_file = await tts_handle_output_paths(params['output_file_name']) + + try: + await generate_audio( + cleaned_part, voice, params['language'], + params['temperature'], params['repetition_penalty'], + params['speed'], params['pitch'], + output_file[0], False + ) + return output_file[0] + except Exception as e: + print_message(f"Error generating audio: {str(e)}", "error", "GEN") + return None + +async def tts_apply_rvc(output_file: Path, part_type: str, params: dict): + """Apply RVC processing if enabled.""" + if not config.rvc_settings.rvc_enabled: + return + + if part_type == 'character': + process_rvc_narrator(part_type, params['rvccharacter_voice_gen'], + params['rvccharacter_pitch'], + config.rvc_settings.rvc_char_model_file, + output_file, infer_pipeline) + elif part_type == 'narrator': + process_rvc_narrator(part_type, params['rvcnarrator_voice_gen'], + params['rvcnarrator_pitch'], + config.rvc_settings.rvc_narr_model_file, + output_file, infer_pipeline) + else: + if params['text_not_inside'] == 'character': + process_rvc_narrator('character', params['rvccharacter_voice_gen'], + params['rvccharacter_pitch'], + config.rvc_settings.rvc_char_model_file, + output_file, infer_pipeline) + elif params['text_not_inside'] == 'narrator': + process_rvc_narrator('narrator', params['rvcnarrator_voice_gen'], + params['rvcnarrator_pitch'], + config.rvc_settings.rvc_narr_model_file, + output_file, infer_pipeline) + +async def tts_process_standard_mode(params: dict, text_input: str) -> Union[StreamingResponse, Tuple[Path, str, str]]: + """Process text with standard mode, without narrator functionality.""" + debug_func_entry() + + print_message("Standard generation mode", "debug_tts", "GEN") + + output_file_path, output_file_url, output_cache_url = await tts_handle_output_paths( + params['output_file_name'], + params['output_file_timestamp'] + ) + + cleaned_text = tts_clean_text(text_input, params['text_filtering']) + + if config.debugging.debug_fullttstext: + print_message(cleaned_text, component="TTS") + else: + print_message(f"{cleaned_text[:90]}{'...' if len(cleaned_text) > 90 else ''}", component="TTS") + + try: + if params.get('streaming', False): + stream = await generate_audio( + cleaned_text, params['character_voice_gen'], params['language'], + params['temperature'], params['repetition_penalty'], + params['speed'], params['pitch'], + output_file_path, True + ) + return StreamingResponse(stream, media_type="audio/wav") + + await generate_audio( + cleaned_text, params['character_voice_gen'], params['language'], + params['temperature'], params['repetition_penalty'], + params['speed'], params['pitch'], + output_file_path, False + ) + + if config.rvc_settings.rvc_enabled: + await tts_handle_rvc_processing(output_file_path, params) + + return await tts_handle_audio_output(output_file_path, params['autoplay'], params['autoplay_volume']) + + except Exception as e: + print_message(f"Error in standard processing: {str(e)}", "error", "GEN") + raise + +async def tts_handle_rvc_processing(output_file_path: Path, params: dict): + """Handle RVC voice conversion if enabled.""" + if params['rvccharacter_voice_gen'].lower() in ["disabled", "disable"]: + print_message("RVC processing skipped", "debug_tts", "GEN") + return + + print_message("Processing with RVC", "debug_tts", "GEN") + rvc_model_path = this_dir / "models" / "rvc_voices" / params['rvccharacter_voice_gen'] + pth_path = rvc_model_path if rvc_model_path else config.rvc_settings.rvc_char_model_file + run_rvc(output_file_path, pth_path, params['rvccharacter_pitch'], infer_pipeline) + +async def tts_handle_audio_output(output_file_path: Path, autoplay: bool, volume: float) -> Tuple[Path, str, str]: + """Handle audio transcoding and playback.""" + model_format = str(model_engine.audio_format).lower() + output_format = str(config.transcode_audio_format).lower() + + output_file_url = f'/audio/{output_file_path.name}' + output_cache_url = f'/audiocache/{output_file_path.name}' + + if config.api_def.api_use_legacy_api: + base_url = f'http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}' + output_file_url = f'{base_url}{output_file_url}' + output_cache_url = f'{base_url}{output_cache_url}' + + if output_format != "disabled" and model_format != output_format: + output_file_path, output_file_url, output_cache_url = await transcode_audio_if_necessary( + output_file_path, model_format, output_format + ) + + if sounddevice_installed and autoplay: + play_audio(output_file_path, volume) + + return output_file_path, output_file_url, output_cache_url + +async def tts_finalize_output(audio_files: List[Path], params: dict) -> Tuple[Path, str, str]: + """Combine audio files and handle final processing.""" + debug_func_entry() + + output_file_path, output_file_url, output_cache_url = combine( + params['output_file_timestamp'], + params['output_file_name'], + audio_files + ) + + model_format = str(model_engine.audio_format).lower() + output_format = str(config.transcode_audio_format).lower() + + if output_format != "disabled" and model_format != output_format: + if not model_engine.tts_narrator_generatingtts: + output_file_path, output_file_url, output_cache_url = await transcode_audio_if_necessary( + output_file_path, model_format, output_format + ) + + if sounddevice_installed and params['autoplay']: + play_audio(output_file_path, params['autoplay_volume']) + + return output_file_path, output_file_url, output_cache_url + @app.post("/api/tts-generate", response_class=JSONResponse) async def apifunction_generate_tts_standard( text_input: str = Form(...), @@ -1260,61 +1862,10 @@ async def apifunction_generate_tts_standard( _repetition_penalty: float = Depends(get_params_repetition), _pitch: float = Depends(get_params_pitch), ): - if config.debugging.debug_tts or config.debugging.debug_tts_variables: - debug_lines = [ - f"[{config.branding}Debug] *******************************************************", - f"[{config.branding}Debug] /api/tts_generate function called (debug_tts_variables)", - f"[{config.branding}Debug] **** PRE validation checks ****", - f"[{config.branding}Debug] *******************************************************", - f"[{config.branding}Debug] Defaults will be pulled from Settings if not specified in API Request!", - f"[{config.branding}Debug] max_characters : {config.api_def.api_max_characters}", - f"[{config.branding}Debug] length_stripping : {config.api_def.api_length_stripping}", - f"[{config.branding}Debug] legacy_api : {config.api_def.api_use_legacy_api}", - f"[{config.branding}Debug] legacy_api IPadd : {config.api_def.api_legacy_ip_address}", - f"[{config.branding}Debug] allowed_filter : {config.api_def.api_allowed_filter}", - f"[{config.branding}Debug] text_filtering : {text_filtering}", - f"[{config.branding}Debug] character_voice_gen : {character_voice_gen}", - f"[{config.branding}Debug] rvccharacter_voice_gen : {rvccharacter_voice_gen}", - f"[{config.branding}Debug] rvccharacter_pitch : {rvccharacter_pitch}", - f"[{config.branding}Debug] narrator_enabled : {narrator_enabled}", - f"[{config.branding}Debug] narrator_voice_gen : {narrator_voice_gen}", - f"[{config.branding}Debug] rvcnarrator_voice_gen : {rvcnarrator_voice_gen}", - f"[{config.branding}Debug] rvcnarrator_pitch : {rvcnarrator_pitch}", - f"[{config.branding}Debug] text_not_inside : {text_not_inside}", - f"[{config.branding}Debug] language : {language}", - f"[{config.branding}Debug] output_file_name : {output_file_name}", - f"[{config.branding}Debug] output_file_timestamp : {output_file_timestamp}", - f"[{config.branding}Debug] autoplay : {autoplay}", - f"[{config.branding}Debug] autoplay_volume : {autoplay_volume}", - f"[{config.branding}Debug] streaming : {streaming}", - f"[{config.branding}Debug] speed : {speed}", - f"[{config.branding}Debug] temperature : {temperature}", - f"[{config.branding}Debug] repetition_penalty : {repetition_penalty}", - f"[{config.branding}Debug] pitch : {pitch}" - ] - debug_info = "\n".join(debug_lines) - print(debug_info) - text_filtering = text_filtering or _text_filtering - character_voice_gen = character_voice_gen or _character_voice_gen - rvccharacter_voice_gen = rvccharacter_voice_gen or _rvccharacter_voice_gen - rvccharacter_pitch = rvccharacter_pitch if rvccharacter_pitch is not None else _rvccharacter_pitch - narrator_enabled = narrator_enabled or _narrator_enabled # NEED TO COME BACK AND LOOK AT THIS!!! - narrator_voice_gen = narrator_voice_gen or _narrator_voice_gen - rvcnarrator_voice_gen = rvcnarrator_voice_gen or _rvcnarrator_voice_gen - rvcnarrator_pitch = rvcnarrator_pitch if rvcnarrator_pitch is not None else _rvcnarrator_pitch - text_not_inside = text_not_inside or _text_not_inside - language = language or _language - output_file_name = output_file_name or _output_file_name - output_file_timestamp = output_file_timestamp if output_file_timestamp is not None else _output_file_timestamp - autoplay = autoplay if autoplay is not None else _autoplay - autoplay_volume = autoplay_volume or _autoplay_volume - speed = speed or _speed - temperature = temperature or _temperature - repetition_penalty = repetition_penalty or _repetition_penalty - pitch = pitch or _pitch + """Generate TTS audio with optional narrator mode and RVC processing.""" try: - json_input_data = { - "text_input": text_input, + # Prepare request parameters + request_params = { "text_filtering": text_filtering, "character_voice_gen": character_voice_gen, "rvccharacter_voice_gen": rvccharacter_voice_gen, @@ -1333,209 +1884,72 @@ async def apifunction_generate_tts_standard( "speed": speed, "temperature": temperature, "repetition_penalty": repetition_penalty, - "pitch": pitch, + "pitch": pitch } - JSONresult = validate_json_input(json_input_data) - if JSONresult is None: - pass - else: - return JSONResponse(content={"error": JSONresult}, status_code=400) + + default_params = { + "text_filtering": _text_filtering, + "character_voice_gen": _character_voice_gen, + "rvccharacter_voice_gen": _rvccharacter_voice_gen, + "rvccharacter_pitch": _rvccharacter_pitch, + "narrator_enabled": _narrator_enabled, + "narrator_voice_gen": _narrator_voice_gen, + "rvcnarrator_voice_gen": _rvcnarrator_voice_gen, + "rvcnarrator_pitch": _rvcnarrator_pitch, + "text_not_inside": _text_not_inside, + "language": _language, + "output_file_name": _output_file_name, + "output_file_timestamp": _output_file_timestamp, + "autoplay": _autoplay, + "autoplay_volume": _autoplay_volume, + "speed": _speed, + "temperature": _temperature, + "repetition_penalty": _repetition_penalty, + "pitch": _pitch + } + + # Validate and prepare input + params, validation_error = await tts_validate_and_prepare_input( + text_input, request_params, default_params + ) - if config.debugging.debug_tts or config.debugging.debug_tts_variables: - debug_lines = [ - f"[{config.branding}Debug] *******************************************************", - f"[{config.branding}Debug] /api/tts_generate function called (debug_tts_variables)", - f"[{config.branding}Debug] **** POST validation checks ****", - f"[{config.branding}Debug] *******************************************************", - f"[{config.branding}Debug] Defaults will be pulled from Settings if not specified in API Request!", - f"[{config.branding}Debug] max_characters : {config.api_def.api_max_characters}", - f"[{config.branding}Debug] length_stripping : {config.api_def.api_length_stripping}", - f"[{config.branding}Debug] legacy_api : {config.api_def.api_use_legacy_api}", - f"[{config.branding}Debug] legacy_api IPadd : {config.api_def.api_legacy_ip_address}", - f"[{config.branding}Debug] allowed_filter : {config.api_def.api_allowed_filter}", - f"[{config.branding}Debug] text_filtering : {text_filtering}", - f"[{config.branding}Debug] character_voice_gen : {character_voice_gen}", - f"[{config.branding}Debug] rvccharacter_voice_gen : {rvccharacter_voice_gen}", - f"[{config.branding}Debug] rvccharacter_pitch : {rvccharacter_pitch}", - f"[{config.branding}Debug] narrator_enabled : {narrator_enabled}", - f"[{config.branding}Debug] narrator_voice_gen : {narrator_voice_gen}", - f"[{config.branding}Debug] rvcnarrator_voice_gen : {rvcnarrator_voice_gen}", - f"[{config.branding}Debug] rvcnarrator_pitch : {rvcnarrator_pitch}", - f"[{config.branding}Debug] text_not_inside : {text_not_inside}", - f"[{config.branding}Debug] language : {language}", - f"[{config.branding}Debug] output_file_name : {output_file_name}", - f"[{config.branding}Debug] output_file_timestamp : {output_file_timestamp}", - f"[{config.branding}Debug] autoplay : {autoplay}", - f"[{config.branding}Debug] autoplay_volume : {autoplay_volume}", - f"[{config.branding}Debug] streaming : {streaming}", - f"[{config.branding}Debug] speed : {speed}", - f"[{config.branding}Debug] temperature : {temperature}", - f"[{config.branding}Debug] repetition_penalty : {repetition_penalty}", - f"[{config.branding}Debug] pitch : {pitch}" - ] - debug_info = "\n".join(debug_lines) - print(debug_info) - if narrator_enabled.lower() == "silent" and text_not_inside.lower() == "silent": - print(f"[{config.branding}GEN] \033[92mBoth Narrator & Text-not-inside are set to \033[91msilent\033[92m. If you get no TTS, this is why.\033[0m") - if narrator_enabled.lower() in ["true", "silent"]: - if model_engine.lowvram_enabled and model_engine.device == "cpu": - await model_engine.handle_lowvram_change() - model_engine.tts_narrator_generatingtts = True - print(f"[{config.branding}Debug] Moved into Narrator") if config.debugging.debug_tts else None - print(f"[{config.branding}Debug] original text : {text_input}") if config.debugging.debug_tts else None - processed_parts = process_text(text_input) - audio_files_all_paragraphs = [] - for part_type, part in processed_parts: - # Skip parts that are too short - if len(part.strip()) <= int(config.api_def.api_length_stripping): - continue - # Determine the voice to use based on the part type - if part_type == 'narrator': - if narrator_enabled.lower() == "silent": - print(f"[{config.branding}GEN] \033[95mNarrator Silent:\033[0m", part) # Green - continue # Skip generating audio for narrator parts if set to "silent" - voice_to_use = narrator_voice_gen - print(f"[{config.branding}GEN] \033[92mNarrator:\033[0m", part) # Purple - elif part_type == 'character': - voice_to_use = character_voice_gen - print(f"[{config.branding}GEN] \033[36mCharacter:\033[0m", part) # Yellow - else: - # Handle ambiguous parts based on user preference - if text_not_inside == "silent": - print(f"[{config.branding}GEN] \033[95mText-not-inside Silent:\033[0m", part) # Purple - continue # Skip generating audio for ambiguous parts if set to "silent" - voice_to_use = character_voice_gen if text_not_inside == "character" else narrator_voice_gen - voice_description = "\033[36mCharacter (Text-not-inside)\033[0m" if text_not_inside == "character" else "\033[92mNarrator (Text-not-inside)\033[0m" - print(f"[{config.branding}GEN] {voice_description}:", part) - # Replace multiple exclamation marks, question marks, or other punctuation with a single instance - cleaned_part = re.sub(r'([!?.])\1+', r'\1', part) - # Further clean to remove any other unwanted characters - cleaned_part = re.sub(rf'{config.api_def.api_allowed_filter}', '', cleaned_part) - print(f"[{config.branding}Debug] text after api_allowed: {text_input}") if config.debugging.debug_tts else None - # Remove all newline characters (single or multiple) - cleaned_part = re.sub(r'\n+', ' ', cleaned_part) - output_file = this_dir / config.get_output_directory() / f'{output_file_name}_{uuid.uuid4()}_{int(time.time())}.{model_engine.audio_format}' - output_file_str = output_file.as_posix() - response = await generate_audio(cleaned_part, voice_to_use, language,temperature, repetition_penalty, speed, pitch, output_file_str, streaming) - audio_path = output_file_str - if config.rvc_settings.rvc_enabled: - if part_type == 'character': - process_rvc_narrator(part_type, rvccharacter_voice_gen, rvccharacter_pitch, config.rvc_settings.rvc_char_model_file, output_file, infer_pipeline) - elif part_type == 'narrator': - process_rvc_narrator(part_type, rvcnarrator_voice_gen, rvcnarrator_pitch, config.rvc_settings.rvc_narr_model_file, output_file, infer_pipeline) - else: - if text_not_inside == 'character': - process_rvc_narrator('character', rvccharacter_voice_gen, rvccharacter_pitch, config.rvc_settings.rvc_char_model_file, output_file, infer_pipeline) - elif text_not_inside == 'narrator': - process_rvc_narrator('narrator', rvcnarrator_voice_gen, rvcnarrator_pitch, config.rvc_settings.rvc_narr_model_file, output_file, infer_pipeline) - print(f"[{config.branding}Debug] Appending audio path to list") if config.debugging.debug_tts else None - audio_files_all_paragraphs.append(audio_path) - # Combine audio files across paragraphs - model_engine.tts_narrator_generatingtts = False - print(f"[{config.branding}Debug] Narrator sending to combine") if config.debugging.debug_tts else None - output_file_path, output_file_url, output_cache_url = combine(output_file_timestamp, output_file_name, audio_files_all_paragraphs) - # Transcode audio if necessary - model_audio_format = str(model_engine.audio_format).lower() - output_audio_format = str(config.transcode_audio_format).lower() - if output_audio_format == "disabled": - pass - else: - if model_audio_format != output_audio_format and not model_engine.tts_narrator_generatingtts: - print(f"[{config.branding}Debug] Pre Transcode call - Output file: {output_file_path}") if config.debugging.debug_tts else None - output_file_path, output_file_url, output_cache_url = await transcode_audio_if_necessary(output_file_path, model_audio_format, output_audio_format) - print(f"[{config.branding}Debug] Post Transcode call - Output file: {output_file_path}") if config.debugging.debug_tts else None - if sounddevice_installed == False: - autoplay = False - if autoplay: - play_audio(output_file_path, autoplay_volume) - # Move model back to cpu system ram if needed. - if model_engine.lowvram_enabled and model_engine.device == "cuda" and model_engine.lowvram_capable: - await model_engine.handle_lowvram_change() - return JSONResponse(content={"status": "generate-success", "output_file_path": str(output_file_path), "output_file_url": str(output_file_url), "output_cache_url": str(output_cache_url)}, status_code=200) - else: - print(f"[{config.branding}Debug] Moved into Standard generation") if config.debugging.debug_tts else None - print(f"[{config.branding}Debug] original text : {text_input}") if config.debugging.debug_tts else None - if output_file_timestamp: - timestamp = int(time.time()) - # Generate a standard UUID - original_uuid = uuid.uuid4() - # Hash the UUID using SHA-256 - hash_object = hashlib.sha256(str(original_uuid).encode()) - hashed_uuid = hash_object.hexdigest() - # Truncate to the desired length, for example, 16 characters - short_uuid = hashed_uuid[:5] - output_file_path = this_dir / config.get_output_directory() / f'{output_file_name}_{timestamp}{short_uuid}.{model_engine.audio_format}' - #Legacy API or New API return - if config.api_def.api_use_legacy_api: - output_file_url = f'http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}/audio/{output_file_name}_{timestamp}{short_uuid}.{model_engine.audio_format}' - output_cache_url = f'http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}/audiocache/{output_file_name}_{timestamp}{short_uuid}.{model_engine.audio_format}' - else: - output_file_url = f'/audio/{output_file_name}_{timestamp}{short_uuid}.{model_engine.audio_format}' - output_cache_url = f'/audiocache/{output_file_name}_{timestamp}{short_uuid}.{model_engine.audio_format}' - else: - output_file_path = this_dir / config.get_output_directory() / f"{output_file_name}.{model_engine.audio_format}" - # Legacy API or New API return - if config.api_def.api_use_legacy_api: - output_file_url = f'http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}/audio/{output_file_name}.{model_engine.audio_format}' - output_cache_url = f'http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}/audiocache/{output_file_name}.{model_engine.audio_format}' - else: - output_file_url = f'/audio/{output_file_name}.{model_engine.audio_format}' - output_cache_url = f'/audiocache/{output_file_name}.{model_engine.audio_format}' - if text_filtering == "html": - cleaned_string = html.unescape(standard_filtering(text_input)) - cleaned_string = re.sub(r'([!?.])\1+', r'\1', cleaned_string) - # Further clean to remove any other unwanted characters - cleaned_string = re.sub(rf'{config.api_def.api_allowed_filter}', '', cleaned_string) - print(f"[{config.branding}Debug] HTML Filtering - text after api_allowed: {cleaned_string}") if config.debugging.debug_tts else None - # Remove all newline characters (single or multiple) - cleaned_string = re.sub(r'\n+', ' ', cleaned_string) - elif text_filtering == "standard": - cleaned_string = re.sub(r'([!?.])\1+', r'\1', text_input) - # Further clean to remove any other unwanted characters - cleaned_string = re.sub(rf'{config.api_def.api_allowed_filter}', '', cleaned_string) - print(f"[{config.branding}Debug] Standard Filtering - text after api_allowed: {cleaned_string}") if config.debugging.debug_tts else None - # Remove all newline characters (single or multiple) - cleaned_string = re.sub(r'\n+', ' ', cleaned_string) - else: - print(f"[{config.branding}Debug] No filtering text : {text_input}") if config.debugging.debug_tts else None - cleaned_string = text_input - print(f"[{config.branding}GEN]", cleaned_string) - print(f"[{config.branding}Debug] Sending request to generate_audio in tts_server.py") if config.debugging.debug_tts else None + if validation_error: + return JSONResponse(content={"error": validation_error}, status_code=400) + + # Process based on mode + if params["narrator_enabled"].lower() in ["true", "silent"]: try: - if streaming: - response = await generate_audio(cleaned_string, character_voice_gen, language, temperature, repetition_penalty, speed, pitch, output_file_path, streaming) - return StreamingResponse(response, media_type="audio/wav") - else: - response = await generate_audio(cleaned_string, character_voice_gen, language, temperature, repetition_penalty, speed, pitch, output_file_path, streaming) - if config.rvc_settings.rvc_enabled: - if rvccharacter_voice_gen.lower() in ["disabled", "disable"]: - print(f"[{config.branding}Debug] Pass rvccharacter_voice_gen") if config.debugging.debug_tts else None - pass # Skip RVC processing for character part - else: - print(f"[{config.branding}Debug] send to rvc") if config.debugging.debug_tts else None - rvccharacter_voice_gen = this_dir / "models" / "rvc_voices" / rvccharacter_voice_gen - pth_path = rvccharacter_voice_gen if rvccharacter_voice_gen else config.rvc_settings.rvc_char_model_file - run_rvc(output_file_path, pth_path, rvccharacter_pitch, infer_pipeline) - # Transcode audio if necessary - model_audio_format = str(model_engine.audio_format).lower() - output_audio_format = str(config.transcode_audio_format).lower() - print(f"[{config.branding}Debug] At Model audio format") if config.debugging.debug_tts else None - if output_audio_format == "disabled": - pass - else: - if model_audio_format != output_audio_format and not model_engine.tts_narrator_generatingtts: - print(f"[{config.branding}Debug] Pre Transcode call - Output file: {output_file_path}") if config.debugging.debug_tts else None - output_file_path, output_file_url, output_cache_url = await transcode_audio_if_necessary(output_file_path, model_audio_format, output_audio_format) - print(f"[{config.branding}Debug] Post Transcode call - Output file: {output_file_path}") if config.debugging.debug_tts else None - if sounddevice_installed == False: - autoplay = False - if autoplay: - play_audio(output_file_path, autoplay_volume) - return JSONResponse(content={"status": "generate-success", "output_file_path": str(output_file_path), "output_file_url": str(output_file_url), "output_cache_url": str(output_cache_url)}, status_code=200) - except Exception as e: - return JSONResponse(content={"status": "generate-failure", "error": "An error occurred"}, status_code=500) + output_file_path, output_file_url, output_cache_url = await tts_process_narrator_mode( + params, text_input + ) + finally: + # Ensure model returns to CPU if using low VRAM + if model_engine.lowvram_enabled and model_engine.device == "cuda" and model_engine.lowvram_capable: + await model_engine.handle_lowvram_change() + else: + output_file_path, output_file_url, output_cache_url = await tts_process_standard_mode( + params, text_input + ) + + if isinstance(output_file_path, StreamingResponse): + return output_file_path + + return JSONResponse( + content={ + "status": "generate-success", + "output_file_path": str(output_file_path), + "output_file_url": str(output_file_url), + "output_cache_url": str(output_cache_url) + }, + status_code=200 + ) + except Exception as e: - return JSONResponse(content={"status": "generate-failure", "error": "An error occurred"}, status_code=500) + print_message(f"Error in TTS generation: {str(e)}", "error", "GEN") + return JSONResponse( + content={"status": "generate-failure", "error": "An error occurred"}, + status_code=500 + ) ############################# #### Word Add-in Sharing #### @@ -1548,6 +1962,7 @@ app.mount("/api/word_addin", StaticFiles(directory=os.path.join(this_dir / 'syst ############################################# import aiofiles class TTSItem(BaseModel): + """Individual TTS item structure for saving.""" id: int fileUrl: str text: str @@ -1555,52 +1970,158 @@ class TTSItem(BaseModel): language: str class TTSData(BaseModel): + """Collection of TTS items.""" ttsList: List[TTSItem] @app.post("/api/save-tts-data") async def apifunction_save_tts_data(tts_data: List[TTSItem]): - # Convert the list of Pydantic models to a list of dictionaries - tts_data_list = [item.dict() for item in tts_data] - # Serialize the list of dictionaries to a JSON string - tts_data_json = json.dumps(tts_data_list, indent=4) - async with aiofiles.open(this_dir / config.get_output_directory() / "ttsList.json", 'w') as f: - await f.write(tts_data_json) - return {"message": "Data saved successfully"} + """Save TTS data to JSON file in output directory.""" + debug_func_entry() + + try: + print_message(f"Saving {len(tts_data)} TTS items", "debug_tts", "GEN") + + # Convert Pydantic models to dictionaries + tts_data_list = [item.dict() for item in tts_data] + tts_data_json = json.dumps(tts_data_list, indent=4) + + output_path = this_dir / config.get_output_directory() / "ttsList.json" + + async with aiofiles.open(output_path, 'w') as f: + await f.write(tts_data_json) + + print_message(f"TTS data saved to {output_path}", "debug_tts", "GEN") + return {"message": "Data saved successfully"} + + except Exception as e: + print_message(f"Error saving TTS data: {str(e)}", "error", "GEN") + raise HTTPException(status_code=500, detail="Failed to save TTS data") ######################################## # Trigger TTS Gen Text/Speech Analysis # ######################################## @app.get("/api/trigger-analysis") async def apifunction_trigger_analysis(threshold: int = Query(default=98)): - venv_path = sys.prefix - env = os.environ.copy() - env["PATH"] = os.path.join(venv_path, "bin") + ":" + env["PATH"] - ttslist_path = this_dir / config.get_output_directory() / "ttsList.json" - wavfile_path = this_dir / config.get_output_directory() - subprocess.run([sys.executable, "tts_diff.py", f"--threshold={threshold}", f"--ttslistpath={ttslist_path}", f"--wavfilespath={wavfile_path}"], cwd=this_dir / "system" / "tts_diff", env=env) - # Read the analysis summary + """Trigger Whisper analysis to compare generated TTS against original text. + + Args: + threshold: Minimum acceptable match percentage (default: 98) + """ + debug_func_entry() + try: - with open(this_dir / config.get_output_directory() / "analysis_summary.json", "r") as summary_file: - summary_data = json.load(summary_file) - except FileNotFoundError: - summary_data = {"error": "Analysis summary file not found."} - return {"message": "Analysis Completed", "summary": summary_data} + # Setup environment + venv_path = sys.prefix + env = os.environ.copy() + env["PATH"] = os.path.join(venv_path, "bin") + ":" + env["PATH"] + + # Define paths + ttslist_path = this_dir / config.get_output_directory() / "ttsList.json" + wavfile_path = this_dir / config.get_output_directory() + + if not ttslist_path.exists(): + print_message("TTS list file not found", "error", "GEN") + raise HTTPException(status_code=404, detail="TTS list file not found") + + print_message(f"Starting TTS analysis with threshold: {threshold}%", "debug_tts", "GEN") + + # Run analysis script + result = subprocess.run( + [ + sys.executable, + "tts_diff.py", + f"--threshold={threshold}", + f"--ttslistpath={ttslist_path}", + f"--wavfilespath={wavfile_path}" + ], + cwd=this_dir / "system" / "tts_diff", + env=env, + capture_output=True, + text=True + ) + + if result.returncode != 0: + print_message(f"Analysis script error: {result.stderr}", "error", "GEN") + raise HTTPException(status_code=500, detail="Analysis script failed") + + # Read analysis results + try: + summary_path = this_dir / config.get_output_directory() / "analysis_summary.json" + with open(summary_path, "r") as summary_file: + summary_data = json.load(summary_file) + print_message("Analysis summary loaded successfully", "debug_tts", "GEN") + except FileNotFoundError: + print_message("Analysis summary file not found", "error", "GEN") + summary_data = {"error": "Analysis summary file not found."} + except json.JSONDecodeError as e: + print_message(f"Error parsing analysis summary: {str(e)}", "error", "GEN") + summary_data = {"error": "Invalid analysis summary format."} + + return {"message": "Analysis Completed", "summary": summary_data} + + except Exception as e: + print_message(f"Error during analysis: {str(e)}", "error", "GEN") + raise HTTPException(status_code=500, detail=str(e)) ########################################### # TTS Generator SRT Subtitiles generation # ########################################### @app.get("/api/srt-generation") async def apifunction_srt_generation(): - venv_path = sys.prefix - env = os.environ.copy() - env["PATH"] = os.path.join(venv_path, "bin") + ":" + env["PATH"] - ttslist_path = this_dir / config.get_output_directory() / "ttsList.json" - wavfile_path = this_dir / config.get_output_directory() - subprocess.run([sys.executable, "tts_srt.py", f"--ttslistpath={ttslist_path}", f"--wavfilespath={wavfile_path}"], cwd=this_dir / "system" / "tts_srt", env=env) - srt_file_path = this_dir / config.get_output_directory() / "subtitles.srt" - if not srt_file_path.exists(): - raise HTTPException(status_code=404, detail="Subtitle file not found.") - return FileResponse(path=srt_file_path, filename="subtitles.srt", media_type='application/octet-stream') + """Generate SRT subtitles for TTS audio files using speech-to-text analysis.""" + debug_func_entry() + + try: + # Setup environment + venv_path = sys.prefix + env = os.environ.copy() + env["PATH"] = os.path.join(venv_path, "bin") + ":" + env["PATH"] + + # Define paths + ttslist_path = this_dir / config.get_output_directory() / "ttsList.json" + wavfile_path = this_dir / config.get_output_directory() + srt_file_path = this_dir / config.get_output_directory() / "subtitles.srt" + + # Validate input file existence + if not ttslist_path.exists(): + print_message("TTS list file not found", "error", "GEN") + raise HTTPException(status_code=404, detail="TTS list file not found") + + print_message("Starting SRT generation process", "debug_tts", "GEN") + + # Run SRT generation script + result = subprocess.run( + [ + sys.executable, + "tts_srt.py", + f"--ttslistpath={ttslist_path}", + f"--wavfilespath={wavfile_path}" + ], + cwd=this_dir / "system" / "tts_srt", + env=env, + capture_output=True, + text=True + ) + + if result.returncode != 0: + print_message(f"SRT generation script error: {result.stderr}", "error", "GEN") + raise HTTPException(status_code=500, detail="SRT generation failed") + + # Verify output file + if not srt_file_path.exists(): + print_message("Generated SRT file not found", "error", "GEN") + raise HTTPException(status_code=404, detail="Subtitle file not found") + + print_message("SRT generation completed successfully", "debug_tts", "GEN") + return FileResponse( + path=srt_file_path, + filename="subtitles.srt", + media_type='application/octet-stream' + ) + + except Exception as e: + print_message(f"Error during SRT generation: {str(e)}", "error", "GEN") + raise HTTPException(status_code=500, detail=str(e)) ################################# # Static Mount for file serving # @@ -1615,11 +2136,32 @@ logging.basicConfig(level=logging.DEBUG) @app.get("/settings") async def get_settings(request: Request): - return templates.TemplateResponse("admin.html", {"request": request, "data": config.to_dict()}) + """Return HTML template with current configuration settings.""" + debug_func_entry() + + try: + settings_data = config.to_dict() + print_message("Rendering admin settings page", "debug_api", "API") + return templates.TemplateResponse("admin.html", { + "request": request, + "data": settings_data + }) + except Exception as e: + print_message(f"Error rendering settings page: {str(e)}", "error", "API") + raise HTTPException(status_code=500, detail="Failed to load settings page") @app.get("/settings-json") async def get_settings_json(): - return config.to_dict() + """Return current configuration settings as JSON.""" + debug_func_entry() + + try: + settings_data = config.to_dict() + print_message("Returning JSON settings data", "debug_api", "API") + return settings_data + except Exception as e: + print_message(f"Error retrieving settings data: {str(e)}", "error", "API") + raise HTTPException(status_code=500, detail="Failed to retrieve settings") @app.post("/update-settings") async def update_settings( @@ -1629,17 +2171,55 @@ async def update_settings( gradio_port_number: int = Form(...), api_port_number: int = Form(...), ): - # Update the settings based on the form values - config.delete_output_wavs = delete_output_wavs - config.gradio_interface = gradio_interface.lower() == 'true' - config.gradio_port_number = gradio_port_number - config.api_def.api_port_number = api_port_number - - # Save the updated settings back to the JSON file - config.save() - - # Redirect to the settings page to display the updated settings - return templates.TemplateResponse("admin.html", {"request": request, "data": config.to_dict()}) + """Update configuration settings from admin form submission.""" + debug_func_entry() + + try: + print_message("Updating configuration settings", "debug_api", "API") + + # Log current values before update + if config.debugging.debug_api: + print_message("Current settings:", "debug_api", "API") + print_message(f"delete_output_wavs: {config.delete_output_wavs}", "debug_api", "API") + print_message(f"gradio_interface: {config.gradio_interface}", "debug_api", "API") + print_message(f"gradio_port: {config.gradio_port_number}", "debug_api", "API") + print_message(f"api_port: {config.api_def.api_port_number}", "debug_api", "API") + + # Validate port numbers + if not (1024 <= gradio_port_number <= 65535 and 1024 <= api_port_number <= 65535): + print_message("Invalid port numbers provided", "error", "API") + raise HTTPException(status_code=400, detail="Port numbers must be between 1024 and 65535") + + # Update configuration + config.delete_output_wavs = delete_output_wavs + config.gradio_interface = gradio_interface.lower() == 'true' + config.gradio_port_number = gradio_port_number + config.api_def.api_port_number = api_port_number + + # Log new values after update + if config.debugging.debug_api: + print_message("Updated settings:", "debug_api", "API") + print_message(f"delete_output_wavs: {config.delete_output_wavs}", "debug_api", "API") + print_message(f"gradio_interface: {config.gradio_interface}", "debug_api", "API") + print_message(f"gradio_port: {config.gradio_port_number}", "debug_api", "API") + print_message(f"api_port: {config.api_def.api_port_number}", "debug_api", "API") + + # Save configuration + config.save() + print_message("Settings saved successfully", "debug_api", "API") + + # Return updated settings page + return templates.TemplateResponse("admin.html", { + "request": request, + "data": config.to_dict() + }) + + except ValueError as e: + print_message(f"Validation error in settings update: {str(e)}", "error", "API") + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + print_message(f"Error updating settings: {str(e)}", "error", "API") + raise HTTPException(status_code=500, detail="Failed to update settings") # Create an instance of Jinja2Templates for rendering HTML templates templates = Jinja2Templates(directory=this_dir / "system") @@ -1653,7 +2233,15 @@ rendered_html = template.render(params=config.to_dict()) ################################################### @app.get("/") async def read_root(): - return HTMLResponse(content=rendered_html, status_code=200) + """Serve the main application HTML page.""" + debug_func_entry() + + try: + print_message("Serving main application page", "debug_api", "API") + return HTMLResponse(content=rendered_html, status_code=200) + except Exception as e: + print_message(f"Error serving main page: {str(e)}", "error", "API") + raise HTTPException(status_code=500, detail="Failed to load application page") # Start Uvicorn Webserver # port_parameter = int(config.api_def.api_port_number)