diff --git a/script.py b/script.py index 544076a..2abb88b 100644 --- a/script.py +++ b/script.py @@ -1,129 +1,283 @@ -import os -import sys -import json -import time -import atexit -import signal as system_signal -import inspect -import requests -import platform -import subprocess -import threading -import soundfile as sf -from pathlib import Path -from datetime import datetime, timedelta -from requests.exceptions import RequestException, ConnectionError -import whisper -import torch -import zipfile -from tqdm import tqdm -import gc -import shutil -import mimetypes -import numpy as np -from scipy import signal as scipy_signal -import plotly """ -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. +Alltalk V2: A Comprehensive TTS and ASR Framework with WebUI Integration -Reserved names: -- output_modifier -- input_modifier -- state_modifier -- ui -- history_modifier -- remove_tts_from_history -- toggle_text_in_history +Github: https://github.com/erew123/ + +This script serves as the core for the Alltalk V2 system, a robust framework combining +Text-to-Speech (TTS) and Automatic Speech Recognition (ASR) capabilities. It is highly +configurable and integrates with Text-Generation-WebUI (TGWUI) while also supporting +standalone deployments. The script initializes configurations, handles subprocess +management, enables API interactions, and provides transcription utilities. + +Key Features: +------------- +1. **TTS Engine Management**: + - Dynamically load and swap between TTS engines and models. + - Manage engine-specific configurations and optimize for GPU or CPU usage. + +2. **ASR/Transcription Utilities**: + - Integrates Whisper for audio-to-text transcription. + - Supports batch processing, live dictation, and various output formats (JSON, TXT, SRT). + +3. **WebUI and API Integration**: + - Extends Text-Generation-WebUI for seamless integration with conversational AI. + - Provides a Gradio-based interface for managing settings and TTS generation. + +4. **Enhanced User Configurations**: + - Centralized configuration loader for system-wide consistency. + - Handles first-time setups and runtime environment detection (Docker, Google Colab, etc.). + +5. **Audio File Processing**: + - Supports multiple audio formats with validation, pre-processing (noise reduction, bandpass filtering), + and file output management. + - Automatically organizes transcription files and generates metadata summaries. + +6. **Real-Time Feedback and Debugging**: + - Extensive debug logging for monitoring function entries, file operations, and network requests. + - Progress tracking for long-running tasks like model loading and transcription. + + +Requirements: +------------- +- Python 3.8 or later. +- Dependencies listed in the repository's requirements.txt. +- Whisper ASR, PyTorch, Gradio, and other specified modules. + +Supported Environments: +----------------------- +- Google Colab +- Docker +- Text-Generation-WebUI (TGWUI) +- Standalone mode """ +import atexit +import gc +import inspect +import importlib +import json +import mimetypes +import os +import platform +import shutil +import signal as system_signal +import subprocess +import sys +import threading +import time +import warnings +import zipfile +from datetime import datetime, timedelta +from pathlib import Path +import gradio as gr +import numpy as np +import plotly.graph_objects as go +import requests +from requests.exceptions import ConnectionError as RequestsConnectionError +from requests.exceptions import RequestException +from scipy import signal as scipy_signal +import soundfile as sf +from tqdm import tqdm +import torch +import whisper +this_dir = Path(__file__).parent.resolve() + +# 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 + +# pylint: disable=import-outside-toplevel def output_modifier(string, state): """Modify chat output (required for TGWUI)""" try: - from .system.TGWUI_Extension.script import output_modifier as tgwui_output_modifier - except ImportError: - from system.TGWUI_Extension.script import output_modifier as tgwui_output_modifier + from .system.TGWUI_Extension.script import ( + output_modifier as tgwui_output_modifier, + ) + except ImportError: + 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)""" try: - from .system.TGWUI_Extension.script import input_modifier as tgwui_input_modifier + from .system.TGWUI_Extension.script import ( + input_modifier as tgwui_input_modifier, + ) except ImportError: 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)""" try: - from .system.TGWUI_Extension.script import state_modifier as tgwui_state_modifier - except ImportError: + from .system.TGWUI_Extension.script import ( + state_modifier as tgwui_state_modifier, + ) + except ImportError: 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)""" try: - from .system.TGWUI_Extension.script import history_modifier as tgwui_history_modifier - except ImportError: - from system.TGWUI_Extension.script import history_modifier as tgwui_history_modifier + from .system.TGWUI_Extension.script import ( + history_modifier as tgwui_history_modifier, + ) + except ImportError: + 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)""" try: - from .system.TGWUI_Extension.script import remove_tts_from_history as tgwui_remove_tts - except ImportError: - from system.TGWUI_Extension.script import remove_tts_from_history as tgwui_remove_tts + from .system.TGWUI_Extension.script import ( + remove_tts_from_history as tgwui_remove_tts, + ) + except ImportError: + 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)""" try: - from .system.TGWUI_Extension.script import toggle_text_in_history as tgwui_toggle_text - except ImportError: - from system.TGWUI_Extension.script import toggle_text_in_history as tgwui_toggle_text + from .system.TGWUI_Extension.script import ( + toggle_text_in_history as tgwui_toggle_text, + ) + except ImportError: + from system.TGWUI_Extension.script import ( + toggle_text_in_history as tgwui_toggle_text, + ) return tgwui_toggle_text(history) + + +# pylint: disable=ungrouped-imports,unused-import,import-outside-toplevel + try: from modules import chat, shared, ui_chat + TGWUI_AVAILABLE = True except ImportError: - class DummyShared: - processing_message = "" - class DummyState: + + class DummyShared: # pylint: disable=too-few-public-methods + "fake class relating to how we import or dont import TGWUI's remote extension" + processing_message = "" + + class DummyState: # pylint: disable=too-few-public-methods + "fake class relating to how we import or dont import TGWUI's remote extension" def __init__(self): - self.mode = 'chat' # Add default mode + self.mode = "chat" # Add default mode + shared = DummyShared() -# pylint: disable=all +# pylint: enable=ungrouped-imports,unused-import,import-outside-toplevel ######################### # Central config loader # ######################### -# Confguration file management for confignew.json +# Confguration file management for confignew.json try: - from .config import AlltalkConfig, AlltalkTTSEnginesConfig, AlltalkNewEnginesConfig # TGWUI import + from .config import ( + AlltalkConfig, + AlltalkTTSEnginesConfig, + AlltalkNewEnginesConfig, + ) # TGWUI import from .system.gradio_pages.help_content import AllTalkHelpContent + from .system.gradio_pages.alltalk_diskspace import get_disk_interface except ImportError: - from config import AlltalkConfig, AlltalkTTSEnginesConfig, AlltalkNewEnginesConfig # Standalone import + from config import ( + AlltalkConfig, + AlltalkTTSEnginesConfig, + AlltalkNewEnginesConfig, + ) # Standalone import from system.gradio_pages.help_content import AllTalkHelpContent + from system.gradio_pages.alltalk_diskspace import get_disk_interface + 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 + config_initalize = AlltalkConfig.get_instance() + tts_engines_config_initalize = AlltalkTTSEnginesConfig.get_instance() + new_engines_config_initalize = AlltalkNewEnginesConfig.get_instance() + return config_initalize, tts_engines_config_initalize, new_engines_config_initalize + +# pylint: enable=import-outside-toplevel # 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 +######################################### +# START-UP # State Dictionary (GLOABLS) # +######################################### +_state = { + 'process': None, + 'running_on_google_colab': False, # Are we running on a Google Colab Server? + 'tunnel_url_1': None, # Used for Google Colab and finding the tunnel URL for API address + 'tunnel_url_2': None, # Used for Google Colab and finding the tunnel URL for Gradio address + 'running_in_docker': False, # Are we running on a Docker Server? + 'docker_url': f"http://localhost:{config.api_def.api_port_number}", # The inital URL used by docker for API communication in gradio + 'alltalk_protocol': "http://", # HTTP is always used, bar Docker or Google Colab. Can be configured here though + 'alltalk_ip_port': f"127.0.0.1:{config.api_def.api_port_number}", # IP/Port used for Docker, Google Colab or default + 'my_current_url': "null", + 'srv_models_available': None, # The current models available for the current TTS engine, pulled from the tts_server's API + 'srv_current_model_loaded': None, # The current model loaded, Initally loaded from central config and later pulled from the tts_server's API + 'srv_engines_available': None, # The current TTS engines available, Initally loaded from central config and later pulled from the tts_server's API + 'srv_current_engine_loaded': None, # The current TTS engines loaded in, Initally loaded from central config and later pulled from the tts_server's API + 'gradio_languages_list': None, # The list of languauges disaplyed in the Gradio interface. + 'whisper_model': None, # Used to track the whisper model used for dictation etc +} + +############################################################ +# START-UP # Populate _state Engine info - Gradio Needs it # +############################################################ +def initialize_engine_state(_state): + """Initialize engine state from central config""" + _state['srv_current_engine_loaded'] = tts_engines_config.engine_loaded + _state['srv_engines_available'] = tts_engines_config.get_engine_names_available() + _state['srv_currnet_model_loaded'] = tts_engines_config.selected_model + return _state + +# Call this after creating _state +_state = initialize_engine_state(_state) + +################################################################## +# START-UP # Populate Language Options Choices - Gradio Needs it # +################################################################## +with open(os.path.join(this_dir, "system", "config", "languages.json"), "r", encoding="utf-8") as f: + _state['gradio_languages_list'] = json.load(f) + ########################## # Central print function # ########################## @@ -136,218 +290,339 @@ def print_message(message, message_type="standard", component="TTS"): """ # ANSI color codes BLUE = "\033[94m" - MAGENTA = "\033[95m" - YELLOW = "\033[93m" + # 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} script.py") + print( + f"{prefix}{BLUE}Debug{RESET} {YELLOW}{message_type}{RESET} Function entry:{GREEN}{message_parts[1]}{RESET} script.py" + ) 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): +def update_settings_at( + delete_output_wavs, + gradio_interface, + gradio_port_number, + upd_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() - + upd_set_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 + upd_set_config.delete_output_wavs = delete_output_wavs + upd_set_config.gradio_interface = gradio_interface == "Enabled" + upd_set_config.output_folder = upd_output_folder + upd_set_config.api_def.api_port_number = api_port_number + upd_set_config.gradio_port_number = gradio_port_number + upd_set_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) + for key in vars(upd_set_config.debugging): + if not key.startswith("_"): # Skip private attributes + setattr(upd_set_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 + upd_set_config.gradio_pages.Generate_Help_page = generate_help_page + upd_set_config.gradio_pages.Voice2RVC_page = voice2rvc_page + upd_set_config.gradio_pages.TTS_Generator_page = tts_generator_page + upd_set_config.gradio_pages.TTS_Engines_Settings_page = tts_engines_settings_page + upd_set_config.gradio_pages.alltalk_documentation_page = alltalk_documentation_page + upd_set_config.gradio_pages.api_documentation_page = api_documentation_page # Save the updated configuration - config.save() + upd_set_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): + print_message("Default Settings Saved") + return "Settings updated successfully!" + except (AttributeError, TypeError) as e: + print_message( + f"Configuration structure error: {str(e)}", + message_type="error" + ) + return "Error updating settings: Invalid configuration structure" + except (OSError, IOError) as e: + print_message( + f"File system error while saving configuration: {str(e)}", + message_type="error" + ) + return "Error saving settings: File system error" + except ValueError as e: + print_message( + f"Invalid value provided for configuration: {str(e)}", + message_type="error" + ) + return "Error updating settings: Invalid value provided" + + +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() - + upd_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 + upd_config.api_def.api_length_stripping = api_length_stripping + upd_config.api_def.api_legacy_ip_address = api_legacy_ip_address + upd_config.api_def.api_allowed_filter = api_allowed_filter + upd_config.api_def.api_max_characters = api_max_characters + upd_config.api_def.api_use_legacy_api = ( + api_use_legacy_api == "AllTalk v1 API (Legacy)" + ) + upd_config.api_def.api_text_filtering = api_text_filtering + upd_config.api_def.api_narrator_enabled = api_narrator_enabled + upd_config.api_def.api_text_not_inside = api_text_not_inside + upd_config.api_def.api_language = api_language + upd_config.api_def.api_output_file_name = api_output_file_name + upd_config.api_def.api_output_file_timestamp = ( + api_output_file_timestamp == "Timestamp files" + ) + upd_config.api_def.api_autoplay = api_autoplay == "Play remotely" + upd_config.api_def.api_autoplay_volume = api_autoplay_volume # Save the updated configuration - config.save() + upd_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)}" + except (AttributeError, TypeError) as e: + print_message( + f"Configuration structure error: {str(e)}", + message_type="error" + ) + return "Error updating settings: Invalid configuration structure" + except (OSError, IOError) as e: + print_message( + f"File system error while saving configuration: {str(e)}", + message_type="error" + ) + return "Error saving settings: File system error" + except ValueError as e: + print_message( + f"Invalid value provided for configuration: {str(e)}", + message_type="error" + ) + return "Error updating settings: Invalid value provided" + 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() - + tgwui_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 + tgwui_config.tgwui.tgwui_activate_tts = activate == "Enabled" + tgwui_config.tgwui.tgwui_autoplay_tts = autoplay == "Enabled" + tgwui_config.tgwui.tgwui_show_text = show_text == "Enabled" + tgwui_config.tgwui.tgwui_language = language + tgwui_config.tgwui.tgwui_narrator_enabled = narrator_enabled # Save the updated configuration - config.save() - + tgwui_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)}" + except (AttributeError, TypeError) as e: + error_msg = f"Configuration structure error: {str(e)}" + return_message = "Error updating settings: Invalid configuration structure" + except (OSError, IOError) as e: + error_msg = f"File system error while saving configuration: {str(e)}" + return_message = "Error saving settings: File system error" + except ValueError as e: + error_msg = f"Invalid value provided for configuration: {str(e)}" + return_message = "Error updating settings: Invalid value provided" -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): + print_message(error_msg, message_type="error") + return return_message + + +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() - + rvc_set_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 + rvc_set_config.rvc_settings.rvc_enabled = rvc_enabled + rvc_set_config.rvc_settings.rvc_char_model_file = rvc_char_model_file + rvc_set_config.rvc_settings.rvc_narr_model_file = rvc_narr_model_file + rvc_set_config.rvc_settings.split_audio = split_audio + rvc_set_config.rvc_settings.autotune = autotune + rvc_set_config.rvc_settings.pitch = pitch + rvc_set_config.rvc_settings.filter_radius = filter_radius + rvc_set_config.rvc_settings.index_rate = index_rate + rvc_set_config.rvc_settings.rms_mix_rate = rms_mix_rate + rvc_set_config.rvc_settings.protect = protect + rvc_set_config.rvc_settings.hop_length = hop_length + rvc_set_config.rvc_settings.f0method = f0method + rvc_set_config.rvc_settings.embedder_model = embedder_model + rvc_set_config.rvc_settings.training_data_size = training_data_size def ensure_directory_exists(directory): + """Confirm a directory exists""" 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) + """Load and return JSON data from specified file path.""" + with open(json_path, "r", encoding="utf-8") as json_file: + return json.load(json_file) - 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 download_file(useurl, dest_path): + """ + Download file from URL and save to specified path. + + Args: + url (str): URL to download from + dest_path (str): Path where file will be saved + """ + # First number (5) = connection timeout (time to establish connection) + # Second number (30) = read timeout (time between bytes received) + file_response = requests.get(useurl, stream=True, timeout=(5, 30)) + file_response.raise_for_status() + with open(dest_path, "wb") as downloaded_file: + for chunk in file_response.iter_content(chunk_size=8192): + downloaded_file.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") + 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 + progress( + idx / len(file_urls), + desc=f"Downloading Required RVC Files: {file}...", + ) + print( + f"[{config.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." + download_result = ( + "All RVC Base Files are present." + if len(file_urls) > 0 + else "All files are present." + ) + print_message(download_result) # Save the updated configuration - config.save() + rvc_set_config.save() # Tell tts_server.py to update - get_alltalk_settings() - + 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)}" + except RequestException as e: + error_msg = f"Error downloading RVC files: {str(e)}" + return_message = "Error downloading RVC files: Network or server error" + except json.JSONDecodeError as e: + error_msg = f"Error reading RVC configuration file: {str(e)}" + return_message = "Error with RVC configuration: Invalid JSON format" + except (AttributeError, TypeError) as e: + error_msg = f"Configuration structure error: {str(e)}" + return_message = "Error updating RVC settings: Invalid configuration structure" + except (OSError, IOError) as e: + error_msg = f"File system error while saving/creating directories: {str(e)}" + return_message = "Error with RVC files: File system error" + print_message(error_msg, message_type="error") + return return_message + ########################################################################### # 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') +warnings.filterwarnings( + "ignore", message="Trying to detect encoding from a tiny portion" +) ########################################### # START-UP # AllTalk allowed startup time # @@ -361,9 +636,7 @@ startup_wait_time = 240 ############################################## # START-UP # Load confignew.json into params # ############################################## -this_dir = Path(__file__).parent.resolve() config = AlltalkConfig.get_instance() -branding = config.branding github_site = "erew123" github_repository = "alltalk_tts" github_branch = "alltalkbeta" @@ -372,20 +645,30 @@ 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 -running_in_docker = False -docker_url = f"http://localhost:{config.api_def.api_port_number}" + ############################################ # START-UP # Display initial splash screen # ############################################ -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 +# pylint: disable=line-too-long,anomalous-backslash-in-string +print_message( + "\033[94m _ _ _ \033[1;35m_____ _ _ \033[0m _____ _____ ____ " +) +print_message( + "\033[94m / \ | | |\033[1;35m_ _|_ _| | | __ \033[0m |_ _|_ _/ ___| " +) +print_message( + "\033[94m / _ \ | | |\033[1;35m | |/ _` | | |/ / \033[0m | | | | \___ \ " +) +print_message( + "\033[94m / ___ \| | |\033[1;35m | | (_| | | < \033[0m | | | | ___) |" +) +print_message( + "\033[94m/_/ \_\_|_|\033[1;35m |_|\__,_|_|_|\_\ \033[0m |_| |_| |____/ " +) print_message("") +# pylint: enable=line-too-long,anomalous-backslash-in-string + ############################################################################# # START-UP # Check current folder name has dashes '-' in it and error if so # @@ -397,34 +680,48 @@ this_script_dir = this_script_path.parent this_current_folder = this_script_dir.name if "-" in 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( # pylint: disable=line-too-long + "The current folder name contains a dash ('\033[93m-\033[0m') and this causes errors/issues. Please ensure", + message_type="warning", + ) + # pylint: disable=line-too-long + 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") + print_message( + "\033[92mCurrent folder:\033[0m {this_current_folder}", message_type="warning" + ) sys.exit(1) +# pylint: disable=ungrouped-imports,unused-import,import-outside-toplevel,import-error + ############################################## # START-UP # Check if we are on Google Colab # ############################################## def check_google_colab(): + """ + Test if we are running on a google colab server + """ debug_func_entry() try: - import google.colab + from google.colab import colab return True except ImportError: return False -running_on_google_colab = check_google_colab() +_state['running_on_google_colab'] = check_google_colab() ############################################################################### # START-UP # Test if we are running within Text-gen-webui or as a Standalone # ############################################################################### try: - import gradio as gr from modules import chat, shared, ui_chat from modules.logging_colors import logger from modules.ui import create_refresh_button from modules.utils import gradio + print_message("\033[92mStart-up Mode : \033[93mText-gen-webui mode\033[0m") running_in_standalone = False running_in_tgwui = True @@ -433,45 +730,63 @@ except ModuleNotFoundError: running_in_tgwui = False print_message("\033[92mStart-up Mode : \033[93mStandalone mode\033[0m") +# pylint: enable=ungrouped-imports,unused-import,import-outside-toplevel,import-error + ###################################################### # START-UP # Check if this is a first time start-up # ###################################################### def run_firsttime_script(): + """ + Run the first time startup script based on the current environment + (Google Colab, standalone, or TGWUI). + """ debug_func_entry() try: - if running_on_google_colab: - script_path = '/content/alltalk_tts/system/config/firstrun.py' + if _state['running_on_google_colab']: + firstrun_script_path = "/content/alltalk_tts/system/config/firstrun.py" elif running_in_standalone: - script_path = os.path.join(this_dir, 'system', 'config', 'firstrun.py') + firstrun_script_path = os.path.join(this_dir, "system", "config", "firstrun.py") elif running_in_tgwui: - script_path = os.path.join(this_dir, 'system', 'config', 'firstrun_tgwui.py') + firstrun_script_path = os.path.join( + this_dir, "system", "config", "firstrun_tgwui.py" + ) else: - script_path = os.path.join(this_dir, 'system', 'config', 'firstrun.py') + firstrun_script_path = os.path.join(this_dir, "system", "config", "firstrun.py") - subprocess.run([sys.executable, script_path], check=True) + subprocess.run([sys.executable, firstrun_script_path], check=True) except subprocess.CalledProcessError as e: - print_message("Error occurred while running the script: " + str(e), message_type="error") - except Exception as e: - print_message("An unexpected error occurred: " + str(e), message_type="error") + error_msg = f"Error running first-time setup script: {str(e)}" + print_message(error_msg, message_type="error") + except (FileNotFoundError, PermissionError) as e: + error_msg = f"File system error during first-time setup: {str(e)}" + print_message(error_msg, message_type="error") + # Call the function to run the startup script run_firsttime_script() + ########################################################### # START-UP # Delete files in outputs folder if configured # ########################################################### -def delete_old_files(folder_path, days_to_keep): +def delete_old_files(folder_path, amt_days_to_keep): + """ + Delete files in the output folder that are X days old + """ debug_func_entry() current_time = datetime.now() - print_message("\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): file_creation_time = datetime.fromtimestamp(os.path.getctime(file_path)) age = current_time - file_creation_time - if age > timedelta(days=days_to_keep): + if age > timedelta(days=amt_days_to_keep): os.remove(file_path) + # Check and perform file deletion if delete_output_wavs_setting.lower() == "disabled": print_message("\033[92mWAV file deletion :\033[93m Disabled\033[0m") @@ -480,153 +795,246 @@ else: days_to_keep = int(delete_output_wavs_setting.split()[0]) delete_old_files(output_folder, days_to_keep) except ValueError: - print_message("\033[92mWAV file deletion :\033[93m Invalid setting for deleting old wav files. Please use 'Disabled' or 'X Days' format\033[0m") + # pylint: disable=line-too-long + 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 format_datetime(iso_str): - """Formats an ISO datetime string into a human-readable format with ordinal day numbers. - + """ + 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")) - + suffix = "th" if 4 <= n % 100 <= 20 else {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th") + return f"{n}{suffix}" + dt = datetime.strptime(iso_str, "%Y-%m-%dT%H:%M:%SZ") return dt.strftime(f"{_ordinal(dt.day)} %B %Y at %H:%M") + def fetch_latest_commit_sha_and_date(owner, repo, branch): + """ + Fetch the latest commit SHA and date from a GitHub repository branch. + + Args: + owner (str): GitHub repository owner + repo (str): Repository name + branch (str): Branch name to check + + Returns: + tuple: (commit_sha, commit_date) or (None, None) if fetch fails + """ debug_func_entry() # Modified URL to include the branch - url = f"https://api.github.com/repos/{owner}/{repo}/commits/{branch}" + github_url = f"https://api.github.com/repos/{owner}/{repo}/commits/{branch}" try: - response = requests.get(url) - if response.status_code == 200: - commit_data = response.json() - latest_commit_sha = commit_data['sha'] - latest_commit_date = commit_data['commit']['committer']['date'] - return latest_commit_sha, latest_commit_date - else: - 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_message("\033[92mGithub updated :\033[91m Could not reach GitHub to check for updates\033[0m") + # Add timeout for both connect and read operations + github_response = requests.get(github_url, timeout=(5, 30)) + if github_response.status_code == 200: + commit_data = github_response.json() + github_latest_commit_sha = commit_data["sha"] + github_latest_commit_date = commit_data["commit"]["committer"]["date"] + return github_latest_commit_sha, github_latest_commit_date + # pylint: disable=line-too-long + 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 (RequestsConnectionError, requests.Timeout): # Added Timeout to caught exceptions + 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): + """ + Read SHA from existing file or initialize with latest commit SHA if file doesn't exist. + """ debug_func_entry() if os.path.exists(file_path): - with open(file_path, 'r') as file: - data = json.load(file) - return data.get("last_known_commit_sha") + with open(file_path, "r", encoding="utf-8") as file: + current_data = json.load(file) + return current_data.get("last_known_commit_sha") else: # File doesn't exist, fetch the latest SHA and create the file - latest_commit_sha, _ = fetch_latest_commit_sha_and_date(owner, repo, branch) + current_commit_sha, _ = fetch_latest_commit_sha_and_date(owner, repo, branch) if latest_commit_sha: - with open(file_path, 'w') as file: - json.dump({"last_known_commit_sha": latest_commit_sha}, file) - return latest_commit_sha + with open(file_path, "w", encoding="utf-8") as file: + json.dump({"last_known_commit_sha": current_commit_sha}, file) + return current_commit_sha return None + def update_sha_file(file_path, new_sha): + """ + Update the stored commit SHA in the specified file. + """ debug_func_entry() - with open(file_path, 'w') as file: + with open(file_path, "w", encoding="utf-8") as file: json.dump({"last_known_commit_sha": new_sha}, file) + # Define the file path based on your directory structure sha_file_path = this_dir / "system" / "config" / "at_github_sha.json" # Read or initialize the SHA -last_known_commit_sha = read_or_initialize_sha(sha_file_path, github_site, github_repository, github_branch) +last_known_commit_sha = read_or_initialize_sha( + sha_file_path, github_site, github_repository, github_branch +) # Fetch the latest commit SHA and date from the specific branch -latest_commit_sha, latest_commit_date = fetch_latest_commit_sha_and_date(github_site, github_repository, github_branch) +latest_commit_sha, latest_commit_date = fetch_latest_commit_sha_and_date( + github_site, github_repository, github_branch +) -formatted_date = format_datetime(latest_commit_date) if latest_commit_date else "an unknown date" +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_message(f"\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_message(f"\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 handler #### ################################################## -def signal_handler(sig, frame): +def signal_handler(sig, frame): # pylint: disable=unused-argument + """Handle Ctrl+C signal by saving config, terminating subprocess, and exiting gracefully.""" 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 + print_message( + "\033[94mReceived Ctrl+C, terminating subprocess. Kill your Python processes if this fails to exit.\033[92m" + ) + if _state["process"].poll() is None: + _state["process"].terminate() + _state["process"].wait() # Wait for the subprocess to finish sys.exit(0) ##################################################################### # START-UP # Start the Subprocess and Check for Google Colab/Docker # ##################################################################### -try: - import google.colab - running_on_google_colab = True - with open('/content/alltalk_tts/googlecolab.json', 'r') as f: - data = json.load(f) - tunnel_url_1, tunnel_url_2 = data.get('google_ip_address', [None, None]) -except FileNotFoundError: - print_message("Could not find IP address") - tunnel_url_1, tunnel_url_2 = None, None -except ImportError: - running_on_google_colab = False +# pylint: disable=ungrouped-imports,unused-import,import-outside-toplevel,import-error +if check_google_colab(): + _state['running_on_google_colab'] = True + try: + with open("/content/alltalk_tts/googlecolab.json", "r", encoding="utf-8") as f: + data = json.load(f) + _state['tunnel_url_1'], _state['tunnel_url_2'] = data.get("google_ip_address", [None, None]) + except FileNotFoundError: + print_message("Could not find IP address") + _state['tunnel_url_1'], _state['tunnel_url_2'] = None, None +else: + _state['running_on_google_colab'] = False +# pylint: enable=ungrouped-imports,unused-import,import-outside-toplevel + # Attach the signal handler to the SIGINT signal (Ctrl+C) system_signal.signal(system_signal.SIGINT, signal_handler) # Check if we're running in docker -if os.path.isfile("/.dockerenv") and 'google.colab' not in sys.modules: +if os.path.isfile("/.dockerenv") and "google.colab" not in sys.modules: 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( + "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( + " - 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( + 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 + 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." + ) + _state['running_in_docker'] = True else: - running_in_docker = False + _state['running_in_docker'] = False # Start the subprocess (now unified for both Docker and non-Docker environments) -process = subprocess.Popen([sys.executable, script_path]) +# Not using 'with' as we want the process to run in the background +# pylint: disable=consider-using-with +_state["process"] = subprocess.Popen([sys.executable, script_path]) # Check if the subprocess has started successfully -if process.poll() is None: - if running_on_google_colab: +if _state["process"].poll() is None: + if _state['running_on_google_colab']: print_message("") print_message("\033[94mGoogle Colab Detected\033[00m") print_message("") else: - 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") + 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", + ) + # pylint: disable=line-too-long + 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", + ) + # pylint: disable=line-too-long + print_message( + "xxxxx is the type of OS you are on e.g. windows, linux or mac.", + message_type="warning", + ) + # pylint: disable=line-too-long + 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) @@ -643,110 +1051,174 @@ warning_displayed = False url = f"http://localhost:{config.api_def.api_port_number}/api/ready" while time.time() - start_time < timeout: try: - response = requests.get(url) + response = requests.get(url, timeout=(5, 30)) if response.status_code == 200 and response.text == "Ready": break - except requests.RequestException as e: + except RequestException as e: # Log the exception if needed pass if not warning_displayed and time.time() - start_time >= warning_delay: - 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") + 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_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( + "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: +if _state['running_on_google_colab']: 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("\033[94mAPI Address :\033[00m \033[92m" + _state['tunnel_url_1'] + "\033[00m") + print_message("\033[94mGradio Light:\033[00m \033[92m" + _state['tunnel_url_2'] + "\033[00m") + print_message( + "\033[94mGradio Dark :\033[00m \033[92m" + + _state['tunnel_url_2'] + + "?__theme=dark\033[00m" + ) print_message("") else: 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( + "\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( + "\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(): + """ + Test to see if espeak-ng is installed and available. It's required + by various TTS engines. + """ 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_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") + + def print_espeak_warning(platform_name, install_instructions): + print_message("") + print_message( + f"Espeak-ng for {platform_name}\033[91m WAS NOT FOUND. \033[0mYou can install\033[0m", + message_type="warning", + ) + print_message( + f"\033[0mit using {install_instructions}\033[0m", + message_type="warning", + ) + + try: + subprocess.run( + ["espeak-ng", "--version"], capture_output=True, text=True, check=True + ) + return + except FileNotFoundError: + if platform.system() == "Windows": + print_espeak_warning( + "Windows", r"this location \033[93m\alltalk_tts\system\espeak-ng\\" + ) + 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_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_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("") + elif platform.system() == "Darwin": # macOS + print_espeak_warning( + "macOS", "\033[93mHomebrew: brew install espeak-ng\033[0m" + ) + else: # Linux + print_espeak_warning( + "Linux", + "\033[93myour package manager, e.g., apt-get install espeak-ng\033[0m", + ) + except subprocess.CalledProcessError: + # Handle cases where `espeak-ng` exists but fails to run + print_message("Error running espeak-ng. Please check the installation.", message_type="error") + print_message("") + + check_espeak_ng() #################################### # START-UP # Subprecess management # #################################### + def start_subprocess(): + """Start a new subprocess if none is running. Return status message.""" debug_func_entry() - global process - if process is None or process.poll() is not None: - process = subprocess.Popen([sys.executable, script_path]) + if _state["process"] is None or _state["process"].poll() is not None: + # pylint: disable=consider-using-with # Process needs to run in background + _state["process"] = subprocess.Popen([sys.executable, script_path]) return "Subprocess started." - else: - return "Subprocess is already running." + return "Subprocess is already running." + def stop_subprocess(): + """Terminate running subprocess and wait for completion. Return status message.""" debug_func_entry() - global process - if process is not None: - process.terminate() - process.wait() - process = None + if _state["process"] is not None: + _state["process"].terminate() + _state["process"].wait() + _state["process"] = None return "Subprocess stopped." - else: - return "Subprocess is not running." + return "Subprocess is not running." + def restart_subprocess(): + """Stop current subprocess, display TTS engine swap message, and start new subprocess.""" debug_func_entry() stop_subprocess() print_message("") @@ -754,44 +1226,33 @@ def restart_subprocess(): print_message("") return start_subprocess() + def check_subprocess_status(): + """Check if subprocess is currently running and return status message.""" debug_func_entry() - global process - if process is None or process.poll() is not None: + if _state["process"] is None or _state["process"].poll() is not None: return "Subprocess is not running." - else: - return "Subprocess is running." + return "Subprocess is running." + ################################################################### # START-UP # Register the termination code to be executed at exit # ################################################################### -atexit.register(lambda: process.terminate() if process.poll() is None else None) +atexit.register(lambda: _state["process"].terminate() if _state["process"].poll() is None else None) ########################## # Setup global variables # ########################## -# 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) -current_model_loaded = None -#deepspeed_installed = True +# deepspeed_installed = True tgwui_lovram = False tgwui_deepspeed = False # Create a global lock for tracking TTS generation occuring 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:{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 -models_available = None # Gets populated with the list of all models/engines available on the AllTalk Server -at_settings = None # Gets populated with the list of all settings currently set on the AllTalk Server -current_model_loaded = None # Gets populated with the name of the current loaded TTS engine/model on the AllTalk Server # Used to detect if a model is loaded in to AllTalk server to block TTS genereation if needed. tts_model_loaded = None - ######################### # Endpoint API Builders # ######################### @@ -802,39 +1263,48 @@ def build_url(endpoint, include_api=True): Examples: - voice2rvc - - reload + - reload - stop-generation Args: endpoint (str): The API endpoint to append to the base URL include_api (bool): Whether to include /api/ in the URL True: Uses '/api/' - False: Uses '' + False: Uses '' Returns: str: Complete URL in format: {base_url}/api/{endpoint} where base_url is determined by runtime environment: - - Colab: tunnel_url_1 - - Docker: docker_url - - Local: alltalk_protocol + alltalk_ip_port + - Colab: _state['tunnel_url_1'], _state['tunnel_url_2'] + - Docker: _state['docker_url'] + - Local: _state['alltalk_protocol'] + _state['alltalk_ip_port'] """ debug_func_entry() - global running_on_google_colab, running_in_docker, tunnel_url_1, docker_url, alltalk_protocol, alltalk_ip_port - print_message(f"\033[94mRunning on Docker is: \033[0m{running_in_docker}", message_type="debug_gradio_IP") - print_message(f"\033[94mRunning on Google is: \033[0m{running_on_google_colab}", message_type="debug_gradio_IP") + print_message( + f"\033[94mRunning on Docker is: \033[0m{_state['running_in_docker']}", + message_type="debug_gradio_IP", + ) + print_message( + f"\033[94mRunning on Google is: \033[0m{_state['running_on_google_colab']}", + message_type="debug_gradio_IP", + ) - if running_on_google_colab: - base_url = tunnel_url_1 - elif running_in_docker: - base_url = docker_url + if _state['running_on_google_colab']: + base_url = _state['tunnel_url_1'] + elif _state['running_in_docker']: + base_url = _state['docker_url'] else: - base_url = f"{alltalk_protocol}{alltalk_ip_port}" + base_url = f"{_state['alltalk_protocol']}{_state['alltalk_ip_port']}" prefix = "/api/" if include_api else "/" final_url = f"{base_url}{prefix}{endpoint.lstrip('/')}" - print_message(f"\033[94mBase URL is set as : \033[0m{final_url}", message_type="debug_gradio_IP") + print_message( + f"\033[94mBase URL is set as : \033[0m{final_url}", + message_type="debug_gradio_IP", + ) return final_url + def build_dynamic_url(endpoint, include_protocol=True, include_api=True): """ Build URL for endpoints that may need the current domain. @@ -843,7 +1313,7 @@ def build_dynamic_url(endpoint, include_protocol=True, include_api=True): Examples: - tts-generate - - tts-generate-streaming + - tts-generate-streaming - File URLs (with include_protocol=True) Args: @@ -857,39 +1327,48 @@ def build_dynamic_url(endpoint, include_protocol=True, include_api=True): Returns: str: Complete URL with format depending on environment: - - Colab: tunnel_url_1/api/{endpoint} - - Docker: docker_url/api/{endpoint} - - Local: {protocol_prefix}{my_current_url}/api/{endpoint} + - Colab: _state['tunnel_url_1']/api/{endpoint} + - Docker: _state['docker_url']/api/{endpoint} + - Local: {protocol_prefix}{_state['my_current_url']}/api/{endpoint} """ debug_func_entry() - global running_on_google_colab, running_in_docker, tunnel_url_1, docker_url, my_current_url, protocol - print_message(f"\033[94mRunning on Docker is: \033[0m{running_in_docker}", message_type="debug_gradio_IP") - print_message(f"\033[94mRunning on Google is: \033[0m{running_on_google_colab}", message_type="debug_gradio_IP") + print_message( + f"\033[94mRunning on Docker is: \033[0m{_state['running_in_docker']}", + message_type="debug_gradio_IP", + ) + print_message( + f"\033[94mRunning on Google is: \033[0m{_state['running_on_google_colab']}", + message_type="debug_gradio_IP", + ) prefix = "/api/" if include_api else "/" - if running_on_google_colab: - base_url = tunnel_url_1 - elif running_in_docker: - base_url = docker_url + if _state['running_on_google_colab']: + base_url = _state['tunnel_url_1'] + elif _state['running_in_docker']: + base_url = _state['docker_url'] else: - protocol_prefix = f"{protocol}" if include_protocol else "http://" - base_url = f"{protocol_prefix}{my_current_url}" + protocol_prefix = f"{_state['alltalk_protocol']}" if include_protocol else "http://" + base_url = f"{protocol_prefix}{_state['my_current_url']}" final_url = f"{base_url}{prefix}{endpoint.lstrip('/')}" - print_message(f"\033[94mBase URL is set as : \033[0m{final_url}", message_type="debug_gradio_IP") + print_message( + f"\033[94mBase URL is set as : \033[0m{final_url}", + message_type="debug_gradio_IP", + ) return final_url + def update_docker_address(new_url): """ Used within the gradio interface to update the IP/URL when running inside a docker environment. """ - global docker_url - docker_url = new_url - print_message(f"\033[94mDocker IP/URL for API set to: \033[0m{new_url}") + _state['docker_url'] = new_url + print_message(f"\033[94mDocker IP/URL for API set to: \033[0m{new_url}") return f"Docker IP/URL for API set to: {new_url}" + ################################################# # Pull all the settings from the AllTalk Server # ################################################# @@ -900,7 +1379,9 @@ def get_alltalk_settings(): debug_func_entry() # Define endpoints - endpoints = {name: build_url(name) for name in ['voices', 'rvcvoices', 'currentsettings']} + endpoints = { + name: build_url(name) for name in ["voices", "rvcvoices", "currentsettings"] + } # Default settings in case of failure default_settings = { @@ -941,14 +1422,19 @@ def get_alltalk_settings(): # 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="TTS") - print_message(f"Please confirm you have the correct IP/URL & that the system isnt currenly restarting", - message_type="warning", - component="TTS") + 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="TTS", + ) + print_message( + "Please confirm you have the correct IP/URL & that the system isnt currenly restarting", + message_type="warning", + component="TTS", + ) return default_settings # Parse responses @@ -956,94 +1442,133 @@ def get_alltalk_settings(): rvcvoices_data = responses["rvcvoices"].json() settings_data = responses["currentsettings"].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"] + # Update state dictionary with all current values + _state.update({ + 'srv_current_voices': sorted(voices_data["voices"], key=lambda s: s.lower()), + 'srv_current_rvcvoices': rvcvoices_data["rvcvoices"], + 'srv_engines_available': sorted(settings_data["engines_available"]), + 'srv_current_engine_loaded': settings_data["current_engine_loaded"], + 'srv_models_available': sorted([model["name"] for model in settings_data["models_available"]]), + 'srv_current_model_loaded': settings_data["current_model_loaded"], + 'srv_character_voice': config.tgwui.tgwui_character_voice, + 'srv_narrator_voice': config.tgwui.tgwui_narrator_voice, + 'srv_rvc_character_voice': config.rvc_settings.rvc_char_model_file, + 'srv_rvc_narrator_voice': config.rvc_settings.rvc_narr_model_file, + 'srv_settings_capabilities': { + 'temperature_capable': settings_data["temperature_capable"], + 'repetitionpenalty_capable': settings_data["repetitionpenalty_capable"], + 'languages_capable': settings_data["languages_capable"], + 'generationspeed_capable': settings_data["generationspeed_capable"], + 'pitch_capable': settings_data["pitch_capable"], + 'streaming_capable': settings_data["streaming_capable"] + } + }) # Construct and return settings dictionary - 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, - **{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"]} + base_settings = { + "voices": _state['srv_current_voices'], + "rvcvoices": _state['srv_current_rvcvoices'], + "engines_available": _state['srv_engines_available'], + "current_engine_loaded": _state['srv_current_engine_loaded'], + "models_available": _state['srv_models_available'], + "current_model_loaded": _state['srv_current_model_loaded'], } - except (RequestException, ConnectionError) as e: - print_message(f"Unable to connect to the {branding} server: {str(e)}", - message_type="warning", - component="TTS") - print_message(f"Please confirm you have the correct IP/URL & that the system isnt currenly restarting", - message_type="warning", - component="TTS") + # Add remaining settings + return { + **base_settings, + **{ + k: settings_data.get(k, v) + for k, v in default_settings.items() + if k not in base_settings + } + } + + except (RequestException, RequestsConnectionError) as e: + print_message( + f"Unable to connect to the {config.branding} API server: {str(e)}", + message_type="warning", + component="TTS", + ) + print_message( + "Please confirm you have the correct IP/URL & that the system isnt currenly restarting", + message_type="warning", + component="TTS", + ) return default_settings + # Pull all the current settings from the AllTalk server, if its online. -at_settings = get_alltalk_settings() +get_alltalk_settings() ############################# #### TTS STOP GENERATION #### ############################# def stop_generate_tts(): + """ + Sends a stop TTS generation request + """ debug_func_entry() - api_url = build_url('stop-generation') + api_url = build_url("stop-generation") try: - response = requests.put(api_url, timeout=connection_timeout) - if response.status_code == 200: - return response.json()["message"] - else: - 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_message(f"Unable to connect to the {branding} server. Status code:\n{str(e)}", message_type="warning") + stop_response = requests.put(api_url, timeout=connection_timeout) + if stop_response.status_code == 200: + return stop_response.json()["message"] + print_message( + f"Failed to stop generation. Status code:\n{stop_response.status_code}", + message_type="warning", + ) return {"message": "Failed to stop generation"} + except (RequestException, RequestsConnectionError) as e: + print_message( + f"Unable to connect to the {config.branding} server. Status code:\n{str(e)}", + message_type="warning", + ) + return {"message": "Failed to stop generation"} + 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 - base_url = build_url(endpoint.lstrip('/')) + base_url = build_url(endpoint.lstrip("/")) - response = requests.post( + api_response = requests.post( base_url, json=payload, headers=headers or {"Content-Type": "application/json"}, - params=params + params=params, + timeout=(5, 30) # 5 sec connect, 30 sec read timeout ) - response.raise_for_status() - + api_response.raise_for_status() + return { "status": "success", - "response": response.json() if response.content else None, - "raw_response": response + "response": api_response.json() if api_response.content else None, + "raw_response": api_response, } - except requests.exceptions.RequestException as e: - print_message(f"Error during request to webserver process: Status code:\n{e}", - message_type="warning") + except 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)} + def send_reload_request(value_sent): """Sends a request to reload the TTS model. - + Args: value_sent: The TTS method/model to load """ @@ -1051,102 +1576,93 @@ def send_reload_request(value_sent): 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 - """ - debug_func_entry() - config.tgwui.tts_model_loaded = False - - response = send_api_request( - "/api/lowvramsetting", - params={"new_low_vram_value": value_sent} - ) - - 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 ######################################## #### Whisper Transcription Handling #### ######################################## +# pylint: disable=too-few-public-methods class TranscriptionProgress: + """Track progress of Whisper's audio transcription tasks including file counts and completion status.""" def __init__(self): self.current_file = "" self.total_files = 0 self.completed_files = 0 self.current_progress = 0 + whisper_progress = TranscriptionProgress() # File size constants (in bytes) WARNING_SIZE = 25 * 1024 * 1024 # 25MB -MAX_SIZE = 100 * 1024 * 1024 # 100MB +MAX_SIZE = 100 * 1024 * 1024 # 100MB # Allowed audio formats ALLOWED_AUDIO_FORMATS = { - '.mp3', '.wav', '.m4a', '.ogg', '.flac', '.aac', - '.wma', '.aiff', '.alac', '.opus' + ".mp3", + ".wav", + ".m4a", + ".ogg", + ".flac", + ".aac", + ".wma", + ".aiff", + ".alac", + ".opus", } -def setup_directories(script_dir): + +def setup_directories(setup_script_dir): """Setup required directories""" debug_func_entry() - base_dir = os.path.join(script_dir, "transcriptions") + base_dir = os.path.join(setup_script_dir, "transcriptions") uploads_dir = os.path.join(base_dir, "uploads") output_dir = os.path.join(base_dir, "output") - + for directory in [base_dir, uploads_dir, output_dir]: os.makedirs(directory, exist_ok=True) print_message(f"Created/verified directory: {directory}", "debug_transcribe") - + return base_dir, uploads_dir, output_dir + def validate_audio_file(file_path): """Validate file type and size""" debug_func_entry() file_size = os.path.getsize(file_path) file_ext = os.path.splitext(file_path)[1].lower() mime_type = mimetypes.guess_type(file_path)[0] - + print_message(f"Validating file: {file_path}", "debug_transcribe") print_message(f"File size: {file_size/1024/1024:.1f}MB", "debug_transcribe") print_message(f"File extension: {file_ext}", "debug_transcribe") print_message(f"MIME type: {mime_type}", "debug_transcribe") - - warnings = [] + + validate_warnings = [] errors = [] - + if file_ext not in ALLOWED_AUDIO_FORMATS: msg = f"Invalid file format: {file_ext}" print_message(msg, "error") errors.append(msg) - - if mime_type and not mime_type.startswith('audio/'): + + if mime_type and not mime_type.startswith("audio/"): msg = f"File does not appear to be audio: {mime_type}" print_message(msg, "error") errors.append(msg) - + if file_size > MAX_SIZE: - msg = f"File too large: {file_size/1024/1024:.1f}MB (max {MAX_SIZE/1024/1024}MB)" + msg = ( + f"File too large: {file_size/1024/1024:.1f}MB (max {MAX_SIZE/1024/1024}MB)" + ) print_message(msg, "error") errors.append(msg) elif file_size > WARNING_SIZE: msg = f"File is large ({file_size/1024/1024:.1f}MB) and may take longer to process" print_message(msg, "warning") - warnings.append(msg) - - return warnings, errors + validate_warnings.append(msg) + + return validate_warnings, errors + def cleanup_gpu_memory(): """Thorough GPU memory cleanup""" @@ -1157,11 +1673,12 @@ def cleanup_gpu_memory(): # Force garbage collection gc.collect() # Clear any remaining CUDA memory - with torch.cuda.device('cuda'): + with torch.cuda.device("cuda"): torch.cuda.empty_cache() torch.cuda.ipc_collect() print_message("GPU memory cleaned", "debug_transcribe") + def create_output_directory(base_dir, prefix=""): """Create a uniquely named output directory for this batch of transcriptions""" debug_func_entry() @@ -1172,6 +1689,7 @@ def create_output_directory(base_dir, prefix=""): print_message(f"Created output directory: {output_dir}", "debug_transcribe") return output_dir + def create_output_filename(input_path, output_dir, prefix=""): """Create an output filename based on the input path""" debug_func_entry() @@ -1180,15 +1698,16 @@ def create_output_filename(input_path, output_dir, prefix=""): return os.path.join(output_dir, f"{prefix}_{stem}") return os.path.join(output_dir, stem) + def delete_uploaded_files(): """Delete all files in the uploads directory""" debug_func_entry() - script_dir = os.path.dirname(os.path.abspath(__file__)) - uploads_dir = os.path.join(script_dir, "transcriptions", "uploads") + del_upload_script_dir = os.path.dirname(os.path.abspath(__file__)) + uploads_dir = os.path.join(del_upload_script_dir, "transcriptions", "uploads") deleted_count = 0 - + print_message(f"Attempting to delete files in: {uploads_dir}", "debug_transcribe") - + if os.path.exists(uploads_dir): for file in os.listdir(uploads_dir): file_path = os.path.join(uploads_dir, file) @@ -1197,208 +1716,325 @@ def delete_uploaded_files(): os.unlink(file_path) deleted_count += 1 print_message(f"Deleted: {file}", "debug_transcribe") - except Exception as e: - error_msg = f"Error while deleting {file}: {str(e)}" + except FileNotFoundError: + error_msg = f"File not found while trying to delete: {file}" print_message(error_msg, "error") - return error_msg - + except PermissionError: + error_msg = f"Permission denied while trying to delete: {file}" + print_message(error_msg, "error") + except OSError as e: + error_msg = f"OS error while deleting {file}: {str(e)}" + print_message(error_msg, "error") + msg = f"Deleted {deleted_count} uploaded audio files" print_message(msg) return msg - + msg = "No files to delete - uploads directory not found" print_message(msg, "warning") return msg + def create_srt_content(segments): """Convert whisper segments to SRT format""" - debug_func_entry() + debug_func_entry() srt_content = [] for i, segment in enumerate(segments, start=1): # Convert start/end times to SRT format (HH:MM:SS,mmm) - start_time = timedelta(seconds=segment['start']) - end_time = timedelta(seconds=segment['end']) - + srt_start_time = timedelta(seconds=segment["start"]) + srt_end_time = timedelta(seconds=segment["end"]) + # Format with milliseconds - start_str = str(start_time).replace('.', ',')[:11] - end_str = str(end_time).replace('.', ',')[:11] - + start_str = str(srt_start_time).replace(".", ",")[:11] + end_str = str(srt_end_time).replace(".", ",")[:11] + # Ensure proper formatting for times less than 1 hour if len(start_str) < 11: start_str = "0" + start_str if len(end_str) < 11: end_str = "0" + end_str - - # Build SRT segment - srt_content.append(f"{i}\n{start_str} --> {end_str}\n{segment['text'].strip()}\n") - - return "\n".join(srt_content) - -def process_audio_files(audio_files, model_size, output_format, delete_after, prefix="", gradio_progress=gr.Progress(track_tqdm=True)): + # Build SRT segment + srt_content.append( + f"{i}\n{start_str} --> {end_str}\n{segment['text'].strip()}\n" + ) + + return "\n".join(srt_content) + + +def process_audio_files( + audio_files, + model_size, + output_format, + delete_after, + prefix="", + gradio_progress=gr.Progress(track_tqdm=True), # pylint: disable=unused-argument # Used by tqdm for progress tracking +): """Process multiple audio files and return paths to transcription files""" debug_func_entry() try: - print_message(f"Starting transcription process with model: {model_size}", "debug_transcribe") - + print_message( + f"Starting transcription process with model: {model_size}", + "debug_transcribe", + ) + # Setup directories - script_dir = os.path.dirname(os.path.abspath(__file__)) - base_dir, uploads_dir, _ = setup_directories(script_dir) - + transcribe_script_dir = os.path.dirname(os.path.abspath(__file__)) + base_dir, uploads_dir, _ = setup_directories(transcribe_script_dir) + # Create specific output directory for this batch output_dir = create_output_directory(base_dir, prefix) - print_message(f"Using prefix: {prefix if prefix else 'none'}", "debug_transcribe") - + print_message( + f"Using prefix: {prefix if prefix else 'none'}", "debug_transcribe" + ) + # Validate and copy files to uploads directory processed_files = [] validation_messages = [] - + + if not audio_files: + msg = "No audio files provided for processing." + print_message(msg, "error") + return None, msg + for audio_file in audio_files: print_message(f"Processing file: {audio_file}", "debug_transcribe") - warnings, errors = validate_audio_file(audio_file) + transcribe_warnings, errors = validate_audio_file(audio_file) file_name = Path(audio_file).name - + if errors: for error in errors: validation_messages.append(f"Skipping {file_name}: {error}") continue - - validation_messages.extend([f"Warning for {file_name}: {warning}" for warning in warnings]) - + + validation_messages.extend( + [f"Warning for {file_name}: {warning}" for warning in transcribe_warnings] + ) + # Copy file to uploads directory upload_path = os.path.join(uploads_dir, file_name) shutil.copy2(audio_file, upload_path) print_message(f"Copied to uploads: {upload_path}", "debug_transcribe") processed_files.append(upload_path) - + if not processed_files: msg = "No valid files to process\n" + "\n".join(validation_messages) print_message(msg, "error") return None, msg - + # Load model device = "cuda" if torch.cuda.is_available() else "cpu" - print_message(f"Loading Whisper model {model_size} on {device}", "debug_transcribe") - model = whisper.load_model(model_size, device=device) - + print_message( + f"Loading Whisper model {model_size} on {device}", "debug_transcribe" + ) + _state['whisper_model'] = whisper.load_model(model_size, device=device) + output_files = [] metadata = [] - + whisper_progress.total_files = len(processed_files) - - for idx, audio_file in enumerate(tqdm(processed_files, desc="Processing files")): + + for idx, audio_file in enumerate( + tqdm(processed_files, desc="Processing files") + ): try: whisper_progress.current_file = Path(audio_file).name whisper_progress.completed_files = idx print_message(f"Transcribing: {audio_file}", "debug_transcribe") - + # Transcribe audio - result = model.transcribe(audio_file) - + result = _state['whisper_model'].transcribe(audio_file) + # Create output file - base_output_path = create_output_filename(audio_file, output_dir, prefix) - + base_output_path = create_output_filename( + audio_file, output_dir, prefix + ) + # Save based on format if output_format == "txt": output_path = f"{base_output_path}.txt" - with open(output_path, 'w', encoding='utf-8') as f: - f.write(result["text"]) + with open(output_path, "w", encoding="utf-8") as file: + file.write(result["text"]) elif output_format == "srt": output_path = f"{base_output_path}.srt" - with open(output_path, 'w', encoding='utf-8') as f: - f.write(create_srt_content(result["segments"])) + with open(output_path, "w", encoding="utf-8") as file: + file.write(create_srt_content(result["segments"])) else: # json output_path = f"{base_output_path}.json" - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(result, f, indent=4, ensure_ascii=False) - - print_message(f"Saved transcription to: {output_path}", "debug_transcribe") + with open(output_path, "w", encoding="utf-8") as file: + json.dump(result, file, indent=4, ensure_ascii=False) + + print_message( + f"Saved transcription to: {output_path}", "debug_transcribe" + ) output_files.append(output_path) - + # Collect metadata - metadata.append({ - "original_file": Path(audio_file).name, - "duration": result["segments"][-1]["end"] if result["segments"] else 0, - "output_file": Path(output_path).name, - "model_used": model_size, - "format": output_format, - "timestamp": datetime.now().isoformat() - }) - + metadata.append( + { + "original_file": Path(audio_file).name, + "duration": ( + result["segments"][-1]["end"] if result["segments"] else 0 + ), + "output_file": Path(output_path).name, + "model_used": model_size, + "format": output_format, + "timestamp": datetime.now().isoformat(), + } + ) + # Delete audio file if requested if delete_after: os.unlink(audio_file) - print_message(f"Deleted audio file: {audio_file}", "debug_transcribe") - - except Exception as e: - error_msg = f"Error processing {Path(audio_file).name}: {str(e)}" + print_message( + f"Deleted audio file: {audio_file}", "debug_transcribe" + ) + + except (whisper.RuntimeError, torch.cuda.OutOfMemoryError) as e: + error_msg = f"Error in transcription of {Path(audio_file).name}: {str(e)}" print_message(error_msg, "error") validation_messages.append(error_msg) continue - + except (OSError, IOError) as e: + error_msg = f"File error processing {Path(audio_file).name}: {str(e)}" + print_message(error_msg, "error") + validation_messages.append(error_msg) + continue + except (ValueError, TypeError) as e: + error_msg = f"Data processing error in {Path(audio_file).name}: {str(e)}" + print_message(error_msg, "error") + validation_messages.append(error_msg) + continue + # Create summary file - summary_path = os.path.join(output_dir, f"summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json") - with open(summary_path, 'w', encoding='utf-8') as f: - json.dump({ - "processed_files": metadata, - "total_files": len(processed_files), - "successful_transcriptions": len(output_files), - "processing_date": datetime.now().isoformat(), - "validation_messages": validation_messages - }, f, indent=4) - + summary_path = os.path.join( + output_dir, f"summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + ) + with open(summary_path, "w", encoding="utf-8") as file: + json.dump( + { + "processed_files": metadata, + "total_files": len(processed_files), + "successful_transcriptions": len(output_files), + "processing_date": datetime.now().isoformat(), + "validation_messages": validation_messages, + }, + f, + indent=4, + ) + print_message(f"Created summary file: {summary_path}", "debug_transcribe") - + # Create ZIP file containing all outputs - zip_path = os.path.join(base_dir, "output", f"{prefix}_transcriptions_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip" if prefix else f"transcriptions_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip") - with zipfile.ZipFile(zip_path, 'w') as zipf: + zip_path = os.path.join( + base_dir, + "output", + ( + f"{prefix}_transcriptions_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip" + if prefix + else f"transcriptions_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip" + ), + ) + with zipfile.ZipFile(zip_path, "w") as zipf: for file in output_files + [summary_path]: # Preserve the directory structure in the ZIP arcname = os.path.relpath(file, os.path.dirname(output_dir)) zipf.write(file, arcname) - + print_message(f"Created ZIP file: {zip_path}", "debug_transcribe") - - status_message = "\n".join(validation_messages + [ - f"Processed {m['original_file']}: {m['duration']:.2f} seconds" - for m in metadata - ]) + + status_message = "\n".join( + validation_messages + + [ + f"Processed {m['original_file']}: {m['duration']:.2f} seconds" + for m in metadata + ] + ) status_message += f"\nFiles saved in: {output_dir}" - + return zip_path, status_message - + + except (OSError, IOError) as e: + print_message(f"File system error: {str(e)}", "error") + return None, f"File system error: {str(e)}" + except torch.cuda.OutOfMemoryError as e: + print_message(f"GPU memory error: {str(e)}", "error") + return None, "GPU memory error: Consider using a smaller model" + except ValueError as e: + print_message(f"Invalid input error: {str(e)}", "error") + return None, f"Configuration error: {str(e)}" finally: # Cleanup GPU memory - if 'model' in locals(): - del model + if 'whisper_model' in _state: + del _state['whisper_model'] cleanup_gpu_memory() + ######################## #### Live Dictation #### ######################## +def reset_audio_stream_handlers(dictate_audio, state, text_output, audio_plot): + """Reset handlers for the dictation audio input.""" + if not isinstance(dictate_audio, gr.Audio): + print_message("Error: 'dictate_audio' is not a valid Gradio Audio component.", "error") + return + + # Clear existing handlers + if hasattr(dictate_audio, "stop_recording"): + dictate_audio.stop_recording(fn=None) + if hasattr(dictate_audio, "start_recording"): + dictate_audio.start_recording(fn=None) + if hasattr(dictate_audio, "stream"): + dictate_audio.stream(fn=None) + + # Re-add the handlers # Ignore them being undefined + dictate_audio.start_recording( + fn=on_start_recording, inputs=[state], outputs=[state] # pylint: disable=undefined-variable + ).success(fn=None, js="() => {console.log('Recording started');}") + + dictate_audio.stop_recording( + fn=on_stop_recording, inputs=[state], outputs=[state] # pylint: disable=undefined-variable + ).success(fn=None, js="() => {console.log('Recording stopped');}") + + dictate_audio.stream( + fn=process_audio, + inputs=[dictate_audio, state], + outputs=[state, text_output, audio_plot], + show_progress=False, + concurrency_limit=1, + ) + + def setup_transcription_directory(): """Setup directory for saving transcriptions""" debug_func_entry() - script_dir = os.path.dirname(os.path.abspath(__file__)) - transcripts_dir = os.path.join(script_dir, "transcriptions", "live_dictation") + trans_script_dir = os.path.dirname(os.path.abspath(__file__)) + transcripts_dir = os.path.join(trans_script_dir, "transcriptions", "live_dictation") os.makedirs(transcripts_dir, exist_ok=True) return transcripts_dir + def create_transcript_file(directory, prefix=""): """Create a new transcript file with timestamp""" debug_func_entry() timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"{prefix}_dictation_{timestamp}.txt" if prefix else f"dictation_{timestamp}.txt" + filename = ( + f"{prefix}_dictation_{timestamp}.txt" + if prefix + else f"dictation_{timestamp}.txt" + ) return os.path.join(directory, filename) + def load_whisper_model(model_name): """Load the Whisper model""" debug_func_entry() print_message(f"Loading Whisper model: {model_name}", "debug_transcribe") device = "cuda" if torch.cuda.is_available() else "cpu" - model = whisper.load_model(model_name, device=device) + _state['whisper_model'] = whisper.load_model(model_name, device=device) print_message(f"Model loaded on {device}", "debug_transcribe") - return model + return _state['whisper_model'] + def butter_bandpass(lowcut, highcut, fs, order=5): """Design a butterworth bandpass filter""" @@ -1406,207 +2042,263 @@ def butter_bandpass(lowcut, highcut, fs, order=5): nyq = 0.5 * fs low = lowcut / nyq high = highcut / nyq - b, a = scipy_signal.butter(order, [low, high], btype='band') + b, a = scipy_signal.butter(order, [low, high], btype="band") return b, a + def apply_audio_processing(audio_data, sample_rate, settings): """Apply audio processing based on settings""" debug_func_entry() if not settings.get("enable_audio_processing"): return audio_data - + # Convert to float32 and normalize audio_data = audio_data.astype(np.float32) - + if settings.get("bandpass_filter"): # Apply bandpass filter b, a = butter_bandpass( settings.get("bandpass_low", 85), settings.get("bandpass_high", 3800), - sample_rate + sample_rate, ) audio_data = scipy_signal.filtfilt(b, a, audio_data) - + if settings.get("noise_reduction"): # Simple noise reduction noise_floor = np.mean(np.abs(audio_data)) * 2 audio_data[np.abs(audio_data) < noise_floor] = 0 - + if settings.get("compression"): # Apply compression threshold = 0.1 ratio = 0.5 makeup_gain = 1.5 - + mask = np.abs(audio_data) > threshold audio_data[mask] = np.sign(audio_data[mask]) * ( threshold + (np.abs(audio_data[mask]) - threshold) * ratio ) audio_data *= makeup_gain - + # Final normalization max_val = np.max(np.abs(audio_data)) if max_val > 0: audio_data = audio_data / max_val - + return audio_data + def identify_speaker(segments): """Simple speaker identification based on segment characteristics""" debug_func_entry() # Very basic approach - could be enhanced with proper speaker diarization current_speaker = 1 last_end_time = 0 - + for segment in segments: # If there's a significant gap, might be a different speaker - if segment['start'] - last_end_time > 1.0: + if segment["start"] - last_end_time > 1.0: current_speaker = 2 if current_speaker == 1 else 1 - last_end_time = segment['end'] - + last_end_time = segment["end"] + return current_speaker - -def visualize_audio_levels(audio_data): - """Create audio level visualization""" +def visualize_audio_levels( + audio_data, + sample_rate=16000, # Default sample rate for Whisper + window_size=1024, + stride=None, + y_axis_range=(0, 1), # Fixed range for audio levels +): + """Create an enhanced audio level visualization.""" debug_func_entry() - import plotly.graph_objects as go - - # Calculate RMS in windows - window_size = 1024 - windows = range(0, len(audio_data), window_size) - rms_values = [np.sqrt(np.mean(audio_data[i:i+window_size]**2)) for i in windows] - - # Create time points - time_points = [i/len(windows) for i in range(len(windows))] - - # Create plot + + # Set stride for overlapping windows + stride = stride or window_size // 2 # Default: 50% overlap + + # Calculate RMS values for each window + windows = range(0, len(audio_data) - window_size, stride) + rms_values = [ + np.sqrt(np.mean(audio_data[i : i + window_size] ** 2)) for i in windows + ] + + # Calculate time points in seconds + time_points = [(i * stride) / sample_rate for i in range(len(rms_values))] + + # Downsample if too many points (performance optimization) + max_points = 1000 + if len(rms_values) > max_points: + step = len(rms_values) // max_points + rms_values = rms_values[::step] + time_points = time_points[::step] + + # Create the plot fig = go.Figure() - fig.add_trace(go.Scatter( - x=time_points, - y=rms_values, - mode='lines', - name='Audio Level' - )) - + + # Add audio levels line + fig.add_trace( + go.Scatter(x=time_points, y=rms_values, mode="lines", name="Audio Level") + ) + + # Add optional dynamic range visualization (e.g., warning thresholds) + safe_level = 0.7 # Example threshold for 'safe' audio level + fig.add_trace( + go.Scatter( + x=time_points, + y=[safe_level] * len(time_points), + mode="lines", + line={"dash": "dash", "color": "red"}, + name="Warning Threshold", + ) + ) + + # Update layout fig.update_layout( title="Real-time Audio Levels", - xaxis_title="Time", - yaxis_title="Level" + xaxis_title="Time (s)", + yaxis_title="Level", + template="plotly_white", # Use a clean white theme + xaxis={"showgrid": True}, + yaxis={ + "showgrid": True, + "range": y_axis_range, # Set fixed range for audio levels + }, ) - return fig + def process_audio(audio, state): """Process audio chunk and update transcript""" debug_func_entry() - # Default return values for error cases + # Default return values for error cases empty_plot = visualize_audio_levels(np.zeros(1024)) default_return = ( state if state else None, state["display_text"] if state and "display_text" in state else "", - empty_plot + empty_plot, ) - + if audio is None or state is None or not state.get("is_active"): return default_return - + try: # Get audio data and sample rate sample_rate, audio_data = audio[0], audio[1] - + # Convert to float32 and normalize audio_data = audio_data.astype(np.float32) / 32768.0 - + # Calculate RMS energy energy = np.sqrt(np.mean(audio_data**2)) print_message(f"Audio energy level: {energy}", "debug_transcribe") - + # Skip if too quiet if energy < 0.005: print_message("Audio too quiet, skipping", "debug_transcribe") plot_data = visualize_audio_levels(audio_data) return state, state["display_text"], plot_data - + # Resample to 16kHz if needed if sample_rate != 16000: print_message(f"Resampling from {sample_rate} to 16000", "debug_transcribe") number_of_samples = round(len(audio_data) * 16000 / sample_rate) - audio_data = scipy_signal.resample(audio_data, number_of_samples).astype(np.float32) - + audio_data = scipy_signal.resample(audio_data, number_of_samples).astype( + np.float32 + ) + # Simple normalization max_val = np.max(np.abs(audio_data)) if max_val > 0: audio_data = audio_data / max_val - - print_message(f"Processing audio chunk - shape: {audio_data.shape}, range: [{audio_data.min():.3f}, {audio_data.max():.3f}]", "debug_transcribe") - + + # pylint: disable=line-too-long + print_message( + f"Processing audio chunk - shape: {audio_data.shape}, range: [{audio_data.min():.3f}, {audio_data.max():.3f}]", + "debug_transcribe", + ) + + if 'whisper_model' not in _state: + error_msg = "Whisper model not loaded" + print_message(error_msg, "error") + return default_return + # Process with Whisper - result = model.transcribe( + result = _state['whisper_model'].transcribe( audio_data, language=state.get("source_language", "auto"), task="translate" if state.get("translate_to_english") else "transcribe", temperature=0.0, - condition_on_previous_text=True + condition_on_previous_text=True, ) - + transcribed_text = result["text"].strip() - + if transcribed_text: # Format text based on timestamps setting if state.get("add_timestamps"): timestamp = datetime.now().strftime("%H:%M:%S") formatted_text = f"[{timestamp}] {transcribed_text}" state["text"] += formatted_text + "\n" - else: - # For non-timestamped text, append with space and handle sentence endings - if state["text"] and not state["text"].endswith(('.', '!', '?', '\n')): - state["text"] += " " - state["text"] += transcribed_text - + # For non-timestamped text, append with space and handle sentence endings + if state["text"] and not state["text"].endswith((".", "!", "?", "\n")): + state["text"] += " " + state["text"] += transcribed_text + # Update display text state["display_text"] = state["text"] - + # Get confidence scores (if available) try: - confidences = [segment.get('confidence', 1.0) for segment in result['segments']] - avg_confidence = sum(confidences) / len(confidences) if confidences else 1.0 + confidences = [ + segment.get("confidence", 1.0) for segment in result["segments"] + ] + avg_confidence = ( + sum(confidences) / len(confidences) if confidences else 1.0 + ) state["display_text"] += f"\n\nConfidence: {avg_confidence:.2%}" - except: - pass - + except (KeyError, TypeError, AttributeError): + pass # Non-critical confidence calculation failed + # Save transcript save_transcript(state, state.get("export_format", "txt")) - + # Create audio plot try: plot_data = visualize_audio_levels(audio_data) - except Exception as plot_error: - print_message(f"Plot error (non-critical): {plot_error}", "debug_transcribe") + except (ValueError, TypeError, np.VisualWarning) as plot_error: + print_message( + f"Plot error (non-critical): {plot_error}", "debug_transcribe" + ) plot_data = None - - return state, state["display_text"], plot_data - else: - return default_return - except Exception as e: - error_msg = f"Error processing audio: {str(e)}" - print_message(error_msg, "error") + return state, state["display_text"], plot_data return default_return + except (ValueError, TypeError) as e: + error_msg = f"Error processing audio data: {str(e)}" + except np.VisualWarning as e: + error_msg = f"Error in numerical operations: {str(e)}" + except RuntimeError as e: + error_msg = f"Error in speech recognition: {str(e)}" + except (OSError, IOError) as e: + error_msg = f"Error saving transcript: {str(e)}" + print_message(error_msg, "error") + return default_return + def create_srt_file(segments, filename): """Create an SRT file from segments""" debug_func_entry() srt_content = create_srt_content(segments) - with open(filename, "w", encoding="utf-8") as f: - f.write(srt_content) + with open(filename, "w", encoding="utf-8") as open_srt_f: + open_srt_f.write(srt_content) def save_transcript(state, format_type): """Save transcript in specified format""" debug_func_entry() if not state or not state.get("text"): return - + # Format text based on timestamps setting if state.get("add_timestamps"): save_text = state["text"] # Already formatted with timestamps @@ -1614,17 +2306,17 @@ def save_transcript(state, format_type): # Format as flowing text with proper sentence spacing sentences = state["text"].replace("\n", " ").split(". ") save_text = ". ".join(s.strip() for s in sentences if s.strip()) - if not save_text.endswith(('.', '!', '?')): + if not save_text.endswith((".", "!", "?")): save_text += "." - + if format_type == "txt": - with open(state["output_file"], "w", encoding="utf-8") as f: - f.write(save_text) - + with open(state["output_file"], "w", encoding="utf-8") as save_transcript_f: + save_transcript_f.write(save_text) + elif format_type == "srt": srt_filename = state["output_file"].replace(".txt", ".srt") create_srt_file(state.get("segments", []), srt_filename) - + elif format_type == "json": json_filename = state["output_file"].replace(".txt", ".json") metadata = { @@ -1632,25 +2324,40 @@ def save_transcript(state, format_type): "segments": state.get("segments", []), "word_count": len(save_text.split()), "model": state["model_name"], - "settings": state["settings"] + "settings": state["settings"], } - with open(json_filename, "w", encoding="utf-8") as f: - json.dump(metadata, f, indent=2) + with open(json_filename, "w", encoding="utf-8") as save_transcript_f: + json.dump(metadata, save_transcript_f, indent=2) print_message(f"Saved transcript to: {state['output_file']}", "debug_transcribe") -def start_new_dictation(model_name, prefix, language, source_lang, translate, export_fmt, timestamps, - diarization, audio_proc, bandpass, noise_red, compress, - silence_thresh, bp_low, bp_high): + +def start_new_dictation( + model_name, + prefix, + language, + source_lang, + translate, + export_fmt, + timestamps, + diarization, + audio_proc, + bandpass, + noise_red, + compress, + silence_thresh, + bp_low, + bp_high, +): """Load model and prepare for dictation""" debug_func_entry() - global model try: - model = load_whisper_model(model_name) - + # Load model into state + load_whisper_model(model_name) + transcripts_dir = setup_transcription_directory() output_file = create_transcript_file(transcripts_dir, prefix) - + state = { "text": "", "display_text": "", @@ -1673,57 +2380,66 @@ def start_new_dictation(model_name, prefix, language, source_lang, translate, ex "compression": compress, "silence_threshold": silence_thresh, "bandpass_low": bp_low, - "bandpass_high": bp_high - } + "bandpass_high": bp_high, + }, } - + print_message(f"Started new transcription: {output_file}", "debug_transcribe") message = "Model loaded! Click the microphone icon to start/stop recording." - + return ( state, message, gr.update(interactive=True), gr.update(interactive=False), - gr.update(interactive=True) - ) - except Exception as e: - error_msg = f"Error loading model: {str(e)}" - print_message(error_msg, "error") - return ( - None, - f"Error: {error_msg}", - gr.update(interactive=False), gr.update(interactive=True), - gr.update(interactive=False) ) - -def finish_dictation(state): - """End dictation and cleanup""" - debug_func_entry() - global model - if state is not None: - print_message(f"Finished dictation: {state['output_file']}", "debug_transcribe") - save_transcript(state, state.get("export_format", "txt")) - - if 'model' in globals(): - # Clear model references - model.to('cpu') # Move model to CPU first - del model - model = None - - # Thorough cleanup - cleanup_gpu_memory() - - # Clear CUDA memory pool - torch.cuda.synchronize() - + except (whisper.RuntimeError, torch.cuda.OutOfMemoryError) as e: + error_msg = f"Model error: {str(e)}" + return_msg = "Error: Model could not be loaded - try a smaller model" + except (OSError, IOError) as e: + error_msg = f"File system error: {str(e)}" + return_msg = "Error: Could not create necessary files/directories" + except ValueError as e: + error_msg = f"Invalid parameter: {str(e)}" + return_msg = "Error: Invalid configuration parameters provided" + print_message(error_msg, "error") return ( None, - "Dictation finished. Please refresh the page before starting a new session.", - gr.update(interactive=False, value=None), + return_msg, + gr.update(interactive=False), gr.update(interactive=True), - gr.update(interactive=False) + gr.update(interactive=False), + ) + +def finish_dictation(state, dictate_audio, text_output, audio_plot): + """End dictation and cleanup.""" + debug_func_entry() + + if state is not None: + print_message(f"Finished dictation: {state.get('output_file', 'Unknown')}", "debug_transcribe") + save_transcript(state, state.get("export_format", "txt")) + state["is_active"] = False + + # Reset the audio stream handlers only if dictate_audio is a valid component + if isinstance(dictate_audio, gr.Audio): + reset_audio_stream_handlers(dictate_audio, state, text_output, audio_plot) + + # Clean up the model + if 'whisper_model' in _state: + _state['whisper_model'].to("cpu") # Move model to CPU first + del _state['whisper_model'] + + # Clean up GPU resources + cleanup_gpu_memory() + torch.cuda.synchronize() + + return ( + state, + "Transcription finished. Ready for a new session!", + gr.update(interactive=True), + gr.update(interactive=True), + gr.update(interactive=False), ) @@ -1736,19 +2452,18 @@ def finish_dictation(state): # # ################################################################## -if gradio_enabled == True: - import importlib - import gradio as gr +if gradio_enabled is True: # Get the directory of the current script script_dir = Path(__file__).resolve().parent - my_current_url = "null" - at_default_voice_gr = config.tgwui.tgwui_character_voice + # at_default_voice_gr = config.tgwui.tgwui_character_voice # Disabled as I think now un-used #################################################### # Dynamically import the Themes builder for Gradio # #################################################### - themesmodule_path = this_dir / 'system' / 'gradio_pages' / 'themes' / 'loadThemes.py' + themesmodule_path = ( + this_dir / "system" / "gradio_pages" / "themes" / "loadThemes.py" + ) # Add the directory containing the module to the system path - sys.path.insert(0, str(this_dir / 'system' / 'gradio_pages' / 'themes')) + sys.path.insert(0, str(this_dir / "system" / "gradio_pages" / "themes")) # Import the module dynamically spec = importlib.util.spec_from_file_location("loadThemes", themesmodule_path) loadThemes = importlib.util.module_from_spec(spec) @@ -1762,154 +2477,177 @@ if gradio_enabled == True: ########################################################### # Check if the script is running in standalone mode if script_dir.name == "alltalk_tts": - # Running in standalone mode, add the script's directory to the Python module search path + # Running in standalone mode, add the script's + # directory to the Python module search path sys.path.insert(0, str(script_dir)) else: - # Running as part of text-generation-webui, add the parent directory of "alltalk_tts" to the Python module search path + # Running as part of text-generation-webui, add the parent + # directory of "alltalk_tts" to the Python module search path sys.path.insert(0, str(script_dir.parent)) - ######################################################################################## - # 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(): #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: - tts_engines_data = json.load(f) - engines_available = [engine["name"] for engine in tts_engines_data["engines_available"]] - engine_loaded = tts_engines_data["engine_loaded"] - selected_model = tts_engines_data["selected_model"] - return engines_available, engine_loaded, selected_model # Determine if running as standalone or within another project + # TODO: Want to re-check on this and how its impacting TGWUI. May not be needed anymore! if __name__ == "__main__" or "text-generation-webui" not in this_dir.parts: # Standalone execution base_package = None # No base package needed for absolute imports - from system.gradio_pages.alltalk_diskspace import get_disk_interface else: # Running within text-generation-webui # Dynamically build the base package using the current folder name - from .system.gradio_pages.alltalk_diskspace import get_disk_interface 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): + def dynamic_import(module_path, package=None): + """ + Dynamically import a module from a given path. + + Args: + module_path (str): Path or name of module to import + package (str, optional): Package to import module from. Defaults to None. + + Returns: + module: Imported module object or None if import fails + """ debug_func_entry() try: if package: - module = importlib.import_module(module_name, package=package) + new_module = importlib.import_module(module_path, package=package) else: - module = importlib.import_module(module_name) - return module + new_module = importlib.import_module(module_path) + return new_module except ModuleNotFoundError as e: - print_message(f"Module not found: {module_name} - {e}", message_type="error") + print_message( + f"Module not found: {module_path} - {str(e)}", message_type="error" + ) return None - except Exception as e: - print_message(f"Error importing {module_name}: {e}", message_type="error") + except (ImportError, SyntaxError) as e: + print_message(f"Error importing {module_path}: {str(e)}", message_type="error") return None - # Call the function to populate engines_available - get_tts_engines_data() #TODO: we should be pulling this from our central function now? + def load_engine_configs(_state): + """Load engine configs into globals for Gradio settings pages""" + for engine_name in _state.get('srv_engines_available', []): + if base_package: + module_name = f"{base_package}.system.tts_engines.{engine_name}.{engine_name}_settings_page" + else: + module_name = f"system.tts_engines.{engine_name}.{engine_name}_settings_page" - # Dynamically import modules and load JSON data for each available engine - for engine_name in engines_available: - if base_package: - module_name = f"{base_package}.system.tts_engines.{engine_name}.{engine_name}_settings_page" - else: - module_name = f"system.tts_engines.{engine_name}.{engine_name}_settings_page" - module = dynamic_import(module_name, base_package) - if module: - globals()[f"{engine_name}_at_gradio_settings_page"] = getattr(module, f"{engine_name}_at_gradio_settings_page") - globals()[f"{engine_name}_model_update_settings"] = getattr(module, f"{engine_name}_model_update_settings") - 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) + module = dynamic_import(module_name, base_package) + if module: + # Load the engine's config from its JSON file + json_file_path = os.path.join(this_dir, "system", "tts_engines", engine_name, "model_settings.json") + try: + with open(json_file_path, "r", encoding="utf-8") as config_file: + globals()[f"{engine_name}_model_config_data"] = json.load(config_file) + except FileNotFoundError: + print_message(f"Could not find settings file for {engine_name}") + except json.JSONDecodeError: + print_message("Invalid JSON in settings file for {engine_name}") + + # Call this before creating the Gradio interface to dynamically import TTS Engine pages + load_engine_configs(_state) ########################################### # 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"""""") - ########################################################################################## # Pulls the current AllTalk Server settings & updates gradio when Refresh button pressed # ########################################################################################## def at_update_dropdowns(): + """Update the gradio dropdowns with the current settings from state""" 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 = config.tgwui.tgwui_character_voice - current_narrator_voice = config.tgwui.tgwui_narrator_voice - rvccurrent_voices = at_settings["rvcvoices"] - 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"] - current_generationspeed_capable = at_settings["generationspeed_capable"] - current_pitch_capable = at_settings["pitch_capable"] - current_languages_capable = at_settings["languages_capable"] - if at_settings["languages_capable"]: - language_label = "Languages" - else: - language_label = "Model not multi language" - if at_settings["streaming_capable"]: + + # Call get_alltalk_settings to update state with latest values + get_alltalk_settings() + + # Build stream choices if streaming is available + if _state['srv_settings_capabilities']['streaming_capable']: gen_choices = [("Standard", "false"), ("Streaming (Disable Narrator)", "true")] else: gen_choices = [("Standard", "false")] - if current_character_voice not in current_voices: - current_character_voice = current_voices[0] if current_voices else "" - if current_narrator_voice not in current_voices: - current_narrator_voice = current_voices[0] if current_voices else "" - if rvccurrent_character_voice not in rvccurrent_voices: - rvccurrent_character_voice = rvccurrent_voices[0] if rvccurrent_voices else "" - if rvccurrent_narrator_voice not in rvccurrent_voices: - rvccurrent_narrator_voice = rvccurrent_voices[0] if rvccurrent_voices else "" + + # Set language label based on capability + language_label = "Languages" if _state[ + 'srv_settings_capabilities' + ][ + 'languages_capable' + ] else "Model not multi language" + + # Handle default voices if they're not in available voices + current_character_voice = _state['srv_character_voice'] + if current_character_voice not in _state['srv_current_voices']: + current_character_voice = _state['srv_current_voices'][0] if _state['srv_current_voices'] else "" + + current_narrator_voice = _state['srv_narrator_voice'] + if current_narrator_voice not in _state['srv_current_voices']: + current_narrator_voice = _state['srv_current_voices'][0] if _state['srv_current_voices'] else "" + + rvc_character_voice = _state['srv_rvc_character_voice'] + if rvc_character_voice not in _state['srv_current_rvcvoices']: + rvc_character_voice = _state['srv_current_rvcvoices'][0] if _state['srv_current_rvcvoices'] else "" + + rvc_narrator_voice = _state['srv_rvc_narrator_voice'] + if rvc_narrator_voice not in _state['srv_current_rvcvoices']: + rvc_narrator_voice = _state['srv_current_rvcvoices'][0] if _state['srv_current_rvcvoices'] else "" + + # Return all Gradio component updates return ( gr.Dropdown(choices=gen_choices, interactive=True), - gr.Dropdown(choices=current_voices, value=current_character_voice, interactive=True), - gr.Dropdown(choices=rvccurrent_voices, value=rvccurrent_character_voice, interactive=True), - gr.Dropdown(choices=current_voices, value=current_narrator_voice, interactive=True), - gr.Dropdown(choices=rvccurrent_voices, value=rvccurrent_narrator_voice, interactive=True), - gr.Slider(interactive=current_generationspeed_capable), - gr.Slider(interactive=current_pitch_capable), - gr.Slider(interactive=current_temperature_capable), - gr.Slider(interactive=current_repetitionpenalty_capable), - 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), + gr.Dropdown(choices=_state['srv_current_voices'], value=current_character_voice, interactive=True), + gr.Dropdown(choices=_state['srv_current_rvcvoices'], value=rvc_character_voice, interactive=True), + gr.Dropdown(choices=_state['srv_current_voices'], value=current_narrator_voice, interactive=True), + gr.Dropdown(choices=_state['srv_current_rvcvoices'], value=rvc_narrator_voice, interactive=True), + gr.Slider(interactive=_state['srv_settings_capabilities']['generationspeed_capable']), + gr.Slider(interactive=_state['srv_settings_capabilities']['pitch_capable']), + gr.Slider(interactive=_state['srv_settings_capabilities']['temperature_capable']), + gr.Slider(interactive=_state['srv_settings_capabilities']['repetitionpenalty_capable']), + gr.Dropdown(interactive=_state['srv_settings_capabilities']['languages_capable'], label=language_label), + gr.Dropdown(choices=_state['srv_models_available'], value=_state['srv_current_model_loaded']), + gr.Dropdown(choices=_state['srv_engines_available'], value=_state['srv_current_engine_loaded']) ) ###################################################################################### # Sends request to reload the current TTS engine & set the default TTS engine loaded # ###################################################################################### - def set_engine_loaded(engine_name): + def set_engine_loaded(chosen_engine_name): + """ + Send an engine reload request and update tts_engines list with the chosen engine + and selected model. + + Args: + chosen_engine_name (str): Name of the engine to load + + Returns: + tuple: Success message and updated UI elements + + Raises: + RequestsConnectionError: If unable to connect to TTS engine + TimeoutError: If engine doesn't become ready within retry limit + """ debug_func_entry() - global models_available, at_update_dropdowns # Use the config system to update the engine - tts_engines_config = AlltalkTTSEnginesConfig.get_instance() - + tts_engines_config_loaded = AlltalkTTSEnginesConfig.get_instance() # 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 + if tts_engines_config_loaded.is_valid_engine(chosen_engine_name): + # Find the engine and its selected_model in engines_available + for engine in tts_engines_config.engines_available: + if engine.name == chosen_engine_name: + # Update both engine_loaded and selected_model at top level + tts_engines_config_loaded.engine_loaded = engine.name + tts_engines_config_loaded.selected_model = engine.selected_model + break + + # Change engine in config - this updates selected_model automatically + tts_engines_config_loaded.change_engine(chosen_engine_name) + tts_engines_config_loaded.save() + + # Update state + _state['srv_current_engine_loaded'] = chosen_engine_name + _state['srv_current_model_loaded'] = tts_engines_config.selected_model # Save changes - tts_engines_config.save() + tts_engines_config_loaded.save() # Restart the subprocess restart_subprocess() @@ -1920,46 +2658,61 @@ if gradio_enabled == True: retries = 0 while retries < max_retries: try: - response = requests.get(url) - if response.status_code == 200 and response.text == "Ready": + ready_response = requests.get(url, timeout=(5, 30)) + if ready_response.status_code == 200 and ready_response.text == "Ready": break - except requests.exceptions.RequestException: + except RequestException: pass retries += 1 if retries == max_retries: - raise Exception("Failed to connect to the TTS engine after multiple retries.") + raise RequestsConnectionError( + "Failed to connect to the TTS engine after maximum retries. " + f"Attempted {max_retries} times over {max_retries * retry_delay} seconds." + ) time.sleep(retry_delay) - # Get updated available models - models_available = at_settings["models_available"] - # Update the dropdowns directly print_message("") print_message("Server Ready") - - # Update UI elements + + # Update dropdowns - this line would change return_values = at_update_dropdowns() - + return ( "TTS Engine changed successfully!", # Output message - *return_values # Unpack all the UI update values + *return_values, # Unpack all the UI update values ) ############################### # Sends voice2rvc request off # ############################### def voice2rvc(audio, rvc_voice, rvc_pitch, rvc_f0method): + """ + Convert voice audio using RVC (Retrieval-based Voice Conversion). + + Args: + audio: Input audio (tuple of sample_rate and data, or file path) + rvc_voice: Selected RVC voice model + rvc_pitch: Pitch adjustment value + rvc_f0method: F0 extraction method + + Returns: + str: Path to converted audio file, or None if conversion fails + """ 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_message("Voice2RVC Convert: No RVC voice was selected") - return - if audio == None: + return None + + if audio is None: print_message("Voice2RVC Convert: No recorded audio was provided") - return + return None + if isinstance(audio, tuple): sample_rate, audio_data = audio # Save the numpy array as a wav file @@ -1970,180 +2723,266 @@ if gradio_enabled == True: # Define the output path for the processed audio output_rvc_path = this_dir / "outputs" / "voice2rvcOutput.wav" - url = build_url('voice2rvc') + voice2rvc_url = build_url("voice2rvc") # Submit the paths to the API endpoint - response = requests.post(url, data={ - "input_tts_path": str(input_tts_path), - "output_rvc_path": str(output_rvc_path), - "pth_name": rvc_voice, - "pitch": rvc_pitch, - "method": str(rvc_f0method), - }) + voice2rvc_response = requests.post( + voice2rvc_url, + data={ + "input_tts_path": str(input_tts_path), + "output_rvc_path": str(output_rvc_path), + "pth_name": rvc_voice, + "pitch": rvc_pitch, + "method": str(rvc_f0method), + }, + timeout=(5, 30) # 5 sec connect timeout, 30 sec read timeout + ) - if response.status_code == 200: - result = response.json() + if voice2rvc_response.status_code == 200: + result = voice2rvc_response.json() if result["status"] == "success": - result_path = result["output_path"] - else: - result_path = None - else: - result_path = None + return result["output_path"] - return result_path + return None ################################################################################################## # Sends request to reload the current TTS engine model & set the default TTS engine model loaded # ################################################################################################## - def change_model_loaded(engine_name, selected_model): + def change_model_loaded(new_engine_name, new_selected_model): + """ + Change the currently loaded TTS model and update configuration. + + Args: + new_engine_name (str): Name of TTS engine to use + new_selected_model (str): Name of model to load + + Returns: + tuple: Status message and updated UI component values, or + dict: Error status and message if request fails + """ debug_func_entry() - global at_update_dropdowns try: print_message("") print_message("\033[94mChanging model loaded. Please wait.\033[00m") print_message("") - url = build_url('reload') - payload = {"tts_method": selected_model} - response = requests.post(url, params=payload) - response.raise_for_status() # Raises an HTTPError for bad responses + model_loaded_url = build_url("reload") + payload = {"tts_method": new_selected_model} + model_loaded_response = requests.post(model_loaded_url, params=payload, timeout=(5, 30)) + model_loaded_response.raise_for_status() # Raises an HTTPError for bad responses # Update the tts_engines.json file - tts_engines_file = os.path.join(this_dir, "system", "tts_engines", "tts_engines.json") - with open(tts_engines_file, "r") as f: - tts_engines_data = json.load(f) - tts_engines_data["selected_model"] = f"{selected_model}" + tts_engines_file = os.path.join( + this_dir, "system", "tts_engines", "tts_engines.json" + ) + with open(tts_engines_file, "r", encoding="utf-8") as change_model_f: + tts_engines_data = json.load(change_model_f) + tts_engines_data["selected_model"] = f"{new_selected_model}" for engine in tts_engines_data["engines_available"]: - if engine["name"] == engine_name: - engine["selected_model"] = f"{selected_model}" + if engine["name"] == new_engine_name: + engine["selected_model"] = f"{new_selected_model}" break - with open(tts_engines_file, "w") as f: - json.dump(tts_engines_data, f) + with open(tts_engines_file, "w", encoding="utf-8") as change_model_f: + json.dump(tts_engines_data, change_model_f) 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: + + return_values = at_update_dropdowns() + + return ("TTS Model changed successfully!", *return_values) + + except RequestException as e: # Handle the HTTP request error - print_message(f"Error during request to webserver process: Status code:\n{e}", message_type="warning") + print_message( + f"Error during request to webserver process: Status code:\n{e}", + message_type="warning", + ) return {"status": "error", "message": str(e)} 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): + 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, + ): + """ + Send TTS request from the gradio interface to the API address + """ debug_func_entry() - api_url = build_dynamic_url('tts-generate', include_protocol=False) - if api_url == "http://null/api/tts-generate": # Check if api_url is null - print_message("The URL in the API request was not set. This is usually because of a TTS request from", message_type="error") - print_message("the Gradio web interface when the AllTalk server has been resarted but the Gradio page", message_type="error") - print_message("was not re-loaded. Please refresh your Gradio page and try again.", message_type="error") - return None, str("Error: Did you restart the AllTalk Server and not refresh your Gradio page? Please see the console/terminal message.") + api_url = build_dynamic_url("tts-generate", include_protocol=False) + if api_url == "http://null/api/tts-generate": # Check if api_url is null + print_message( + "The URL in the API request was not set. This is usually because of a TTS request from", + message_type="error", + ) + print_message( + "the Gradio web interface when the AllTalk server has been resarted but the Gradio page", + message_type="error", + ) + print_message( + "was not re-loaded. Please refresh your Gradio page and try again.", + message_type="error", + ) + # pylint: disable=line-too-long + return None, str( + "Error: Did you restart the AllTalk Server and not refresh your Gradio page? Please see the console/terminal message." + ) if gen_text == "": 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() if gen_stream == "true": - api_url = build_dynamic_url('tts-generate-streaming', include_protocol=False) + api_url = build_dynamic_url( + "tts-generate-streaming", include_protocol=False + ) encoded_text = requests.utils.quote(gen_text) + # pylint: disable=line-too-long streaming_url = f"{api_url}?text={encoded_text}&voice={gen_char}&language={gen_lang}&output_file={gen_filen}" return streaming_url, str("TTS Streaming Audio Generated") - else: - data = { - "text_input": gen_text, - "text_filtering": gen_filter, - "character_voice_gen": gen_char, - "rvccharacter_voice_gen": rvcgen_char, - "rvccharacter_pitch": rvcgen_char_pitch, - "narrator_enabled": str(gen_narren).lower(), - "narrator_voice_gen": gen_narr, - "rvcnarrator_voice_gen": rvcgen_narr, - "rvcnarrator_pitch": rvcgen_narr_pitch, - "text_not_inside": gen_textni, - "language": gen_lang, - "output_file_name": gen_filen, - "output_file_timestamp": str(gen_filetime).lower(), - "autoplay": str(gen_autopl).lower(), - "autoplay_volume": str(gen_autoplvol), - "speed": str(gen_speed), - "pitch": str(gen_pitch), - "temperature": str(gen_temperature), - "repetition_penalty": str(gen_repetition), - } - 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() - result = response.json() - if gen_autopl == "true": - return None, str("TTS Audio Generated (Played remotely)") - else: - if config.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 = build_dynamic_url(result['output_file_url'].lstrip('/'), include_protocol=False, include_api=False) - return output_file_url, str("TTS Audio Generated") - except requests.exceptions.Timeout: - retries += 1 - if retries == max_retries: + # pylint: disable=line-too-long + tts_data = { + "text_input": gen_text, + "text_filtering": gen_filter, + "character_voice_gen": gen_char, + "rvccharacter_voice_gen": rvcgen_char, + "rvccharacter_pitch": rvcgen_char_pitch, + "narrator_enabled": str(gen_narren).lower(), + "narrator_voice_gen": gen_narr, + "rvcnarrator_voice_gen": rvcgen_narr, + "rvcnarrator_pitch": rvcgen_narr_pitch, + "text_not_inside": gen_textni, + "language": gen_lang, + "output_file_name": gen_filen, + "output_file_timestamp": str(gen_filetime).lower(), + "autoplay": str(gen_autopl).lower(), + "autoplay_volume": str(gen_autoplvol), + "speed": str(gen_speed), + "pitch": str(gen_pitch), + "temperature": str(gen_temperature), + "repetition_penalty": str(gen_repetition), + } + print_message( + "\033[94mDebug of data > def generate_tts > script.py\033[0m", + message_type="debug_tts_variables", + ) + + keys = list(tts_data.keys()) + for i, key in enumerate(keys): + # Use └─ for the last item, and ├─ for others + prefix = "└─" if i == len(keys) - 1 else "├─" + print_message(f"{prefix} {key}: {tts_data[key]}", 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: + tts_response = requests.post(api_url, data=tts_data, timeout=60) + tts_response.raise_for_status() + result = tts_response.json() + + if gen_autopl == "true": + return None, str("TTS Audio Generated (Played remotely)") + if config.api_def.api_use_legacy_api: + return result["output_file_url"], str("TTS Audio Generated") + + # Prepend the URL and PORT to the output_file_url + output_file_url = build_dynamic_url( + result["output_file_url"].lstrip("/"), + include_protocol=False, + include_api=False, + ) + return output_file_url, str("TTS Audio Generated") + + except (requests.exceptions.Timeout, json.JSONDecodeError, RequestException) as e: + retries += 1 + error_message = None + if retries == max_retries: + if isinstance(e, requests.exceptions.Timeout): 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: + elif isinstance(e, json.JSONDecodeError): 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: + print_message(f"Raw response: {tts_response.content}", message_type="error") + elif isinstance(e, RequestException): 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) + print_message(f"Error: {error_message}", message_type="error") + return None, str(error_message) + if isinstance(e, RequestException): time.sleep(retry_delay) - def alltalk_gradio(): + def alltalk_gradio(): # pylint: disable=too-many-statements, too-many-branches + """ + Setup the main gradio interface for AllTalk + """ debug_func_entry() - global languages_list, at_settings + # Get the URL IP or domain name def get_domain_name(request: gr.Request): - global my_current_url - if running_on_google_colab: - my_current_url = tunnel_url_1 + """ + Get domain name based on environment and request headers. + Args: + request (gr.Request): Gradio request object + Returns: + str: Error message if domain can't be retrieved, None otherwise + """ + if _state['running_on_google_colab']: + _state['my_current_url'] = _state['tunnel_url_1'] return None - elif running_in_docker: - my_current_url = docker_url + + if _state['running_in_docker']: + _state['my_current_url'] = _state['docker_url'] 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(config.api_def.api_port_number) + _state['my_current_url'] = host.split(":")[0] # Split the host by ":" and take the first part + _state['my_current_url'] = f"{_state['my_current_url']}:{config.api_def.api_port_number}" return None - else: - return "Unable to retrieve the domain name." - with gr.Blocks(css=AllTalkHelpContent.custom_css, theme=selected_theme, title="AllTalk", analytics_enabled=False) as app: + return "Unable to retrieve the domain name." + + with gr.Blocks( + css=AllTalkHelpContent.custom_css, + theme=selected_theme, + title="AllTalk", + analytics_enabled=False, + ) as app: # pylint: disable=redefined-outer-name with gr.Row(): gr.Markdown("## AllTalk TTS V2") gr.Markdown("") gr.Markdown("") - dark_mode_btn = gr.Button("Light/Dark Mode", variant="primary", size="sm") - dark_mode_btn.click(None, None, None, - js="""() => { + dark_mode_btn = gr.Button( + "Light/Dark Mode", variant="primary", size="sm" + ) + dark_mode_btn.click( + None, + None, + None, + js="""() => { if (document.querySelectorAll('.dark').length) { document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark')); // localStorage.setItem('darkMode', 'disabled'); @@ -2151,145 +2990,409 @@ if gradio_enabled == True: document.querySelector('body').classList.add('dark'); // localStorage.setItem('darkMode', 'enabled'); } - }""", show_api=False) + }""", + show_api=False, + ) if config.firstrun_splash: with gr.Tab("AllTalk v2 Welcome page"): + def modify_config(): config.firstrun_splash = False config.save() - return f"Welcome screen is disabled." - gr.Markdown(AllTalkHelpContent.WELCOME, elem_classes="custom-markdown") + return "Welcome screen is disabled." + + gr.Markdown( + AllTalkHelpContent.WELCOME, elem_classes="custom-markdown" + ) with gr.Row(): - gr.Markdown(AllTalkHelpContent.WELCOME1, elem_classes="custom-markdown") - gr.Markdown(AllTalkHelpContent.WELCOME2, elem_classes="custom-markdown") - update_btn = gr.Button("Click here to hide welcome screen on the next startup") + gr.Markdown( + AllTalkHelpContent.WELCOME1, elem_classes="custom-markdown" + ) + gr.Markdown( + AllTalkHelpContent.WELCOME2, elem_classes="custom-markdown" + ) + update_btn = gr.Button( + "Click here to hide welcome screen on the next startup" + ) update_btn.click(fn=modify_config, inputs=None, outputs=None) with gr.Tab("Generate TTS"): + with gr.Row(): + gen_text = gr.Textbox(label="Text Input", lines=6) + if _state['running_in_docker']: with gr.Row(): - gen_text = gr.Textbox(label="Text Input", lines=6) - if running_in_docker: - with gr.Row(): - with gr.Accordion("Docker IP/URL for API Address updater", open=False): - with gr.Row(): - 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, scale=2) - update_docker_btn = gr.Button("Update Docker IP/URL API Address", scale=1) - with gr.Accordion("HELP - 🐳 Docker IP/URL for API Addresss", open=False): - with gr.Row(): - gr.Markdown(AllTalkHelpContent.DOCKER_EXPLAINER, elem_classes="custom-markdown") - with gr.Row(): - with gr.Group(): + with gr.Accordion( + "Docker IP/URL for API Address updater", open=False + ): with gr.Row(): - engine_choices = gr.Dropdown(choices=engines_available, label="TTS Engine", value=engine_loaded, show_label=False) - engine_btn = gr.Button("Swap TTS Engine") - with gr.Group(): - with gr.Row(): - model_choices_gr = gr.Dropdown(choices=models_available, label="TTS Models", value=current_model_loaded, interactive=True, show_label=False, allow_custom_value=True) - model_btn_gr = gr.Button("Load Different Model") - with gr.Group(): - at_available_voices_gr = at_settings["voices"] - rvcat_available_voices_gr = at_settings["rvcvoices"] - with gr.Row(): - 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 = 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 = 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 = 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,) + _state['docker_url'] = ( + f"http://localhost:{config.api_def.api_port_number}" + ) + docker_upd = gr.Textbox( + label="Docker IP/URL for API Address", + value=_state['docker_url'], + show_label=False, + scale=2, + ) + update_docker_btn = gr.Button( + "Update Docker IP/URL API Address", scale=1 + ) + with gr.Accordion( + "HELP - 🐳 Docker IP/URL for API Addresss", open=False + ): + with gr.Row(): + gr.Markdown( + AllTalkHelpContent.DOCKER_EXPLAINER, + elem_classes="custom-markdown", + ) + with gr.Row(): 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=config.rvc_settings.pitch, - interactive=True, - visible=False + engine_choices = gr.Dropdown( + choices=_state['srv_engines_available'], + label="TTS Engine", + value=_state['srv_current_engine_loaded'], + show_label=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, + engine_btn = gr.Button("Swap TTS Engine") + with gr.Group(): + with gr.Row(): + model_choices_gr = gr.Dropdown( + choices=_state['srv_models_available'], + label="TTS Models", + value=_state['srv_current_model_loaded'], interactive=True, - visible=False + show_label=False, + allow_custom_value=True, ) - 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]) + model_btn_gr = gr.Button("Load Different Model") + with gr.Group(): + at_available_voices_gr = _state['srv_current_voices'] + rvcat_available_voices_gr = _state['srv_current_rvcvoices'] + with gr.Row(): + 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 = 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 = 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 = ( + 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.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=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, + ) - 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) - def on_load(request: gr.Request): - domain_name = get_domain_name(request) - domain_name_output.value = domain_name - app.load(on_load, inputs=None, outputs=None) - if at_settings["streaming_capable"]: - gen_choices = [("Standard", "false"), ("Streaming (Disable Narrator)", "true")] - else: - 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=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 + def update_visibility(char_voice, narr_voice): + is_visible = ( + char_voice != "Disabled" or narr_voice != "Disabled" ) - 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_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"]) - #Toggle narrator selection on Streaming select - def update_narren_and_autopl(gen_stream): - debug_func_entry() - if gen_stream == "true": - return "false", "false" + 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.Row(): + # Get the current URL from the page + domain_name_output = gr.Textbox( + label="Domain Name", visible=False + ) + + def on_load(request: gr.Request): + domain_name = get_domain_name(request) + domain_name_output.value = domain_name + + app.load(on_load, inputs=None, outputs=None) + if _state['srv_settings_capabilities']['streaming_capable']: + gen_choices = [ + ("Standard", "false"), + ("Streaming (Disable Narrator)", "true"), + ] else: - return gen_narren.value, gen_autopl.value - gen_stream.change(update_narren_and_autopl, inputs=[gen_stream], outputs=[gen_narren, gen_autopl]) + 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=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 _state['srv_settings_capabilities']['languages_capable'] + else "Model not multi language" + ), + interactive=_state['srv_settings_capabilities']['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(): - output_audio = gr.Audio(show_label=False, label="Generated Audio", autoplay=True, scale=3) - output_message = gr.Textbox( label="Status/Result", lines=5, scale=1) + 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(): - dark_mode_btn = gr.Button("Light/Dark Mode", variant="primary") - refresh_button = gr.Button("Refresh Server Settings", elem_id="refresh_button") - stop_button = gr.Button("Interupt TTS Generation") - submit_button = gr.Button("Generate TTS") + gen_speed = gr.Slider( + minimum=0.25, + maximum=2.00, + step=0.25, + label="Speed", + value="1.00", + interactive=_state['srv_settings_capabilities']['generationspeed_capable'], + ) + gen_pitch = gr.Slider( + minimum=-10, + maximum=10, + step=1, + label="Pitch", + value="1", + interactive=_state['srv_settings_capabilities']['pitch_capable'], + ) + gen_temperature = gr.Slider( + minimum=0.1, + maximum=1.0, + step=0.05, + label="Temperature", + value=0.75, + interactive=_state['srv_settings_capabilities']['temperature_capable'], + ) + gen_repetition = gr.Slider( + minimum=1.0, + maximum=20.0, + step=1.0, + label="Repetition Penalty", + value=10, + interactive=_state['srv_settings_capabilities']['repetitionpenalty_capable'] + ) - 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]) + # Toggle narrator selection on Streaming select + def update_narren_and_autopl(gen_stream): + debug_func_entry() + if gen_stream == "true": + return "false", "false" + return gen_narren.value, gen_autopl.value - dark_mode_btn.click(None, None, None, - js="""() => { + 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="Status/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" + ) + stop_button = gr.Button("Interupt TTS Generation") + submit_button = gr.Button("Generate TTS") + + 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) { document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark')); // localStorage.setItem('darkMode', 'disabled'); @@ -2297,211 +3400,355 @@ if gradio_enabled == True: document.querySelector('body').classList.add('dark'); // localStorage.setItem('darkMode', 'enabled'); } - }""", show_api=False) - refresh_button.click(at_update_dropdowns, None, [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]) - 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 running_in_docker: - update_docker_btn.click( - fn=update_docker_address, - inputs=[docker_upd], - outputs=[output_message] - ) - if config.gradio_pages.Generate_Help_page: - with gr.Accordion("HELP - 🎯 TTS Generation Basics", open=False): - with gr.Row(): - gr.Markdown(AllTalkHelpContent.GENERATE_SCREEN1, elem_classes="custom-markdown") - gr.Markdown(AllTalkHelpContent.GENERATE_SCREEN2, elem_classes="custom-markdown") - with gr.Accordion("HELP - ⚙️ Advanced TTS Features", open=False): - with gr.Row(): - gr.Markdown(AllTalkHelpContent.GENERATE_SCREEN3, elem_classes="custom-markdown") - gr.Markdown(AllTalkHelpContent.GENERATE_SCREEN4, elem_classes="custom-markdown") + }""", + show_api=False, + ) + refresh_button.click( + at_update_dropdowns, + None, + [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], + ) + 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 _state['running_in_docker']: + update_docker_btn.click( + fn=update_docker_address, + inputs=[docker_upd], + outputs=[output_message], + ) + if config.gradio_pages.Generate_Help_page: + with gr.Accordion("HELP - 🎯 TTS Generation Basics", open=False): + with gr.Row(): + gr.Markdown( + AllTalkHelpContent.GENERATE_SCREEN1, + elem_classes="custom-markdown", + ) + gr.Markdown( + AllTalkHelpContent.GENERATE_SCREEN2, + elem_classes="custom-markdown", + ) + with gr.Accordion("HELP - ⚙️ Advanced TTS Features", open=False): + with gr.Row(): + gr.Markdown( + AllTalkHelpContent.GENERATE_SCREEN3, + elem_classes="custom-markdown", + ) + gr.Markdown( + AllTalkHelpContent.GENERATE_SCREEN4, + elem_classes="custom-markdown", + ) if config.gradio_pages.Voice2RVC_page: with gr.Tab("Voice2RVC"): 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: + audio_input = gr.Audio( + sources=["microphone", "upload"], + type="numpy", + label="Record audio or Upload a spoken audio file", + ) + if _state['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:{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") + _state['docker_url'] = f"http://localhost:{config.api_def.api_port_number}" + docker_upd = gr.Textbox( + label="Docker IP/URL for API Address", + value=_state['docker_url'], + show_label=False, + ) + update_docker_btn = gr.Button( + "Update Docker IP/URL API Address" + ) update_docker_btn.click( fn=update_docker_address, inputs=[docker_upd], - outputs=[gr.Text(label="Status")] + 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) + rvc_voices_dropdown = gr.Dropdown( + choices=_state['srv_current_rvcvoices'], + label="Select RVC Voice to generate as", + value=_state['srv_current_rvcvoices'][0], + scale=1, + ) + # pylint: disable=line-too-long + 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=config.rvc_settings.f0method, interactive=True, scale=1) + # pylint: disable=line-too-long + 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) + submit_button.click( + fn=voice2rvc, + inputs=[ + audio_input, + rvc_voices_dropdown, + rvc_pitch_slider, + rvc_f0method, + ], + outputs=audio_output, + ) with gr.Accordion("HELP - 🎯 Voice2RVC Basics", open=False): with gr.Row(): - gr.Markdown(AllTalkHelpContent.VOICE2RVC, elem_classes="custom-markdown") + gr.Markdown( + AllTalkHelpContent.VOICE2RVC, + elem_classes="custom-markdown", + ) with gr.Row(): - gr.Markdown(AllTalkHelpContent.VOICE2RVC1, elem_classes="custom-markdown") - gr.Markdown(AllTalkHelpContent.VOICE2RVC2, elem_classes="custom-markdown") - + gr.Markdown( + AllTalkHelpContent.VOICE2RVC1, + elem_classes="custom-markdown", + ) + gr.Markdown( + AllTalkHelpContent.VOICE2RVC2, + elem_classes="custom-markdown", + ) + with gr.Tab("Transcribe"): with gr.Row(): with gr.Group(): audio_files = gr.File( - file_count="multiple", + file_count="multiple", label="Upload Audio Files", - elem_classes="small-file-upload" + elem_classes="small-file-upload", ) delete_after_process = gr.Checkbox( label="Clean Up Temporary Audio Files After Processing", - value=False - ) + value=False, + ) with gr.Row(): with gr.Group(): - with gr.Row(): + with gr.Row(): prefix_input = gr.Textbox( label="Output Prefix (optional)", placeholder="e.g., project_name, meeting_date", value="", - scale=3 - ) + scale=3, + ) model_choices = gr.Dropdown( - choices=["tiny", "base", "small", "medium", "large-v3"], - value="base", + choices=[ + "tiny", + "base", + "small", + "medium", + "turbo", + "large-v3", + "large-v3-turbo", + ], + value="turbo", label="Whisper Model Size", - scale=1 + scale=1, ) format_choices = gr.Dropdown( choices=["txt", "json", "srt"], value="txt", label="Output Format", - scale=1 + scale=1, ) with gr.Row(): delete_btn = gr.Button("Delete Uploaded Audio") process_btn = gr.Button("Transcribe", variant="primary") - with gr.Row(): - status_output = gr.Textbox(label="Processing Status", lines=2) - + with gr.Row(): + status_output = gr.Textbox( + label="Processing Status", lines=2 + ) + with gr.Row(): - output_zip = gr.File(label="Download Transcriptions (ZIP)", elem_classes="small-file-upload2") + output_zip = gr.File( + label="Download Transcriptions (ZIP)", + elem_classes="small-file-upload2", + ) with gr.Accordion("HELP - 🎯 Transcribe Basics", open=False): with gr.Row(): - gr.Markdown(AllTalkHelpContent.TRANSCRIBE, elem_classes="custom-markdown") + gr.Markdown( + AllTalkHelpContent.TRANSCRIBE, + elem_classes="custom-markdown", + ) with gr.Row(): - gr.Markdown(AllTalkHelpContent.TRANSCRIBE1, elem_classes="custom-markdown") - gr.Markdown(AllTalkHelpContent.TRANSCRIBE2, elem_classes="custom-markdown") + gr.Markdown( + AllTalkHelpContent.TRANSCRIBE1, + elem_classes="custom-markdown", + ) + gr.Markdown( + AllTalkHelpContent.TRANSCRIBE2, + elem_classes="custom-markdown", + ) process_btn.click( fn=process_audio_files, - inputs=[audio_files, model_choices, format_choices, delete_after_process, prefix_input], - outputs=[output_zip, status_output] + inputs=[ + audio_files, + model_choices, + format_choices, + delete_after_process, + prefix_input, + ], + outputs=[output_zip, status_output], ) - + delete_btn.click( - fn=delete_uploaded_files, - inputs=[], - outputs=[status_output] + fn=delete_uploaded_files, inputs=[], outputs=[status_output] ) with gr.Tab("Dictate"): state = gr.State(None) # Sort the dictionary by full names alphabetically - sorted_languages = dict(sorted(AllTalkHelpContent.WHISPER_LANGUAGES.items(), key=lambda item: item[1])) + sorted_languages = dict( + sorted( + AllTalkHelpContent.WHISPER_LANGUAGES.items(), + key=lambda item: item[1], + ) + ) # Reverse the dictionary for lookups (Full Name -> Code) name_to_code = {name: code for code, name in sorted_languages.items()} + def process_language(language_name): # Convert selected language name back to its 2-digit code - return name_to_code[language_name] + return name_to_code[language_name] + with gr.Row(): model_choices = gr.Dropdown( - choices=["tiny", "base", "small", "medium", "turbo", "large-v3", "large-v3-turbo"], - value="large-v3", + choices=[ + "tiny", + "base", + "small", + "medium", + "turbo", + "large-v3", + "large-v3-turbo", + ], + value="turbo", label="Whisper Model", - scale=1 + scale=1, ) language_select = gr.Dropdown( choices=list(sorted_languages.values()), value="English", label="Language", - allow_custom_value=True + allow_custom_value=True, ) language_select.change( - fn=process_language, + fn=process_language, inputs=language_select, - ) + ) export_format = gr.Dropdown( choices=["txt", "srt", "json"], value="txt", label="Output Format", - scale=1 - ) + scale=1, + ) prefix_input = gr.Textbox( label="Output File Prefix (optional)", placeholder="e.g., meeting_notes, todo_list", - scale=2 + scale=2, ) start_btn = gr.Button("Load Model", variant="primary") finish_btn = gr.Button("Finish & Unload", interactive=False) - with gr.Accordion("Advanced Settings (Change Before Loading The Model)", open=False): + with gr.Accordion( + "Advanced Settings (Change Before Loading The Model)", open=False + ): with gr.Row(): # Update the dropdowns translate_to_english = gr.Dropdown( label="Translate to English", choices={False, True}, value=False, - allow_custom_value=True - ) + allow_custom_value=True, + ) source_language = gr.Dropdown( choices=list(sorted_languages.values()), value="English", label="Source Language", - allow_custom_value=True + allow_custom_value=True, ) source_language.change( - fn=process_language, + fn=process_language, inputs=source_language, ) add_timestamps = gr.Dropdown( - label="Add Timestamps", - choices={False, True}, - value=False + label="Add Timestamps", choices={False, True}, value=False ) enable_diarization = gr.Dropdown( label="Enable Speaker Diarization", choices={False, True}, - value=False + value=False, ) - - with gr.Row(): + + with gr.Row(): enable_audio_processing = gr.Checkbox( - label="Enable ALL Audio Enhancements", - value=True + label="Enable ALL Audio Enhancements", value=True ) bandpass_filter = gr.Checkbox( - label="Apply Bandpass Filter", - value=True + label="Apply Bandpass Filter", value=True ) noise_reduction = gr.Checkbox( - label="Apply Noise Reduction", - value=True + label="Apply Noise Reduction", value=True ) compression = gr.Checkbox( - label="Apply Audio Compression", - value=True + label="Apply Audio Compression", value=True ) # At the start of interface setup - checkbox_state = gr.State({ - "processing": False - }) + checkbox_state = gr.State({"processing": False}) def update_audio_settings(main_enable, state_dict): """Enable all individual settings when main is checked""" @@ -2513,13 +3760,13 @@ if gradio_enabled == True: gr.update(value=main_enable), # bandpass gr.update(value=main_enable), # noise gr.update(value=main_enable), # compression - state_dict + state_dict, ) return ( gr.update(), # bandpass gr.update(), # noise gr.update(), # compression - state_dict + state_dict, ) def update_main_setting(bandpass, noise, compress, state_dict): @@ -2535,14 +3782,24 @@ if gradio_enabled == True: enable_audio_processing.change( fn=update_audio_settings, inputs=[enable_audio_processing, checkbox_state], - outputs=[bandpass_filter, noise_reduction, compression, checkbox_state] + outputs=[ + bandpass_filter, + noise_reduction, + compression, + checkbox_state, + ], ) for checkbox in [bandpass_filter, noise_reduction, compression]: checkbox.change( fn=update_main_setting, - inputs=[bandpass_filter, noise_reduction, compression, checkbox_state], - outputs=[enable_audio_processing, checkbox_state] + inputs=[ + bandpass_filter, + noise_reduction, + compression, + checkbox_state, + ], + outputs=[enable_audio_processing, checkbox_state], ) with gr.Row(): @@ -2550,19 +3807,19 @@ if gradio_enabled == True: minimum=0.001, maximum=0.02, value=0.008, - label="Silence Threshold" + label="Silence Threshold", ) bandpass_low = gr.Slider( minimum=50, maximum=200, - value=85, - label="Bandpass Low Freq (Hz)" + value=75, + label="Bandpass Low Freq (Hz)", ) bandpass_high = gr.Slider( minimum=2000, maximum=8000, value=3800, - label="Bandpass High Freq (Hz)" + label="Bandpass High Freq (Hz)", ) with gr.Row(): @@ -2572,53 +3829,69 @@ if gradio_enabled == True: show_recording_waveform=True, show_controls=False, waveform_color="#1f77b4", - waveform_progress_color="#2ecc71" + waveform_progress_color="#2ecc71", ) - + # Audio input with configured waveform - audio_input = gr.Audio( + dictate_audio = gr.Audio( sources=["microphone"], type="numpy", streaming=True, label="Click `Record` to start dictation and `Stop` to pause or stop dictation", show_label=True, interactive=False, - waveform_options=waveform_opts + waveform_options=waveform_opts, ) - with gr.Accordion("Show Audio Levels Graph", open=False): - with gr.Row(): - audio_plot = gr.Plot( - label="Audio Levels", - show_label=True, - container=True, - ) - with gr.Row(): + with gr.Accordion("Show Audio Levels Graph", open=False, elem_classes="fixed-accordion"): + audio_plot = gr.Plot( + label="Audio Levels", + show_label=True, + container=True, + elem_classes="fixed-plot", + ) + with gr.Row(): text_output = gr.Textbox( - label="Live Transcription", + label="Live Transcription", lines=10, placeholder="Transcription will appear here as you speak...", - show_copy_button=True + show_copy_button=True, ) # Dictatation Button click handlers start_btn.click( fn=start_new_dictation, inputs=[ - model_choices, prefix_input, - language_select, source_language, translate_to_english, export_format, - add_timestamps, enable_diarization, - enable_audio_processing, bandpass_filter, noise_reduction, compression, - silence_threshold, bandpass_low, bandpass_high + model_choices, + prefix_input, + language_select, + source_language, + translate_to_english, + export_format, + add_timestamps, + enable_diarization, + enable_audio_processing, + bandpass_filter, + noise_reduction, + compression, + silence_threshold, + bandpass_low, + bandpass_high, ], - outputs=[state, text_output, audio_input, start_btn, finish_btn] + outputs=[state, text_output, dictate_audio, start_btn, finish_btn], ) finish_btn.click( fn=finish_dictation, - inputs=[state], - outputs=[state, text_output, audio_input, start_btn, finish_btn] + inputs=[state, dictate_audio, text_output, audio_plot], # Pass the actual components + outputs=[ + state, # Update the state + text_output, # Update the transcription text output + start_btn, # Update the start button + finish_btn, # Update the finish button + ], ) - + + def on_start_recording(state): if state and state.get("is_active"): print_message("Recording started", "debug_transcribe") @@ -2631,115 +3904,222 @@ if gradio_enabled == True: state["is_recording"] = False return state - # Add the handlers to the audio input - audio_input.start_recording( - fn=on_start_recording, - inputs=[state], - outputs=[state] - ).success( - fn=None, - js="() => {console.log('Recording started');}" - ) - - audio_input.stop_recording( - fn=on_stop_recording, - inputs=[state], - outputs=[state] - ).success( - fn=None, - js="() => {console.log('Recording stopped');}" - ) - - audio_input.stream( + + dictate_audio.start_recording( + fn=on_start_recording, inputs=[state], outputs=[state] + ).success(fn=None, js="() => {console.log('Recording started');}") + + dictate_audio.stop_recording( + fn=on_stop_recording, inputs=[state], outputs=[state] + ).success(fn=None, js="() => {console.log('Recording stopped');}") + + dictate_audio.stream( fn=process_audio, - inputs=[audio_input, state], + inputs=[dictate_audio, state], outputs=[state, text_output, audio_plot], show_progress=False, - concurrency_limit=1 + concurrency_limit=1, ) + with gr.Accordion("HELP - 🎯 Dictate Basics", open=False): with gr.Row(): - gr.Markdown(AllTalkHelpContent.WHISPER_HELP, elem_classes="custom-markdown") + gr.Markdown( + AllTalkHelpContent.WHISPER_HELP, + elem_classes="custom-markdown", + ) with gr.Row(): - gr.Markdown(AllTalkHelpContent.WHISPER_HELP1, elem_classes="custom-markdown") - gr.Markdown(AllTalkHelpContent.WHISPER_HELP2, elem_classes="custom-markdown") + gr.Markdown( + AllTalkHelpContent.WHISPER_HELP1, + elem_classes="custom-markdown", + ) + gr.Markdown( + AllTalkHelpContent.WHISPER_HELP2, + elem_classes="custom-markdown", + ) if config.gradio_pages.TTS_Generator_page: with gr.Tab("TTS Generator"): - gr.Markdown("""### TTS Generator for long audio generation tasks. [Click here for access](http://127.0.0.1:7851/static/tts_generator/tts_generator.html)""") + # pylint: disable=line-too-long + gr.Markdown( + """### TTS Generator for long audio generation tasks. [Click here for access](http://127.0.0.1:7851/static/tts_generator/tts_generator.html)""" + ) with gr.Accordion("HELP - 🎯 TTS Generator Basics", open=False): with gr.Row(): - gr.Markdown(AllTalkHelpContent.TTS_GENERATOR, elem_classes="custom-markdown") + gr.Markdown( + AllTalkHelpContent.TTS_GENERATOR, + elem_classes="custom-markdown", + ) with gr.Row(): - gr.Markdown(AllTalkHelpContent.TTS_GENERATOR1, elem_classes="custom-markdown") - gr.Markdown(AllTalkHelpContent.TTS_GENERATOR2, elem_classes="custom-markdown") + gr.Markdown( + AllTalkHelpContent.TTS_GENERATOR1, + elem_classes="custom-markdown", + ) + gr.Markdown( + AllTalkHelpContent.TTS_GENERATOR2, + elem_classes="custom-markdown", + ) with gr.Tab("Global Settings"): with gr.Tab("AllTalk Settings"): with gr.Row(): - 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"]) + 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 + precision=0, ) gradio_port_number = gr.Number( value=config.gradio_port_number, label="Gradio Port Number", - precision=0 + precision=0, ) - output_folder = gr.Textbox( + settings_output_folder = gr.Textbox( value=config.output_folder, - label=f"Output Folder name (sub {config.branding})" + 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=config.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 = 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." + updtheme_config = AlltalkConfig.get_instance() + updtheme_config.theme.clazz = theme_name + updtheme_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")] + 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) + 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 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) + # pylint: disable=line-too-long + 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, + ) with gr.Group(): gr.Markdown("### Disable Gradio Interface Tabs") with gr.Row(): - 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) + 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) - + output_message = gr.Textbox( + label="Output Message", interactive=False, show_label=False + ) + with gr.Accordion("⚙️ HELP - AllTalk Settings Page", open=False): with gr.Row(): - gr.Markdown(AllTalkHelpContent.ALLTALK_SETTINGS_PAGE1, elem_classes="custom-markdown") - gr.Markdown(AllTalkHelpContent.ALLTALK_SETTINGS_PAGE2, elem_classes="custom-markdown") - + gr.Markdown( + AllTalkHelpContent.ALLTALK_SETTINGS_PAGE1, + elem_classes="custom-markdown", + ) + gr.Markdown( + AllTalkHelpContent.ALLTALK_SETTINGS_PAGE2, + elem_classes="custom-markdown", + ) + with gr.Accordion("🔍 HELP - AllTalk Debug Settings", open=False): with gr.Row(): - gr.Markdown(AllTalkHelpContent.DEBUG_HELP1, elem_classes="custom-markdown") - gr.Markdown(AllTalkHelpContent.DEBUG_HELP2, elem_classes="custom-markdown") - + gr.Markdown( + AllTalkHelpContent.DEBUG_HELP1, + elem_classes="custom-markdown", + ) + gr.Markdown( + AllTalkHelpContent.DEBUG_HELP2, + elem_classes="custom-markdown", + ) + # Update the function to include these new settings submit_button.click( 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 + inputs=[ + delete_output_wavs, + gradio_interface, + gradio_port_number, + settings_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, ) with gr.Tab("AllTalk API Defaults"): with gr.Row(): @@ -2748,211 +4128,578 @@ if gradio_enabled == True: with gr.Group(): with gr.Row(): api_use_legacy_api = gr.Dropdown( - choices=["AllTalk v2 API", "AllTalk v1 API (Legacy)"], + choices=[ + "AllTalk v2 API", + "AllTalk v1 API (Legacy)", + ], label="API Version", scale=1, - allow_custom_value=True, - value=config.api_def.api_use_legacy_api + allow_custom_value=True, + value=config.api_def.api_use_legacy_api, ) api_legacy_ip_address = gr.Textbox( value=config.api_def.api_legacy_ip_address, scale=1, - label="Legacy API IP Address" + label="Legacy API IP Address", ) # Request Processing with gr.Column(scale=2): with gr.Group(): with gr.Row(): api_length_stripping = gr.Slider( - minimum=1, maximum=20, step=1, + minimum=1, + maximum=20, + step=1, value=int(config.api_def.api_length_stripping), scale=2, - label="Minimum Sentence Length" + label="Minimum Sentence Length", ) api_max_characters = gr.Slider( - minimum=50, maximum=10000, step=50, + minimum=50, + maximum=10000, + step=50, value=int(config.api_def.api_max_characters), scale=2, - label="Maximum Request Characters" + label="Maximum Request Characters", ) api_text_filtering = gr.Dropdown( value=config.api_def.api_text_filtering, label="Text Filtering Mode", scale=2, - choices=["none", "standard", "html"] + choices=["none", "standard", "html"], ) api_language = gr.Dropdown( value=config.api_def.api_language, label="Default Language", scale=2, - choices=["ar", "zh", "cs", "nl", "en", "fr", "de", "hi", "hu", "it", "ja", "ko", "pl", "pt", "ru", "es", "tr"] + choices=[ + "ar", + "zh", + "cs", + "nl", + "en", + "fr", + "de", + "hi", + "hu", + "it", + "ja", + "ko", + "pl", + "pt", + "ru", + "es", + "tr", + ], ) - with gr.Row(): + with gr.Row(): with gr.Column(scale=1): with gr.Group(): with gr.Row(): api_narrator_enabled = gr.Dropdown( - choices=[("Enabled", "true"), ("Disabled", "false"), ("Enabled (Silent)", "silent")], + choices=[ + ("Enabled", "true"), + ("Disabled", "false"), + ("Enabled (Silent)", "silent"), + ], label="Narrator Mode", allow_custom_value=True, - value="true" if config.api_def.api_narrator_enabled == "true" else ("silent" if config.api_def.api_narrator_enabled == "silent" else "false") + value=( + "true" + if config.api_def.api_narrator_enabled + == "true" + else ( + "silent" + if config.api_def.api_narrator_enabled + == "silent" + else "false" + ) + ), ) api_text_not_inside = gr.Dropdown( choices=["character", "narrator", "silent"], label="Text-Not-Inside Handling", - allow_custom_value=True, - value=config.api_def.api_text_not_inside - ) + allow_custom_value=True, + value=config.api_def.api_text_not_inside, + ) with gr.Column(scale=2): with gr.Group(): - with gr.Row(): + with gr.Row(): api_output_file_name = gr.Textbox( value=config.api_def.api_output_file_name, - label="Default Filename" + label="Default Filename", ) api_output_file_timestamp = gr.Dropdown( - choices=["Timestamp files", "Dont Timestamp (Over-write)"], + choices=[ + "Timestamp files", + "Dont Timestamp (Over-write)", + ], label="File Timestamping", allow_custom_value=True, - value="Timestamp files" if config.api_def.api_output_file_timestamp else "Dont Timestamp (Over-write)" + value=( + "Timestamp files" + if config.api_def.api_output_file_timestamp + else "Dont Timestamp (Over-write)" + ), ) api_autoplay = gr.Dropdown( choices=["Play locally", "Play remotely"], label="Playback Location", allow_custom_value=True, - value="Play remotely" if config.api_def.api_autoplay else "Play locally" + value=( + "Play remotely" + if config.api_def.api_autoplay + else "Play locally" + ), ) api_autoplay_volume = gr.Slider( - minimum=0.1, maximum=0.9, step=0.1, + minimum=0.1, + maximum=0.9, + step=0.1, label="Remote Playback Volume", - value=float(config.api_def.api_autoplay_volume) + value=float(config.api_def.api_autoplay_volume), ) - with gr.Row(): + with gr.Row(): with gr.Column(scale=2): with gr.Group(): with gr.Row(): api_allowed_filter = gr.Textbox( value=config.api_def.api_allowed_filter, label="Allowed Characters & Unicode Ranges", - lines=3 + lines=3, ) with gr.Column(scale=1): with gr.Group(): with gr.Row(): with gr.Column(scale=2): output_message = gr.Textbox( - label="Status", - interactive=False + label="Status", interactive=False ) - submit_button = gr.Button("Update Settings") + submit_button = gr.Button("Update Settings") # Help Accordions with gr.Accordion("HELP - 🎯 Quick Start Guide", open=False): - gr.Markdown(AllTalkHelpContent.API_DEFAULTS1, elem_classes="custom-markdown") - with gr.Accordion("HELP - 🔄 Version & Compatibility Settings (API v1/v2, Legacy support)", open=False): - gr.Markdown(AllTalkHelpContent.API_DEFAULTS2, elem_classes="custom-markdown") - with gr.Accordion("HELP - 🎛️ Request Configuration (Character limits, language, file names)", open=False): - gr.Markdown(AllTalkHelpContent.API_DEFAULTS3, elem_classes="custom-markdown") - with gr.Accordion("HELP - 📝 Text Processing & Filtering (Filtering modes, character sets, language)", open=False): - gr.Markdown(AllTalkHelpContent.API_DEFAULTS4, elem_classes="custom-markdown") - with gr.Accordion("HELP - 🗣️ Narrator & Playback (Narrator modes, text handling, playback options", open=False): - gr.Markdown(AllTalkHelpContent.API_DEFAULTS5, elem_classes="custom-markdown") - with gr.Accordion("HELP - 📚 API Allowed Text Filtering/Passthrough", open=False): - gr.Markdown(AllTalkHelpContent.API_DEFAULTS6, elem_classes="custom-markdown") - with gr.Accordion("HELP - ❗ Troubleshooting & Best Practices", open=False): - gr.Markdown(AllTalkHelpContent.API_DEFAULTS7, elem_classes="custom-markdown") + gr.Markdown( + AllTalkHelpContent.API_DEFAULTS1, + elem_classes="custom-markdown", + ) + with gr.Accordion( + "HELP - 🔄 Version & Compatibility Settings (API v1/v2, Legacy support)", + open=False, + ): + gr.Markdown( + AllTalkHelpContent.API_DEFAULTS2, + elem_classes="custom-markdown", + ) + with gr.Accordion( + "HELP - 🎛️ Request Configuration (Character limits, language, file names)", + open=False, + ): + gr.Markdown( + AllTalkHelpContent.API_DEFAULTS3, + elem_classes="custom-markdown", + ) + with gr.Accordion( + "HELP - 📝 Text Processing & Filtering (Filtering modes, character sets, language)", + open=False, + ): + gr.Markdown( + AllTalkHelpContent.API_DEFAULTS4, + elem_classes="custom-markdown", + ) + with gr.Accordion( + "HELP - 🗣️ Narrator & Playback (Narrator modes, text handling, playback options", + open=False, + ): + gr.Markdown( + AllTalkHelpContent.API_DEFAULTS5, + elem_classes="custom-markdown", + ) + with gr.Accordion( + "HELP - 📚 API Allowed Text Filtering/Passthrough", open=False + ): + gr.Markdown( + AllTalkHelpContent.API_DEFAULTS6, + elem_classes="custom-markdown", + ) + with gr.Accordion( + "HELP - ❗ Troubleshooting & Best Practices", open=False + ): + gr.Markdown( + AllTalkHelpContent.API_DEFAULTS7, + elem_classes="custom-markdown", + ) - 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 = 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: - current_narr = current_voices[0] if current_voices else "" - return ( - gr.Dropdown(choices=current_voices, value=current_char, interactive=True), - gr.Dropdown(choices=current_voices, value=current_narr, interactive=True), + 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 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): + def rvc_update_dropdowns(): + """Update RVC dropdowns with current values from state""" + debug_func_entry() + + # Update state with current settings + get_alltalk_settings() + + # Get values from state + current_voices = _state['srv_current_rvcvoices'] + current_char = _state['srv_rvc_character_voice'] + current_narr = _state['srv_rvc_narrator_voice'] + + # Handle default voices if they're not in available voices + if current_char not in current_voices: + current_char = current_voices[0] if current_voices else "" + if current_narr not in current_voices: + current_narr = current_voices[0] if current_voices else "" + + return ( + gr.Dropdown(choices=current_voices, value=current_char, interactive=True), + gr.Dropdown(choices=current_voices, value=current_narr, interactive=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) + 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, + ) with gr.Tab("RVC Settings"): with gr.Row(): with gr.Column(scale=3): with gr.Row(): - rvc_model_file_available = at_settings["rvcvoices"] - 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 = 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_model_file_available = _state['srv_current_rvcvoices'] + 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 = ( + 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_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=config.rvc_settings.rvc_enabled, interactive=True) + # pylint: disable=line-too-long + 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=config.rvc_settings.pitch, interactive=True) + # pylint: disable=line-too-long + 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=config.rvc_settings.hop_length, interactive=True,) + # pylint: disable=line-too-long + 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(): - 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) + # pylint: disable=line-too-long + 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) + # pylint: disable=line-too-long + 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,) + # pylint: disable=line-too-long + 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,) + # pylint: disable=line-too-long + 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,) + # pylint: disable=line-too-long + 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(): + with gr.Row(): with gr.Row(): - 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,) + 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,) + # pylint: disable=line-too-long + 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, + ) + # pylint: disable=line-too-long + 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=config.rvc_settings.f0method, interactive=True,) + # pylint: disable=line-too-long + 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]) + 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.Accordion("🗣️ HELP - RVC Settings Page", open=False, elem_classes=["gr-accordion"]): + with gr.Accordion( + "🗣️ HELP - RVC Settings Page", + open=False, + elem_classes=["gr-accordion"], + ): with gr.Row(): - gr.Markdown(AllTalkHelpContent.RVC_PAGE1, elem_classes="custom-markdown") - gr.Markdown(AllTalkHelpContent.RVC_PAGE2, elem_classes="custom-markdown") - + gr.Markdown( + AllTalkHelpContent.RVC_PAGE1, + elem_classes="custom-markdown", + ) + gr.Markdown( + AllTalkHelpContent.RVC_PAGE2, + elem_classes="custom-markdown", + ) with gr.Tab("Text-generation-webui Settings"): with gr.Row(): - 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) + 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=_state['gradio_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) + output_message = gr.Textbox( + label="Output Message", interactive=False, show_label=False + ) - submit_button.click(update_settings_tgwui, 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() @@ -2960,43 +4707,107 @@ if gradio_enabled == 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"]) + for engine_name in _state.get('srv_engines_available', []): + if base_package: + module_name = ( + f"{base_package}.system.tts_engines." + f"{engine_name}." + f"{engine_name}_settings_page" + ) + else: + module_name = f"system.tts_engines.{engine_name}.{engine_name}_settings_page" + + module = dynamic_import(module_name, base_package) + if module: + with gr.Tab(f"{engine_name.capitalize()} TTS"): + gr.Markdown(f"###   {engine_name.capitalize()} TTS") + getattr( + module, + f"{engine_name}_at_gradio_settings_page" + )( + globals()[f"{engine_name}_model_config_data"] + ) if config.gradio_pages.alltalk_documentation_page: with gr.Tab("Documentation"): with gr.Accordion("📖 HELP - Narrator Function", open=False): - gr.Markdown(AllTalkHelpContent.NARRATOR, elem_classes="custom-markdown") + gr.Markdown( + AllTalkHelpContent.NARRATOR, elem_classes="custom-markdown" + ) with gr.Row(): - gr.Markdown(AllTalkHelpContent.NARRATOR1, elem_classes="custom-markdown") - gr.Markdown(AllTalkHelpContent.NARRATOR2, elem_classes="custom-markdown") - with gr.Accordion("🔌 HELP - API Standard TTS Generation Endpoints", open=False): - gr.Markdown(AllTalkHelpContent.API_STANDARD, elem_classes="custom-markdown") - with gr.Accordion("🔌 HELP - API Streaming TTS Generation Endpoints", open=False): - gr.Markdown(AllTalkHelpContent.API_STREAMING, elem_classes="custom-markdown") - with gr.Accordion("🔌 HELP - API Server Control Endpoints", open=False): - gr.Markdown(AllTalkHelpContent.API_CONTROL, elem_classes="custom-markdown") - with gr.Accordion("🔌 HELP - API Server Status Endpoints", open=False): - gr.Markdown(AllTalkHelpContent.API_STATUS, elem_classes="custom-markdown") - with gr.Accordion("🔌 HELP - API OpenAI V1 Speech Endpoints", open=False): - gr.Markdown(AllTalkHelpContent.API_OPENAPI, elem_classes="custom-markdown") + gr.Markdown( + AllTalkHelpContent.NARRATOR1, + elem_classes="custom-markdown", + ) + gr.Markdown( + AllTalkHelpContent.NARRATOR2, + elem_classes="custom-markdown", + ) + with gr.Accordion( + "🔌 HELP - API Standard TTS Generation Endpoints", open=False + ): + gr.Markdown( + AllTalkHelpContent.API_STANDARD, + elem_classes="custom-markdown", + ) + with gr.Accordion( + "🔌 HELP - API Streaming TTS Generation Endpoints", open=False + ): + gr.Markdown( + AllTalkHelpContent.API_STREAMING, + elem_classes="custom-markdown", + ) + with gr.Accordion( + "🔌 HELP - API Server Control Endpoints", open=False + ): + gr.Markdown( + AllTalkHelpContent.API_CONTROL, + elem_classes="custom-markdown", + ) + with gr.Accordion( + "🔌 HELP - API Server Status Endpoints", open=False + ): + gr.Markdown( + AllTalkHelpContent.API_STATUS, + elem_classes="custom-markdown", + ) + with gr.Accordion( + "🔌 HELP - API OpenAI V1 Speech Endpoints", open=False + ): + gr.Markdown( + AllTalkHelpContent.API_OPENAPI, + elem_classes="custom-markdown", + ) with gr.Tab("About this project"): with gr.Row(): - gr.Markdown(AllTalkHelpContent.WELCOME1, elem_classes="custom-markdown") - gr.Markdown(AllTalkHelpContent.WELCOME2, elem_classes="custom-markdown") + gr.Markdown( + AllTalkHelpContent.WELCOME1, elem_classes="custom-markdown" + ) + gr.Markdown( + AllTalkHelpContent.WELCOME2, elem_classes="custom-markdown" + ) return app + # pylint: disable=redefined-outer-name if __name__ == "__main__": app = alltalk_gradio().queue() - app.launch(server_name="0.0.0.0", server_port=config.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=config.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 # diff --git a/tts_server.py b/tts_server.py index f963003..8d4bfad 100644 --- a/tts_server.py +++ b/tts_server.py @@ -1,41 +1,48 @@ -import os -import sys -import json -import time -import inspect -import argparse -import librosa -import logging -import importlib -import subprocess -from pathlib import Path -from config import AlltalkConfig, AlltalkTTSEnginesConfig +""" +AllTalk TTS server implementation for managing and serving text-to-speech generation. -logging.disable(logging.WARNING) -##################### -# Webserver Imports # -##################### +This script provides endpoints for generating TTS, handling voice configurations, +and managing audio processing, including RVC-based voice conversion and OpenAI-compatible APIs. +It supports features like streaming, file format transcoding, and real-time configuration updates. + +Github: https://github.com/erew123/ +""" +import warnings from contextlib import asynccontextmanager -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates -from fastapi.middleware.cors import CORSMiddleware -from fastapi import FastAPI, Form, Request, Response, Depends, HTTPException, Query -from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse, FileResponse, StreamingResponse -########################## -# Generation API Imports # -########################## +import argparse +import asyncio +from asyncio import Lock +import importlib +import inspect +import json +import logging +import os import re import html import uuid import hashlib +import sys +import time +import subprocess +from pathlib import Path +from typing import Union, List, Optional, Tuple +import aiofiles +import uvicorn +from pydantic import BaseModel, ValidationError, Field, field_validator +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from fastapi.middleware.cors import CORSMiddleware +from fastapi import FastAPI, Form, Request, Response, Depends, HTTPException, Query +from fastapi.responses import JSONResponse, HTMLResponse, FileResponse, StreamingResponse import numpy as np import soundfile as sf -from typing import Union, Dict, List, Optional, Tuple -from pydantic import BaseModel, ValidationError, Field, field_validator +import librosa +from config import AlltalkConfig, AlltalkTTSEnginesConfig +logging.disable(logging.WARNING) + ######################################################################################## # START-UP # Silence RVC warning about torch.nn.utils.weight_norm even though not used # ######################################################################################## -import warnings warnings.filterwarnings("ignore", category=UserWarning, module="torch.nn.utils.weight_norm") warnings.filterwarnings("ignore", category=UserWarning, module="torch.nn.functional", lineno=5476) # Filter ComplexHalf support warning @@ -52,22 +59,23 @@ config: AlltalkConfig | None = None tts_engines_config: AlltalkTTSEnginesConfig | None = None def load_config(force_reload = False): - global config, tts_engines_config + """Initialize all configuration instances""" + global config, tts_engines_config # pylint: disable=global-statement config = AlltalkConfig.get_instance(force_reload) tts_engines_config = AlltalkTTSEnginesConfig.get_instance(force_reload) after_config_load() def after_config_load(): - global infer_pipeline + """Initialize the infer_pipeline based on RVC settings.""" + global infer_pipeline # pylint: disable=global-statement if config.rvc_settings.rvc_enabled: - from system.tts_engines.rvc.infer.infer import infer_pipeline + from system.tts_engines.rvc.infer.infer import infer_pipeline as rvc_pipeline + infer_pipeline = rvc_pipeline else: infer_pipeline = None load_config() -# pylint: disable=all - ########################## # Central print function # ########################## @@ -80,34 +88,34 @@ def print_message(message, message_type="standard", component="TTS"): """ # ANSI color codes BLUE = "\033[94m" - MAGENTA = "\033[95m" - YELLOW = "\033[93m" + # 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} tts_server.py") 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}") @@ -120,13 +128,13 @@ def debug_func_entry(): #################### # Check for FFMPEG # #################### -def check_ffmpeg(this_dir): +def check_ffmpeg(ffmpeg_dir): """Verify FFmpeg availability in the system.""" debug_func_entry() message = "" - + if sys.platform == "win32": - ffmpeg_path = os.path.join(this_dir, "system", "win_ffmpeg", "ffmpeg.exe") + ffmpeg_path = os.path.join(ffmpeg_dir, "system", "win_ffmpeg", "ffmpeg.exe") if os.path.exists(ffmpeg_path): return True, "FFmpeg found in Windows directory" message = "FFmpeg not found in Windows directory" @@ -136,7 +144,7 @@ def check_ffmpeg(this_dir): return True, "FFmpeg found in system PATH" except (subprocess.CalledProcessError, FileNotFoundError): message = "FFmpeg not found in system PATH" - + print_message(message, "warning") return False, message @@ -147,7 +155,7 @@ if not ffmpeg_installed: 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_message("\033[92mTranscoding :\033[91m ffmpeg not found\033[0m", component="ENG") print_message("Installation instructions for Windows:", component="ENG") @@ -158,16 +166,17 @@ if not ffmpeg_installed: 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") +FFmpeg = None if ffmpeg_installed: from ffmpeg.asyncio import FFmpeg 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_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") @@ -193,7 +202,7 @@ else: # Run setup function in the model_engine # ########################################## @asynccontextmanager -async def startup_shutdown(no_actual_value_it_demanded_something_be_here): +async def startup_shutdown(no_actual_value_it_demanded_something_be_here): # pylint: disable=unused-argument """Initialize model engine and handle graceful shutdown. This is a context manager.""" debug_func_entry() try: @@ -215,45 +224,41 @@ app.add_middleware( allow_headers=["*"], ) +# Global lock +model_change_lock = Lock() ############################## # API Endpoint - /api/reload # ############################## -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.""" + """Handle API request to change TTS model.""" debug_func_entry() - - global model_currently_changing - if model_currently_changing: + + if model_change_lock.locked(): # Check if the lock is already held 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 # Fixed assignment operator - requested_model = request.query_params.get("tts_method") - - if requested_model not in model_engine.available_models: - 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"} - - 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 - 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: + + async with model_change_lock: # Acquire the lock for this operation + requested_model = request.query_params.get("tts_method") + if requested_model not in model_engine.available_models: + print_message(f"Invalid TTS method requested: {requested_model}", "error", "API") + return {"status": "error", "message": "Invalid TTS method specified"} + + print_message(f"Attempting to change model to: {requested_model}", "debug_api", "API") + success = await model_engine.handle_tts_method_change(requested_model) + + if success: + model_engine.current_model_loaded = requested_model + print_message(f"Model successfully changed to: {requested_model}", "debug_api", "API") + return Response(content=json.dumps({"status": "model-success"}), media_type="application/json") + print_message(f"Failed to change model to: {requested_model}", "error", "API") return Response(content=json.dumps({"status": "model-failure"}), media_type="application/json") - + #################################### # API Endpoint - /api/enginereload # #################################### -import asyncio -uvicorn_server = None +uvicorn_server = None # Global variable to track the server def restart_self(): """Restart the current Python process.""" debug_func_entry() @@ -263,39 +268,39 @@ def restart_self(): async def handle_restart(): """Handle graceful shutdown of uvicorn server before restart.""" debug_func_entry() - global uvicorn_server - + global uvicorn_server # pylint: disable=global-statement,disable=global-variable-not-assigned + if uvicorn_server: print_message("Stopping uvicorn server...", component="ENG") uvicorn_server.should_exit = True uvicorn_server.force_exit = True - + while not uvicorn_server.is_stopped: await asyncio.sleep(0.1) - + restart_self() @app.post("/api/enginereload") -async def apifunction_reload(request: Request): +async def apifunction_enginereload(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_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: tts_engines_config.reload() print_message("Configuration reloaded", "debug_api", "API") - + asyncio.create_task(handle_restart()) return Response(content=json.dumps({"status": "engine-success"}), media_type="application/json") @@ -330,14 +335,14 @@ async def apifunction_get_audio(filename: str): # API Endpoint - /api/audiocache # ################################## @app.get("/audiocache/{filename}") -async def apifunction_get_audio(filename: str): +async def apifunction_get_audiocache(filename: str): """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) response.headers["Cache-Control"] = "public, max-age=604800" response.headers["ETag"] = str(audio_path.stat().st_mtime) @@ -350,18 +355,27 @@ async def apifunction_get_audio(filename: str): async def apifunction_get_voices(): """Get available voices for current TTS engine.""" debug_func_entry() - + 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) + except AttributeError as e: + print_message(f"Attribute error in model engine: {str(e)}", "error", "API") + return JSONResponse( + content={"status": "error", "message": "Model engine configuration is invalid. Please check your setup."}, + status_code=500 + ) + except FileNotFoundError as e: + print_message(f"Voices file not found: {str(e)}", "error", "API") + return JSONResponse( + content={"status": "error", "message": "The voices file could not be located. Please ensure the file exists."}, + status_code=404 + ) ################################# # API Endpoint - /api/rvcvoices # @@ -370,37 +384,50 @@ async def apifunction_get_voices(): async def apifunction_get_rvcvoices(): """Get available RVC voice models.""" debug_func_entry() - + response_data = {"status": "success", "rvcvoices": ["Disabled"]} + try: load_config() if not config.rvc_settings.rvc_enabled: - return {"status": "success", "rvcvoices": ["Disabled"]} - + return response_data + 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"]} - + return response_data + 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) - + raise PermissionError("Cannot access RVC voices directory") + 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) + 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 pth_files: + response_data["rvcvoices"] = ["Disabled"] + sorted(pth_files) + return response_data + + except FileNotFoundError: + print_message("RVC voices directory not found", "error", "API") + return JSONResponse( + content={"status": "error", "message": "RVC voices directory not found"}, + status_code=404 + ) + except PermissionError as e: + print_message(f"Permission denied accessing RVC directory: {str(e)}", "error", "API") + return JSONResponse( + content={"status": "error", "message": "Permission denied accessing RVC directory"}, + status_code=403 + ) + except OSError as e: + print_message(f"OS error processing RVC voices: {str(e)}", "error", "API") + return JSONResponse( + content={"status": "error", "message": "System error accessing RVC voices"}, + status_code=500 + ) ##################################### # API Endpoint - /api/reload_config # @@ -409,14 +436,20 @@ async def apifunction_get_rvcvoices(): async def apifunction_reload_config(): """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) + except json.JSONDecodeError as e: + print_message(f"Invalid JSON in configuration: {str(e)}", "error", "API") + return JSONResponse(content={"status": "error", "message": "Invalid configuration format"}, status_code=400) + except (ValueError, TypeError) as e: + print_message(f"Invalid configuration value: {str(e)}", "error", "API") + return JSONResponse(content={"status": "error", "message": "Invalid configuration"}, status_code=400) + except OSError as e: + print_message(f"Error accessing configuration file: {str(e)}", "error", "API") + return JSONResponse(content={"status": "error", "message": "Could not access configuration"}, status_code=500) ############################# # API Endpoint - /api/ready # @@ -436,8 +469,8 @@ async def apifunction_ready(): def apifunction_get_current_settings(): """Get comprehensive dictionary of current engine settings and capabilities.""" debug_func_entry() - load_config() try: + load_config() settings = { "engines_available": tts_engines_config.get_engine_names_available(), "current_engine_loaded": model_engine.engine_loaded, @@ -466,162 +499,232 @@ def apifunction_get_current_settings(): } 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) + except AttributeError as e: + print_message(f"Missing required engine attribute: {str(e)}", "error", "API") + return JSONResponse( + content={"status": "error", "message": "Invalid engine configuration"}, + status_code=500 + ) + except KeyError as e: + print_message(f"Missing required configuration key: {str(e)}", "error", "API") + return JSONResponse( + content={"status": "error", "message": "Invalid engine configuration"}, + status_code=500 + ) + except (ValueError, TypeError) as e: + print_message(f"Invalid configuration value: {str(e)}", "error", "API") + return JSONResponse( + content={"status": "error", "message": "Invalid configuration value"}, + status_code=500 + ) ###################################### # API Endpoint - /api/lowvramsetting # -###################################### +###################################### @app.post("/api/lowvramsetting") -async def apifunction_low_vram(request: Request, new_low_vram_value: bool): +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() - + + response_content = {"status": "error", "message": ""} + + # Early return for unsupported engine if not model_engine.lowvram_capable: + msg = f"The currently loaded TTS engine '{model_engine.engine_loaded}' does not support lowvram." 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."} - + response_content["message"] = msg + return Response(content=json.dumps(response_content)) + try: if new_low_vram_value is None: raise ValueError("Missing 'low_vram' parameter") - + + # Handle no-change case if model_engine.lowvram_enabled == new_low_vram_value: 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_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_message("\033[94mLowVRAM Disabled.\033[0m Model will stay in \033[93mVRAM(cuda)\033[0m", component="ENG") - await model_engine.setup() + response_content.update({"status": "success", "message": msg}) else: - 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)})) + # Process LowVRAM change + 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_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_message("\033[94mLowVRAM Disabled.\033[0m Model will stay in \033[93mVRAM(cuda)\033[0m", component="ENG") + await model_engine.setup() + else: + print_message("Nvidia CUDA is not available on this system. Unable to use LowVRAM mode.", "error", "ENG") + model_engine.lowvram_enabled = False + + response_content.update({"status": "lowvram-success", "message": ""}) + + except ValueError as e: + print_message(f"Invalid parameter value: {str(e)}", "error", "API") + response_content["message"] = str(e) + except AttributeError as e: + print_message(f"Model engine configuration error: {str(e)}", "error", "API") + response_content["message"] = "Invalid model engine configuration" + except RuntimeError as e: + print_message(f"CUDA/Device error: {str(e)}", "error", "API") + response_content["message"] = "Device allocation error" + except json.JSONEncodeError as e: + print_message(f"JSON encoding error: {str(e)}", "error", "API") + response_content["message"] = "Response encoding error" + except OSError as e: + print_message(f"System resource error: {str(e)}", "error", "API") + response_content["message"] = "System resource error" + + return Response(content=json.dumps(response_content)) ################################# # API Endpoint - /api/deepspeed # ################################# @app.post("/api/deepspeed") -async def deepspeed(request: Request, new_deepspeed_value: bool): +async def deepspeed(_request: Request, new_deepspeed_value: bool): """Handle DeepSpeed mode toggle. Verifies capability and handles model changes.""" debug_func_entry() - + + response_content = {"status": "error", "message": ""} + + # Early check for DeepSpeed capability if not model_engine.deepspeed_capable or not model_engine.deepspeed_available: + msg = f"The currently loaded TTS engine '{model_engine.engine_loaded}' does not support DeepSpeed or DeepSpeed is not available on this system." 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."} - + response_content["message"] = msg + return Response(content=json.dumps(response_content)) + try: if new_deepspeed_value is None: raise ValueError("Missing 'deepspeed' parameter") - + + # Handle no-change case if model_engine.deepspeed_enabled == new_deepspeed_value: 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)})) + response_content = {"status": "success", "message": msg} + else: + # Process DeepSpeed change + 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") + response_content = {"status": "deepspeed-success"} + + except ValueError as e: + print_message(f"Value error: {str(e)}", "error", "API") + response_content["message"] = f"Invalid input: {str(e)}" + except AttributeError as e: + print_message(f"Attribute error: {str(e)}", "error", "API") + response_content["message"] = "Unexpected attribute error. Check model engine configuration." + except RuntimeError as e: + print_message(f"Runtime error: {str(e)}", "error", "API") + response_content["message"] = f"Runtime error: {str(e)}" + except TypeError as e: + print_message(f"Type error: {str(e)}", "error", "API") + response_content["message"] = f"Type error: {str(e)}" + except OSError as e: + print_message(f"System error: {str(e)}", "error", "API") + response_content["message"] = "System resource or file error" + except json.JSONEncodeError as e: + print_message(f"JSON encoding error: {str(e)}", "error", "API") + response_content["message"] = "Error encoding response" + except ImportError as e: + print_message(f"DeepSpeed import error: {str(e)}", "error", "API") + response_content["message"] = "DeepSpeed module not properly installed" + except MemoryError as e: + print_message(f"Memory allocation error: {str(e)}", "error", "API") + response_content["message"] = "Insufficient memory for DeepSpeed operation" + + return Response(content=json.dumps(response_content)) ################################# # API Endpoint - /api/voice2rvc # ################################# @app.post("/api/voice2rvc") -async def voice2rvc(input_tts_path: str = Form(...), output_rvc_path: 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: if pth_name.lower() in ["disabled", "disable"]: 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) - + 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.") - + pth_path = this_dir / "models" / "rvc_voices" / pth_name 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.") - + 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.") - + print_message("RVC conversion failed", "error") + raise HTTPException(status_code=500, detail="RVC conversion failed.") + except Exception as e: print_message(f"Error during Voice2RVC conversion: {e}", "error") - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=str(e)) from e -def run_voice2rvc(input_tts_path, output_rvc_path, pth_path, pitch, method): +def run_voice2rvc(input_tts_path, output_rvc_path, pth_path, pitch, method) -> Optional[str]: """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 - rms_mix_rate = config.rvc_settings.rms_mix_rate - protect = config.rvc_settings.protect - hop_length = config.rvc_settings.hop_length - f0method = method - 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 + 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": method, # This might need to be converted or mapped + "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) - + if not os.path.isfile(pth_path): print_message(f"Model file {pth_path} does not exist. Exiting.", "error", "GEN") - return - + return None + model_dir = os.path.dirname(pth_path) index_files = [file for file in os.listdir(model_dir) if file.endswith(".index")] 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) - + + 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"\033[94mVoice2RVC Convert: \033[91m{generate_elapsed_time:.2f} seconds.\033[0m", component="GEN") - + return output_rvc_path ################################## @@ -630,36 +733,36 @@ def run_voice2rvc(input_tts_path, output_rvc_path, pth_path, pitch, method): async def transcode_audio(input_file, output_format): """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_message("Transcode format is set to Disabled so skipping transcode.", "debug_transcode") return input_file - + if not ffmpeg_installed: print_message("FFmpeg is not installed. Format conversion is not possible.", "error") - raise Exception("FFmpeg is not installed. Format conversion is not possible.") - + raise RuntimeError("FFmpeg is not installed. Format conversion is not possible.") + input_extension = os.path.splitext(input_file)[1][1:].lower() print_message(f"Input file extension: {input_extension}", "debug_transcode") - + if input_extension == output_format.lower(): 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_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_message("Configuring Opus options", "debug_transcode") @@ -686,7 +789,7 @@ async def transcode_audio(input_file, output_format): else: print_message(f"Unsupported output format: {output_format}", "error") raise ValueError(f"Unsupported output format: {output_format}") - + try: print_message("Starting transcoding process", "debug_transcode") await ffmpeg.execute() @@ -694,10 +797,10 @@ async def transcode_audio(input_file, output_format): except Exception as e: print_message(f"Error occurred during transcoding: {str(e)}", "error") raise - + print_message("Deleting original input file", "debug_transcode") os.remove(input_file) - + print_message("Transcoding process completed", "debug_transcode") print_message(f"Transcoded file: {output_file}", "debug_transcode") return output_file @@ -708,11 +811,11 @@ async def transcode_audio(input_file, output_format): async def transcode_audio_if_necessary(output_file, model_audio_format, output_audio_format): """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: print_message("Calling transcode_audio function", "debug_transcode") output_file = await transcode_audio(output_file, output_audio_format) @@ -720,10 +823,10 @@ async def transcode_audio_if_necessary(output_file, model_audio_format, output_a except Exception as e: print_message(f"Error occurred during transcoding: {str(e)}", "error") raise - + 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)}' @@ -731,7 +834,7 @@ async def transcode_audio_if_necessary(output_file, model_audio_format, output_a else: output_file_url = f'/audio/{os.path.basename(output_file)}' output_cache_url = f'/audiocache/{os.path.basename(output_file)}' - + print_message("Output file paths and URLs updated", "debug_tts") return output_file, output_file_url, output_cache_url @@ -742,44 +845,63 @@ async def transcode_audio_if_necessary(output_file, model_audio_format, output_a async def apifunction_generate_streaming(text: str, voice: str, language: str, output_file: str): """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}' 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, + 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_message(f"An error occurred: {e}", "error", "GEN") - return JSONResponse(content={"error": "An error occurred"}, status_code=500) + except ValueError as e: + print_message(f"Value error occurred: {str(e)}", "error", "GEN") + return JSONResponse(content={"error": f"Invalid value: {str(e)}"}, status_code=400) + except KeyError as e: + print_message(f"Key error occurred: Missing key {str(e)}", "error", "GEN") + return JSONResponse(content={"error": f"Missing required field: {str(e)}"}, status_code=400) + except FileNotFoundError as e: + print_message(f"File not found: {str(e)}", "error", "GEN") + return JSONResponse(content={"error": f"File not found: {str(e)}"}, status_code=404) + except TypeError as e: + print_message(f"Type error occurred: {str(e)}", "error", "GEN") + return JSONResponse(content={"error": f"Type error: {str(e)}"}, status_code=400) + except RuntimeError as e: + print_message(f"Runtime error occurred: {str(e)}", "error", "GEN") + return JSONResponse(content={"error": "An internal runtime 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(...), +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}' 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", + 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_message(f"An error occurred: {e}", "error", "GEN") - return JSONResponse(content={"error": "An error occurred"}, status_code=500) + except ValueError as e: + print_message(f"Invalid streaming parameters: {str(e)}", "error", "GEN") + raise HTTPException(status_code=400, detail=str(e)) from e + except IOError as e: + print_message(f"IO error during streaming: {str(e)}", "error", "GEN") + raise HTTPException(status_code=500, detail="Audio streaming failed") from e + except RuntimeError as e: + print_message(f"Runtime error during streaming: {str(e)}", "error", "GEN") + raise HTTPException(status_code=500, detail="Audio generation failed") from e + ################################### # Central generate_audio function # @@ -787,15 +909,15 @@ async def tts_generate_streaming(request: Request, text: str = Form(...), voice: async def generate_audio(text, voice, language, temperature, repetition_penalty, speed, pitch, output_file, streaming=False): """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: async def stream_response(): try: @@ -805,49 +927,48 @@ async def generate_audio(text, voice, language, temperature, repetition_penalty, print_message(f"Error during streaming audio generation: {str(e)}", "error", "GEN") raise return stream_response() - else: - try: - async for _ in response: - pass - except Exception as e: - print_message(f"Error during audio generation: {str(e)}", "error", "GEN") - raise + try: + async for _ in response: + pass + except Exception as e: + print_message(f"Error during audio generation: {str(e)}", "error", "GEN") + raise ########################### #### PREVIEW VOICE API #### ########################### @app.post("/api/previewvoice/", response_class=JSONResponse) async def apifunction_preview_voice( - request: Request, + _request: Request, voice: str = Form(...), 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: language = "en" 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) text = f"Hello, this is a preview of voice {clean_voice_filename}." - + rvccharacter_voice_gen = rvccharacter_voice_gen or "Disabled" rvccharacter_pitch = rvccharacter_pitch if rvccharacter_pitch is not None else 0 - + 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, - model_engine.temperature_set, - model_engine.repetitionpenalty_set, - 1.0, 0, output_file_path, + text, voice, language, + model_engine.temperature_set, + model_engine.repetitionpenalty_set, + 1.0, 0, output_file_path, streaming=False ) - + if config.rvc_settings.rvc_enabled: if rvccharacter_voice_gen.lower() in ["disabled", "disable"]: print_message("def apifunction_preview_voice RVC processing skipped", "debug_tts") @@ -855,12 +976,12 @@ async def apifunction_preview_voice( 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 - + 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 JSONResponse( content={ @@ -870,13 +991,23 @@ async def apifunction_preview_voice( }, status_code=200, ) - + + except ValueError as e: + print_message(f"Value error occurred: {str(e)}", "error", "GEN") + return JSONResponse(content={"error": f"Invalid value: {str(e)}"}, status_code=400) + except KeyError as e: + print_message(f"Key error occurred: Missing key {str(e)}", "error", "GEN") + return JSONResponse(content={"error": f"Missing required field: {str(e)}"}, status_code=400) except FileNotFoundError as e: print_message(str(e), "error", "GEN") - return JSONResponse(content={"error": str(e)}, status_code=404) - except Exception as e: - print_message(f"Error generating preview: {str(e)}", "error", "GEN") - return JSONResponse(content={"error": str(e)}, status_code=500) + return JSONResponse(content={"error": f"File not found: {str(e)}"}, status_code=404) + except TypeError as e: + print_message(f"Type error occurred: {str(e)}", "error", "GEN") + return JSONResponse(content={"error": f"Type error: {str(e)}"}, status_code=400) + except RuntimeError as e: + print_message(f"Runtime error occurred: {str(e)}", "error", "GEN") + return JSONResponse(content={"error": "An internal runtime error occurred"}, status_code=500) + ################################################################## # API Endpoint - OpenAI Speech API compatable endpoint Validator # @@ -889,17 +1020,23 @@ class OpenAIInput(BaseModel): response_format: str = Field(default="wav", description="The format of the audio. Currently only 'wav' is supported.") speed: float = Field(default=1.0, ge=0.25, le=4.0, description="The speed of the generated audio.") - @field_validator('voice') + @field_validator('voice', mode='before') + @classmethod 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") + raise ValueError(f"Voice must be one of {supported_voices}") return value class OpenAIGenerator: + """ + Validates the input parameters for an OpenAI TTS request. + + This method checks the provided JSON data against the expected schema defined by the `OpenAIInput` model. + If validation succeeds, it returns `None`. If validation fails, it returns a string detailing the errors, + including the field name, validation error message, and field description. + """ @staticmethod def validate_input(json_data: dict) -> Union[None, str]: """Validate OpenAI TTS request parameters.""" @@ -926,26 +1063,33 @@ class OpenAIGenerator: async def openai_tts_generate(request: Request): """Handle OpenAI-compatible TTS generation requests.""" debug_func_entry() - + + # Initialize response variables + response = None + status_code = 200 + try: json_data = await request.json() print_message(f"Received JSON data: {json_data}", "debug_openai", "TTS") - + + # Validation check validation_error = OpenAIGenerator.validate_input(json_data) if validation_error: print_message(f"Validation error: {validation_error}", "debug_openai", "TTS") - return JSONResponse(content={"error": validation_error}, status_code=400) - + raise ValueError(validation_error) + + # Extract and validate parameters 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_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)) + + # Process text and map voice + cleaned_string = html.unescape(standard_filtering(input_text)) voice_mapping = { "alloy": model_engine.openai_alloy, "echo": model_engine.openai_echo, @@ -954,14 +1098,15 @@ 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_message(f"Unsupported voice: {voice}", "error", "TTS") - return JSONResponse(content={"error": "Unsupported voice"}, status_code=400) - + raise ValueError("Unsupported voice") + print_message(f"Mapped voice: {mapped_voice}", "debug_openai", "TTS") - + + # Generate audio 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}"}' @@ -970,13 +1115,14 @@ async def openai_tts_generate(request: Request): 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_message(f"Audio generated at: {output_file_path}", "debug_openai", "TTS") - + + # Handle RVC processing if config.rvc_settings.rvc_enabled: if config.rvc_settings.rvc_char_model_file.lower() in ["disabled", "disable"]: print_message("Pass rvccharacter_voice_gen", "debug_openai", "TTS") @@ -985,16 +1131,37 @@ async def openai_tts_generate(request: Request): 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) - + transcoded_file_path = await transcode_for_openai(output_file_path, 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_message(f"An error occurred: {str(e)}", "error", "TTS") - return JSONResponse(content={"error": "An error occurred"}, status_code=500) + + response = FileResponse(transcoded_file_path, media_type=f"audio/{response_format}", + filename=f"output.{response_format}") + + except ValueError as e: + print_message(f"Value error occurred: {str(e)}", "error", "TTS") + response = JSONResponse(content={"error": f"Invalid value: {str(e)}"}) + status_code = 400 + except KeyError as e: + print_message(f"Key error occurred: Missing key {str(e)}", "error", "TTS") + response = JSONResponse(content={"error": f"Missing required field: {str(e)}"}) + status_code = 400 + except FileNotFoundError as e: + print_message(f"File not found error: {str(e)}", "error", "TTS") + response = JSONResponse(content={"error": "Required file not found"}) + status_code = 404 + except TypeError as e: + print_message(f"Type error occurred: {str(e)}", "error", "TTS") + response = JSONResponse(content={"error": f"Type error: {str(e)}"}) + status_code = 400 + except RuntimeError as e: + print_message(f"Runtime error occurred: {str(e)}", "error", "TTS") + response = JSONResponse(content={"error": "An internal runtime error occurred"}) + status_code = 500 + + if status_code != 200: + return response.status_code(status_code) + return response ########################################################################### # API Endpoint - OpenAI Speech API compatable endpoint Transcode Function # @@ -1002,32 +1169,32 @@ async def openai_tts_generate(request: Request): async def transcode_for_openai(input_file, output_format): """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_message("FFmpeg is not installed. Format conversion is not possible.", "error") - raise Exception("FFmpeg is not installed. Format conversion is not possible.") - + raise RuntimeError("FFmpeg is not installed. Format conversion is not possible.") + input_extension = os.path.splitext(input_file)[1][1:].lower() print_message(f"Input file extension: {input_extension}", "debug_openai", "TTS") - + if input_extension == output_format.lower(): 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_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"}, @@ -1038,14 +1205,14 @@ async def transcode_for_openai(input_file, output_format): "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_message(f"Unsupported output format: {output_format}", "error", "TTS") raise ValueError(f"Unsupported output format: {output_format}") - + try: print_message("Starting transcoding process", "debug_openai", "TTS") await ffmpeg.execute() @@ -1053,7 +1220,7 @@ async def transcode_for_openai(input_file, output_format): except Exception as e: print_message(f"Error occurred during transcoding: {str(e)}", "error", "TTS") raise - + print_message("Transcoding process completed", "debug_openai", "TTS") print_message(f"Transcoded file: {output_file}", "debug_openai", "TTS") return output_file @@ -1075,7 +1242,7 @@ class VoiceMappings(BaseModel): async def update_openai_voice_mappings(mappings: VoiceMappings): """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") @@ -1085,22 +1252,22 @@ async def update_openai_voice_mappings(mappings: VoiceMappings): 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) + with open(settings_file, "r+", encoding="utf-8") as model_file: + settings = json.load(model_file) settings["openai_voices"] = mappings.dict() - f.seek(0) - json.dump(settings, f, indent=4) - f.truncate() - + model_file.seek(0) + json.dump(settings, model_file, indent=4) + model_file.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)}") + raise HTTPException(status_code=500, detail=f"Failed to update model settings file: {str(e)}") from e ####################### # Play at the console # @@ -1108,57 +1275,63 @@ async def update_openai_voice_mappings(mappings: VoiceMappings): def play_audio(file_path, volume): """Play audio file with specified volume through system speakers at the console/terminal window""" debug_func_entry() - + try: 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_message(f"\033[94mError playing audio file: {e}\033[0m", "error", "GEN") - -class Request(BaseModel): - """Define the structure of the 'Request' class if needed""" - pass + except FileNotFoundError as e: + print_message(f"\033[94mAudio file not found: {e}\033[0m", "error", "GEN") + except IOError as e: + print_message(f"\033[94mError reading audio file: {e}\033[0m", "error", "GEN") + except sd.PortAudioError as e: + print_message(f"\033[94mAudio playback error: {e}\033[0m", "error", "GEN") class TTSGenerator: + """ + Validate JSON input parameters for TTS generation. + """ @staticmethod - def validate_json_input(json_data: Union[Dict, str]) -> Union[None, str]: - """Validate TTS generation input parameters.""" + def validate_json_input(json_input_data): + """ + Validate JSON input against JSONInput model schema. + """ debug_func_entry() - try: - if isinstance(json_data, str): - json_data = json.loads(json_data) - JSONInput(**json_data) - print_message("JSON input validation successful", "debug_tts", "GEN") + JSONInput(**json_input_data) return None except ValidationError as e: - print_message(f"JSON validation failed: {str(e)}", "error", "GEN") - return str(e) + error_messages = [] + for error in e.errors(): + field = error["loc"][0] + description = JSONInput.model_fields[field].description # Updated for Pydantic v2.x + error_messages.append(f"{field}: {description}") + error_message = "\n".join(error_messages) + print_message(f"Error with API request: {error_message}", "error", "API") + return error_message ################################################################# # /api/tts-generate Generation API Endpoint Narration Filtering # @@ -1166,16 +1339,16 @@ class TTSGenerator: def process_text(text): """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) text = re.sub(r'\.{3,}', '.', text) - + # Pattern for identifying speech segments combined_pattern = r'(\*[^*"]+\*|"[^"*]+")' ordered_parts = [] start = 0 - + # Process text segments for match in re.finditer(combined_pattern, text): # Handle pre-match ambiguous text @@ -1184,7 +1357,7 @@ def process_text(text): if ambiguous_text: ordered_parts.append(('ambiguous', ambiguous_text)) 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('*'): @@ -1203,22 +1376,22 @@ def process_text(text): else: ordered_parts.append(('character', text_part)) print_message(f"Added mixed character segment: {text_part}", "debug_tts", "GEN") - + start = match.end() - + # 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("**", "") @@ -1226,72 +1399,103 @@ def standard_filtering(text_input): .replace("\n\n", "\n") .replace("'", "'") ) - - print_message(f"Filtered text: {text_output[:50]}{'...' if len(text_output) > 50 else ''}", + + 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): - """Combine multiple audio files into one, with optional resampling and timestamping.""" - debug_func_entry() +def combine(output_file_timestamp, output_file_name, audio_files, target_sample_rate=44100, delete_originals=True): + """ + Combine multiple audio files into one, with optional resampling and timestamping. + Args: + output_file_timestamp (bool): Whether to add timestamp to output filename + output_file_name (str): Base name for output file + audio_files (list): List of audio files to combine + target_sample_rate (int, optional): Target sample rate for output. Defaults to 44100. + delete_originals (bool, optional): Whether to delete original files after combining. Defaults to False. + """ + debug_func_entry() + audio = np.array([]) + processed_files = [] # Keep track of successfully processed files + try: for audio_file in audio_files: normalized_audio_file = os.path.normpath(audio_file) print_message(f"Processing file: {normalized_audio_file}", "debug_concat", "TTS") - + if not os.path.isfile(normalized_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_message(f"Read file: {normalized_audio_file}, Sample rate: {current_sample_rate}, Data shape: {audio_data.shape}", + 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_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) - + audio = audio_data if audio.size == 0 else np.concatenate((audio, audio_data)) - + processed_files.append(normalized_audio_file) # Add to processed files list + # Prepare output paths if output_file_timestamp: timestamp = int(time.time()) filename = f'{output_file_name}_{timestamp}_combined.wav' else: 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") - + + # Delete original files if requested and combination was successful + if delete_originals: + for file_path in processed_files: + try: + os.remove(file_path) + print_message(f"Deleted original file: {file_path}", "debug_concat", "TTS") + except OSError as e: + print_message(f"Error deleting file {file_path}: {str(e)}", "error", "TTS") + return output_file_path, output_file_url, output_cache_url - - except Exception as e: - print_message(f"Error combining audio files: {str(e)}", "error", "TTS") - return None, None, None + + except (OSError, IOError) as e: + print_message(f"File system error while processing audio: {str(e)}", "error", "TTS") + except sf.SoundFileError as e: + print_message(f"Error reading audio file: {str(e)}", "error", "TTS") + except ValueError as e: + print_message(f"Invalid audio data or sample rate: {str(e)}", "error", "TTS") + except MemoryError as e: + print_message(f"Out of memory while processing audio: {str(e)}", "error", "TTS") + except librosa.ParameterError as e: + print_message(f"Error during audio resampling: {str(e)}", "error", "TTS") + except Exception as e: # pylint: disable=broad-exception-caught + print_message(f"An unexpected error occurred: {str(e)}", "error", "TTS") + return None, None, None ################################## # Central RVC Generation request # ################################## -def run_rvc(input_tts_path, pth_path, pitch, infer_pipeline): +def run_rvc(input_tts_path, pth_path, pitch, inference_pipeline): """Run RVC (Retrieval-based Voice Conversion) processing on input audio.""" debug_func_entry() - + generate_start_time = time.time() - + # Config settings settings = { "f0up_key": pitch, @@ -1306,19 +1510,19 @@ def run_rvc(input_tts_path, pth_path, pitch, infer_pipeline): "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) output_rvc_path = input_tts_path - + if not os.path.isfile(pth_path): print_message(f"Model file {pth_path} does not exist. Exiting.", "error", "GEN") return - + model_dir = os.path.dirname(pth_path) pth_filename = os.path.basename(pth_path) 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])) index_filename_print = os.path.basename(index_path) @@ -1330,7 +1534,7 @@ def run_rvc(input_tts_path, pth_path, pitch, infer_pipeline): index_path = "" index_filename_print = "None used" index_size_print = "N/A" - + if config.debugging.debug_rvc: print_message("RVC Pipeline Configuration:", "debug_rvc", "GEN") for key, value in settings.items(): @@ -1339,48 +1543,59 @@ def run_rvc(input_tts_path, pth_path, pitch, infer_pipeline): 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"], + + inference_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}", + 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): +def process_rvc_narrator(part_type, voice_gen, pitch, default_model_file, output_file, narr_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 - 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") - + run_rvc(output_file, pth_path, pitch, narr_infer_pipeline) + except FileNotFoundError as e: + print_message(f"File not found during RVC processing for {part_type} segment: {str(e)}", "error", "GEN") + except TypeError as e: + print_message(f"Type error during RVC processing for {part_type} segment: {str(e)}", "error", "GEN") + except ValueError as e: + print_message(f"Value error during RVC processing for {part_type} segment: {str(e)}", "error", "GEN") + except OSError as e: + print_message(f"OS error during RVC processing for {part_type} segment: {str(e)}", "error", "GEN") + ############################################################# # /api/tts-generate Generation API Endpoint JSON validation # ############################################################# class JSONInput(BaseModel): + """ + Represents the schema for validating input parameters for JSON-based TTS generation. + This class defines the expected structure and constraints for all input fields, such as `text_input`, + `language`, `output_file_name`, and TTS-related settings like `autoplay_volume`, `pitch`, and `temperature`. + Validation is performed using Pydantic's `Field` definitions with patterns, ranges, and descriptions. + Custom validation methods ensure specific fields like RVC voice files and pitch values meet the required formats. + """ text_input: str = Field(..., max_length=int(config.api_def.api_max_characters), description=f"text_input needs to be {config.api_def.api_max_characters} characters or less.") text_filtering: str = Field(..., pattern="^(none|standard|html)$", description="text_filtering needs to be 'none', 'standard' or 'html'.") #character_voice_gen: str = Field(..., pattern=r'^[\(\)a-zA-Z0-9\_\-./\s]+$', description="character_voice_gen needs to be the name of a valid voice for the loaded TTS engine.") @@ -1389,7 +1604,7 @@ class JSONInput(BaseModel): narrator_enabled: str = Field(..., pattern="^(true|false|silent)$", description="narrator_enabled needs to be 'true', 'false' or 'silent'.") narrator_voice_gen: str = Field(..., pattern=r'^[\(\)a-zA-Z0-9\_\-./\s]+$', description="character_voice_gen needs to be the name of a valid voice for the loaded TTS engine.") rvcnarrator_voice_gen: str = Field(..., description="rvcnarrator_voice_gen needs to be the name of a valid pth file in the 'folder\\file.pth' format or the word 'Disabled'.") - rvcnarrator_pitch: float = Field(..., description="RVC Narrator pitch needs to be a number between -24 and 24") + rvcnarrator_pitch: float = Field(..., description="RVC Narrator pitch needs to be a number between -24 and 24") text_not_inside: str = Field(..., pattern="^(character|narrator|silent)$", description="text_not_inside needs to be 'character', 'narrator' or 'silent'.") language: str = Field(..., pattern="^(ar|zh-cn|zh|cs|nl|en|fr|de|hu|hi|it|ja|ko|pl|pt|ru|es|tr)$", description="language needs to be one of the following: ar, zh-cn, zh, cs, nl, en, fr, de, hu, hi, it, ja, ko, pl, pt, ru, es, tr.") output_file_name: str = Field(..., pattern="^[a-zA-Z0-9_]+$", description="output_file_name needs to be the name without any special characters or file extension, e.g., 'filename'.") @@ -1406,7 +1621,7 @@ class JSONInput(BaseModel): """Validate autoplay volume is within acceptable range (0.1-1.0).""" debug_func_entry() try: - if not (0.1 <= value <= 1.0): + 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") @@ -1462,9 +1677,9 @@ class JSONInput(BaseModel): 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: + except ValueError as exc: print_message(f"Invalid pitch format: {value}", "error", "GEN") - raise ValueError("Pitch must be a number or a string representing a number.") + raise ValueError("Pitch must be a number or a string representing a number.") from exc except Exception as e: print_message(f"Error validating pitch: {str(e)}", "error", "GEN") raise @@ -1479,35 +1694,71 @@ def validate_json_input(json_input_data): error_messages = [] for error in e.errors(): field = error["loc"][0] - description = JSONInput.__fields__[field].description + description = JSONInput.__fields__.get(field).field_info.description error_messages.append(f"{field}: {description}") error_message = "\n".join(error_messages) print_message(f"Error with API request: {error_message}", "error", "API") return error_message # 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 +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 +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 +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 # @@ -1519,41 +1770,52 @@ async def tts_validate_and_prepare_input( ) -> 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 { + config_items = { "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") - + } + keys = list(config_items.keys()) + for i, key in enumerate(keys): + # Use └─ for the last item, and ├─ for others + prefix = "└─" if i == len(keys) - 1 else "├─" + print_message(f"{prefix} {key}: {config_items[key]}", "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") - + keys = list(request_params.keys()) + for i, key in enumerate(keys): + # Use └─ for the last item, and ├─ for others + prefix = "└─" if i == len(keys) - 1 else "├─" + print_message(f"{prefix} {key}: {request_params[key]}", "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") - + keys = list(params.keys()) + for i, key in enumerate(keys): + # Use └─ for the last item, and ├─ for others + prefix = "└─" if i == len(keys) - 1 else "├─" + print_message(f"{prefix} {key}: {params[key]}", "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]: @@ -1569,7 +1831,7 @@ async def tts_handle_output_paths(output_file_name: str, timestamp: bool = True) 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}' @@ -1586,9 +1848,9 @@ async def tts_handle_output_paths(output_file_name: str, timestamp: bool = True) 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": @@ -1596,12 +1858,12 @@ def tts_clean_text(text: str, filtering_type: str) -> str: 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 @@ -1609,77 +1871,79 @@ def tts_clean_text(text: str, filtering_type: str) -> str: 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") - + 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.""" + debug_func_entry() 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.""" + debug_func_entry() 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'], @@ -1688,55 +1952,67 @@ async def tts_generate_part(part: str, voice: str, params: dict) -> Optional[Pat output_file[0], False ) return output_file[0] - except Exception as e: - print_message(f"Error generating audio: {str(e)}", "error", "GEN") + except ValueError as e: + print_message(f"Invalid parameter value: {str(e)}", "error", "GEN") + return None + except (OSError, IOError) as e: + print_message(f"File system error: {str(e)}", "error", "GEN") + return None + except RuntimeError as e: + print_message(f"Audio generation runtime error: {str(e)}", "error", "GEN") + return None + except KeyError as e: + print_message(f"Missing required parameter: {str(e)}", "error", "GEN") + return None + except MemoryError as e: + print_message(f"Insufficient memory for audio generation: {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, + 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, + 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, + 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, + 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'], + + output_file_path, output_file_url, output_cache_url = await tts_handle_output_paths( # pylint: disable=unused-variable # Do not remove output_file_url, output_cache_url + 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( @@ -1746,29 +2022,30 @@ async def tts_process_standard_mode(params: dict, text_input: str) -> Union[Stre 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.""" + debug_func_entry() 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 @@ -1776,49 +2053,50 @@ async def tts_handle_rvc_processing(output_file_path: Path, params: dict): async def tts_handle_audio_output(output_file_path: Path, autoplay: bool, volume: float) -> Tuple[Path, str, str]: """Handle audio transcoding and playback.""" + debug_func_entry() 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: + + if output_format not in ("disabled", model_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 output_format not in ("disabled", model_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) @@ -1863,6 +2141,7 @@ async def apifunction_generate_tts_standard( _pitch: float = Depends(get_params_pitch), ): """Generate TTS audio with optional narrator mode and RVC processing.""" + debug_func_entry() try: # Prepare request parameters request_params = { @@ -1912,7 +2191,7 @@ async def apifunction_generate_tts_standard( params, validation_error = await tts_validate_and_prepare_input( text_input, request_params, default_params ) - + if validation_error: return JSONResponse(content={"error": validation_error}, status_code=400) @@ -1944,12 +2223,27 @@ async def apifunction_generate_tts_standard( status_code=200 ) - except Exception as e: - print_message(f"Error in TTS generation: {str(e)}", "error", "GEN") - return JSONResponse( - content={"status": "generate-failure", "error": "An error occurred"}, - status_code=500 - ) + except ValueError as e: + print_message(f"Invalid parameter value: {str(e)}", "error", "GEN") + return JSONResponse(content={"status": "generate-failure", "error": str(e)}, status_code=400) + except ValidationError as e: + print_message(f"Input validation error: {str(e)}", "error", "GEN") + return JSONResponse(content={"status": "generate-failure", "error": str(e)}, status_code=400) + except FileNotFoundError as e: + print_message(f"File or directory not found: {str(e)}", "error", "GEN") + return JSONResponse(content={"status": "generate-failure", "error": "Resource not found"}, status_code=404) + except PermissionError as e: + print_message(f"Permission denied: {str(e)}", "error", "GEN") + return JSONResponse(content={"status": "generate-failure", "error": "Permission denied"}, status_code=403) + except (OSError, IOError) as e: + print_message(f"I/O operation failed: {str(e)}", "error", "GEN") + return JSONResponse(content={"status": "generate-failure", "error": "File operation failed"}, status_code=500) + except RuntimeError as e: + print_message(f"Runtime error in TTS generation: {str(e)}", "error", "GEN") + return JSONResponse(content={"status": "generate-failure", "error": "TTS generation failed"}, status_code=500) + except MemoryError as e: + print_message(f"Out of memory: {str(e)}", "error", "GEN") + return JSONResponse(content={"status": "generate-failure", "error": "Insufficient memory"}, status_code=507) ############################# #### Word Add-in Sharing #### @@ -1960,7 +2254,6 @@ app.mount("/api/word_addin", StaticFiles(directory=os.path.join(this_dir / 'syst ############################################# #### TTS Generator Comparision Endpoints #### ############################################# -import aiofiles class TTSItem(BaseModel): """Individual TTS item structure for saving.""" id: int @@ -1977,54 +2270,54 @@ class TTSData(BaseModel): async def apifunction_save_tts_data(tts_data: List[TTSItem]): """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_list = [item.model_dump() 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") + raise HTTPException(status_code=500, detail="Failed to save TTS data") from e ######################################## # Trigger TTS Gen Text/Speech Analysis # ######################################## @app.get("/api/trigger-analysis") -async def apifunction_trigger_analysis(threshold: int = Query(default=98)): +async def apifunction_trigger_analysis(threshold: int = Query(default=98), whisper_model: str = Query(default="large-v3")): """Trigger Whisper analysis to compare generated TTS against original text. Args: threshold: Minimum acceptable match percentage (default: 98) """ 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() - + 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") - + + print_message(f"Starting TTS analysis with threshold: {threshold}%, using Whisper model: {whisper_model}", "debug_tts", "GEN") + # Run analysis script result = subprocess.run( [ @@ -2032,22 +2325,23 @@ async def apifunction_trigger_analysis(threshold: int = Query(default=98)): "tts_diff.py", f"--threshold={threshold}", f"--ttslistpath={ttslist_path}", - f"--wavfilespath={wavfile_path}" + f"--wavfilespath={wavfile_path}", + f"--whisper-model={whisper_model}" ], cwd=this_dir / "system" / "tts_diff", env=env, - capture_output=True, - text=True + text=True, + check=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: + with open(summary_path, "r", encoding="utf-8") as summary_file: summary_data = json.load(summary_file) print_message("Analysis summary loaded successfully", "debug_tts", "GEN") except FileNotFoundError: @@ -2056,12 +2350,12 @@ async def apifunction_trigger_analysis(threshold: int = Query(default=98)): 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)) + raise HTTPException(status_code=500, detail=str(e)) from e ########################################### # TTS Generator SRT Subtitiles generation # @@ -2070,25 +2364,25 @@ async def apifunction_trigger_analysis(threshold: int = Query(default=98)): async def apifunction_srt_generation(): """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( [ @@ -2100,34 +2394,42 @@ async def apifunction_srt_generation(): cwd=this_dir / "system" / "tts_srt", env=env, capture_output=True, - text=True + text=True, + check=True ) - + + # Validate subprocess result if result.returncode != 0: - print_message(f"SRT generation script error: {result.stderr}", "error", "GEN") - raise HTTPException(status_code=500, detail="SRT generation failed") - + stderr = result.stderr.strip() if result.stderr else "No error details available." + print_message(f"SRT generation script error: {stderr}", "error", "GEN") + raise HTTPException(status_code=500, detail=f"SRT generation failed: {stderr}") + # 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 subprocess.CalledProcessError as e: + print_message(f"Subprocess execution failed: {str(e)}", "error", "GEN") + raise HTTPException(status_code=500, detail=f"Subprocess execution failed: {str(e)}") from e + except Exception as e: print_message(f"Error during SRT generation: {str(e)}", "error", "GEN") - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=str(e)) from e + ################################# # Static Mount for file serving # ################################# app.mount("/static", StaticFiles(directory=str(this_dir / "system")), name="static") - + ######################################## # Legacy JSON update settings function # ######################################## @@ -2138,7 +2440,7 @@ logging.basicConfig(level=logging.DEBUG) async def get_settings(request: Request): """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") @@ -2148,20 +2450,20 @@ async def get_settings(request: Request): }) 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") + raise HTTPException(status_code=500, detail="Failed to load settings page") from e @app.get("/settings-json") async def get_settings_json(): """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") + raise HTTPException(status_code=500, detail="Failed to retrieve settings") from e @app.post("/update-settings") async def update_settings( @@ -2173,10 +2475,10 @@ async def update_settings( ): """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") @@ -2184,18 +2486,18 @@ async def update_settings( 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") @@ -2203,31 +2505,31 @@ async def update_settings( 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)) + raise HTTPException(status_code=400, detail=str(e)) from 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") - + raise HTTPException(status_code=500, detail="Failed to update settings") from e + # Create an instance of Jinja2Templates for rendering HTML templates templates = Jinja2Templates(directory=this_dir / "system") # Get the admin interface template template = templates.get_template("admin.html") # Render the template with the dynamic values rendered_html = template.render(params=config.to_dict()) - + ################################################### #### Webserver Startup & Initial model Loading #### ################################################### @@ -2235,19 +2537,27 @@ rendered_html = template.render(params=config.to_dict()) async def read_root(): """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") + raise HTTPException(status_code=500, detail="Failed to load application page") from e # Start Uvicorn Webserver # port_parameter = int(config.api_def.api_port_number) -if __name__ == "__main__": - import uvicorn +def start_server(): + """ + Starts the Uvicorn server for the AllTalk TTS application. + + This function initializes the Uvicorn web server using the specified port + from command line arguments or a default port from the configuration. + It assigns the server instance to the global `uvicorn_server` variable + (although Uvicorn's `run` function does not return a useful value). + """ + global uvicorn_server # pylint: disable=global-statement # Command line argument parser parser = argparse.ArgumentParser(description="AllTalk TTS Server") parser.add_argument("--port", type=int, help="Port number for the server") @@ -2256,5 +2566,8 @@ if __name__ == "__main__": config_port = int(config.api_def.api_port_number) port_to_use = args.port if args.port is not None else config_port # Start Uvicorn Webserver - uvicorn_server = uvicorn.run(app, host="0.0.0.0", port=port_to_use, log_level="debug") + uvicorn_server = uvicorn.run(app, host="0.0.0.0", port=port_to_use, log_level="debug") # pylint: disable=assignment-from-no-return +if __name__ == "__main__": + start_server() + \ No newline at end of file