diff --git a/tts_server.py b/tts_server.py index ebaefd0..92f07ed 100644 --- a/tts_server.py +++ b/tts_server.py @@ -22,6 +22,7 @@ from fastapi.templating import Jinja2Templates from fastapi.staticfiles import StaticFiles from jinja2 import Template from contextlib import asynccontextmanager +from pydantic import field_validator ########################### #### STARTUP VARIABLES #### @@ -617,6 +618,218 @@ async def get_audio(filename: str): audio_path = this_dir / "outputs" / filename return FileResponse(audio_path) +######################## +#### GENERATION API #### +######################## +import html +import re +import numpy as np +import soundfile as sf +from typing import Union, Dict +from pydantic import BaseModel, ValidationError, Field + +class Request(BaseModel): + # Define the structure of the 'Request' class if needed + pass + +class JSONInput(BaseModel): + text_input: str = Field(..., max_length=1000, description="text_input needs to be 1000 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="^.*\.wav$", description="character_voice_gen needs to be the name of a wav file e.g. mysample.wav.") + narrator_enabled: bool = Field(..., description="narrator_enabled needs to be true or false.") + narrator_voice_gen: str = Field(..., pattern="^.*\.wav$", description="narrator_voice_gen needs to be the name of a wav file e.g. mysample.wav.") + text_not_inside: str = Field(..., pattern="^(character|narrator)$", description="text_not_inside needs to be 'character' or 'narrator'.") + language: str = Field(..., pattern="^(ar|zh-cn|cs|nl|en|fr|de|hu|it|ja|ko|pl|pt|ru|es|tr)$", description="language needs to be one of the following ar|zh-cn|cs|nl|en|fr|de|hu|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") + + @field_validator("autoplay_volume") + @classmethod + def validate_autoplay_volume(cls, value): + if not (0.1 <= value <= 1.0): + raise ValidationError("Autoplay volume must be between 0.1 and 1.0") + return value + + +class TTSGenerator: + @staticmethod + def validate_json_input(json_data: Union[Dict, str]) -> Union[None, str]: + try: + if isinstance(json_data, str): + json_data = json.loads(json_data) + JSONInput(**json_data) + return None # JSON is valid + except ValidationError as e: + return str(e) + +def standard_filtering(text_input): + text_output = (text_input + .replace("***", "") + .replace("**", "") + .replace("*", "") + .replace("\n\n", "\n") + .replace("'", "'") + ) + return text_output + +def narrator_filtering(narrator_text_input): + processed_string = re.sub(r'\.\*\n\*', '. ', narrator_text_input) + processed_string = ( + processed_string + .replace("\n", " ") + .replace('"', '"<') + .replace('"', '"<') + ) + processed_string = processed_string.replace('"<. *', '"< *"') + processed_string = processed_string.replace('< *"', '< *') + processed_string = processed_string.replace('. *', '< *') + text_output = html.unescape(processed_string) + return text_output + +def combine(output_file_timestamp, output_file_name, audio_files): + audio = np.array([]) + sample_rate = None + try: + for audio_file in audio_files: + audio_data, current_sample_rate = sf.read(audio_file) + if audio.size == 0: + audio = audio_data + sample_rate = current_sample_rate + elif sample_rate == current_sample_rate: + audio = np.concatenate((audio, audio_data)) + else: + raise ValueError("Sample rates of input files are not consistent.") + except Exception as e: + # Handle exceptions (e.g., file not found, invalid audio format) + return None, None + if output_file_timestamp: + timestamp = int(time.time()) + output_file_path = os.path.join(this_dir / "outputs" / f'{output_file_name}_{timestamp}_combined.wav') + output_file_url = f'http://{params["ip_address"]}:{params["port_number"]}/audio/{output_file_name}_{timestamp}_combined.wav' + else: + output_file_path = os.path.join(this_dir / "outputs" / f'{output_file_name}_combined.wav') + output_file_url = f'http://{params["ip_address"]}:{params["port_number"]}/audio/{output_file_name}.wav' + try: + sf.write(output_file_path, audio, samplerate=sample_rate) + # Clean up unnecessary files + for audio_file in audio_files: + os.remove(audio_file) + except Exception as e: + # Handle exceptions (e.g., failed to write output file) + return None, None + return output_file_path, output_file_url + +# Generation API (separate from text-generation-webui) +@app.post("/api/tts-generate", response_class=JSONResponse) +async def tts_generate( + text_input: str = Form(...), + text_filtering: str = Form(...), + character_voice_gen: str = Form(...), + narrator_enabled: bool = Form(...), + narrator_voice_gen: str = Form(...), + text_not_inside: str = Form(...), + language: str = Form(...), + output_file_name: str = Form(...), + output_file_timestamp: bool = Form(...), + autoplay: bool = Form(...), + autoplay_volume: float = Form(...), +): + try: + #print(f"text_filtering: {text_filtering}") + #print(f"narrator_enabled: {narrator_enabled}") + #print(f"text_not_inside: {text_not_inside}") + #print(f"output_file_timestamp: {output_file_timestamp}") + json_input_data = { + "text_input": text_input, + "text_filtering": text_filtering, + "character_voice_gen": character_voice_gen, + "narrator_enabled": narrator_enabled, + "narrator_voice_gen": narrator_voice_gen, + "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, + } + JSONresult = TTSGenerator.validate_json_input(json_input_data) + if JSONresult is None: + pass + else: + return JSONResponse(content={"error": JSONresult}, status_code=400) + if narrator_enabled: + print("ORIGINAL UNTOUCHED STRING IS:", text_input,"\n") + if text_filtering in ["standard", "none"]: + cleaned_string = ( + text_input + .replace('"', '"<') + .replace('*', '<*') + ) + print("STANDARD FILTERING IS NOW:", cleaned_string,"\n") + #parts = re.split(r'\.(?

