diff --git a/script.py b/script.py index 8b3ce4a..63b20d0 100644 --- a/script.py +++ b/script.py @@ -123,11 +123,76 @@ update_config(config_file_path, update_config_path, downgrade_config_path) ########################################################################################### tts_engines_path = this_dir / "system" / "tts_engines" / "tts_engines.json" new_engines_path = this_dir / "system" / "tts_engines" / "new_engines.json" +tts_engines_backup_path = tts_engines_path.with_suffix(".backup") # Backup file path -# Load the JSON files -with open(tts_engines_path, 'r') as f: - tts_engines_data = json.load(f) +# Function to safely load JSON with error handling +def safe_load_json(file_path, backup_path=None): + try: + # Attempt to open and load the JSON file + with open(file_path, 'r') as f: + return json.load(f) + except FileNotFoundError: + # If the file is not found, handle it by either restoring from a backup or creating a default + print(f"[{branding}TTS] File not found: {file_path}") + if backup_path and os.path.exists(backup_path): + # Load the backup if it exists + print(f"[{branding}TTS] Restoring from backup: {backup_path}") + with open(backup_path, 'r') as f: + data = json.load(f) + # Save the backup data to the original path + with open(file_path, 'w') as f: + json.dump(data, f, indent=4) + print(f"[{branding}TTS] Restored {file_path} from backup.") + return data + else: + # If no backup is available, create a new default JSON structure + print(f"[{branding}TTS] No backup found, creating a new default tts_engine JSON structure.") + default_data = { + "engine_loaded": "piper", + "selected_model": "piper", + "engines_available": [ + { + "name": "parler", + "selected_model": "parler - parler_tts_mini_v0.1" + }, + { + "name": "piper", + "selected_model": "piper" + }, + { + "name": "vits", + "selected_model": "vits - tts_models--en--vctk--vits" + }, + { + "name": "xtts", + "selected_model": "xtts - xttsv2_2.0.3" + } + ] + } + with open(file_path, 'w') as f: + json.dump(default_data, f, indent=4) + print(f"[{branding}TTS] Created new default file at: {file_path}") + return default_data + except json.JSONDecodeError as e: + # Handle JSON corruption + print(f"[{branding}TTS] JSON decoding error in file {file_path}: {e}") + if backup_path and os.path.exists(backup_path): + print(f"[{branding}TTS] Restoring from backup due to corrupted file: {backup_path}") + with open(backup_path, 'r') as f: + data = json.load(f) + # Save the backup data to the original path + with open(file_path, 'w') as f: + json.dump(data, f, indent=4) + print(f"[{branding}TTS] Restored {file_path} from backup.") + return data + else: + raise Exception(f"File {file_path} is corrupted and no backup is available.") + +# Load tts_engines.json with retry if corrupted +tts_engines_data = safe_load_json(tts_engines_path, tts_engines_backup_path) + +# Load new_engines.json with open(new_engines_path, 'r') as f: new_engines_data = json.load(f) @@ -144,11 +209,15 @@ for engine in new_engines: if engine_dir.is_dir(): # Add the new engine to the list tts_engines_data['engines_available'].append(engine) - print(f"[{branding}TTS] \033[92mNew TTS Engines : \033[91mAdded {engine_name}\033[0m") + print(f"[{branding}TTS] \033[92mNew TTS Engine : \033[91mAdded {engine_name}\033[0m") -# Save the updated tts_engines.json -with open(tts_engines_path, 'w') as f: - json.dump(tts_engines_data, f, indent=4) + # Backup the current tts_engines.json before saving the updated file + shutil.copy(tts_engines_path, tts_engines_backup_path) + + # Save the updated tts_engines.json + with open(tts_engines_path, 'w') as f: + json.dump(tts_engines_data, f, indent=4) + print(f"{tts_engines_path} updated and saved successfully.") #################################################### # START-UP # Re-load from confignew.json to params # @@ -1303,6 +1372,7 @@ if gradio_enabled == True: json_file_path = os.path.join(this_dir, "system", "tts_engines", engine_name, "model_settings.json") with open(json_file_path, "r") as f: globals()[f"{engine_name}_model_config_data"] = json.load(f) + ########################################### # Finishing Dynamically importing Modules # ########################################### @@ -1399,21 +1469,39 @@ if gradio_enabled == True: # Sends request to reload the current TTS engine & set the default TTS engine loaded # ###################################################################################### def set_engine_loaded(engine_name): - global engine_loaded, selected_model, tts_engines_data, models_available, at_update_dropdowns - 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) + global engine_loaded, selected_model, tts_engines_data, models_available, tts_engines_path, tts_engines_backup_path, at_update_dropdowns + + # Load the JSON with a retry mechanism + safe_load_json(tts_engines_path, tts_engines_backup_path) + + # Iterate and update engines for engine in tts_engines_data["engines_available"]: if engine["name"] == engine_name: tts_engines_data["engine_loaded"] = engine_name tts_engines_data["selected_model"] = engine["selected_model"] break - with open(tts_engines_file, "w") as f: - json.dump(tts_engines_data, f) + + # Backup the existing file before writing + shutil.copy(tts_engines_path, tts_engines_backup_path) + + # Write the updated JSON data safely + with open(tts_engines_path, "w") as f: + try: + json.dump(tts_engines_data, f, indent=4) + except Exception as e: + print(f"Error writing to {tts_engines_path}: {e}") + # Restore the backup if writing failed + shutil.copy(tts_engines_backup_path, tts_engines_path) + raise Exception("Failed to save tts_engines.json, restored from backup.") + engine_loaded = engine_name selected_model = tts_engines_data["selected_model"] + + # Add a brief delay to ensure file is fully written + time.sleep(0.1) # 100ms delay # Restart the subprocess restart_subprocess() + # Wait for the engine to be ready with error handling and retries max_retries = 80 retry_delay = 1 # seconds @@ -1431,15 +1519,18 @@ if gradio_enabled == True: raise Exception("Failed to connect to the TTS engine after multiple retries.") time.sleep(retry_delay) + models_available = at_settings["models_available"] + # Update the dropdowns directly print(f"[{branding}ENG]") print(f"[{branding}ENG] {branding}Server Ready") gen_stream_value, gen_char_choices, rvcgen_char_choices, gen_narr_choices, rvcgen_narr_choices, gen_speed_interactive, gen_pitch_interactive, gen_temperature_interactive, gen_repetition_interactive, gen_lang_interactive, model_choices_gr_interactive, engine_choices_interactive = at_update_dropdowns() + return ("TTS Engine changed successfully!", # This is your output message - gen_stream_value, gen_char_choices, rvcgen_char_choices, gen_narr_choices, rvcgen_narr_choices, gen_speed_interactive, gen_pitch_interactive, gen_temperature_interactive, gen_repetition_interactive, gen_lang_interactive, model_choices_gr_interactive, engine_choices_interactive - ) - + gen_stream_value, gen_char_choices, rvcgen_char_choices, gen_narr_choices, rvcgen_narr_choices, gen_speed_interactive, gen_pitch_interactive, gen_temperature_interactive, gen_repetition_interactive, gen_lang_interactive, model_choices_gr_interactive, engine_choices_interactive + ) + ############################### # Sends voice2rvc request off # ############################### diff --git a/tts_server.py b/tts_server.py index c45c778..1f115b6 100644 --- a/tts_server.py +++ b/tts_server.py @@ -2,6 +2,7 @@ import os import sys import json import time +import shutil import argparse import librosa import logging @@ -247,28 +248,81 @@ async def handle_restart(): @app.post("/api/enginereload") async def apifunction_reload(request: Request): requested_engine = request.query_params.get("engine") + tts_engines_file = this_dir / "system" / "tts_engines" / "tts_engines.json" + tts_engines_file_backup_path = tts_engines_file.with_suffix(".backup") # Backup file path + if requested_engine not in engines_available: return {"status": "error", "message": "Invalid TTS engine specified"} + print(f"[{branding}ENG]") print(f"[{branding}ENG] \033[94mChanging model loaded. Please wait.\033[00m") print(f"[{branding}ENG]") - # 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) + + # Function to safely load JSON with error handling and backup restoration + def safe_load_json(file_path, backup_path=None): + try: + # Attempt to open and load the JSON file + with open(file_path, 'r') as f: + return json.load(f) + except FileNotFoundError: + # Handle missing file case, try restoring from backup or creating default + print(f"File not found: {file_path}") + if backup_path and os.path.exists(backup_path): + print(f"Restoring from backup: {backup_path}") + with open(backup_path, 'r') as f: + data = json.load(f) + # Restore the backup to the original file + with open(file_path, 'w') as f: + json.dump(data, f, indent=4) + print(f"Restored {file_path} from backup.") + return data + else: + raise Exception(f"File {file_path} is missing, and no backup is available.") + except json.JSONDecodeError as e: + # Handle JSON corruption case + print(f"JSON decoding error in file {file_path}: {e}") + if backup_path and os.path.exists(backup_path): + print(f"Restoring from backup due to corrupted file: {backup_path}") + with open(backup_path, 'r') as f: + data = json.load(f) + # Restore the backup to the original file + with open(file_path, 'w') as f: + json.dump(data, f, indent=4) + print(f"Restored {file_path} from backup.") + return data + else: + raise Exception(f"File {file_path} is corrupted, and no backup is available.") + + # Load the tts_engines.json with resilience + tts_engines_data = safe_load_json(tts_engines_file, tts_engines_file_backup_path) + + # Update the tts_engines_data with the requested engine for engine in tts_engines_data["engines_available"]: if engine["name"] == requested_engine: tts_engines_data["engine_loaded"] = requested_engine tts_engines_data["selected_model"] = engine["selected_model"] break - with open(tts_engines_file, "w") as f: - json.dump(tts_engines_data, f) - #print(f"[{branding}ENG] Reloading the engine...") + + # Write the updated JSON data safely and create a backup + try: + # First, create a backup of the current file before writing + shutil.copy(tts_engines_file, tts_engines_file_backup_path) + + with open(tts_engines_file, "w") as f: + json.dump(tts_engines_data, f, indent=4) + # print(f"Updated and saved {tts_engines_file}. Backup created at {tts_engines_file_backup_path}.") + except Exception as e: + print(f"Error writing to {tts_engines_file}: {e}") + # If writing failed, restore the backup + shutil.copy(tts_engines_file_backup_path, tts_engines_file) + raise Exception(f"Failed to save {tts_engines_file}, restored from backup.") + # Start the restart process in the background asyncio.create_task(handle_restart()) - # Never goes past the above asyncio as the script is killed at that point + return Response(content=json.dumps({"status": "engine-success"}), media_type="application/json") + ####################################### # API Endpoint - /api/stop-generation # #######################################