1106 lines
63 KiB
Python
1106 lines
63 KiB
Python
###############################################
|
|
# DONT CHANGE # These are base imports needed #
|
|
###############################################
|
|
import os
|
|
import sys
|
|
import json
|
|
import time
|
|
import torch
|
|
import logging
|
|
from pathlib import Path
|
|
from fastapi import (HTTPException)
|
|
logging.disable(logging.WARNING)
|
|
#################################################################
|
|
# DONT CHANGE # Get Pytorch & Python versions & setup DeepSpeed #
|
|
#################################################################
|
|
pytorch_version = torch.__version__
|
|
cuda_version = torch.version.cuda
|
|
major, minor, micro = sys.version_info[:3]
|
|
python_version = f"{major}.{minor}.{micro}"
|
|
deepspeed_available = False
|
|
|
|
#############################################################################################################
|
|
#############################################################################################################
|
|
# CHANGE ME # Run any specifc imports, requirements or setup any global vaiables needed for this TTS Engine #
|
|
#############################################################################################################
|
|
#############################################################################################################
|
|
# In this section you will import any imports that your specific TTS Engine will use. You will provide any
|
|
# start-up errors for those bits, as if you were starting up a normal Python script. Note the logging.disable
|
|
# a few lines up from here, you may want to # that out while debugging!
|
|
import soundfile as sf
|
|
import tempfile
|
|
from pydub import AudioSegment, silence
|
|
import re
|
|
import torchaudio
|
|
import numpy as np
|
|
import subprocess
|
|
import sys
|
|
|
|
def install_and_restart():
|
|
try:
|
|
print("#################################################################")
|
|
print("Installing required packages for F5-TTS... This may take a while.")
|
|
print("################################################################")
|
|
|
|
# Install vocos first
|
|
print("Installing vocos...")
|
|
subprocess.check_call([
|
|
sys.executable,
|
|
"-m",
|
|
"pip",
|
|
"install",
|
|
"vocos"
|
|
])
|
|
|
|
# Then install F5-TTS
|
|
print("Installing F5-TTS...")
|
|
subprocess.check_call([
|
|
sys.executable,
|
|
"-m",
|
|
"pip",
|
|
"install",
|
|
"git+https://github.com/SWivid/F5-TTS.git"
|
|
])
|
|
|
|
print("##############################################################")
|
|
print("All packages installed successfully! Restarting application...")
|
|
print("##############################################################")
|
|
|
|
# Get the current script's path
|
|
script_path = sys.argv[0]
|
|
|
|
# Restart the current script
|
|
os.execv(sys.executable, ['python'] + sys.argv)
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
print("########################################################")
|
|
print(f"Failed to install required packages: {str(e)}")
|
|
print("########################################################")
|
|
raise ImportError("Could not install required packages")
|
|
|
|
try:
|
|
from f5_tts.model import CFM, DiT, UNetT
|
|
from f5_tts.model.utils import (
|
|
get_tokenizer,
|
|
convert_char_to_pinyin,
|
|
)
|
|
from vocos import Vocos
|
|
except ImportError:
|
|
install_and_restart()
|
|
|
|
|
|
#################################################################################################################################
|
|
# DONT CHANGE # Do not change the Class name from tts_class as this is what will be imported into the main tts_server.py script #
|
|
#################################################################################################################################
|
|
class tts_class:
|
|
def __init__(self):
|
|
########################################################################
|
|
# DONT CHANGE # Sets up the base variables required for any tts engine #
|
|
########################################################################
|
|
self.branding = None
|
|
self.this_dir = Path(__file__).parent.resolve() # Sets up self.this_dir as a variable for the folder THIS script is running in.
|
|
self.main_dir = Path(__file__).parent.parent.parent.parent.resolve() # Sets up self.main_dir as a variable for the folder AllTalk is running in
|
|
self.device = "cuda" if torch.cuda.is_available() else "cpu" # Sets up self.device to cuda if torch exists with Nvidia/CUDA, otherwise sets to cpu
|
|
self.cuda_is_available = torch.cuda.is_available() # Sets up cuda_is_available as a True/False to track if Nvidia/CUDA was found on the system
|
|
self.tts_generating_lock = False # Used to lock and unlock the tts generation process at the start/end of tts generation.
|
|
self.tts_stop_generation = False # Used in conjunction with tts_generating_lock to call for a stop to the current generation. If called (set True) it needs to be set back to False when generation has been stopped.
|
|
self.tts_narrator_generatingtts = False # Used to track if the current tts processes is narrator based. This can be used in conjunction with lowvram and device to avoid moving model between GPU(CUDA)<>RAM(CPU) each chunk of narrated text generated.
|
|
self.model = None # If loading a model into CUDA/VRAM/RAM "model" is used as the variable name to load and interact with (see the XTTS model_engine script for examples.)
|
|
self.is_tts_model_loaded = False # Used to track if a model is actually loaded in and error/fail things like TTS generation if its False
|
|
self.current_model_loaded = None # Stores the name of the currenly loaded in model
|
|
self.available_models = None # List of available models found by "def scan_models_folder"
|
|
self.setup_has_run = False # Tracks if async def setup(self) has run, by setting to True, so that the /api/ready endpoint can provide a "Ready" status
|
|
##############################################################################################
|
|
# DONT CHANGE # Load in a list of the available TTS engines and the currently set TTS engine #
|
|
##############################################################################################
|
|
tts_engines_file = os.path.join(self.main_dir, "system", "tts_engines", "tts_engines.json")
|
|
with open(tts_engines_file, "r") as f:
|
|
tts_engines_data = json.load(f)
|
|
self.engines_available = [engine["name"] for engine in tts_engines_data["engines_available"]] # A list of ALL the TTS engines available to be loaded by AllTalk
|
|
self.engine_loaded = tts_engines_data["engine_loaded"] # In "tts_engines.json" what is the currently set TTS engine loading into AllTalk
|
|
self.selected_model = tts_engines_data["selected_model"] # In "tts_engines.json" what is the currently set TTS model loading into AllTalk
|
|
############################################################################
|
|
# DONT CHANGE # Pull out all the settings for the currently set TTS engine #
|
|
############################################################################
|
|
with open(os.path.join(self.this_dir, "model_settings.json"), "r") as f:
|
|
tts_model_loaded = json.load(f)
|
|
# Access the model details
|
|
self.manufacturer_name = tts_model_loaded["model_details"]["manufacturer_name"] # The company/person/body that generated the TTS engine/models etc
|
|
self.manufacturer_website = tts_model_loaded["model_details"]["manufacturer_website"] # The website of the company/person/body where people can find more information
|
|
# Access the features the model is capable of:
|
|
self.audio_format = tts_model_loaded["model_capabilties"]["audio_format"] # This details the audio format your TTS engine is set to generate TTS in e.g. wav, mp3, flac, opus, acc, pcm. Please use only 1x format.
|
|
self.deepspeed_capable = tts_model_loaded["model_capabilties"]["deepspeed_capable"] # Is your model capable of DeepSpeed
|
|
self.deepspeed_available = 'deepspeed' in globals() # When we did the import earlier, at the top of this script, was DeepSpeed available for use
|
|
self.generationspeed_capable = tts_model_loaded["model_capabilties"]["generationspeed_capable"] # Does this TTS engine support changing the speed of the generated TTS
|
|
self.languages_capable = tts_model_loaded["model_capabilties"]["languages_capable"] # Are the actual models themselves capable of generating in multiple languages OR is each model language specific
|
|
self.lowvram_capable = tts_model_loaded["model_capabilties"]["lowvram_capable"] # Is this engine capable of using low VRAM (moving the model between CPU And GPU memory)
|
|
self.multimodel_capable = tts_model_loaded["model_capabilties"]["multimodel_capable"] # Is there just the one model or are there multiple models this engine supports.
|
|
self.repetitionpenalty_capable = tts_model_loaded["model_capabilties"]["repetitionpenalty_capable"] # Is this TTS engine capable of changing the repititon penalty
|
|
self.streaming_capable = tts_model_loaded["model_capabilties"]["streaming_capable"] # Is this TTS engine capabale of generating streaming audio
|
|
self.temperature_capable = tts_model_loaded["model_capabilties"]["temperature_capable"] # Is this TTS engine capable of changing the temperature of the models
|
|
self.multivoice_capable = tts_model_loaded["model_capabilties"]["multivoice_capable"] # Are the models multi-voice or single vocice models
|
|
self.pitch_capable = tts_model_loaded["model_capabilties"]["pitch_capable"] # Is this TTS engine capable of changing the pitch of the genrated TTS
|
|
# Access the current enginesettings
|
|
self.def_character_voice = tts_model_loaded["settings"]["def_character_voice"] # What is the current default main/character voice that will be used if no voice specified.
|
|
self.def_narrator_voice = tts_model_loaded["settings"]["def_narrator_voice"] # What is the current default narrator voice that will be used if no voice specified.
|
|
self.deepspeed_enabled = tts_model_loaded["settings"]["deepspeed_enabled"] # If its available, is DeepSpeed enabled for the TTS engine
|
|
self.engine_installed = tts_model_loaded["settings"]["engine_installed"] # Has the TTS engine been setup/installed (not curently used)
|
|
self.generationspeed_set = tts_model_loaded["settings"]["generationspeed_set"] # What is the set/stored speed for generation.
|
|
self.lowvram_enabled = tts_model_loaded["settings"]["lowvram_enabled"] # If its available, is LowVRAM enabled for the TTS engine
|
|
# Check if someone has enabled lowvram on a system that's not CUDA enabled
|
|
self.lowvram_enabled = False if not torch.cuda.is_available() else self.lowvram_enabled # If LowVRAM is mistakenly set and CUDA is not available, this will force it back off
|
|
self.repetitionpenalty_set = tts_model_loaded["settings"]["repetitionpenalty_set"] # What is the currenly set repitition policy of the model (If it support repetition)
|
|
self.temperature_set = tts_model_loaded["settings"]["temperature_set"] # What is the currenly set temperature of the model (If it support temp)
|
|
self.pitch_set = tts_model_loaded["settings"]["pitch_set"] # What is the currenly set pitch of the model (If it support temp)
|
|
# Gather the OpenAI API Voice Mappings
|
|
self.openai_alloy = tts_model_loaded["openai_voices"]["alloy"] # The TTS engine voice that will be mapped to Open AI Alloy voice
|
|
self.openai_echo = tts_model_loaded["openai_voices"]["echo"] # The TTS engine voice that will be mapped to Open AI Echo voice
|
|
self.openai_fable = tts_model_loaded["openai_voices"]["fable"] # The TTS engine voice that will be mapped to Open AI Fable voice
|
|
self.openai_nova = tts_model_loaded["openai_voices"]["nova"] # The TTS engine voice that will be mapped to Open AI Nova voice
|
|
self.openai_onyx = tts_model_loaded["openai_voices"]["onyx"] # The TTS engine voice that will be mapped to Open AI Onyx voice
|
|
self.openai_shimmer = tts_model_loaded["openai_voices"]["shimmer"] # The TTS engine voice that will be mapped to Open AI Shimmer voice
|
|
###################################################################
|
|
# DONT CHANGE # Load params and api_defaults from confignew.json #
|
|
###################################################################
|
|
# Define the path to the confignew.json file
|
|
configfile_path = self.main_dir / "confignew.json"
|
|
# Load config file and get settings
|
|
with open(configfile_path, "r") as configfile:
|
|
configfile_data = json.load(configfile)
|
|
self.branding = configfile_data.get("branding", "") # Sets up self.branding for outputting the name stored in the "confgnew.json" file, as used in print statements.
|
|
self.params = configfile_data # Loads in the curent "confgnew.json" file to self.params.
|
|
self.debug_tts = configfile_data.get("debugging").get("debug_tts") # Can be used within this script as a True/False flag for generally debugging the TTS generation process.
|
|
self.debug_tts_variables = configfile_data.get("debugging").get("debug_tts_variables") # Can be used within this script as a True/False flag for generally debugging variables (if you wish).
|
|
|
|
############################################################################
|
|
# DONT CHANGE # These settings are specific to the F5-TTS Model/Engine ####
|
|
############################################################################
|
|
# Add F5-TTS specific parameters
|
|
self.target_sample_rate = 24000
|
|
self.n_mel_channels = 100
|
|
self.hop_length = 256
|
|
self.win_length = 1024
|
|
self.n_fft = 1024
|
|
self.mel_spec_type = 'vocos'
|
|
self.target_rms = 0.1
|
|
self.cross_fade_duration = 0.15
|
|
self.ode_method = 'euler'
|
|
self.nfe_step = 32
|
|
self.cfg_strength = 2.0
|
|
self.sway_sampling_coef = -1.0
|
|
self.speed = 1.0
|
|
# F5-TTS model configuration
|
|
self.f5_model_cfg = dict(
|
|
dim=1024,
|
|
depth=22,
|
|
heads=16,
|
|
ff_mult=2,
|
|
text_dim=512,
|
|
conv_layers=4
|
|
)
|
|
# Add E2-TTS model configuration
|
|
self.e2_model_cfg = dict(
|
|
dim=1024,
|
|
depth=24,
|
|
heads=16,
|
|
ff_mult=4
|
|
)
|
|
################################################################
|
|
# DONT CHANGE # Print out Python, CUDA, DeepSpeed versions ####
|
|
################################################################
|
|
def printout_versions(self):
|
|
if deepspeed_available:
|
|
print(f"[{self.branding}ENG] \033[92mDeepSpeed version :\033[93m",deepspeed.__version__,"\033[0m")
|
|
else:
|
|
print(f"[{self.branding}ENG] \033[92mDeepSpeed version :\033[93m Not available\033[0m")
|
|
print(f"[{self.branding}ENG] \033[92mPython Version :\033[93m {python_version}\033[0m")
|
|
print(f"[{self.branding}ENG] \033[92mPyTorch Version :\033[93m {pytorch_version}\033[0m")
|
|
if cuda_version is None:
|
|
print(f"[{self.branding}ENG] \033[92mCUDA Version :\033[91m Not available\033[0m")
|
|
else:
|
|
print(f"[{self.branding}ENG] \033[92mCUDA Version :\033[93m {cuda_version}\033[0m")
|
|
print(f"[{self.branding}ENG]")
|
|
return
|
|
|
|
###################################################################################
|
|
###################################################################################
|
|
# CHANGE ME # Inital setup of the model and engine. Called when the script starts #
|
|
###################################################################################
|
|
###################################################################################
|
|
# In here you will add code to load in your model and do its inital setup. So you
|
|
# may be calling your model loader via handle_tts_method_change or if your TTS
|
|
# engine doesnt actually load a model into CUDA or System RAM, you may be doing
|
|
# Something to fake its start-up.
|
|
async def setup(self):
|
|
self.printout_versions()
|
|
# ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
|
# ↑↑↑ Keep everything above this line ↑↑↑
|
|
# ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
|
|
|
# Record the start time of loading the model
|
|
generate_start_time = time.time()
|
|
|
|
# Scan for available models
|
|
self.available_models = self.scan_models_folder()
|
|
print("in setup - self.selected_model:", self.selected_model) if self.debug_tts else None
|
|
|
|
# Load the model if one is selected
|
|
if self.selected_model and self.selected_model != "No Models Found":
|
|
try:
|
|
await self.api_manual_load_model(self.selected_model)
|
|
self.current_model_loaded = self.selected_model
|
|
except Exception as e:
|
|
print(f"[{self.branding}ENG] \033[91mError loading model: {str(e)}\033[0m")
|
|
self.is_tts_model_loaded = False
|
|
|
|
generate_end_time = time.time()
|
|
generate_elapsed_time = generate_end_time - generate_start_time
|
|
|
|
print(f"[{self.branding}ENG]\033[94m Model/Engine :\033[93m F5-TTS\033[94m Ready\033[0m")
|
|
print(f"[{self.branding}ENG] \033[94mLoad time :\033[93m {generate_elapsed_time:.2f} seconds.\033[0m")
|
|
|
|
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
|
# ↓↓↓ Keep everything below this line ↓↓↓
|
|
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
|
self.setup_has_run = True # Flag that setup has run, so the /api/ready endpoint will send a "Ready" status and load the webui
|
|
|
|
##################################
|
|
##################################
|
|
# CHANGE ME # Low VRAM Swapping #
|
|
##################################
|
|
##################################
|
|
# If your model does load into CUDA and you want to support LowVRAM, aka moving the model
|
|
# on the fly between CUDA and System RAM on each generation request, you will be adding
|
|
# The code in here to do it. See the XTTS tts engine for an example. Piper however, doesnt
|
|
# Load into VRAM or System RAM, also low vram is set globally disabled by the model_settings.JSON
|
|
# file, so this would never get called anywyay, however, we still need to keep an empty
|
|
# function in place. The "pass" tells the function to just exit out cleanly if called.
|
|
# However, its quite a simple check along the lines of "if CUDA is available and model is
|
|
# in X place, then send it to Y place (or Y to X).
|
|
async def handle_lowvram_change(self):
|
|
"""Handle moving model between CPU and GPU for low VRAM operation"""
|
|
if not self.lowvram_capable or not self.cuda_is_available:
|
|
return
|
|
|
|
if not self.is_tts_model_loaded:
|
|
return
|
|
|
|
try:
|
|
if self.lowvram_enabled and self.device == 'cuda':
|
|
# Moving to CPU - everything must be float32
|
|
print(f"[{self.branding}Debug] Moving models to CPU") if self.debug_tts else None
|
|
|
|
if hasattr(self, 'model'):
|
|
self.model = self.model.float().to('cpu')
|
|
torch.cuda.empty_cache()
|
|
print(f"[{self.branding}Debug] Model moved to CPU") if self.debug_tts else None
|
|
|
|
if hasattr(self, 'vocoder'):
|
|
# Move vocoder without checking device
|
|
try:
|
|
self.vocoder = self.vocoder.float().to('cpu')
|
|
torch.cuda.empty_cache()
|
|
print(f"[{self.branding}Debug] Vocoder moved to CPU") if self.debug_tts else None
|
|
except Exception as e:
|
|
print(f"[{self.branding}Debug] Vocoder move warning: {str(e)}") if self.debug_tts else None
|
|
|
|
self.device = 'cpu'
|
|
print(f"[{self.branding}ENG] Models moved to CPU for low VRAM mode") if self.debug_tts else None
|
|
|
|
elif self.lowvram_enabled and self.device == 'cpu':
|
|
# Moving to GPU - everything must be float16
|
|
print(f"[{self.branding}Debug] Moving models to GPU") if self.debug_tts else None
|
|
|
|
if hasattr(self, 'model'):
|
|
self.model = self.model.to('cuda').half()
|
|
print(f"[{self.branding}Debug] Model moved to GPU") if self.debug_tts else None
|
|
|
|
if hasattr(self, 'vocoder'):
|
|
try:
|
|
self.vocoder = self.vocoder.to('cuda').half()
|
|
print(f"[{self.branding}Debug] Vocoder moved to GPU") if self.debug_tts else None
|
|
except Exception as e:
|
|
print(f"[{self.branding}Debug] Vocoder move warning: {str(e)}") if self.debug_tts else None
|
|
|
|
self.device = 'cuda'
|
|
print(f"[{self.branding}ENG] Models moved to GPU") if self.debug_tts else None
|
|
|
|
# Verify movements and precision
|
|
if self.debug_tts:
|
|
if hasattr(self, 'model'):
|
|
# Check model parameters only
|
|
params_dtype = next(self.model.parameters()).dtype
|
|
print(f"[{self.branding}Debug] Model parameters dtype: {params_dtype}")
|
|
print(f"[{self.branding}Debug] Final model device: {next(self.model.parameters()).device}")
|
|
|
|
if hasattr(self, 'vocoder'):
|
|
try:
|
|
# Try to check vocoder parameters
|
|
params = next(self.vocoder.parameters())
|
|
print(f"[{self.branding}Debug] Vocoder parameters dtype: {params.dtype}")
|
|
except Exception:
|
|
pass # Ignore vocoder parameter checking errors
|
|
|
|
except Exception as e:
|
|
print(f"[{self.branding}ENG] \033[91mWarning during model movement: {str(e)}\033[0m")
|
|
if self.lowvram_enabled:
|
|
self.device = 'cpu'
|
|
else:
|
|
self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
|
|
|
|
|
########################################
|
|
########################################
|
|
# CHANGE ME # DeepSpeed model loading #
|
|
########################################
|
|
########################################
|
|
# If the model supports CUDA and DeepSpeed, this is where you can handle re-loading
|
|
# the model as/when DeepSpeed is enabled/selected in the user interface. If your
|
|
# TTS model doesnt support DeepSpeed, then it should be globally set in your
|
|
# model_settings.JSON and this function will never get called, however it still needs
|
|
# to exist as a function.
|
|
async def handle_deepspeed_change(self, value):
|
|
"""F5-TTS currently doesn't support DeepSpeed"""
|
|
print(f"[{self.branding}ENG] DeepSpeed not supported for F5-TTS")
|
|
return False
|
|
|
|
|
|
##############################################################################################################################################
|
|
##############################################################################################################################################
|
|
# CHANGE ME # scan for available models/voices that are relevant to this TTS engine # XTTS is very unusal as it has 2x model loading methods #
|
|
##############################################################################################################################################
|
|
##############################################################################################################################################
|
|
# This function looks and reports back the list of possible models your TTS engine can
|
|
# load in. Some TTS engines have multiple models they can load and you will want to use
|
|
# code for checking if the models are in the correct location/placement within the disk
|
|
# the correct files exst per model etc (check XTTS for an example of this). Some models
|
|
# like Piper, the actual models are the voices, so in the Piper scan_models_folder
|
|
# function, we fake the only model being available as {'piper': 'piper'} aka, model name
|
|
# then engine name, then we use the voices_file_list to populate the models as available
|
|
# voices that can be selected in the interface.
|
|
# If no models are found, we return "No Models Available" and continue on with the script.
|
|
def scan_models_folder(self):
|
|
# ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
|
# ↑↑↑ Keep everything above this line ↑↑↑
|
|
# ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
|
|
|
"""Scan for available F5-TTS models"""
|
|
self.available_models = {}
|
|
models_dir = self.main_dir / "models" / "f5tts"
|
|
|
|
if not models_dir.exists():
|
|
print(f"[{self.branding}ENG] Models directory not found: {models_dir}")
|
|
self.available_models["No Models Found"] = "No Models Found"
|
|
return self.available_models
|
|
|
|
# Look for model directories that match the pattern f5tts_v*
|
|
for model_dir in models_dir.glob("*tts_v*"):
|
|
if model_dir.is_dir():
|
|
# First try to find model_*.safetensors files
|
|
model_files = list(model_dir.glob("model_*.safetensors"))
|
|
if not model_files:
|
|
# If no model_*.safetensors found, try any .safetensors file
|
|
model_files = list(model_dir.glob("*.safetensors"))
|
|
|
|
vocab_file = model_dir / "vocab.txt"
|
|
vocos_dir = model_dir / "vocos"
|
|
vocos_config = vocos_dir / "config.yaml"
|
|
vocos_model = vocos_dir / "pytorch_model.bin"
|
|
|
|
# Check if we have at least one model file and all other required files
|
|
if model_files and all(f.exists() for f in [vocab_file, vocos_config, vocos_model]):
|
|
model_name = model_dir.name
|
|
self.available_models[model_name] = model_name
|
|
|
|
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
|
# ↓↓↓ Keep everything below this line ↓↓↓
|
|
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
|
if not self.available_models:
|
|
self.available_models["No Models Found"] = "No Models Found" # Return a list with {'No Models Found'} if there are no models found.
|
|
return self.available_models # Return a list of models in the format {'engine name': 'model 1', 'engine name': 'model 2", etc}
|
|
|
|
#############################################################
|
|
#############################################################
|
|
# CHANGE ME # POPULATE FILES LIST FROM VOICES DIRECTORY ####
|
|
#############################################################
|
|
#############################################################
|
|
# This function looks and reports back the list of possible voice your TTS engine can
|
|
# load in. Some TTS engines the voices are wav file samples (XTTS), some are models
|
|
# (Piper) and some are text (Parler) thats stored in a JSON file. We just need to
|
|
# populate the "voices" variable somehow and if no voices are found, we return
|
|
# "No Voices Found" back to the interface/api.
|
|
def voices_file_list(self):
|
|
try:
|
|
voices = []
|
|
# Function to check if a wav file has a corresponding reference text file
|
|
def has_reference_text(wav_path):
|
|
text_path = wav_path.with_suffix('.reference.txt')
|
|
return text_path.exists()
|
|
|
|
directory = self.main_dir / "voices"
|
|
|
|
# Step 1: Add .wav files in the main "voices" directory to the list (only if they have matching .reference.txt)
|
|
for f in directory.glob("*.wav"):
|
|
if has_reference_text(f):
|
|
voices.append(f.name)
|
|
else:
|
|
print(f"[{self.branding}ENG] Warning: {f.name} does not have a matching reference text file") if self.debug_tts else None
|
|
|
|
# Step 2: Walk through subfolders and add subfolder names if they contain valid wav+reference.txt pairs
|
|
for folder in directory.iterdir():
|
|
if folder.is_dir():
|
|
valid_pairs = False
|
|
for wav_file in folder.glob("*.wav"):
|
|
if has_reference_text(wav_file):
|
|
valid_pairs = True
|
|
break
|
|
|
|
if valid_pairs:
|
|
folder_name = folder.name + "/"
|
|
voices.append(folder_name)
|
|
elif self.debug_tts:
|
|
print(f"[{self.branding}ENG] Warning: Folder {folder.name} has no valid wav+reference.txt pairs")
|
|
|
|
# Remove "voices/" from the list if it somehow got added
|
|
voices = [v for v in voices if v != "voices/"]
|
|
|
|
if not voices:
|
|
return ["No Voices Found"]
|
|
return voices
|
|
except Exception as e:
|
|
print(f"[{self.branding}ENG] \033[91mError\033[0m: Voices/Voice Models not found. Cannot load a list of voices.")
|
|
print(f"[{self.branding}ENG]")
|
|
return ["No Voices Found"]
|
|
|
|
#################################################################################
|
|
#################################################################################
|
|
# CHANGE ME # Model loading #####################################################
|
|
#################################################################################
|
|
#################################################################################
|
|
# This function will handle the loading of your model, into VRAM/CUDA, System RAM or whatever.
|
|
async def api_manual_load_model(self, model_name):
|
|
if model_name == "No Models Found":
|
|
print(f"[{self.branding}ENG] \033[91mError\033[0m: No models for this TTS engine were found to load.")
|
|
raise HTTPException(status_code=400, detail="No models for this TTS engine were found to load.")
|
|
|
|
print(f"[{self.branding}ENG]\033[94m Model/Engine :\033[93m {model_name}\033[94m loading into\033[93m", self.device,"\033[0m")
|
|
|
|
# Split the engine name from model name and get just the model folder name
|
|
model_folder = model_name.split(" - ")[-1]
|
|
|
|
try:
|
|
# Set up paths using the correct model folder name
|
|
model_dir = self.main_dir / "models" / "f5tts" / model_folder
|
|
vocab_path = model_dir / "vocab.txt"
|
|
vocos_path = model_dir / "vocos"
|
|
|
|
# Dynamically find the safetensors model file
|
|
model_files = list(model_dir.glob("model_*.safetensors"))
|
|
if not model_files:
|
|
# Try finding any safetensors file as fallback
|
|
model_files = list(model_dir.glob("*.safetensors"))
|
|
|
|
if not model_files:
|
|
raise FileNotFoundError("No safetensors model file found in the model directory")
|
|
|
|
# Use the first found model file
|
|
model_path = model_files[0]
|
|
|
|
print(f"[{self.branding}ENG] Loading model from path: {model_path}") if self.debug_tts else None
|
|
|
|
# Initialize vocoder - always in float32 initially
|
|
self.vocoder = Vocos.from_hparams(str(vocos_path / "config.yaml"))
|
|
vocoder_state = torch.load(str(vocos_path / "pytorch_model.bin"), map_location="cpu")
|
|
self.vocoder.load_state_dict(vocoder_state)
|
|
self.vocoder = self.vocoder.eval()
|
|
|
|
# Determine if this is an E2-TTS model from the folder name
|
|
is_e2_model = "e2tts" in model_folder.lower()
|
|
|
|
# Initialize model with appropriate architecture
|
|
vocab_char_map, vocab_size = get_tokenizer(str(vocab_path), "custom")
|
|
|
|
mel_spec_kwargs = dict(
|
|
n_fft=self.n_fft,
|
|
hop_length=self.hop_length,
|
|
win_length=self.win_length,
|
|
n_mel_channels=self.n_mel_channels,
|
|
target_sample_rate=self.target_sample_rate,
|
|
mel_spec_type=self.mel_spec_type,
|
|
)
|
|
|
|
if is_e2_model:
|
|
print(f"[{self.branding}ENG] Initializing E2-TTS model") if self.debug_tts else None
|
|
self.model = CFM(
|
|
transformer=UNetT(
|
|
**self.e2_model_cfg,
|
|
text_num_embeds=vocab_size,
|
|
mel_dim=self.n_mel_channels
|
|
),
|
|
mel_spec_kwargs=mel_spec_kwargs,
|
|
odeint_kwargs=dict(
|
|
method=self.ode_method,
|
|
),
|
|
vocab_char_map=vocab_char_map
|
|
).float() # Ensure float32
|
|
else:
|
|
print(f"[{self.branding}ENG] Initializing F5-TTS model") if self.debug_tts else None
|
|
self.model = CFM(
|
|
transformer=DiT(
|
|
**self.f5_model_cfg,
|
|
text_num_embeds=vocab_size,
|
|
mel_dim=self.n_mel_channels
|
|
),
|
|
mel_spec_kwargs=mel_spec_kwargs,
|
|
odeint_kwargs=dict(
|
|
method=self.ode_method,
|
|
),
|
|
vocab_char_map=vocab_char_map
|
|
).float() # Ensure float32
|
|
|
|
# Load model weights
|
|
if str(model_path).endswith('.safetensors'):
|
|
from safetensors.torch import load_file
|
|
checkpoint = load_file(model_path)
|
|
checkpoint = {"ema_model_state_dict": checkpoint}
|
|
else:
|
|
checkpoint = torch.load(model_path, weights_only=True)
|
|
|
|
# Handle backward compatibility before loading weights
|
|
model_state_dict = {
|
|
k.replace("ema_model.", ""): v
|
|
for k, v in checkpoint["ema_model_state_dict"].items()
|
|
if k not in ["initted", "step"]
|
|
}
|
|
|
|
# Remove backward compatibility keys before loading
|
|
backward_compat_keys = [
|
|
"mel_spec.mel_stft.mel_scale.fb",
|
|
"mel_spec.mel_stft.spectrogram.window"
|
|
]
|
|
for key in backward_compat_keys:
|
|
if key in model_state_dict:
|
|
del model_state_dict[key]
|
|
|
|
self.model.load_state_dict(model_state_dict)
|
|
self.model.eval()
|
|
|
|
# Move to device and handle precision
|
|
if self.device == "cuda":
|
|
self.model = self.model.to(self.device).half()
|
|
self.vocoder = self.vocoder.to(self.device).half()
|
|
|
|
# Ensure all parts are in half precision
|
|
for module in [self.model, self.vocoder]:
|
|
for param in module.parameters():
|
|
param.data = param.data.half()
|
|
for buffer in module.buffers():
|
|
if buffer.dtype == torch.float32:
|
|
buffer.data = buffer.data.half()
|
|
else:
|
|
self.model = self.model.to(self.device)
|
|
self.vocoder = self.vocoder.to(self.device)
|
|
|
|
self.is_tts_model_loaded = True
|
|
model_type = "E2-TTS" if is_e2_model else "F5-TTS"
|
|
print(f"[{self.branding}ENG] {model_type} model loaded successfully") if self.debug_tts else None
|
|
|
|
except Exception as e:
|
|
print(f"[{self.branding}ENG] \033[91mError loading model: {str(e)}\033[0m")
|
|
raise HTTPException(status_code=500, detail=f"Error loading model: {str(e)}")
|
|
|
|
###############################
|
|
###############################
|
|
# CHANGE ME # Model unloading #
|
|
###############################
|
|
###############################
|
|
# This function will handle the UN-loading of your model, from VRAM/CUDA, System RAM or whatever.
|
|
async def unload_model(self):
|
|
# ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
|
# ↑↑↑ Keep everything above this line ↑↑↑
|
|
# ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
|
|
|
"""Unload the F5-TTS model from memory"""
|
|
if hasattr(self, 'model'):
|
|
del self.model
|
|
self.model = None
|
|
|
|
if hasattr(self, 'vocoder'):
|
|
del self.vocoder
|
|
self.vocoder = None
|
|
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
|
|
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
|
# ↓↓↓ Keep everything below this line ↓↓↓
|
|
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
|
self.is_tts_model_loaded = False
|
|
return None
|
|
|
|
###################################################################################################################################
|
|
###################################################################################################################################
|
|
# CHANGE ME # Model changing. Unload out old model and load in a new one ##########################################################
|
|
###################################################################################################################################
|
|
###################################################################################################################################
|
|
# This function is your central model loading/unloading handler that deals with the above functions as necesary, to call loading, unloading,
|
|
# swappng DeepSpeed, Low vram etc. This function gets called with a "engine name - model name" type call.
|
|
async def handle_tts_method_change(self, tts_method):
|
|
generate_start_time = time.time() # Record the start time of loading the model
|
|
if "No Models Available" in self.available_models:
|
|
print(f"[{self.branding}ENG] \033[91mError\033[0m: No models for this TTS engine were found to load. Please download a model.")
|
|
return False
|
|
# ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
|
# ↑↑↑ Keep everything above this line ↑↑↑
|
|
# ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
|
|
|
# Unload current model if one is loaded
|
|
if self.is_tts_model_loaded:
|
|
await self.unload_model()
|
|
|
|
# Load the new model
|
|
try:
|
|
await self.api_manual_load_model(tts_method)
|
|
self.current_model_loaded = tts_method
|
|
except Exception as e:
|
|
print(f"[{self.branding}ENG] \033[91mError loading model: {str(e)}\033[0m")
|
|
return False
|
|
|
|
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
|
# ↓↓↓ Keep everything below this line ↓↓↓
|
|
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
|
generate_end_time = time.time() # Create an end timer for calculating load times
|
|
generate_elapsed_time = generate_end_time - generate_start_time # Calculate start time minus end time
|
|
print(f"[{self.branding}ENG] \033[94mLoad time :\033[93m {generate_elapsed_time:.2f} seconds.\033[0m") # Print out the result of the load time
|
|
return True
|
|
|
|
|
|
async def preprocess_ref_audio_text(self, ref_audio_orig, ref_text, clip_short=True):
|
|
print(f"[{self.branding}ENG] Converting audio...") if self.debug_tts else None
|
|
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as f:
|
|
aseg = AudioSegment.from_file(ref_audio_orig)
|
|
|
|
if clip_short:
|
|
# 1. try to find long silence for clipping
|
|
non_silent_segs = silence.split_on_silence(
|
|
aseg, min_silence_len=1000, silence_thresh=-50, keep_silence=1000
|
|
)
|
|
non_silent_wave = AudioSegment.silent(duration=0)
|
|
for non_silent_seg in non_silent_segs:
|
|
if len(non_silent_wave) > 6000 and len(non_silent_wave + non_silent_seg) > 15000:
|
|
print(f"[{self.branding}ENG] Audio is over 15s, clipping short. (1)") if self.debug_tts else None
|
|
break
|
|
non_silent_wave += non_silent_seg
|
|
|
|
# 2. try to find short silence for clipping if 1. failed
|
|
if len(non_silent_wave) > 15000:
|
|
non_silent_segs = silence.split_on_silence(
|
|
aseg, min_silence_len=100, silence_thresh=-40, keep_silence=1000
|
|
)
|
|
non_silent_wave = AudioSegment.silent(duration=0)
|
|
for non_silent_seg in non_silent_segs:
|
|
if len(non_silent_wave) > 6000 and len(non_silent_wave + non_silent_seg) > 15000:
|
|
print(f"[{self.branding}ENG] Audio is over 15s, clipping short. (2)") if self.debug_tts else None
|
|
break
|
|
non_silent_wave += non_silent_seg
|
|
|
|
aseg = non_silent_wave
|
|
|
|
# 3. if no proper silence found for clipping
|
|
if len(aseg) > 15000:
|
|
aseg = aseg[:15000]
|
|
print(f"[{self.branding}ENG] Audio is over 15s, clipping short. (3)") if self.debug_tts else None
|
|
|
|
aseg.export(f.name, format="wav")
|
|
ref_audio = f.name
|
|
|
|
# Ensure ref_text ends with a proper sentence-ending punctuation
|
|
if not ref_text.endswith(". ") and not ref_text.endswith("。"):
|
|
if ref_text.endswith("."):
|
|
ref_text += " "
|
|
else:
|
|
ref_text += ". "
|
|
|
|
return ref_audio, ref_text
|
|
|
|
async def infer_process(
|
|
self,
|
|
ref_audio,
|
|
ref_text,
|
|
gen_text,
|
|
model_obj,
|
|
vocoder,
|
|
target_rms=0.1,
|
|
cross_fade_duration=0.15,
|
|
nfe_step=32,
|
|
cfg_strength=2.0,
|
|
sway_sampling_coef=-1,
|
|
speed=1,
|
|
fix_duration=None,
|
|
device=None
|
|
):
|
|
"""Process text and prepare for batch inference"""
|
|
# Split the input text into batches
|
|
audio, sr = torchaudio.load(ref_audio)
|
|
max_chars = int(len(ref_text.encode("utf-8")) / (audio.shape[-1] / sr) * (25 - audio.shape[-1] / sr))
|
|
gen_text_batches = self.chunk_text(gen_text, max_chars=max_chars)
|
|
|
|
for i, gen_text_batch in enumerate(gen_text_batches):
|
|
print(f"[{self.branding}ENG] gen_text {i}", gen_text_batch) if self.debug_tts else None
|
|
|
|
print(f"[{self.branding}ENG] Generating audio in {len(gen_text_batches)} batches...") if self.debug_tts else None
|
|
|
|
return await self.infer_batch_process(
|
|
(audio, sr),
|
|
ref_text,
|
|
gen_text_batches,
|
|
model_obj,
|
|
vocoder,
|
|
target_rms=target_rms,
|
|
cross_fade_duration=cross_fade_duration,
|
|
nfe_step=nfe_step,
|
|
cfg_strength=cfg_strength,
|
|
sway_sampling_coef=sway_sampling_coef,
|
|
speed=speed,
|
|
fix_duration=fix_duration,
|
|
device=device
|
|
)
|
|
|
|
async def infer_batch_process(
|
|
self,
|
|
ref_audio,
|
|
ref_text,
|
|
gen_text_batches,
|
|
model_obj,
|
|
vocoder,
|
|
target_rms=0.1,
|
|
cross_fade_duration=0.15,
|
|
nfe_step=32,
|
|
cfg_strength=2.0,
|
|
sway_sampling_coef=-1,
|
|
speed=1,
|
|
fix_duration=None,
|
|
device=None
|
|
):
|
|
"""Process batches for inference"""
|
|
device = device or self.device
|
|
audio, sr = ref_audio
|
|
|
|
# Always process initial audio in float32
|
|
if audio.shape[0] > 1:
|
|
audio = torch.mean(audio, dim=0, keepdim=True)
|
|
|
|
audio = audio.to(torch.float32) # Ensure float32 for initial processing
|
|
|
|
# Normalize audio if needed
|
|
rms = torch.sqrt(torch.mean(torch.square(audio)))
|
|
if rms < target_rms:
|
|
audio = audio * target_rms / rms
|
|
|
|
# Resample if needed
|
|
if sr != self.target_sample_rate:
|
|
resampler = torchaudio.transforms.Resample(sr, self.target_sample_rate)
|
|
audio = resampler(audio)
|
|
|
|
# Move to device and adjust precision
|
|
audio = audio.to(device)
|
|
if device == 'cuda':
|
|
audio = audio.half()
|
|
|
|
generated_waves = []
|
|
spectrograms = []
|
|
|
|
if len(ref_text[-1].encode("utf-8")) == 1:
|
|
ref_text = ref_text + " "
|
|
|
|
for gen_text in gen_text_batches:
|
|
# Prepare the text
|
|
text_list = [ref_text + gen_text]
|
|
final_text_list = convert_char_to_pinyin(text_list)
|
|
|
|
ref_audio_len = audio.shape[-1] // self.hop_length
|
|
if fix_duration is not None:
|
|
duration = int(fix_duration * self.target_sample_rate / self.hop_length)
|
|
else:
|
|
# Calculate duration
|
|
ref_text_len = len(ref_text.encode("utf-8"))
|
|
gen_text_len = len(gen_text.encode("utf-8"))
|
|
duration = ref_audio_len + int(ref_audio_len / ref_text_len * gen_text_len / speed)
|
|
|
|
# inference
|
|
with torch.inference_mode():
|
|
generated, _ = model_obj.sample(
|
|
cond=audio,
|
|
text=final_text_list,
|
|
duration=duration,
|
|
steps=nfe_step,
|
|
cfg_strength=cfg_strength,
|
|
sway_sampling_coef=sway_sampling_coef,
|
|
)
|
|
|
|
# Handle precision for post-processing
|
|
if self.device == 'cuda':
|
|
generated = generated.half()
|
|
else:
|
|
generated = generated.float()
|
|
|
|
generated = generated[:, ref_audio_len:, :]
|
|
generated_mel_spec = generated.permute(0, 2, 1)
|
|
|
|
# Convert to the correct precision for vocoder
|
|
if self.device == 'cuda':
|
|
generated_mel_spec = generated_mel_spec.half()
|
|
else:
|
|
generated_mel_spec = generated_mel_spec.float()
|
|
|
|
generated_wave = vocoder.decode(generated_mel_spec)
|
|
if rms < target_rms:
|
|
generated_wave = generated_wave * rms / target_rms
|
|
|
|
# wav -> numpy
|
|
generated_wave = generated_wave.squeeze().cpu().float().numpy() # Always convert to float32 for numpy
|
|
|
|
generated_waves.append(generated_wave)
|
|
spectrograms.append(generated_mel_spec[0].cpu().numpy())
|
|
|
|
# Combine all generated waves with cross-fading
|
|
if cross_fade_duration <= 0:
|
|
final_wave = np.concatenate(generated_waves)
|
|
else:
|
|
final_wave = generated_waves[0]
|
|
for i in range(1, len(generated_waves)):
|
|
prev_wave = final_wave
|
|
next_wave = generated_waves[i]
|
|
|
|
# Calculate cross-fade samples
|
|
cross_fade_samples = int(cross_fade_duration * self.target_sample_rate)
|
|
cross_fade_samples = min(cross_fade_samples, len(prev_wave), len(next_wave))
|
|
|
|
if cross_fade_samples <= 0:
|
|
final_wave = np.concatenate([prev_wave, next_wave])
|
|
continue
|
|
|
|
# Overlapping parts
|
|
prev_overlap = prev_wave[-cross_fade_samples:]
|
|
next_overlap = next_wave[:cross_fade_samples]
|
|
|
|
# Fade out and fade in
|
|
fade_out = np.linspace(1, 0, cross_fade_samples)
|
|
fade_in = np.linspace(0, 1, cross_fade_samples)
|
|
|
|
# Cross-faded overlap
|
|
cross_faded_overlap = prev_overlap * fade_out + next_overlap * fade_in
|
|
|
|
# Combine
|
|
new_wave = np.concatenate(
|
|
[prev_wave[:-cross_fade_samples], cross_faded_overlap, next_wave[cross_fade_samples:]]
|
|
)
|
|
|
|
final_wave = new_wave
|
|
|
|
# Create a combined spectrogram
|
|
combined_spectrogram = np.concatenate(spectrograms, axis=1)
|
|
|
|
return final_wave, self.target_sample_rate, combined_spectrogram
|
|
|
|
def chunk_text(self, text, max_chars=135):
|
|
"""
|
|
Splits the input text into chunks, each with a maximum number of characters.
|
|
"""
|
|
chunks = []
|
|
current_chunk = ""
|
|
# Split the text into sentences based on punctuation followed by whitespace
|
|
sentences = re.split(r"(?<=[;:,.!?])\s+|(?<=[;:,。!?])", text)
|
|
|
|
for sentence in sentences:
|
|
if len(current_chunk.encode("utf-8")) + len(sentence.encode("utf-8")) <= max_chars:
|
|
current_chunk += sentence + " " if sentence and len(sentence[-1].encode("utf-8")) == 1 else sentence
|
|
else:
|
|
if current_chunk:
|
|
chunks.append(current_chunk.strip())
|
|
current_chunk = sentence + " " if sentence and len(sentence[-1].encode("utf-8")) == 1 else sentence
|
|
|
|
if current_chunk:
|
|
chunks.append(current_chunk.strip())
|
|
|
|
return chunks
|
|
|
|
#############################################################################################################################
|
|
#############################################################################################################################
|
|
# DONT CHANGE ME # De-Capitalise words (Not used currently) #################################################################
|
|
#############################################################################################################################
|
|
#############################################################################################################################
|
|
def process_text_for_tts(self, text):
|
|
"""
|
|
Process text by converting fully capitalized words to lowercase,
|
|
while preserving other words' capitalization.
|
|
|
|
Args:
|
|
self: The class instance
|
|
text (str): Input text string
|
|
|
|
Returns:
|
|
str: Processed text with fully capitalized words converted to lowercase
|
|
"""
|
|
# Split text into words
|
|
words = text.split()
|
|
|
|
# Process each word
|
|
processed_words = []
|
|
for word in words:
|
|
# Check if word is fully uppercase and longer than 1 character
|
|
if word.isupper() and len(word) > 1:
|
|
processed_words.append(word.lower())
|
|
else:
|
|
processed_words.append(word)
|
|
|
|
# Join words back together
|
|
return ' '.join(processed_words)
|
|
|
|
##########################################################################################################################################
|
|
##########################################################################################################################################
|
|
# CHANGE ME # Model changing. Unload out old model and load in a new one #################################################################
|
|
##########################################################################################################################################
|
|
##########################################################################################################################################
|
|
# In here all the possible options are sent over (text, voice to use, lanugage, speed etc etc) and its up to you how you use them, or not.
|
|
# obviously if your TTS engine doesnt support speed for example, generationspeed_capable should be set False in your model_settings.JSON file
|
|
# and a fake "generationspeed_set" value should be set. This allows a fake value to be sent over from the main script, even though it
|
|
# wouldnt actually ever be used in the generation below. Nonethless all these values, real or just whats inside the configuration file
|
|
# will be sent over for use.
|
|
# Setting the xxxxxxx_capabale in the model_settings.JSON file, will enable/disable them being selectable by the user. For example, if you
|
|
# set "generationspeed_capable" as false in the model_settings.JSON file, a user will not be able to select OR set the setting for
|
|
# generation speed.
|
|
# One thing to note is that we HAVE to have something in this generation request that is synchronous from the way its called, which means
|
|
# we have to have an option for Streaming, even if our TTS engine doesnt support streaming. So in that case, we would set streaming_capable
|
|
# as false in our model_settings.JSON file, meaning streaming will never be called. However, we have to put a fake streaming routine in our
|
|
# function below (or a real function if it does support streaming of course). Parler has an example of a fake streaming function, which is
|
|
# very clearly highlighted in its model_engine.py script.
|
|
async def generate_tts(self, text, voice, language, temperature, repetition_penalty, speed, pitch, output_file, streaming):
|
|
if voice == "No Voices Found":
|
|
print(f"[{self.branding}ENG] \033[91mError\033[0m: No voices found to generate TTS.")
|
|
raise HTTPException(status_code=400, detail="No voices found to generate TTS.")
|
|
if not self.is_tts_model_loaded:
|
|
print(f"[{self.branding}ENG] \033[91mError\033[0m: You currently have no TTS model loaded.")
|
|
raise HTTPException(status_code=400, detail="You currently have no TTS model loaded.")
|
|
|
|
# Language validation - converted to lowercase for case-insensitive comparison
|
|
language = language.lower()
|
|
if language not in ['en', 'eng', 'zh', 'zho', 'chi']:
|
|
print(f"[{self.branding}ENG] \033[91mError\033[0m: F5-TTS only supports English (EN) or Chinese (ZH) language TTS generation.")
|
|
print(f"[{self.branding}ENG] \033[91mError\033[0m: You may get an error message or gibberish output.")
|
|
print(f"[{self.branding}ENG] \033[91mError\033[0m: Please use only EN or ZH/ZHO for the language with F5-TTS.")
|
|
|
|
self.tts_generating_lock = True
|
|
generate_start_time = time.time()
|
|
|
|
try:
|
|
if self.lowvram_enabled and self.device == "cpu":
|
|
print(f"[{self.branding}Debug] Moving models to GPU for generation") if self.debug_tts else None
|
|
await self.handle_lowvram_change()
|
|
|
|
# Verify precision is consistent
|
|
if self.debug_tts:
|
|
print(f"[{self.branding}Debug] Verifying model precision...")
|
|
if hasattr(self, 'model'):
|
|
params_dtype = {p.dtype for p in self.model.parameters()}
|
|
buffers_dtype = {b.dtype for b in self.model.buffers()}
|
|
if len(params_dtype) > 1 or len(buffers_dtype) > 1:
|
|
print(f"[{self.branding}ENG] Warning: Mixed precision detected in model")
|
|
print(f"[{self.branding}Debug] Model parameters dtypes: {params_dtype}")
|
|
print(f"[{self.branding}Debug] Model buffers dtypes: {buffers_dtype}")
|
|
|
|
if hasattr(self, 'vocoder'):
|
|
params_dtype = {p.dtype for p in self.vocoder.parameters()}
|
|
buffers_dtype = {b.dtype for b in self.vocoder.buffers()}
|
|
if len(params_dtype) > 1 or len(buffers_dtype) > 1:
|
|
print(f"[{self.branding}ENG] Warning: Mixed precision detected in vocoder")
|
|
print(f"[{self.branding}Debug] Vocoder parameters dtypes: {params_dtype}")
|
|
print(f"[{self.branding}Debug] Vocoder buffers dtypes: {buffers_dtype}")
|
|
|
|
# Handle voice folder or direct file
|
|
if voice.endswith('/'):
|
|
# It's a folder, pick the first valid wav+reference.txt pair
|
|
voice_dir = self.main_dir / "voices" / voice.rstrip('/')
|
|
for wav_file in voice_dir.glob("*.wav"):
|
|
ref_text_path = wav_file.with_suffix('.reference.txt')
|
|
if ref_text_path.exists():
|
|
ref_audio_path = wav_file
|
|
break
|
|
else:
|
|
# It's a direct file
|
|
ref_audio_path = self.main_dir / "voices" / voice
|
|
|
|
# Get the corresponding reference text file
|
|
ref_text_path = ref_audio_path.with_suffix('.reference.txt')
|
|
if not ref_text_path.exists():
|
|
raise FileNotFoundError(f"Reference text file not found for {voice}")
|
|
|
|
with open(ref_text_path, 'r', encoding='utf-8') as f:
|
|
ref_text = f.read().strip()
|
|
|
|
# Process reference audio and text
|
|
ref_audio, processed_ref_text = await self.preprocess_ref_audio_text(
|
|
str(ref_audio_path), ref_text
|
|
)
|
|
|
|
text = self.process_text_for_tts(text)
|
|
|
|
# Generate the audio using infer_process
|
|
final_wave, final_sample_rate, _ = await self.infer_process(
|
|
ref_audio,
|
|
processed_ref_text,
|
|
text,
|
|
self.model,
|
|
self.vocoder,
|
|
target_rms=self.target_rms,
|
|
cross_fade_duration=self.cross_fade_duration,
|
|
nfe_step=self.nfe_step,
|
|
cfg_strength=self.cfg_strength,
|
|
sway_sampling_coef=self.sway_sampling_coef,
|
|
speed=float(speed),
|
|
fix_duration=None,
|
|
device=self.device
|
|
)
|
|
|
|
# Save the audio
|
|
sf.write(output_file, final_wave, final_sample_rate)
|
|
|
|
generate_end_time = time.time()
|
|
generate_elapsed_time = generate_end_time - generate_start_time
|
|
print(f"[{self.branding}GEN] \033[94mTTS Generate: \033[93m{generate_elapsed_time:.2f} seconds. \033[94mLowVRAM: \033[33m{self.lowvram_enabled} \033[94mDeepSpeed: \033[33m{self.deepspeed_enabled}\033[0m")
|
|
|
|
if streaming:
|
|
with open(output_file, 'rb') as f:
|
|
yield f.read()
|
|
|
|
except FileNotFoundError as e:
|
|
print(f"[{self.branding}ENG] \033[91mError: {str(e)}\033[0m")
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except Exception as e:
|
|
print(f"[{self.branding}ENG] \033[91mError during TTS generation: {str(e)}\033[0m")
|
|
raise HTTPException(status_code=500, detail=f"Error during TTS generation: {str(e)}")
|
|
finally:
|
|
try:
|
|
# Handle low VRAM mode - move back to CPU if needed
|
|
if self.lowvram_enabled and not self.tts_narrator_generatingtts:
|
|
print(f"[{self.branding}Debug] Moving models back to CPU after generation") if self.debug_tts else None
|
|
await self.handle_lowvram_change()
|
|
|
|
# Verify final device state in debug mode using parameter checking
|
|
if self.debug_tts:
|
|
if hasattr(self, 'model'):
|
|
model_device = next(self.model.parameters()).device
|
|
print(f"[{self.branding}Debug] Final model device: {model_device}") if self.debug_tts else None
|
|
if hasattr(self, 'vocoder'):
|
|
try:
|
|
vocoder_device = next(self.vocoder.parameters()).device
|
|
print(f"[{self.branding}Debug] Final vocoder device: {vocoder_device}") if self.debug_tts else None
|
|
except Exception:
|
|
# Skip vocoder device checking if it fails
|
|
pass
|
|
except Exception as e:
|
|
print(f"[{self.branding}Debug] Device verification warning: {str(e)}") if self.debug_tts else None
|
|
|
|
self.tts_generating_lock = False
|