Files
alltalk_tts/tts_server.py

2620 lines
118 KiB
Python

"""
AllTalk TTS server implementation for managing and serving text-to-speech generation.
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
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 shutil
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 ffmpeg
import numpy as np
import soundfile as sf
import librosa
from langdetect import detect, DetectorFactory
from langdetect.lang_detect_exception import LangDetectException
from config import AlltalkConfig, AlltalkTTSEnginesConfig
logging.disable(logging.WARNING)
DetectorFactory.seed = 0 # Ensure deterministic behavior of language detector
LANG_FALLBACKS = {
"en": "en", "es": "es", "fr": "fr", "de": "de", "it": "it",
"pt": "pt", "pl": "pl", "tr": "tr", "ru": "ru", "nl": "nl",
"cs": "cs", "ar": "ar", "zh": "zh", "zh-cn": "zh-cn", "ja": "ja",
"hu": "hu", "ko": "ko",
# Additional fallbacks for unsupported languages
"uk": "ru", # Ukrainian → Russian
"bg": "ru", # Bulgarian → Russian
"ca": "fr",
}
########################################################################################
# START-UP # Silence RVC warning about torch.nn.utils.weight_norm even though not used #
########################################################################################
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
warnings.filterwarnings("ignore", message="ComplexHalf support is experimental")
# Filter Flash Attention warning
warnings.filterwarnings("ignore", message="1Torch was not compiled with flash attention")
####################
# Setup local path #
####################
this_dir = Path(__file__).parent.resolve() # Set this_dir as the current alltalk_tts folder
infer_pipeline = None
config: AlltalkConfig | None = None
tts_engines_config: AlltalkTTSEnginesConfig | None = None
def load_config(force_reload = False):
"""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():
"""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 as rvc_pipeline # pylint: disable=import-outside-toplevel
infer_pipeline = rvc_pipeline
else:
infer_pipeline = None
load_config()
##########################
# Central print function #
##########################
# ANSI color codes
BLUE = "\033[94m"
# MAGENTA = "\033[95m"
YELLOW = "\033[93m"
RED = "\033[91m"
GREEN = "\033[92m"
RESET = "\033[0m"
def print_message(message, message_type="standard", component="TTS"):
"""Centralized print function for AllTalk messages
Args:
message (str): The message to print
message_type (str): Type of message (standard/warning/error/debug_*/debug)
component (str): Component identifier (TTS/ENG/GEN/API/etc.)
"""
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}")
def debug_func_entry():
"""Print debug message for function entry if debug_func is enabled"""
if config.debugging.debug_func:
current_func = inspect.currentframe().f_back.f_code.co_name
print_message(f"Function entry: {current_func}", "debug_func")
####################
# Check for FFMPEG #
####################
def check_ffmpeg():
"""Verify FFmpeg availability in the conda environment."""
debug_func_entry()
try:
# Check if ffmpeg is in PATH
ffmpeg_path = shutil.which('ffmpeg')
ffprobe_path = shutil.which('ffprobe')
if not ffmpeg_path or not ffprobe_path:
return False, "FFmpeg not found in conda environment"
# Verify FFmpeg works
subprocess.run(['ffmpeg', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
return True, "FFmpeg found in conda environment"
except (subprocess.CalledProcessError, FileNotFoundError):
return False, "FFmpeg not functioning correctly"
def print_ffmpeg_status(is_ffmpeg_installed, _message):
"""Print FFmpeg installation status and instructions."""
if not is_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 using:", component="ENG")
print_message("conda install -c conda-forge ffmpeg", component="ENG")
else:
print_message("\033[92mTranscoding :\033[93m ffmpeg found\033[0m", component="ENG")
# Implementation
ffmpeg_installed, ffmpeg_message = check_ffmpeg()
print_ffmpeg_status(ffmpeg_installed, ffmpeg_message)
################################
# Check for portaudio on Linux #
################################
try:
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")
print_message("You can still play audio through web browsers without PortAudio.", "warning")
print_message("Installing PortAudio is optional and not strictly required.")
sounddevice_installed=False
if sys.platform.startswith('linux'):
print_message("On Linux, you can use the following command to install PortAudio:", "warning")
print_message("sudo apt-get install portaudio19-dev", "warning")
#######################################################################
# Attempt to import the ModelLoader class for the selected TTS engine #
#######################################################################
if tts_engines_config.is_valid_engine(tts_engines_config.engine_loaded):
loader_module = importlib.import_module(f"system.tts_engines.{tts_engines_config.engine_loaded}.model_engine")
tts_class = getattr(loader_module, "tts_class")
# Setup model_engine as the way to call the functions within the Class.
model_engine = tts_class()
else:
raise ValueError(f"Invalid TTS engine: {tts_engines_config.engine_loaded}")
##########################################
# Run setup function in the model_engine #
##########################################
@asynccontextmanager
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:
await model_engine.setup()
except FileNotFoundError as e:
print_message(f"Error during setup: {e}. Continuing without the TTS model.", "error")
yield
###############################
# Setup FastAPI with Lifespan #
###############################
app = FastAPI(lifespan=startup_shutdown)
# Allow all origins, and set other CORS options
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Set this to the specific origins you want to allow
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Global lock
model_change_lock = Lock()
##############################
# API Endpoint - /api/reload #
##############################
@app.route("/api/reload", methods=["POST"])
async def apifunction_reload(request: Request):
"""Handle API request to change TTS model."""
debug_func_entry()
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")
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 #
####################################
uvicorn_server = None # Global variable to track the server
def restart_self():
"""Restart the current Python process."""
debug_func_entry()
print_message("Restarting subprocess...", component="ENG")
os.execv(sys.executable, ['python'] + sys.argv)
async def handle_restart():
"""Handle graceful shutdown of uvicorn server before restart."""
debug_func_entry()
global uvicorn_server # 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_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")
#######################################
# API Endpoint - /api/stop-generation #
#######################################
# When this endpoint it called it will set tts_stop_generation in the model_engine to True, which can be used to interrupt the current TTS generation.
@app.put("/api/stop-generation")
async def apifunction_stop_generation():
"""Handle request to stop ongoing TTS generation."""
debug_func_entry()
if model_engine.tts_generating_lock and not model_engine.tts_stop_generation:
model_engine.tts_stop_generation = True
print_message("Stopping TTS generation", "debug_api", "API")
return {"message": "Cancelling current TTS generation"}
#############################
# API Endpoint - /api/audio #
#############################
@app.get("/audio/{filename}")
async def apifunction_get_audio(filename: str):
"""Serve audio file from output directory."""
debug_func_entry()
audio_path = this_dir / config.get_output_directory() / filename
if not audio_path.is_file():
print_message(f"Audio file not found: {filename}", "error", "API")
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(audio_path)
##################################
# API Endpoint - /api/audiocache #
##################################
@app.get("/audiocache/{filename}")
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)
return response
##############################
# API Endpoint - /api/voices #
##############################
@app.get("/api/voices")
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 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 #
#################################
@app.get("/api/rvcvoices")
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 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 response_data
if not os.access(directory, os.R_OK):
print_message("No read permission for RVC voices directory", "error", "API")
raise PermissionError("Cannot access RVC voices directory")
pth_files = []
for root, _dirs, files in os.walk(directory):
for file in files:
if file.endswith(".pth"):
pth_files.append(os.path.relpath(os.path.join(root, file), start=directory))
if 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 #
#####################################
@app.get("/api/reload_config")
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 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 #
#############################
@app.get("/api/ready")
async def apifunction_ready():
"""Check if model engine setup has completed."""
debug_func_entry()
status = "Ready" if model_engine.setup_has_run else "Unloaded"
print_message(f"Engine status: {status}", "debug_api", "API")
return Response(status)
#######################################
# API Endpoint - /api/currentsettings #
#######################################
@app.get('/api/currentsettings')
def apifunction_get_current_settings():
"""Get comprehensive dictionary of current engine settings and capabilities."""
debug_func_entry()
try:
load_config()
settings = {
"engines_available": tts_engines_config.get_engine_names_available(),
"current_engine_loaded": model_engine.engine_loaded,
"models_available": [{"name": name} for name in model_engine.available_models.keys()],
"current_model_loaded": model_engine.current_model_loaded,
"manufacturer_name": model_engine.manufacturer_name,
"audio_format": model_engine.audio_format,
"deepspeed_capable": model_engine.deepspeed_capable,
"deepspeed_available": model_engine.deepspeed_available,
"deepspeed_enabled": model_engine.deepspeed_enabled,
"generationspeed_capable": model_engine.generationspeed_capable,
"generationspeed_set": model_engine.generationspeed_set,
"lowvram_capable": model_engine.lowvram_capable,
"lowvram_enabled": model_engine.lowvram_enabled,
"pitch_capable": model_engine.pitch_capable,
"pitch_set": model_engine.pitch_set,
"repetitionpenalty_capable": model_engine.repetitionpenalty_capable,
"repetitionpenalty_set": model_engine.repetitionpenalty_set,
"streaming_capable": model_engine.streaming_capable,
"temperature_capable": model_engine.temperature_capable,
"temperature_set": model_engine.temperature_set,
"ttsengines_installed": model_engine.engine_installed,
"languages_capable": model_engine.languages_capable,
"multivoice_capable": model_engine.multivoice_capable,
"multimodel_capable": model_engine.multimodel_capable,
}
print_message("Current settings retrieved successfully", "debug_api", "API")
return settings
except 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):
"""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")
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")
response_content.update({"status": "success", "message": msg})
else:
# 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):
"""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")
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")
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(...),
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)}
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)) from e
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
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 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(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
##################################
# Transcode between file formats #
##################################
async def get_audio_duration(file_path):
"""Get duration of audio file using FFprobe."""
debug_func_entry()
file_path_str = str(file_path)
print_message("\033[94mGet Audio Duration > get_audio_duration > tts_server.py\033[0m", "debug_transcode" or "debug_openai")
print_message(f"├─ Input file: {file_path_str}", "debug_transcode")
try:
probe = ffmpeg.probe(file_path_str)
duration = float(probe['format']['duration'])
print_message(f"└─ Duration: {duration} seconds", "debug_transcode")
return duration
except Exception as e:
print_message(f"Error getting audio duration: {str(e)}", "error")
raise
async def transcode_audio(input_file, output_format, output_file=None):
"""Transcode audio files between different formats using FFmpeg."""
debug_func_entry()
input_file_str = str(input_file)
print_message("\033[94mTranscode Function Entry > transcode_audio > tts_server.py\033[0m", "debug_transcode")
print_message(f"├─ Input file : {input_file_str}", "debug_transcode")
print_message(f"└─ Output format : {output_format}", "debug_transcode")
if output_file is None:
output_file = os.path.splitext(input_file_str)[0] + f".{output_format}"
print_message(f"└─ Output file : {output_file}", "debug_transcode")
if not ffmpeg_installed:
print_message("FFmpeg is not installed. Format conversion is not possible.", "error")
raise RuntimeError("FFmpeg is not installed. Format conversion is not possible.")
input_extension = os.path.splitext(input_file_str)[1][1:].lower()
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_str)[0] + f".{output_format}"
print_message(f"└─ Output file : {output_file}", "debug_transcode")
try:
print_message("\033[94mStarting Transcode Process\033[0m", "debug_transcode")
stream = ffmpeg.input(input_file_str)
# Configure format-specific options
format_options = {
'mp3': {
'acodec': 'libmp3lame',
**{'b:a': '192k'},
'ar': 44100,
'ac': 2
},
'opus': {
'acodec': 'libopus',
**{'b:a': '128k'},
'vbr': 'on',
'compression_level': '10',
'frame_duration': '60',
'application': 'voip',
'ar': 48000,
'ac': 2
},
'aac': {
'acodec': 'aac',
**{'b:a': '192k'},
'ar': 44100,
'ac': 2
},
'vorbis': { # for ogg
'acodec': 'libvorbis',
**{'b:a': '192k'},
'ar': 44100,
'ac': 2,
'f': 'ogg' # Force OGG container format
},
'flac': {
'acodec': 'flac',
'compression_level': '8',
'ar': 44100,
'ac': 2
},
'wav': {
'acodec': 'pcm_s16le',
'ar': 44100,
'ac': 2
}
}
if output_format not in format_options:
raise ValueError(f"Unsupported output format: {output_format}")
# Add options and force overwrite
stream = ffmpeg.output(stream, output_file, **format_options[output_format], y=None)
print_message(f"FFmpeg command: {' '.join(ffmpeg.compile(stream))}", "debug_transcode")
_out, _err = ffmpeg.run(stream, capture_stdout=True, capture_stderr=True)
print_message("Transcoding completed successfully", "debug_transcode")
os.remove(input_file_str)
print_message("\033[94mTranscode Complete\033[0m", "debug_transcode")
print_message(f"└─ Output file: {output_file}", "debug_transcode")
return output_file
except ffmpeg.Error as e:
print_message("FFmpeg error:", "error")
print_message(f"stdout: {e.stdout.decode('utf8')}", "error")
print_message(f"stderr: {e.stderr.decode('utf8')}", "error")
raise
except Exception as e:
print_message(f"Error during transcoding: {str(e)}", "error")
raise
##############################
# Central Transcode function #
##############################
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)
print_message("Transcode completed successfully", "debug_transcode")
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_transcode")
# Generate appropriate URLs based on API configuration
if config.api_def.api_use_legacy_api:
output_file_url = f'http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}/audio/{os.path.basename(output_file)}'
output_cache_url = f'http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}/audiocache/{os.path.basename(output_file)}'
else:
output_file_url = f'/audio/{os.path.basename(output_file)}'
output_cache_url = f'/audiocache/{os.path.basename(output_file)}'
print_message("Output file paths and URLs updated", "debug_transcode")
print_message(f"Transcode output_file is now : {output_file}", "debug_transcode")
print_message(f"Transcode output_file_url is now : {output_file_url}", "debug_transcode")
print_message(f"Transcode output_cache_url is now: {output_cache_url}", "debug_transcode")
return output_file, output_file_url, output_cache_url
##############################
#### Streaming Generation ####
##############################
@app.get("/api/tts-generate-streaming", response_class=StreamingResponse)
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,
output_file_path, streaming=True)
return StreamingResponse(stream, media_type="audio/wav")
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(...),
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",
output_file_path, streaming=False)
return JSONResponse(content={"output_file_path": str(output_file)}, status_code=200)
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 #
###################################
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")
if language == "auto":
language = detect_language(text)
response = model_engine.generate_tts(text, voice, language, temperature, repetition_penalty, speed, pitch, output_file, streaming)
if streaming:
async def stream_response():
try:
async for chunk in response:
yield chunk
except Exception as e:
print_message(f"Error during streaming audio generation: {str(e)}", "error", "GEN")
raise
return stream_response()
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,
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,
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")
else:
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={
"status": "generate-success",
"output_file_path": str(output_file_path),
"output_file_url": str(output_file_url),
},
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": 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 #
##################################################################
class OpenAIInput(BaseModel):
"""Validates input parameters for OpenAI TTS generation."""
model: str = Field(..., description="The TTS model to use. Currently ignored.")
input: str = Field(..., max_length=4096, description="The text to generate audio for.")
voice: str = Field(..., description="The voice to use when generating the audio.")
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', 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:
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."""
debug_func_entry()
try:
OpenAIInput(**json_data)
print_message("OpenAI input validation successful", "debug_openai", "TTS")
return None
except ValidationError as e:
errors = []
for err in e.errors():
field = err['loc'][0]
message = err['msg']
description = OpenAIInput.model_fields[field].field_info.description
errors.append(f"Error in field '{field}': {message}. Description: {description}")
error_msg = ', '.join(errors)
print_message(f"OpenAI input validation failed: {error_msg}", "error", "TTS")
return error_msg
#########################################################################
# API Endpoint - OpenAI Speech API compatable endpoint /v1/audio/speech #
#########################################################################
@app.post("/v1/audio/speech", response_class=JSONResponse)
async def openai_tts_generate(request: Request):
"""Handle OpenAI-compatible TTS generation requests."""
debug_func_entry()
# 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")
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")
# 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,
"fable": model_engine.openai_fable,
"nova": model_engine.openai_nova,
"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")
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}"}'
if config.debugging.debug_fullttstext:
print_message(cleaned_string, component="TTS")
else:
print_message(f"{cleaned_string[:90]}{'...' if len(cleaned_string) > 90 else ''}", component="TTS")
await generate_audio(cleaned_string, mapped_voice, "auto", 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")
else:
print_message("send to rvc", "debug_openai", "TTS")
pth_path = this_dir / "models" / "rvc_voices" / config.rvc_settings.rvc_char_model_file
pitch = config.rvc_settings.pitch
run_rvc(output_file_path, pth_path, pitch, infer_pipeline)
transcoded_file_path = await transcode_for_openai(output_file_path, response_format)
print_message(f"Audio transcoded to: {transcoded_file_path}", "debug_openai", "TTS")
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 #
###########################################################################
async def transcode_for_openai(input_file, output_format):
"""Transcode audio files for OpenAI API compatibility."""
debug_func_entry()
print_message("************************************", "debug_openai", "TTS")
print_message("transcode_for_openai function called", "debug_openai", "TTS")
print_message(f"Input file : {input_file}", "debug_openai", "TTS")
print_message(f"Output format : {output_format}", "debug_openai", "TTS")
# Map formats to codecs but preserve original extension
format_mapping = {
"m4a": ("aac", "m4a"), # (codec, extension)
"ogg": ("vorbis", "ogg"),
"aac": ("aac", "aac"),
"mp3": ("mp3", "mp3"),
"opus": ("opus", "opus"),
"flac": ("flac", "flac"),
"wav": ("wav", "wav")
}
if output_format not in format_mapping:
raise ValueError(f"Unsupported format: {output_format}")
codec, extension = format_mapping[output_format]
# Create output path with desired extension
output_file = os.path.splitext(input_file)[0] + f".{extension}"
try:
# Use main transcode but rename file if needed
result = await transcode_audio(input_file, codec, output_file)
return result
except Exception as e:
print_message(f"Error in OpenAI transcoding: {str(e)}", "error", "TTS")
raise
######################################################################################
# API Endpoint - OpenAI Speech API compatable endpoint change engine voices Function #
######################################################################################
class VoiceMappings(BaseModel):
"""OpenAI to engine voice mapping configuration."""
alloy: str
echo: str
fable: str
nova: str
onyx: str
shimmer: str
@app.put("/api/openai-voicemap")
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")
model_engine.openai_alloy = mappings.alloy
model_engine.openai_echo = mappings.echo
model_engine.openai_fable = mappings.fable
model_engine.openai_nova = mappings.nova
model_engine.openai_onyx = mappings.onyx
model_engine.openai_shimmer = mappings.shimmer
# Update settings file
settings_file = this_dir / "system" / "tts_engines" / model_engine.engine_loaded / "model_settings.json"
with open(settings_file, "r+", encoding="utf-8") as model_file:
settings = json.load(model_file)
settings["openai_voices"] = mappings.dict()
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)}") from e
#######################
# Play at the console #
#######################
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 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_input_data):
"""
Validate JSON input against JSONInput model schema.
"""
debug_func_entry()
try:
JSONInput(**json_input_data)
return None
except ValidationError as e:
error_messages = []
for error in e.errors():
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 #
#################################################################
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
if start < match.start():
ambiguous_text = text[start:match.start()].strip()
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('*'):
text_part = matched_text.strip('*').strip()
ordered_parts.append(('narrator', text_part))
print_message(f"Added narrator segment: {text_part}", "debug_tts", "GEN")
elif matched_text.startswith('"') and matched_text.endswith('"'):
text_part = matched_text.strip('"').strip()
ordered_parts.append(('character', text_part))
print_message(f"Added character segment: {text_part}", "debug_tts", "GEN")
else:
text_part = matched_text.strip('*').strip('"')
if '*' in matched_text:
ordered_parts.append(('narrator', text_part))
print_message(f"Added mixed narrator segment: {text_part}", "debug_tts", "GEN")
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("**", "")
.replace("*", "")
.replace("\n\n", "\n")
.replace("&#x27;", "'")
)
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, 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}",
"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 (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, 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,
"filter_radius": config.rvc_settings.filter_radius,
"index_rate": config.rvc_settings.index_rate,
"rms_mix_rate": config.rvc_settings.rms_mix_rate,
"protect": config.rvc_settings.protect,
"hop_length": config.rvc_settings.hop_length,
"f0method": config.rvc_settings.f0method,
"split_audio": config.rvc_settings.split_audio,
"f0autotune": config.rvc_settings.autotune,
"embedder_model": config.rvc_settings.embedder_model,
"training_data_size": config.rvc_settings.training_data_size,
}
input_tts_path = str(input_tts_path)
pth_path = str(pth_path)
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)
index_size_print = settings["training_data_size"]
else:
if len(index_files) > 1:
print_message(f"RVC Convert: Multiple RVC index files found in the models folder where {pth_filename} is", "warning", "GEN")
print_message("RVC Convert: located. Unable to determine which index to use. Continuing without an index file.", "warning", "GEN")
index_path = ""
index_filename_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():
print_message(f"{key}: {value}", "debug_rvc", "GEN")
print_message(f"input_tts_path: {input_tts_path}", "debug_rvc", "GEN")
print_message(f"output_rvc_path: {output_rvc_path}", "debug_rvc", "GEN")
print_message(f"pth_path: {pth_path}", "debug_rvc", "GEN")
print_message(f"index_path: {index_path}", "debug_rvc", "GEN")
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}",
"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, 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, 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'^[^<>"|?*]+$', description="character_voice_gen needs to be the name of a valid voice for the loaded TTS engine.")
rvccharacter_voice_gen: str = Field(..., description="rvccharacter_voice_gen needs to be the name of a valid pth file in the 'folder\\file.pth' format or the word 'Disabled'.")
rvccharacter_pitch: float = Field(..., description="RVC Character pitch needs to be a number between -24 and 24")
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'^[^<>"|?*]+$', 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")
text_not_inside: str = Field(..., pattern="^(character|narrator|silent)$", description="text_not_inside needs to be 'character', 'narrator' or 'silent'.")
language: str = Field(..., pattern="^(auto|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: auto, 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'.")
output_file_timestamp: bool = Field(..., description="output_file_timestamp needs to be true or false.")
autoplay: bool = Field(..., description="autoplay needs to be a true or false value.")
autoplay_volume: float = Field(..., ge=0.1, le=1.0, description="autoplay_volume needs to be from 0.1 to 1.0.")
speed: float = Field(..., ge=0.25, le=2.0, description="speed needs to be between 0.25 and 2.0.")
pitch: float = Field(..., ge=-10, le=10, description="pitch needs to be between -10 and 10.")
temperature: float = Field(..., ge=0.1, le=1.0, description="temperature needs to be between 0.1 and 1.0.")
repetition_penalty: float = Field(..., ge=1.0, le=20.0, description="repetition_penalty needs to be between 1.0 and 20.0.")
@classmethod
def validate_autoplay_volume(cls, value):
"""Validate autoplay volume is within acceptable range (0.1-1.0)."""
debug_func_entry()
try:
if not 0.1 <= value <= 1.0:
print_message(f"Invalid autoplay volume: {value}", "error", "GEN")
raise ValueError("Autoplay volume must be between 0.1 and 1.0")
print_message(f"Autoplay volume validated: {value}", "debug_tts", "GEN")
return value
except Exception as e:
print_message(f"Error validating autoplay volume: {str(e)}", "error", "GEN")
raise
@classmethod
def validate_rvccharacter_voice_gen(cls, v):
"""Validate RVC character voice file format (folder/file.pth or 'Disabled')."""
debug_func_entry()
try:
if v.lower() == "disabled":
print_message("RVC character voice disabled", "debug_rvc", "GEN")
return v
pattern = re.compile(r'^.*\.(pth)$')
if not pattern.match(v):
print_message(f"Invalid RVC character voice format: {v}", "error", "GEN")
raise ValueError("rvccharacter_voice_gen needs to be the name of a valid pth file in the 'folder\\file.pth' format or 'Disabled'.")
print_message(f"RVC character voice validated: {v}", "debug_rvc", "GEN")
return v
except Exception as e:
print_message(f"Error validating RVC character voice: {str(e)}", "error", "GEN")
raise
@classmethod
def validate_rvnarrator_voice_gen(cls, v):
"""Validate RVC narrator voice file format (folder/file.pth or 'Disabled')."""
debug_func_entry()
try:
if v.lower() == "disabled":
print_message("RVC narrator voice disabled", "debug_rvc", "GEN")
return v
pattern = re.compile(r'^.*\.(pth)$')
if not pattern.match(v):
print_message(f"Invalid RVC narrator voice format: {v}", "error", "GEN")
raise ValueError("rvcnarrator_voice_gen needs to be the name of a valid pth file in the 'folder\\file.pth' format or 'Disabled'.")
print_message(f"RVC narrator voice validated: {v}", "debug_rvc", "GEN")
return v
except Exception as e:
print_message(f"Error validating RVC narrator voice: {str(e)}", "error", "GEN")
raise
@classmethod
def validate_pitches(cls, value):
"""Validate pitch value is within acceptable range (-24 to 24)."""
debug_func_entry()
try:
num_value = float(value)
if not -24 <= num_value <= 24:
print_message(f"Invalid pitch value: {value}", "error", "GEN")
raise ValueError("Pitch needs to be a number between -24 and 24.")
print_message(f"Pitch validated: {value}", "debug_rvc", "GEN")
return value
except ValueError as exc:
print_message(f"Invalid pitch format: {value}", "error", "GEN")
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
def validate_json_input(json_input_data):
"""Validate JSON input against JSONInput model schema."""
debug_func_entry()
try:
JSONInput(**json_input_data)
return None
except ValidationError as e:
error_messages = []
for error in e.errors():
field = error["loc"][0]
description = JSONInput.model_fields[field].description # Updated line
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
# Engine Parameter Getters
def get_params_speed():
"""Get speech generation speed."""
return model_engine.generationspeed_set
def get_params_temperature():
"""Get model temperature."""
return model_engine.temperature_set
def get_params_repetition():
"""Get repetition penalty."""
return float(str(model_engine.repetitionpenalty_set).replace(',', '.'))
def get_params_pitch():
"""Get voice pitch setting."""
return model_engine.pitch_set
def get_character_voice_gen():
"""Get default character voice."""
return model_engine.def_character_voice
def get_narrator_voice_gen():
"""Get default narrator voice."""
return model_engine.def_narrator_voice
# RVC Settings Getters
def get_rvccharacter_voice_gen():
"""Get RVC character model."""
return config.rvc_settings.rvc_char_model_file
def get_rvccharacter_pitch():
"""Get RVC character pitch."""
return config.rvc_settings.pitch
def get_rvcnarrator_voice_gen():
"""Get RVC narrator model."""
return config.rvc_settings.rvc_narr_model_file
def get_rvcnarrator_pitch():
"""Get RVC narrator pitch."""
return config.rvc_settings.pitch
#############################################
# /api/tts-generate Generation API Endpoint #
#############################################
async def tts_validate_and_prepare_input(
text_input: str,
request_params: dict,
default_params: dict
) -> Tuple[dict, Union[None, str]]:
"""Validate TTS input parameters and prepare settings."""
debug_func_entry()
if config.debugging.debug_tts or config.debugging.debug_tts_variables:
print_message("\033[94mPre-validation parameter check > tts_validate_and_prepare_input > tts_server.py\033[0m", "debug_tts_variables", "GEN")
print_message("API Configuration:", "debug_tts_variables", "GEN")
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
}
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")
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")
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]:
"""Generate output paths and URLs for TTS files."""
debug_func_entry()
if timestamp:
hash_object = hashlib.sha256(str(uuid.uuid4()).encode())
short_uuid = hash_object.hexdigest()[:5]
timestamp_str = str(int(time.time()))
filename = f'{output_file_name}_{timestamp_str}{short_uuid}.{model_engine.audio_format}'
else:
filename = f"{output_file_name}.{model_engine.audio_format}"
output_path = this_dir / config.get_output_directory() / filename
if config.api_def.api_use_legacy_api:
base_url = f'http://{config.api_def.api_legacy_ip_address}:{config.api_def.api_port_number}'
file_url = f'{base_url}/audio/{filename}'
cache_url = f'{base_url}/audiocache/{filename}'
else:
file_url = f'/audio/{filename}'
cache_url = f'/audiocache/{filename}'
print_message(f"Generated output path: {output_path}", "debug_tts", "GEN")
print_message(f"Generated URLs: {file_url}, {cache_url}", "debug_tts", "GEN")
return output_path, file_url, cache_url
def tts_clean_text(text: str, filtering_type: str) -> str:
"""Clean and filter input text based on specified filtering type."""
debug_func_entry()
print_message(f"Cleaning text with filter type: {filtering_type}", "debug_tts", "GEN")
if filtering_type == "html":
cleaned = html.unescape(standard_filtering(text))
elif filtering_type == "standard":
cleaned = text
else:
print_message("No filtering applied", "debug_tts", "GEN")
return text
# Common cleaning steps
cleaned = re.sub(r'([!?.])\1+', r'\1', cleaned)
cleaned = re.sub(rf'{config.api_def.api_allowed_filter}', '', cleaned)
cleaned = re.sub(r'\n+', ' ', cleaned)
print_message(f"Cleaned text: {cleaned}", "debug_tts", "GEN")
return cleaned
async def tts_process_narrator_mode(params: dict, text_input: str) -> Tuple[Path, str, str]:
"""Process text with narrator mode, handling different voice types."""
debug_func_entry()
if config.debugging.debug_fullttstext:
print_message(text_input, component="TTS")
else:
print_message(f"{text_input[:90]}{'...' if len(text_input) > 90 else ''}", component="TTS")
if params['narrator_enabled'].lower() == "silent" and params['text_not_inside'].lower() == "silent":
print_message("Both Narrator & Text-not-inside are set to silent. If you get no TTS, this is why.", "warning", "GEN")
if model_engine.lowvram_enabled and model_engine.device == "cpu":
await model_engine.handle_lowvram_change()
model_engine.tts_narrator_generatingtts = True
print_message("Processing with narrator mode", "debug_tts", "GEN")
processed_parts = process_text(text_input)
audio_files = []
for part_type, part in processed_parts:
if len(part.strip()) <= int(config.api_def.api_length_stripping):
continue
voice_to_use = tts_get_voice_for_part(part_type, params, part)
if not voice_to_use:
continue
output_file = await tts_generate_part(part, voice_to_use, params)
if output_file:
await tts_apply_rvc(output_file, part_type, params)
audio_files.append(output_file)
model_engine.tts_narrator_generatingtts = False
if audio_files:
print_message("\033[92mNarrated TTS generation complete\033[0m", component="GEN")
return await tts_finalize_output(audio_files, params)
print_message("\033[92mNarrated TTS generation complete\033[0m", component="GEN")
return await tts_finalize_output(audio_files, params)
def tts_get_voice_for_part(part_type: str, params: dict, part: str) -> Optional[str]:
"""Determine appropriate voice for text part."""
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'],
params['temperature'], params['repetition_penalty'],
params['speed'], params['pitch'],
output_file[0], False
)
return output_file[0]
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,
output_file, infer_pipeline)
elif part_type == 'narrator':
process_rvc_narrator(part_type, params['rvcnarrator_voice_gen'],
params['rvcnarrator_pitch'],
config.rvc_settings.rvc_narr_model_file,
output_file, infer_pipeline)
else:
if params['text_not_inside'] == 'character':
process_rvc_narrator('character', params['rvccharacter_voice_gen'],
params['rvccharacter_pitch'],
config.rvc_settings.rvc_char_model_file,
output_file, infer_pipeline)
elif params['text_not_inside'] == 'narrator':
process_rvc_narrator('narrator', params['rvcnarrator_voice_gen'],
params['rvcnarrator_pitch'],
config.rvc_settings.rvc_narr_model_file,
output_file, infer_pipeline)
async def tts_process_standard_mode(params: dict, text_input: str) -> Union[StreamingResponse, Tuple[Path, str, str]]:
"""Process text with standard mode, without narrator functionality."""
debug_func_entry()
print_message("Standard generation mode", "debug_tts", "GEN")
output_file_path, output_file_url, output_cache_url = await tts_handle_output_paths( # 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(
cleaned_text, params['character_voice_gen'], params['language'],
params['temperature'], params['repetition_penalty'],
params['speed'], params['pitch'],
output_file_path, True
)
return StreamingResponse(stream, media_type="audio/wav")
await generate_audio(
cleaned_text, params['character_voice_gen'], params['language'],
params['temperature'], params['repetition_penalty'],
params['speed'], params['pitch'],
output_file_path, False
)
if config.rvc_settings.rvc_enabled:
await tts_handle_rvc_processing(output_file_path, params)
return await tts_handle_audio_output(output_file_path, params['autoplay'], params['autoplay_volume'])
except Exception as e:
print_message(f"Error in standard processing: {str(e)}", "error", "GEN")
raise
async def tts_handle_rvc_processing(output_file_path: Path, params: dict):
"""Handle RVC voice conversion if enabled."""
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
run_rvc(output_file_path, pth_path, params['rvccharacter_pitch'], infer_pipeline)
async def tts_handle_audio_output(output_file_path: Path, autoplay: bool, volume: float) -> Tuple[Path, str, str]:
"""Handle audio transcoding and playback."""
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 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 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
def detect_language(text: str) -> str:
"""
Detect the language of the given text and apply a fallback for unsupported languages.
:param text: Text to analyze.
:return: A supported language code (e.g., 'en', 'fr').
"""
try:
# Detect the language of the text
detected_lang = detect(text)
print_message(f"Detected language: {detected_lang}",
message_type="debug_tts")
# Use the fallback language if the detected one is unsupported
fallback_lang = LANG_FALLBACKS.get(detected_lang, "en") # Default fallback: French
if detected_lang != fallback_lang:
print_message(f"Language '{detected_lang}' not supported, using fallback '{fallback_lang}'", message_type="debug_tts")
return fallback_lang
except LangDetectException as e:
# Handle errors in language detection
print_message(f"Language detection error: {str(e)}", message_type="debug_tts")
raise ValueError("Could not detect language")
@app.post("/api/tts-generate", response_class=JSONResponse)
async def apifunction_generate_tts_standard(
text_input: str = Form(...),
text_filtering: str = Form(None),
character_voice_gen: str = Form(None),
rvccharacter_voice_gen: str = Form(None),
rvccharacter_pitch: float = Form(None),
narrator_enabled: str = Form(None),
narrator_voice_gen: str = Form(None),
rvcnarrator_voice_gen: str = Form(None),
rvcnarrator_pitch: float = Form(None),
text_not_inside: str = Form(None),
language: str = Form(None),
output_file_name: str = Form(None),
output_file_timestamp: bool = Form(None),
autoplay: bool = Form(None),
autoplay_volume: float = Form(None),
streaming: bool = Form(False),
speed: float = Form(None),
temperature: float = Form(None),
repetition_penalty: float = Form(None),
pitch: float = Form(None),
_text_filtering: str = Depends(get_api_text_filtering),
_character_voice_gen: str = Depends(get_character_voice_gen),
_rvccharacter_voice_gen: str = Depends(get_rvccharacter_voice_gen),
_rvccharacter_pitch: str = Depends(get_rvccharacter_pitch),
_narrator_enabled: str = Depends(get_api_narrator_enabled),
_narrator_voice_gen: str = Depends(get_narrator_voice_gen),
_rvcnarrator_voice_gen: str = Depends(get_rvcnarrator_voice_gen),
_rvcnarrator_pitch: str = Depends(get_rvcnarrator_pitch),
_text_not_inside: str = Depends(get_api_text_not_inside),
_language: str = Depends(get_api_language),
_output_file_name: str = Depends(get_api_output_file_name),
_output_file_timestamp: bool = Depends(get_api_output_file_timestamp),
_autoplay: bool = Depends(get_api_autoplay),
_autoplay_volume: float = Depends(get_api_autoplay_volume),
_speed: float = Depends(get_params_speed),
_temperature: float = Depends(get_params_temperature),
_repetition_penalty: float = Depends(get_params_repetition),
_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 = {
"text_filtering": text_filtering,
"character_voice_gen": character_voice_gen,
"rvccharacter_voice_gen": rvccharacter_voice_gen,
"rvccharacter_pitch": rvccharacter_pitch,
"narrator_enabled": narrator_enabled,
"narrator_voice_gen": narrator_voice_gen,
"rvcnarrator_voice_gen": rvcnarrator_voice_gen,
"rvcnarrator_pitch": rvcnarrator_pitch,
"text_not_inside": text_not_inside,
"language": language,
"output_file_name": output_file_name,
"output_file_timestamp": output_file_timestamp,
"autoplay": autoplay,
"autoplay_volume": autoplay_volume,
"streaming": streaming,
"speed": speed,
"temperature": temperature,
"repetition_penalty": repetition_penalty,
"pitch": pitch
}
default_params = {
"text_filtering": _text_filtering,
"character_voice_gen": _character_voice_gen,
"rvccharacter_voice_gen": _rvccharacter_voice_gen,
"rvccharacter_pitch": _rvccharacter_pitch,
"narrator_enabled": _narrator_enabled,
"narrator_voice_gen": _narrator_voice_gen,
"rvcnarrator_voice_gen": _rvcnarrator_voice_gen,
"rvcnarrator_pitch": _rvcnarrator_pitch,
"text_not_inside": _text_not_inside,
"language": _language,
"output_file_name": _output_file_name,
"output_file_timestamp": _output_file_timestamp,
"autoplay": _autoplay,
"autoplay_volume": _autoplay_volume,
"speed": _speed,
"temperature": _temperature,
"repetition_penalty": _repetition_penalty,
"pitch": _pitch
}
# Validate and prepare input
params, validation_error = await tts_validate_and_prepare_input(
text_input, request_params, default_params
)
if validation_error:
return JSONResponse(content={"error": validation_error}, status_code=400)
# Process based on mode
if params["narrator_enabled"].lower() in ["true", "silent"]:
try:
output_file_path, output_file_url, output_cache_url = await tts_process_narrator_mode(
params, text_input
)
finally:
# Ensure model returns to CPU if using low VRAM
if model_engine.lowvram_enabled and model_engine.device == "cuda" and model_engine.lowvram_capable:
await model_engine.handle_lowvram_change()
else:
output_file_path, output_file_url, output_cache_url = await tts_process_standard_mode(
params, text_input
)
print_message(f"Final output_file_path is: {output_file_path}", "debug_tts", "GEN")
print_message(f"Final output_file_url is : {output_file_url}", "debug_tts", "GEN")
print_message(f"Final output_cache_url is: {output_cache_url}", "debug_tts", "GEN")
if isinstance(output_file_path, StreamingResponse):
return output_file_path
return JSONResponse(
content={
"status": "generate-success",
"output_file_path": str(output_file_path),
"output_file_url": str(output_file_url),
"output_cache_url": str(output_cache_url)
},
status_code=200
)
except 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 ####
#############################
# Mount the static files from the 'word_addin' directory
app.mount("/api/word_addin", StaticFiles(directory=os.path.join(this_dir / 'system' / 'word_addin')), name="word_addin")
#############################################
#### TTS Generator Comparision Endpoints ####
#############################################
class TTSItem(BaseModel):
"""Individual TTS item structure for saving."""
id: int
fileUrl: str
text: str
characterVoice: str
language: str
class TTSData(BaseModel):
"""Collection of TTS items."""
ttsList: List[TTSItem]
@app.post("/api/save-tts-data")
async def apifunction_save_tts_data(tts_data: List[TTSItem]):
"""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.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") from e
########################################
# Trigger TTS Gen Text/Speech Analysis #
########################################
@app.get("/api/trigger-analysis")
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}%, using Whisper model: {whisper_model}", "debug_tts", "GEN")
# Run analysis script
result = subprocess.run(
[
sys.executable,
"tts_diff.py",
f"--threshold={threshold}",
f"--ttslistpath={ttslist_path}",
f"--wavfilespath={wavfile_path}",
f"--whisper-model={whisper_model}"
],
cwd=this_dir / "system" / "tts_diff",
env=env,
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", encoding="utf-8") as summary_file:
summary_data = json.load(summary_file)
print_message("Analysis summary loaded successfully", "debug_tts", "GEN")
except FileNotFoundError:
print_message("Analysis summary file not found", "error", "GEN")
summary_data = {"error": "Analysis summary file not found."}
except json.JSONDecodeError as e:
print_message(f"Error parsing analysis summary: {str(e)}", "error", "GEN")
summary_data = {"error": "Invalid analysis summary format."}
return {"message": "Analysis Completed", "summary": summary_data}
except Exception as e:
print_message(f"Error during analysis: {str(e)}", "error", "GEN")
raise HTTPException(status_code=500, detail=str(e)) from e
###########################################
# TTS Generator SRT Subtitiles generation #
###########################################
@app.get("/api/srt-generation")
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(
[
sys.executable,
"tts_srt.py",
f"--ttslistpath={ttslist_path}",
f"--wavfilespath={wavfile_path}"
],
cwd=this_dir / "system" / "tts_srt",
env=env,
capture_output=True,
text=True,
check=True
)
# Validate subprocess result
if result.returncode != 0:
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)) from e
#################################
# Static Mount for file serving #
#################################
app.mount("/static", StaticFiles(directory=str(this_dir / "system")), name="static")
########################################
# Legacy JSON update settings function #
########################################
# Setup logging
logging.basicConfig(level=logging.DEBUG)
@app.get("/settings")
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")
return templates.TemplateResponse("admin.html", {
"request": request,
"data": settings_data
})
except Exception as e:
print_message(f"Error rendering settings page: {str(e)}", "error", "API")
raise HTTPException(status_code=500, detail="Failed to load settings page") 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") from e
@app.post("/update-settings")
async def update_settings(
request: Request,
delete_output_wavs: str = Form(...),
gradio_interface: str = Form(...),
gradio_port_number: int = Form(...),
api_port_number: int = Form(...),
):
"""Update configuration settings from admin form submission."""
debug_func_entry()
try:
print_message("Updating configuration settings", "debug_api", "API")
# Log current values before update
if config.debugging.debug_api:
print_message("Current settings:", "debug_api", "API")
print_message(f"delete_output_wavs: {config.delete_output_wavs}", "debug_api", "API")
print_message(f"gradio_interface: {config.gradio_interface}", "debug_api", "API")
print_message(f"gradio_port: {config.gradio_port_number}", "debug_api", "API")
print_message(f"api_port: {config.api_def.api_port_number}", "debug_api", "API")
# Validate port numbers
if not (1024 <= gradio_port_number <= 65535 and 1024 <= api_port_number <= 65535):
print_message("Invalid port numbers provided", "error", "API")
raise HTTPException(status_code=400, detail="Port numbers must be between 1024 and 65535")
# Update configuration
config.delete_output_wavs = delete_output_wavs
config.gradio_interface = gradio_interface.lower() == 'true'
config.gradio_port_number = gradio_port_number
config.api_def.api_port_number = api_port_number
# Log new values after update
if config.debugging.debug_api:
print_message("Updated settings:", "debug_api", "API")
print_message(f"delete_output_wavs: {config.delete_output_wavs}", "debug_api", "API")
print_message(f"gradio_interface: {config.gradio_interface}", "debug_api", "API")
print_message(f"gradio_port: {config.gradio_port_number}", "debug_api", "API")
print_message(f"api_port: {config.api_def.api_port_number}", "debug_api", "API")
# Save configuration
config.save()
print_message("Settings saved successfully", "debug_api", "API")
# Return updated settings page
return templates.TemplateResponse("admin.html", {
"request": request,
"data": config.to_dict()
})
except ValueError as e:
print_message(f"Validation error in settings update: {str(e)}", "error", "API")
raise HTTPException(status_code=400, detail=str(e)) 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") 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 ####
###################################################
@app.get("/")
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") from e
# Start Uvicorn Webserver
# port_parameter = int(config.api_def.api_port_number)
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")
args = parser.parse_args()
# Determine the port to use
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") # pylint: disable=assignment-from-no-return
if __name__ == "__main__":
start_server()