JSON calls & CURL Commands

-

Generating TTS - Linux

-

curl -X POST -H "Content-Type: application/json" -d '{"text": "This is text to generate as TTS","voice": "female_01.wav", "language": "en", "output_file": "outputfile.wav"}' "http://127.0.0.1:7851/api/generate"

-

Generating TTS - Windows

-

curl -X POST -H "Content-Type: application/json" -d "{\"text\": \"This is text to generate as TTS\", \"voice\": \"female_01.wav\", \"language\": \"en\", \"output_file\": \"outputfile.wav\"}" http://127.0.0.1:7851/api/generate

-

Generating TTS - Notes

-

Replace This is text to generate as TTS with whatever you want it to say. female_01.wav with the voice sample you want to use. output_file.wav with the file name you want it to create.

-

JSON return {"status":"generate-success","data":{"audio_path":"outputfile.wav"}}

+ +

Overview

+

The Text-to-Speech (TTS) Generation API allows you to generate speech from text input using various configuration options. This API supports both character and narrator voices, providing flexibility for creating dynamic and engaging audio content.

+

TTS Generation Endpoint

+ +

Example command line

+

Standard TTS speech Example (standard text) generating a time-stamped file

+

curl -X POST "http://127.0.0.1:7851/api/tts-generate" -d "text_input=All of this is text spoken by the character. This is text not inside quotes, though that doesnt matter in the slightest" -d "text_filtering=standard" -d "character_voice_gen=female_01.wav" -d "narrator_enabled=false" -d "narrator_voice_gen=male_01.wav" -d "text_not_inside=character" -d "language=en" -d "output_file_name=myoutputfile" -d "output_file_timestamp=true" -d "autoplay=true" -d "autoplay_volume=0.8"

+

Narrator Example (standard text) generating a time-stamped file

+

curl -X POST "http://127.0.0.1:7851/api/tts-generate" -d "text_input=*This is text spoken by the narrator* \"This is text spoken by the character\". This is text not inside quotes." -d "text_filtering=standard" -d "character_voice_gen=female_01.wav" -d "narrator_enabled=true" -d "narrator_voice_gen=male_01.wav" -d "text_not_inside=character" -d "language=en" -d "output_file_name=myoutputfile" -d "output_file_timestamp=true" -d "autoplay=true" -d "autoplay_volume=0.8"

+

Note that if your text that needs to be generated contains double quotes you will need to escape them with \" (Please see the narrator example).

+

 Request Parameters

+ +

TTS Generation Response

+

The API returns a JSON object with the following properties:

+ +

Example JSON TTS Generation Response:

+

{"status": "generate-success", "output_file_path": "C:\\text-generation-webui\\extensions\\alltalk_tts\\outputs\\myoutputfile_1703149973.wav", "output_file_url": "http://127.0.0.1:7851/audio/myoutputfile_1703149973.wav"}

+

Switching Model

curl -X POST "http://127.0.0.1:7851/api/reload?tts_method=API%20Local"
curl -X POST "http://127.0.0.1:7851/api/reload?tts_method=API%20TTS"
curl -X POST "http://127.0.0.1:7851/api/reload?tts_method=XTTSv2%20Local"

Switch between the 3 models respectively.

JSON return {"status": "model-success"}

Switch DeepSpeed

-

curl -X POST -H "Content-Type: application/json" "http://127.0.0.1:7851/api/deepspeed?new_deepspeed_value=True"

+

curl -X POST "http://127.0.0.1:7851/api/deepspeed?new_deepspeed_value=True"

Replace True with False to disable DeepSpeed mode.

JSON return {"status": "deepspeed-success"}

Switching Low VRAM

-

curl -X POST -H "Content-Type: application/json" "http://127.0.0.1:7851/api/lowvramsetting?new_low_vram_value=True"

+

curl -X POST "http://127.0.0.1:7851/api/lowvramsetting?new_low_vram_value=True"

Replace True with False to disable Low VRAM mode.

JSON return {"status": "lowvram-success"}

Back to top of page