diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..6132bf2
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+ko_fi: erew123
diff --git a/README.md b/README.md
index aa312d7..8683d12 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ AllTalk is an updated version of the Coqui_tts extension for Text Generation web
- **API Suite and 3rd Party support via JSON calls:** Can be used with 3rd party applications via JSON calls.
- **SillyTavern integration:** Full integration with SillyTavern. [Screenshot](https://github.com/erew123/screenshots/raw/main/sillytavern.jpg)
-## Index
+### Index
- 🟩 [Installation](https://github.com/erew123/alltalk_tts/#-quick-setup-text-generation-webui--standalone-installation)
- 🟪 [Updating & problems with updating](https://github.com/erew123/alltalk_tts?#-updating)
@@ -27,18 +27,25 @@ AllTalk is an updated version of the Coqui_tts extension for Text Generation web
- 🟠 [API Suite and JSON-CURL](https://github.com/erew123/alltalk_tts?#-api-suite-and-json-curl)
- 🔴 [Future to-do list & Upcoming updates](https://github.com/erew123/alltalk_tts?#-future-to-do-list)
-## 🔄 Updates & Bug fixes
+### 🔄 Feature requests, Updates & Bug fixes
Please check the below link to find a list of all recent updates and changes.
#### 🔄 **Updates list & bug fixes list** can be found [here](https://github.com/erew123/alltalk_tts/issues/25)
+#### 🔄 **Current Feature request list** can be found [here](https://github.com/erew123/alltalk_tts/discussions/74)
-## 🟩 Quick Setup (Text-generation-webui & Standalone Installation)
-For Windows 10/11 and Linux machines there is a quick setup script. Click to expand the correct section below:
+I welcome your input and ideas for new features, suggestions, and improvements. Feel free to share your thoughts and collaborate in the discussions area. If you find this project valuable and would like to show your appreciation, you can make a donation on my [Ko-fi](https://ko-fi.com/erew123) page. Your support goes a long way in ensuring that I can continue to deliver even better features and experiences.
+
+**ERROR** `ImportError: cannot import name 'SampleOutput' from 'transformers.generation.utils'` please see this issue [here](https://github.com/erew123/alltalk_tts/issues/82)
+
+### 🟩 Quick Setup (Text-generation-webui & Standalone Installation)
+For Windows 10/11 and Linux machines there is a quick setup script. Please note, Python on Windows requires you install the C++ development [tools](https://wiki.python.org/moin/WindowsCompilers) to compile packages, further details can be found in the help section.
+
+Click to expand the correct section below:
QUICK SETUP - Text-Generation-webui
If you wish to see this as a video, please go [here](https://www.youtube.com/watch?v=icn2XS5rUH8)
1) To download the AllTalk setup you can either:
- - A) On this page select **CODE** > **Download ZIP** then extract it to the text-generation-webui extensions folder e.g. `\text-generation-webui\extensions\alltalk_tts\`
+ - A) Go to the [Releases page](https://github.com/erew123/alltalk_tts/releases) and download the latest `alltalk_tts.zip` then extract it to the text-generation-webui extensions folder e.g. `\text-generation-webui\extensions\alltalk_tts\`.
- B) Go to a terminal/console, move into the `\text-generation-webui\extensions\` folder and `git clone https://github.com/erew123/alltalk_tts`
3) In a terminal/command prompt, in the text-generation-webui folder you will start its Python environment with either `cmd_windows.bat` or `./cmd_linux.sh`
4) Move into the AllTalk folder e.g. `cd extensions` then `cd alltalk_tts`
@@ -56,7 +63,7 @@ For Windows 10/11 and Linux machines there is a quick setup script. Click to exp
If you wish to see this as a video, please go [here](https://www.youtube.com/watch?v=AQYCccDRbaY)
1) To download the AllTalk setup you can either:
- - A) On this page select **CODE** > **Download ZIP** then extract it to the folder of your choice e.g. `C:\myfiles\alltalk_tts\`
+ - A) Go to the [Releases page](https://github.com/erew123/alltalk_tts/releases) and download the latest `alltalk_tts.zip` and extract it to the folder of your choice e.g. `C:\myfiles\alltalk_tts\`.
- B) Go to a terminal/console, move into the folder of your choice e.g `C:\myfiles\` folder and `git clone https://github.com/erew123/alltalk_tts`
4) In a terminal/command prompt, move into the AllTalk folder e.g. `cd alltalk_tts`
5) Start the AllTalk setup script `atsetup.bat` or `./atsetup.sh`
@@ -66,10 +73,12 @@ For Windows 10/11 and Linux machines there is a quick setup script. Click to exp
Please read the `🟩 Other installation notes` (also additional voices are available there).
+ You cannot have a dash in your folder path e.g. `c:\myfiles\alltalk_tts-main` so please ensure you remove any `-` from your folder path. This is a conda specific requirement and will cause AllTalk not to start at all.
+
Finetuning has other installation requirements so please read any instructions in the setup utility and refer back here to this page for detailed instructions.
-## 🟩 Manual Installation - As part of Text generation web UI
+### 🟩 Manual Installation - As part of Text generation web UI
On Mac's or if you wish to perform a manual installation. Click to expand the correct section below:
MANUAL INSTALLATION - Text-Generation-webui
@@ -109,7 +118,7 @@ This has been tested on the current Dec 2023 release of Text generation webUI. I
-## 🟩 Manual Installation - As a Standalone Application
+### 🟩 Manual Installation - As a Standalone Application
On Mac's or if you wish to perform a manual installation. Click to expand the correct section below:
MANUAL INSTALLATION - I want to run AllTalk as a standalone when installed with Text-generation-webui
@@ -160,7 +169,7 @@ Deepspeed and other such things can be installed. Please read the relevant instr
#### 🟩 Other installation notes
-On first startup, AllTalk will download the Coqui XTTSv2 2.0.2 model to its **models** folder (1.8GB space required). Check the command prompt/terminal window if you want to know what its doing. After it says "Model Loaded" the Text generation webUI is usually available on its IP address a few seconds later, for you to connect to in your browser.
+On first startup, AllTalk will download the Coqui XTTSv2 2.0.2 model to its **models** folder (1.8GB space required). Check the command prompt/terminal window if you want to know what its doing. After it says "Model Loaded" the Text generation webUI is usually available on its IP address a few seconds later, for you to connect to in your browser. If you are running a headless system and need to change the IP, please see the Help with problems section down below.
Once the extension is loaded, please find all documentation and settings on the link provided in the interface (as shown in the screenshot below).
@@ -179,7 +188,7 @@ Most narrator/character systems switch voices upon encountering an asterisk or q
With improvements to the splitter/processor, I'm confident it's functioning well. You can monitor what AllTalk identifies as Narrator lines on the command line and adjust its behavior if needed (Text Not Inside - Function).
-## 🟪 Updating
+### 🟪 Updating
UPDATING - I am using Text-Generation-webui
This is pretty much a repeat of the installation process.
@@ -258,12 +267,12 @@ You can now start text-generation-webui or AllTalk (standalone) and it should st
Assuming its all working fine and you are happy, you can delete the old alltalk_tts.old folder.
-## 🟫 Screenshots
+### 🟫 Screenshots
| |  |  | |
|:---:|:---:|:---:|:---:|
| |  |  ||
-## 🟨 Help with problems
+### 🟨 Help with problems
#### 🔄 **Minor updates/bug fixes list** can be found [here](https://github.com/erew123/alltalk_tts/issues/25)
@@ -296,7 +305,7 @@ If you are on a Windows machine or a Linux machine, you should be able to use th
**A)** Its trying to load the voice model into your graphics card VRAM (assuming you have a Nvidia Graphics card, otherwise its your system RAM)
**B)** Its trying to start up the mini-webserver and send the "ready" signal back to the main process.
-Before giving other possibilities a go, some people with **old machines** are finding their startup times are **very** slow 2-3 minutes. Ive extended the allowed time within the script from 1 minute to 2 minutes. **If you have an older machine** and wish to try extending this further, you can do so by editing `script.py` and changing line 251 `timeout = 120` changing the timeout to a larger value e.g `timeout = 240` (4 minutes).
+Before giving other possibilities a go, some people with **old machines** are finding their startup times are **very** slow 2-3 minutes. Ive extended the allowed time within the script from 1 minute to 2 minutes. **If you have an older machine** and wish to try extending this further, you can do so by editing `script.py` and changing `startup_wait_time = 120` (120 seconds, aka 2 minutes) at the top of the script.py file, to a larger value e.g `startup_wait_time = 240` (240 seconds aka 4 minutes).
**Note:** If you need to create a support ticket, please create a `diagnostics.log` report file to submit with a support request. Details on doing this are above.
@@ -325,6 +334,21 @@ Other possibilities for this issue are:
11) You have built yourself a custom Python environment and something is funky with it. This is very hard to diagnose as its not a standard environment. You may want to updating text-generation-webui and re installing its requirements file (whichever one you use that comes down with text-generation-webui).
+#### 🟨 Windows & Python requirements for compiling packages
+
+`ERROR: Microsoft Visual C++ 14.0 or greater is required` or `ERROR: Could not build wheels for TTS.`
+
+
+ Click to expand
+
+ Python requires that you install C++ development tools on Windows. This is detailed on the [Python site here](https://wiki.python.org/moin/WindowsCompilers). You would need to install `MSVCv142 - VS 2019 C++ x64/x86 build tools` and `Windows 10/11 SDK` from the C++ Build tools section.
+
+ You can get hold of the **Community** edition [here](https://visualstudio.microsoft.com/downloads/) the during installation, selecting `C++ Build tools` and then `MSVCv142 - VS 2019 C++ x64/x86 build tools` and `Windows 10/11 SDK`.
+
+
+
+
+
#### 🟨 I think AllTalks requirements file has installed something another extension doesn't like
Click to expand
@@ -347,6 +371,15 @@ You can either follow the [Problems Updating](https://github.com/erew123/alltalk
If you want to use DeepSpeed, you need an Nvidia Graphics card and to install DeepSpeed on your system. Instructions are [here](https://github.com/erew123/alltalk_tts#-deepspeed-installation-options)
+#### 🟨 I am running a Headless system and need to change the IP Address manually as I cannot reach the config page.
+
+ Click to expand
+
+To do this you can edit the `confignew.json` file within the `alltalk_tts` folder. You would look for `"ip_address": "127.0.0.1",` and change the `127.0.0.1` to your chosen IP address,then save the file and start AllTalk.
+
+When doing this, be careful not to impact the formatting of the JSON file. Worst case, you can re-download a fresh copy of `confignew.json` from this website and that will put you back to a factory setting.
+
+
#### 🟨 I am having problems updating/some other issue where it wont start up/Im sure this is a bug
Click to expand
@@ -356,6 +389,14 @@ Please see [Problems Updating](https://github.com/erew123/alltalk_tts#-problems-
Also, is your text-generation-webui up to date? [instructions here](https://github.com/oobabooga/text-generation-webui?tab=readme-ov-file#how-to-install)
+#### 🟨 Standalone Install - start_{youros}.xx opens and closes instantly and AllTalk doesnt start
+
+ Click to expand
+
+This is more than likely caused by having a `-` in your folder path e.g. `c:\myfiles\alltalk_tts-main`. In this circumstance you would be best renaming the folder to remove the `-` from its name e.g. `c:\myfiles\alltalk_tts`, delete the `alltalk_environment` folder and `start_alltalk.bat` or `start_alltalk.sh` and then re-run `atsetup` to re-create the environment and startup files.
+
+
+
#### 🟨 I am having problems getting AllTalk to start after changing settings or making a custom setup/model setup.
@@ -391,7 +432,7 @@ Depending on which of your Nvidia GPU's is the more powerful one, you can change
-## ⚫ Finetuning a model
+### ⚫ Finetuning a model
If you have a voice that the model doesnt quite reproduce correctly, or indeed you just want to improve the reproduced voice, then finetuning is a way to train your "XTTSv2 local" model **(stored in `/alltalk_tts/models/xxxxx/`)** on a specific voice. For this you will need:
- An Nvidia graphics card. (Please see this [note](https://github.com/erew123/alltalk_tts#-i-have-multiple-gpus-and-i-have-problems-running-finetuning) if you have multiple Nvidia GPU's).
@@ -406,6 +447,14 @@ Everything has been done to make this as simple as possible. At its simplest, yo
#### ⚫ The audio you will use
I would suggest that if its in an interview format, you cut out the interviewer speaking in audacity or your chosen audio editing package. You dont have to worry about being perfect with your cuts, the finetuning Step 1 will go and find spoken audio and cut it out for you. Is there is music over the spoken parts, for best quality you would cut out those parts, though its not 100% necessary. As always, try to avoid bad quality audio with noises in it (humming sounds, hiss etc). You can try something like [Audioenhancer](https://audioenhancer.ai/) to try clean up noisier audio. There is no need to down-sample any of the audio, all of that is handled for you. Just give the finetuning some good quality audio to work with.
+#### ⚫ A note about anonymous training Telemetry information & disabling it
+Portions of Coqui's TTS trainer scripts gather anonymous training information which you can disable. Their statement on this is listed [here](https://github.com/coqui-ai/Trainer?tab=readme-ov-file#anonymized-telemetry). Although I have tried to pass `TRAINER_TELEMETRY=0` through AllTalk, it appears you will have to set this manually if you wish to finetune on a non-internet enabled computer or disable the anonymous data being sent. You can do this by:
+
+- On Windows by typing `set TRAINER_TELEMETRY=0`
+- On Linux & Mac by typing `export TRAINER_TELEMETRY=0`
+
+Before you start `finetune.py`. You will now be able to finetune offline and no anonymous training data will be sent.
+
#### ⚫ Important requirements CUDA 11.8
As mentioned you must have a small portion of the Nvidia CUDA Toolkit **11.8** installed. Not higher or lower versions. Specifically **11.8**. You do not have to uninstall any other versions, change any graphics drivers, reinstall torch or anything like that. This requirement is for Step1 of Finetuning. To keep the download+install as small as possible, you will need to:
- Download the **xxx (network)** install of the Nvidia CUDA Toolkit 11.8 from [here](https://developer.nvidia.com/cuda-11-8-0-download-archive)
@@ -455,6 +504,19 @@ As mentioned you must have a small portion of the Nvidia CUDA Toolkit **11.8** i
11) Follow the on-screen instructions when the web interface starts up.
12) When you have finished finetuning, the final tab will tell you what to do with your files and how to move your newly trained model to the correct location on disk.
+#### ⚫ Evaluation Data Percentage
+In the process of finetuning, it's crucial to balance the data used for training the model against the data reserved for evaluating its performance. Typically, a portion of the dataset is set aside as an 'evaluation set' to assess the model's capabilities in dealing with unseen data. On Step 1 of finetuning you have the option to adjust this evaluation data percentage, offering more control over your model training process.
+**Why Adjust the Evaluation Percentage?**
+Adjusting the evaluation percentage **can** be beneficial in scenarios with limited voice samples. When dealing with a smaller dataset, allocating a slightly larger portion to training could enhance the model's ability to learn from these scarce samples. Conversely, with abundant data, a higher evaluation percentage might be more appropriate to rigorously test the model's performance. There are currently no absolutely optimal split percentages as it varies by dataset.
+- **Default Setting:** The default evaluation percentage is set at 15%, which is a balanced choice for most datasets.
+- **Adjustable Range:** Users can now adjust this percentage, but it’s generally recommend keeping it between 5% and 30%.
+ - **Lower Bound:** A minimum of 5% ensures that there's enough data to evaluate model performance.
+ - **Upper Bound:** Its suggested not exceeding 30% for evaluation to avoid limiting the amount of data available for training.
+
+- **Understanding the Impact:** Before adjusting this setting, it's important to understand its impact on model training and evaluation. Incorrect adjustments can lead to suboptimal model performance.
+- **Gradual Adjustments:** For those unfamiliar with the process, we recommend reading up on training data and training sets, then making small, incremental changes and observing their effects.
+- **Data Quality:** Regardless of the split, the quality of the audio data is paramount. Ensure that your datasets are built from good quality audio with enough data within them.
+
#### ⚫ Using a Finetuned model in Text-generation-webui
At the end of the finetune process, you will have an option to `Compact and move model to /trainedmodel/` this will compact the raw training file and move it to `/model/trainedmodel/`. When AllTalk starts up within Text-generation-webui, if it finds a model in this location a new loader will appear in the interface for `XTTSv2 FT` and you can use this to load your finetuned model.
**Be careful** not to train a new model from the base model, then overwrite your current `/model/trainedmodel/` **if** you want a seperately trained model. This is why there is an `OPTION B` to move your just trained model to `/models/lastfinetuned/`.
@@ -467,7 +529,7 @@ At the end of the finetune process, you will have an option to `Compact and move
If you've compacted and moved your model, its highly unlikely you would want to keep that data, however the choice is there to keep it if you wish. It will be between 5-10GB in size, so most people will want to delete it.
-## 🔵🟢🟡 DeepSpeed Installation Options
+### 🔵🟢🟡 DeepSpeed Installation Options
**NOTE**: You **DO NOT** need to set Text-generation-webUI's **--deepspeed** setting for AllTalk to be able to use DeepSpeed. These are two completely separate things and incorrectly setting that on Text-generation-webUI may cause other complications.
#### 🔵 Linux Installation
@@ -645,7 +707,7 @@ def get_shm_size():
-## ⬜ AllTalk TTS Generator
+### ⬜ AllTalk TTS Generator
AllTalk TTS Generator is the solution for converting large volumes of text into speech using the voice of your choice. Whether you're creating audio content or just want to hear text read aloud, the TTS Generator is equipped to handle it all efficiently. Please see here for a quick [demo](https://www.youtube.com/watch?v=hunvXn0mLzc)
The link to open the TTS generator can be found on the built-in Settings and Documentation page.
**DeepSpeed** is **highly** recommended to speed up generation. **Low VRAM** would be best turned off and your LLM model unloaded from your GPU VRAM (unload your model). **No Playback** will reduce memory overhead on very large generations (15,000 words or more). Splitting **Export to Wav** into smaller groups will also reduce memory overhead at the point of exporting your wav files (so good for low memory systems).
#### ⬜ Estimated Throughput
@@ -682,7 +744,7 @@ For generating **large amounts of TTS**, it's recommended to select the **No Pla
- Generated audio can be played back from the list, which also highlights the currently playing chunk.
- The TTS Generator remembers your settings, so you can pick up where you left off even after refreshing the page.
-## 🟠 API Suite and JSON-CURL
+### 🟠 API Suite and JSON-CURL
### 🟠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.
@@ -765,6 +827,7 @@ Generate a preview of a specified voice with hardcoded settings.
JSON return `{"status": "lowvram-success"}`
### 🟠 TTS Generation Endpoint (Standard Generation)
+Streaming endpoint details are further down the page.
- URL: `http://127.0.0.1:7851/api/tts-generate` - Method: `POST` - Content-Type: `application/x-www-form-urlencoded`
@@ -872,7 +935,7 @@ Example JSON TTS Generation Response:
`{"status":"generate-success","output_file_path":"C:\\text-generation-webui\\extensions\\alltalk_tts\\outputs\\myoutputfile_1704141936.wav","output_file_url":"http://127.0.0.1:7851/audio/myoutputfile_1704141936.wav","output_cache_url":"http://127.0.0.1:7851/audiocache/myoutputfile_1704141936.wav"}`
-### 🟠 Example command lines (Streaming Generation)
+### 🟠 TTS Generation Endpoint (Streaming Generation)
Streaming TTS generation does NOT support Narration and will generate an audio stream. Streaming TTS speech JavaScript Example:
- URL: `http://localhost:7851/api/tts-generate-streaming` - Method: `POST` - Content-Type: `application/x-www-form-urlencoded`
@@ -898,6 +961,6 @@ audioElement.play(); // Play the audio stream directly
### 🔴 Future to-do list
+- I am maintaining a list of things people request [here](https://github.com/erew123/alltalk_tts/discussions/74)
- Possibly add some additional TTS engines (TBD).
-- Tagging support within text.
- Have a break!
diff --git a/atsetup.bat b/atsetup.bat
index b996754..2bc07f0 100644
--- a/atsetup.bat
+++ b/atsetup.bat
@@ -341,6 +341,7 @@ if "%UserChoice%" == "1" (
pip install fsspec>=2023.6.0
pip install soundfile==0.12.1
pip install uvicorn==0.24.0.post1
+ pip install transformers==4.36.2
pip install TTS==0.21.3
pip install fastapi==0.104.1
pip install Jinja2==3.1.2
@@ -351,6 +352,8 @@ if "%UserChoice%" == "1" (
pip install pydantic==1.10.13
pip install sounddevice==0.4.6
pip install python-multipart==0.0.6
+ pip install cutlet>=0.3.0
+ pip install unidic-lite>=1.0.8
echo Downloading DeepSpeed...
curl -LO https://github.com/erew123/alltalk_tts/releases/download/DeepSpeed-12.7/deepspeed-0.12.7+d058d4b-cp311-cp311-win_amd64.whl
echo Installing DeepSpeed...
@@ -367,7 +370,6 @@ echo cd /D "%~dp0" >> start_environment.bat
echo set CONDA_ROOT_PREFIX=%cd%\alltalk_environment\conda >> start_environment.bat
echo set INSTALL_ENV_DIR=%cd%\alltalk_environment\env >> start_environment.bat
echo call "%CONDA_ROOT_PREFIX%\condabin\conda.bat" activate "%INSTALL_ENV_DIR%" >> start_environment.bat
-echo exit >> start_environment.bat
@rem Create start_alltalk.bat to run AllTalk
echo @echo off > start_alltalk.bat
echo cd /D "%~dp0" >> start_alltalk.bat
@@ -375,7 +377,6 @@ echo set CONDA_ROOT_PREFIX=%cd%\alltalk_environment\conda >> start_alltalk.bat
echo set INSTALL_ENV_DIR=%cd%\alltalk_environment\env >> start_alltalk.bat
echo call "%CONDA_ROOT_PREFIX%\condabin\conda.bat" activate "%INSTALL_ENV_DIR%" >> start_alltalk.bat
echo call python script.py >> start_alltalk.bat
-echo exit >> start_alltalk.bat
Echo.
Echo start_alltalk.bat has been created.
Echo You can now start AllTalk with %L_YELLOW%start_alltalk.bat%RESET%
diff --git a/atsetup.sh b/atsetup.sh
index 3700b82..7969a62 100644
--- a/atsetup.sh
+++ b/atsetup.sh
@@ -301,6 +301,7 @@ install_custom_standalone() {
pip install fsspec>=2023.6.0
pip install soundfile==0.12.1
pip install uvicorn==0.24.0.post1
+ pip install transformers==4.36.2
pip install TTS==0.21.3
pip install fastapi==0.104.1
pip install Jinja2==3.1.2
@@ -311,6 +312,8 @@ install_custom_standalone() {
pip install pydantic==1.10.13
pip install sounddevice==0.4.6
pip install python-multipart==0.0.6
+ pip install cutlet>=0.3.0
+ pip install unidic-lite>=1.0.8
else
pip install -r requirements_other.txt
fi
diff --git a/finetune.py b/finetune.py
index 5583137..f35fc43 100644
--- a/finetune.py
+++ b/finetune.py
@@ -75,10 +75,10 @@ def create_temporary_file(folder, suffix=".wav"):
unique_filename = f"custom_tempfile_{int(time.time())}_{random.randint(1, 1000)}{suffix}"
return os.path.join(folder, unique_filename)
-def format_audio_list(target_language, whisper_model, out_path, gradio_progress=progress):
+def format_audio_list(target_language, whisper_model, out_path, eval_split_number, gradio_progress=progress):
audio_files = [os.path.join(audio_folder, file) for file in os.listdir(audio_folder) if file.endswith(('.mp3', '.flac', '.wav'))]
buffer=0.2
- eval_percentage=0.15
+ eval_percentage = eval_split_number / 100.0
speaker_name="coqui"
audio_total_size = 0
os.makedirs(out_path, exist_ok=True)
@@ -87,6 +87,7 @@ def format_audio_list(target_language, whisper_model, out_path, gradio_progress=
print("[FINETUNE] \033[94mPart of AllTalk\033[0m https://github.com/erew123/alltalk_tts/")
print("[FINETUNE] \033[94mCoqui Public Model License\033[0m")
print("[FINETUNE] \033[94mhttps://coqui.ai/cpml.txt\033[0m")
+ print(f"[FINETUNE] \033[94mWhisper model: \033[92m{whisper_model} \033[94mLanguage: \033[92m{target_language} \033[94mEvaluation data percentage: \033[92m{eval_split_number}%\033[0m")
print("[FINETUNE] \033[94mStarting Step 1\033[0m - Preparing Audio/Generating the dataset")
# Write the target language to lang.txt in the output directory
lang_file_path = os.path.join(out_path, "lang.txt")
@@ -968,6 +969,14 @@ if __name__ == "__main__":
"ja"
],
)
+
+ eval_split_number = gr.Number(
+ label="Evaluation data Split (the % to use for Evaluation data)",
+ value=15, # Default value
+ minimum=5, # Minimum value
+ maximum=95, # Maximum value
+ step=1, # Increment step
+ )
progress_data = gr.Label(
label="Progress:"
)
@@ -979,14 +988,14 @@ if __name__ == "__main__":
prompt_compute_btn = gr.Button(value="Step 1 - Create dataset")
- def preprocess_dataset(language, whisper_model, out_path, progress=gr.Progress(track_tqdm=True)):
+ def preprocess_dataset(language, whisper_model, out_path, eval_split_number, progress=gr.Progress(track_tqdm=True)):
clear_gpu_cache()
test_for_audio_files = [file for file in os.listdir(audio_folder) if any(file.lower().endswith(ext) for ext in ['.wav', '.mp3', '.flac'])]
if not test_for_audio_files:
return "I cannot find any mp3, wav or flac files in the folder called 'put-voice-samples-in-here'", "", ""
else:
try:
- train_meta, eval_meta, audio_total_size = format_audio_list(target_language=language, whisper_model=whisper_model ,out_path=out_path, gradio_progress=progress)
+ train_meta, eval_meta, audio_total_size = format_audio_list(target_language=language, whisper_model=whisper_model, out_path=out_path, eval_split_number=eval_split_number, gradio_progress=progress)
except:
traceback.print_exc()
error = traceback.format_exc()
@@ -1274,6 +1283,7 @@ if __name__ == "__main__":
lang,
whisper_model,
out_path,
+ eval_split_number,
],
outputs=[
progress_data,
diff --git a/requirements_nvidia.txt b/requirements_nvidia.txt
index b8d22e9..31903f5 100644
--- a/requirements_nvidia.txt
+++ b/requirements_nvidia.txt
@@ -1,6 +1,7 @@
numpy>=1.22.0
soundfile>=0.12.1
uvicorn>=0.24.0.post1
+transformers==4.36.2
TTS>=0.21.3
torch>=2.1.0+cu118
torchaudio>=2.1.0+cu118
@@ -13,3 +14,5 @@ packaging>=23.2
pydantic>=1.10.13
sounddevice>=0.4.6
python-multipart>=0.0.6
+cutlet>=0.3.0
+unidic-lite>=1.0.8
diff --git a/requirements_other.txt b/requirements_other.txt
index 6566ced..1f00475 100644
--- a/requirements_other.txt
+++ b/requirements_other.txt
@@ -1,6 +1,7 @@
numpy>=1.22.0
soundfile>=0.12.1
uvicorn>=0.24.0.post1
+transformers==4.36.2
TTS>=0.21.3
torch>=2.1.0
torchaudio>=2.1.0
@@ -13,3 +14,5 @@ packaging>=23.2
pydantic>=1.10.13
sounddevice>=0.4.6
python-multipart>=0.0.6
+cutlet>=0.3.0
+unidic-lite>=1.0.8
diff --git a/script.py b/script.py
index 677dd2d..74f23f5 100644
--- a/script.py
+++ b/script.py
@@ -16,6 +16,14 @@ import numpy as np
import soundfile as sf
import uuid
+######################################
+#### ALLTALK ALLOWED STARTUP TIME ####
+######################################
+startup_wait_time = 120
+
+# You can change the above setting to a larger number to allow AllTAlk more time to start up. The default setting is 120 seconds (2 minutes).
+# On some older systems you may need to allow AllTalk more time. So you could set it to 240 (4 minutes) which will give AllTalk more to load.
+
#################################################################
#### LOAD PARAMS FROM confignew.json - REQUIRED FOR BRANDING ####
#################################################################
@@ -239,7 +247,7 @@ if process.poll() is None:
print(f"[{params['branding']}Startup] TTS Subprocess starting")
print(f"[{params['branding']}Startup]")
print(
- f"[{params['branding']}Startup] \033[94mSettings & Documentation:\033[00m",
+ f"[{params['branding']}Startup] \033[94m {params['branding']}Settings & Documentation:\033[00m",
f"\033[92mhttp://{params['ip_address']}:{params['port_number']}\033[00m",
)
print(f"[{params['branding']}Startup]")
@@ -266,7 +274,7 @@ else:
# Cleanly kill off this script, but allow text-generation-webui to keep running, albeit without this alltalk_tts
sys.exit(1)
-timeout = 120 # Adjust the timeout as needed
+timeout = startup_wait_time # Gather timeout setting from startup_wait_time
# Introduce a delay before starting the check loop
time.sleep(26) # Wait 26 secs before checking if the tts_server.py has started up.
@@ -279,13 +287,15 @@ while time.time() - start_time < timeout:
except requests.RequestException as e:
# Print the exception for debugging purposes
print(
- f"[{params['branding']}Startup] \033[91mWarning\033[0m TTS Subprocess has NOT started up yet, Will keep trying for 120 seconds maximum. Please wait."
+ f"[{params['branding']}Startup] \033[91mWarning\033[0m TTS Subprocess has NOT started up yet, Will keep trying for {timeout} seconds maximum. Please wait."
)
- time.sleep(4)
+ time.sleep(5)
else:
- print(
- f"[{params['branding']}Startup] Startup timed out. Check the server logs for more information."
- )
+ print(f"\n[{params['branding']}Startup] Startup timed out. Full help available here \033[92mhttps://github.com/erew123/alltalk_tts#-help-with-problems\033[0m")
+ print(f"[{params['branding']}Startup] On older system you may wish to open and edit \033[94mscript.py\033[0m with a text editor and changing the")
+ print(f"[{params['branding']}Startup] \033[94mstartup_wait_time = 120\033[0m setting to something like \033[94mstartup_wait_time = 240\033[0m as this will allow")
+ print(f"[{params['branding']}Startup] AllTalk more time to try load the model into your VRAM. Otherise please visit the Github for")
+ print(f"[{params['branding']}Startup] a list of other possible troubleshooting options.")
# Cleanly kill off this script, but allow text-generation-webui to keep running, albeit without this alltalk_tts
sys.exit(1)
@@ -489,12 +499,14 @@ def voice_preview(string):
output_file = Path(params["output_folder_wav"]) / "voice_preview.wav"
# Generate the audio
language_code = languages.get(params["language"])
+ temperature = params["local_temperature"]
+ repetition_penalty = params["local_repetition_penalty"]
# Convert the WindowsPath object to a string before using it in JSON payload
output_file_str = output_file.as_posix()
# Lock before making the generate request
with process_lock:
generate_response = send_generate_request(
- string, params["voice"], language_code, output_file_str
+ string, params["voice"], language_code, temperature, repetition_penalty, output_file_str
)
# Check if lock is already acquired
if process_lock.locked():
@@ -573,6 +585,8 @@ def output_modifier(string, state):
return
string = cleaned_string
language_code = languages.get(params["language"])
+ temperature = params["local_temperature"]
+ repetition_penalty = params["local_repetition_penalty"]
# Create a list to store generated audio paths
audio_files = []
if process_lock.acquire(blocking=False):
@@ -609,7 +623,7 @@ def output_modifier(string, state):
# Generate TTS and output to a file
output_filename = get_output_filename(state)
generate_response = send_generate_request(
- cleaned_part, voice_to_use, language_code, output_filename
+ cleaned_part, voice_to_use, language_code, temperature, repetition_penalty, output_filename
)
audio_path = generate_response.get("data", {}).get("audio_path")
audio_files_all_paragraphs.append(audio_path)
@@ -635,7 +649,7 @@ def output_modifier(string, state):
output_file_str = output_file.as_posix()
output_file = get_output_filename(state)
generate_response = send_generate_request(
- cleaned_part, params["voice"], language_code, output_file_str
+ cleaned_part, params["voice"], language_code, temperature, repetition_penalty, output_file_str
)
audio_path = generate_response.get("data", {}).get("audio_path")
final_output_file = audio_path
@@ -684,12 +698,14 @@ def get_output_filename(state):
###############################################
#### SEND GENERATION REQUEST TO TTS ENGINE ####
###############################################
-def send_generate_request(text, voice, language, output_file):
+def send_generate_request(text, voice, language, temperature, repetition_penalty, output_file):
url = f"{base_url}/api/generate"
payload = {
"text": text,
"voice": voice,
"language": language,
+ "temperature": temperature,
+ "repetition_penalty": repetition_penalty,
"output_file": output_file,
}
headers = {"Content-Type": "application/json"}
@@ -737,11 +753,13 @@ def input_modifier(string, state):
def ui():
with gr.Accordion(params["branding"] + " TTS (XTTSv2)"):
+ # Activate alltalk_tts, Enable autoplay, Show text
with gr.Row():
activate = gr.Checkbox(value=params['activate'], label='Enable TTS')
autoplay = gr.Checkbox(value=params['autoplay'], label='Autoplay TTS')
show_text = gr.Checkbox(value=params['show_text'], label='Show Text')
-
+
+ # Low vram enable, Deepspeed enable, Remove trailing dots
with gr.Row():
low_vram = gr.Checkbox(value=params['low_vram'], label='Enable Low VRAM Mode')
low_vram_play = gr.HTML(visible=False)
@@ -749,6 +767,7 @@ def ui():
deepspeed_checkbox_play = gr.HTML(visible=False)
remove_trailing_dots = gr.Checkbox(value=params['remove_trailing_dots'], label='Remove trailing "."')
+ # TTS method, Character voice selection
with gr.Row():
model_loader_choices=["API TTS", "API Local", "XTTSv2 Local"]
if finetuned_model:
@@ -769,17 +788,19 @@ def ui():
voice = gr.Dropdown(available_voices, label="Character Voice", value=default_voice, allow_custom_value=True, )
create_refresh_button(voice, lambda: None, lambda: {"choices": get_available_voices(), "value": params["voice"],}, "refresh-button",)
+ # Language, Narrator voice
with gr.Row():
language = gr.Dropdown(languages.keys(), label="Language", value=params["language"])
with gr.Row():
narrator_voice_gr = gr.Dropdown(get_available_voices(), label="Narrator Voice", allow_custom_value=True, value=params["narrator_voice"])
create_refresh_button(narrator_voice_gr, lambda: None, lambda: {"choices": get_available_voices(), "value": params["narrator_voice"],}, "refresh-button")
- # Temperature and Repetition Penalty (Not yet parsed to api/implemented)
- # with gr.Row():
- # local_temperature_gr = gr.Slider(minimum=0, maximum=1, step=0.05, label="Temperature", value=params["local_temperature"])
- # local_repetition_penalty_gr = gr.Slider(minimum=0, maximum=20, step=0.5, label="Repetition Penalty", value=params["local_repetition_penalty"])
+ # Temperature, Repetition Penalty
+ with gr.Row():
+ local_temperature_gr = gr.Slider(minimum=0.05, maximum=1, step=0.05, label="Temperature", value=params["local_temperature"])
+ local_repetition_penalty_gr = gr.Slider(minimum=0.5, maximum=20, step=0.5, label="Repetition Penalty", value=params["local_repetition_penalty"])
+ # Narrator enable, Non quoted text, Explanation text
with gr.Row():
with gr.Row():
narrator_enabled_gr = gr.Radio(choices={"Enabled": "true", "Disabled": "false"}, label="Narrator", value="Enabled" if params.get("narrator_enabled") else "Disabled")
@@ -787,6 +808,7 @@ def ui():
explanation_text = gr.HTML(
f"
⚙️ Settings and Documentation Page⚙️ - Low VRAM Mode and Deepspeed take 15 seconds to be enabled or disabled. - The DeepSpeed checkbox is only visible if DeepSpeed is present.
")
+ # Preview speech
with gr.Row():
preview_text = gr.Text(
show_label=False,
@@ -795,7 +817,7 @@ def ui():
)
preview_play = gr.Button("Preview")
preview_audio = gr.HTML(visible=False)
-
+
with gr.Row():
convert = gr.Button("Permanently replace audios with the message texts")
convert_cancel = gr.Button("Cancel", visible=False)
@@ -864,10 +886,9 @@ def ui():
voice.change(lambda x: params.update({"voice": x}), voice, None)
language.change(lambda x: params.update({"language": x}), language, None)
-
- # TS Settings (Not yet parsed to api/implemented)
- # local_temperature_gr.change(lambda x: params.update({"local_temperature": x}), local_temperature_gr, None)
- # local_repetition_penalty_gr.change(lambda x: params.update({"local_repetition_penalty": x}), local_repetition_penalty_gr, None)
+ # TSS Settings
+ local_temperature_gr.change(lambda x: params.update({"local_temperature": x}), local_temperature_gr, None)
+ local_repetition_penalty_gr.change(lambda x: params.update({"local_repetition_penalty": x}), local_repetition_penalty_gr, None)
# Narrator selection actions
narrator_enabled_gr.change(update_narrator_enabled, narrator_enabled_gr, None)
@@ -888,4 +909,4 @@ if running_in_standalone:
try:
time.sleep(1) # Add a small delay to avoid high CPU usage
except KeyboardInterrupt:
- break # Allow graceful exit on Ctrl+C
\ No newline at end of file
+ break # Allow graceful exit on Ctrl+C
diff --git a/templates/STfiles/alltalk.js b/templates/STfiles/alltalk.js
index e44b312..f70daf5 100644
--- a/templates/STfiles/alltalk.js
+++ b/templates/STfiles/alltalk.js
@@ -3,92 +3,33 @@ import { saveTtsProviderSettings } from './index.js';
export { AllTalkTtsProvider };
-const css = `
-.settings-row {
- display: flex;
- justify-content: space-between; /* Distribute space evenly between the children */
- align-items: center;
- width: 100%; /* Full width to spread out the children */
-}
-
-.settings-option {
- flex: 1; /* Each child will take equal space */
- margin: 0 10px; /* Adjust spacing as needed */
-}
-
-.endpoint-option {
- flex: 1; /* Each child will take equal space */
- margin: 0 10px; /* Adjust spacing as needed */
- margin-right: 25px;
- width: 38%; /* Full width to spread out the children */
-}
-
-.website-row {
- display: flex;
- justify-content: start; /* Aligns items to the start of the row */
- align-items: center;
- margin-top: 10px; /* Top margin */
- margin-bottom: 10px; /* Bottom margin */
-}
-
-.website-option {
- flex: 1; /* Allows each option to take equal space */
- margin-right: 10px; /* Spacing between the two options, adjust as needed */
- margin-left: 10px; /* Adjust this value to align as desired */
- /* Remove the individual margin top and bottom declarations if they are set globally on the .settings-row */
-}
-
-.settings-separator {
- margin-top: 10px;
- margin-bottom: 10px;
- padding: 18x;
- font-weight: bold;
- border-top: 1px solid #e1e1e1; /* Grey line */
- border-bottom: 1px solid #e1e1e1; /* Grey line */
- text-align: center;
-}
-
-.status-message {
- flex: 1; /* Adjust as needed */
- margin: 0 10px;
- /* Additional styling */
-}
-
-.model-endpoint-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- width: 100%;
-}
-
-.model-option, .endpoint-option {
- flex: 1;
- margin: 0 10px;
- margin-left: 10px; /* Adjust this value to align as desired */
- /* Adjust width as needed to fit in one line */
-}
-
-.endpoint-option {
- /* Adjust width to align with model dropdown */
- width: 38%;
-}
-
-#status_info {
- color: lightgreen; /* Or any specific shade of green you prefer */
-}
-`;
-
-const style = document.createElement('style');
-style.type = 'text/css';
-style.appendChild(document.createTextNode(css));
-document.head.appendChild(style);
-
class AllTalkTtsProvider {
//########//
// Config //
//########//
- settings;
+ settings = {};
+ constructor() {
+ // Initialize with default settings if they are not already set
+ this.settings = {
+ provider_endpoint: this.settings.provider_endpoint || 'http://localhost:7851',
+ language: this.settings.language || 'en',
+ voiceMap: this.settings.voiceMap || {},
+ at_generation_method: this.settings.at_generation_method || 'standard_generation',
+ narrator_enabled: this.settings.narrator_enabled || 'false',
+ at_narrator_text_not_inside: this.settings.at_narrator_text_not_inside || 'narrator',
+ narrator_voice_gen: this.settings.narrator_voice_gen || 'female_01.wav',
+ finetuned_model: this.settings.finetuned_model || 'false'
+ };
+ // Separate property for dynamically updated settings from the server
+ this.dynamicSettings = {
+ modelsAvailable: [],
+ currentModel: '',
+ deepspeed_available: false,
+ deepSpeedEnabled: false,
+ lowVramEnabled: false,
+ };
+ }
ready = false;
voices = [];
separator = '. ';
@@ -114,23 +55,12 @@ class AllTalkTtsProvider {
'Hindi': 'hi',
};
- Settings = {
- provider_endpoint: 'http://localhost:7851',
- language: 'en',
- voiceMap: {},
- at_generation_method: 'standard_generation',
- narrator_enabled: 'false',
- at_narrator_text_not_inside: 'narrator',
- narrator_voice_gen: 'female_01.wav',
- finetuned_model: 'false'
- };
-
get settingsHtml() {
- let html = `
async loadSettings(settings) {
updateStatus('Offline');
- // Use default settings if no settings are provided
- this.settings = Object.keys(settings).length === 0 ? this.Settings : settings;
+ if (Object.keys(settings).length === 0) {
+ console.info('Using default AllTalk TTS Provider settings');
+ } else {
+ // Populate settings with provided values, ignoring server-provided settings
+ for (const key in settings) {
+ if (key in this.settings) {
+ this.settings[key] = settings[key];
+ } else {
+ console.debug(`Ignoring non-user-configurable setting: ${key}`);
+ }
+ }
+ }
+
+ // Update UI elements to reflect the loaded settings
+ $('#at_server').val(this.settings.provider_endpoint);
+ $('#language_options').val(this.settings.language);
+ //$('#voicemap').val(this.settings.voiceMap);
+ $('#at_generation_method').val(this.settings.at_generation_method);
+ $('#at_narrator_enabled').val(this.settings.narrator_enabled);
+ $('#at_narrator_text_not_inside').val(this.settings.at_narrator_text_not_inside);
+ $('#narrator_voice').val(this.settings.narrator_voice_gen);
+
+ console.debug('AllTalkTTS: Settings loaded');
try {
// Check if TTS provider is ready
await this.checkReady();
+ await this.updateSettingsFromServer(); // Fetch dynamic settings from the TTS server
await this.fetchTtsVoiceObjects(); // Fetch voices only if service is ready
- await this.updateSettingsFromServer();
this.updateNarratorVoicesDropdown();
this.updateLanguageDropdown();
this.setupEventListeners();
@@ -259,29 +208,23 @@ html += `
updateStatus('Ready');
} catch (error) {
console.error("Error loading settings:", error);
- // Set status to Error if there is an issue in loading settings
updateStatus('Offline');
}
}
applySettingsToHTML() {
- // Load AllTalk specific settings first
- const loadedSettings = loadAllTalkSettings();
// Apply loaded settings or use defaults
- this.settings = loadedSettings ? { ...this.Settings, ...loadedSettings } : this.Settings;
const narratorVoiceSelect = document.getElementById('narrator_voice');
const atNarratorSelect = document.getElementById('at_narrator_enabled');
const textNotInsideSelect = document.getElementById('at_narrator_text_not_inside');
const generationMethodSelect = document.getElementById('at_generation_method');
+ this.settings.narrator_voice = this.settings.narrator_voice_gen;
// Apply settings to Narrator Voice dropdown
if (narratorVoiceSelect && this.settings.narrator_voice) {
narratorVoiceSelect.value = this.settings.narrator_voice.replace('.wav', '');
- this.settings.narrator_voice_gen = this.settings.narrator_voice;
- //console.log(this.settings.narrator_voice_gen)
}
// Apply settings to AT Narrator Enabled dropdown
if (atNarratorSelect) {
- //console.log(this.settings.narrator_enabled)
// Sync the state with the checkbox in index.js
const ttsPassAsterisksCheckbox = document.getElementById('tts_pass_asterisks'); // Access the checkbox from index.js
const ttsNarrateQuotedCheckbox = document.getElementById('tts_narrate_quoted'); // Access the checkbox from index.js
@@ -314,18 +257,15 @@ html += `
const languageSelect = document.getElementById('language_options');
if (languageSelect && this.settings.language) {
languageSelect.value = this.settings.language;
- //console.log(this.settings.language)
}
// Apply settings to Text Not Inside dropdown
if (textNotInsideSelect && this.settings.text_not_inside) {
textNotInsideSelect.value = this.settings.text_not_inside;
this.settings.at_narrator_text_not_inside = this.settings.text_not_inside;
- //console.log(this.settings.at_narrator_text_not_inside)
}
// Apply settings to Generation Method dropdown
if (generationMethodSelect && this.settings.at_generation_method) {
generationMethodSelect.value = this.settings.at_generation_method;
- //console.log(this.settings.at_generation_method)
}
// Additional logic to disable/enable dropdowns based on the selected generation method
const isStreamingEnabled = this.settings.at_generation_method === 'streaming_enabled';
@@ -347,7 +287,6 @@ html += `
ftOption.textContent = 'XTTSv2 FT';
modelSelect.appendChild(ftOption);
}
- //console.log("Settings applied to HTML.");
}
//##############################//
@@ -486,7 +425,7 @@ html += `
const languageSelect = document.getElementById('language_options');
if (languageSelect) {
// Ensure default language is set
- this.settings.language = this.Settings.language;
+ this.settings.language = this.settings.language;
languageSelect.innerHTML = '';
for (let language in this.languageLabels) {
@@ -610,7 +549,7 @@ html += `
if (narratorVoiceSelect) {
narratorVoiceSelect.addEventListener('change', (event) => {
this.settings.narrator_voice_gen = `${event.target.value}.wav`;
- this.onSettingsChangeAllTalk(); // Save the settings after change
+ this.onSettingsChange(); // Save the settings after change
});
}
@@ -618,7 +557,7 @@ html += `
if (textNotInsideSelect) {
textNotInsideSelect.addEventListener('change', (event) => {
this.settings.text_not_inside = event.target.value;
- this.onSettingsChangeAllTalk(); // Save the settings after change
+ this.onSettingsChange(); // Save the settings after change
});
}
@@ -656,12 +595,11 @@ html += `
$('#tts_narrate_dialogues').click();
$('#tts_narrate_dialogues').trigger('change');
}
- this.onSettingsChangeAllTalk(); // Save the settings after change
+ this.onSettingsChange(); // Save the settings after change
});
}
-
// Event Listener for AT Generation Method Dropdown
const atGenerationMethodSelect = document.getElementById('at_generation_method');
const atNarratorEnabledSelect = document.getElementById('at_narrator_enabled');
@@ -680,7 +618,7 @@ html += `
atNarratorEnabledSelect.disabled = false;
}
this.settings.at_generation_method = selectedMethod; // Update the setting here
- this.onSettingsChangeAllTalk(); // Save the settings after change
+ this.onSettingsChange(); // Save the settings after change
});
}
@@ -689,9 +627,19 @@ html += `
if (languageSelect) {
languageSelect.addEventListener('change', (event) => {
this.settings.language = event.target.value;
- this.onSettingsChangeAllTalk(); // Save the settings after change
+ this.onSettingsChange(); // Save the settings after change
});
}
+
+ // Listener for AllTalk Endpoint Input
+ const atServerInput = document.getElementById('at_server');
+ if (atServerInput) {
+ atServerInput.addEventListener('input', (event) => {
+ this.settings.provider_endpoint = event.target.value;
+ this.onSettingsChange(); // Save the settings after change
+ });
+ }
+
}
//#############################//
@@ -699,30 +647,16 @@ html += `
//#############################//
onSettingsChange() {
- // Update settings that SillyTavern will save
- this.settings.provider_endpoint = $('#at_server').val();
+ // Update settings based on the UI elements
+ //this.settings.provider_endpoint = $('#at_server').val();
this.settings.language = $('#language_options').val();
- saveTtsProviderSettings(); // This function should save settings handled by SillyTavern
- // Call the function to handle AllTalk specific settings
- this.onSettingsChangeAllTalk(); // Save the settings after change
- }
-
- onSettingsChangeAllTalk() {
- // Update AllTalk specific settings and save to localStorage
- this.settings.narrator_enabled = $('#at_narrator_enabled').val() === 'true';
- this.settings.narrator_voice = $('#narrator_voice').val() + '.wav'; // assuming you need to append .wav
- this.settings.text_not_inside = $('#at_narrator_text_not_inside').val(); // "character" or "narrator"
- this.settings.at_generation_method = $('#at_generation_method').val(); // Streaming or standard
- this.settings.language = $('#language_options').val(); // Streaming or standard
-
- // Call the save function with the current settings
- saveAllTalkSettings({
- narrator_voice: this.settings.narrator_voice,
- narrator_enabled: this.settings.narrator_enabled,
- text_not_inside: this.settings.text_not_inside,
- at_generation_method: this.settings.at_generation_method,
- language: this.settings.language
- });
+ //this.settings.voiceMap = $('#voicemap').val();
+ this.settings.at_generation_method = $('#at_generation_method').val();
+ this.settings.narrator_enabled = $('#at_narrator_enabled').val();
+ this.settings.at_narrator_text_not_inside = $('#at_narrator_text_not_inside').val();
+ this.settings.narrator_voice_gen = $('#narrator_voice').val();
+ // Save the updated settings
+ saveTtsProviderSettings();
}
//#########################//
@@ -802,38 +736,20 @@ html += `
async generateTts(inputText, voiceId) {
try {
if (this.settings.at_generation_method === 'streaming_enabled') {
- // For streaming method
+ // Construct the streaming URL
const streamingUrl = `${this.settings.provider_endpoint}/api/tts-generate-streaming?text=${encodeURIComponent(inputText)}&voice=${encodeURIComponent(voiceId)}.wav&language=${encodeURIComponent(this.settings.language)}&output_file=stream_output.wav`;
- const audioElement = new Audio(streamingUrl);
- audioElement.play(); // Play the audio stream directly
console.log("Streaming URL:", streamingUrl);
- return new Response(null, {
- status: 200,
- statusText: "OK",
- headers: {
- "Content-Type": "audio/wav",
- "Content-Location": streamingUrl
- }
- });
+
+ // Return the streaming URL directly
+ return streamingUrl;
} else {
// For standard method
const outputUrl = await this.fetchTtsGeneration(inputText, voiceId);
- // Fetch the audio data as a blob from the URL
const audioResponse = await fetch(outputUrl);
if (!audioResponse.ok) {
throw new Error(`HTTP ${audioResponse.status}: Failed to fetch audio data`);
}
- const audioBlob = await audioResponse.blob();
- if (!audioBlob.type.startsWith('audio/')) {
- throw new Error(`Invalid audio data format. Expecting audio/*, got ${audioBlob.type}`);
- }
- return new Response(audioBlob, {
- status: 200,
- statusText: "OK",
- headers: {
- "Content-Type": audioBlob.type
- }
- });
+ return audioResponse; // Return the fetch response directly
}
} catch (error) {
console.error("Error in generateTts:", error);
@@ -841,6 +757,7 @@ html += `
}
}
+
//####################//
// Generate Standard //
//####################//
@@ -852,8 +769,8 @@ html += `
if (!response.ok) {
const errorText = await response.text();
console.error(`[fetchTtsGeneration] Error Response Text:`, errorText);
- // Uncomment the following line if you have a UI element for displaying errors
// toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
@@ -912,27 +828,6 @@ function updateStatus(message) {
case 'Error':
statusElement.style.color = 'red';
break;
- // Add more cases as needed
}
}
-}
-
-//########################//
-// Save/load AT Settings //
-//########################//
-
-function saveAllTalkSettings(settingsToSave) {
- // Save the specific settings to localStorage
- console.log("settings", settingsToSave)
- localStorage.setItem('AllTalkSettings', JSON.stringify(settingsToSave));
-}
-
-function loadAllTalkSettings() {
- // Retrieve the settings from localStorage
- const settings = localStorage.getItem('AllTalkSettings');
- // If settings exist, parse them back into an object
- if (settings) {
- return JSON.parse(settings);
- }
- return null;
-}
+}
\ No newline at end of file
diff --git a/templates/STfiles/index.js b/templates/STfiles/index.js
index d07aea6..dd84a22 100644
--- a/templates/STfiles/index.js
+++ b/templates/STfiles/index.js
@@ -1,1028 +1,1046 @@
-import { callPopup, cancelTtsPlay, eventSource, event_types, name2, saveSettingsDebounced } from '../../../script.js';
-import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules } from '../../extensions.js';
-import { delay, escapeRegex, getStringHash } from '../../utils.js';
-import { EdgeTtsProvider } from './edge.js';
-import { ElevenLabsTtsProvider } from './elevenlabs.js';
-import { SileroTtsProvider } from './silerotts.js';
-import { CoquiTtsProvider } from './coqui.js';
-import { SystemTtsProvider } from './system.js';
-import { NovelTtsProvider } from './novel.js';
-import { power_user } from '../../power-user.js';
-import { registerSlashCommand } from '../../slash-commands.js';
-import { OpenAITtsProvider } from './openai.js';
-import { XTTSTtsProvider } from './xtts.js';
-import { AllTalkTtsProvider } from './alltalk.js';
-export { talkingAnimation };
-
-const UPDATE_INTERVAL = 1000;
-
-let voiceMapEntries = [];
-let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
-let storedvalue = false;
-let lastChatId = null;
-let lastMessageHash = null;
-
-const DEFAULT_VOICE_MARKER = '[Default Voice]';
-const DISABLED_VOICE_MARKER = 'disabled';
-
-export function getPreviewString(lang) {
- const previewStrings = {
- 'en-US': 'The quick brown fox jumps over the lazy dog',
- 'en-GB': 'Sphinx of black quartz, judge my vow',
- 'fr-FR': 'Portez ce vieux whisky au juge blond qui fume',
- 'de-DE': 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich',
- 'it-IT': 'Pranzo d\'acqua fa volti sghembi',
- 'es-ES': 'Quiere la boca exhausta vid, kiwi, piña y fugaz jamón',
- 'es-MX': 'Fabio me exige, sin tapujos, que añada cerveza al whisky',
- 'ru-RU': 'В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!',
- 'pt-BR': 'Vejo xá gritando que fez show sem playback.',
- 'pt-PR': 'Todo pajé vulgar faz boquinha sexy com kiwi.',
- 'uk-UA': 'Фабрикуймо гідність, лящім їжею, ґав хапаймо, з\'єднавці чаш!',
- 'pl-PL': 'Pchnąć w tę łódź jeża lub ośm skrzyń fig',
- 'cs-CZ': 'Příliš žluťoučký kůň úpěl ďábelské ódy',
- 'sk-SK': 'Vyhŕňme si rukávy a vyprážajme čínske ryžové cestoviny',
- 'hu-HU': 'Árvíztűrő tükörfúrógép',
- 'tr-TR': 'Pijamalı hasta yağız şoföre çabucak güvendi',
- 'nl-NL': 'De waard heeft een kalfje en een pinkje opgegeten',
- 'sv-SE': 'Yxskaftbud, ge vårbygd, zinkqvarn',
- 'da-DK': 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon',
- 'ja-JP': 'いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす',
- 'ko-KR': '가나다라마바사아자차카타파하',
- 'zh-CN': '我能吞下玻璃而不伤身体',
- 'ro-RO': 'Muzicologă în bej vând whisky și tequila, preț fix',
- 'bg-BG': 'Щъркелите се разпръснаха по цялото небе',
- 'el-GR': 'Ταχίστη αλώπηξ βαφής ψημένη γη, δρασκελίζει υπέρ νωθρού κυνός',
- 'fi-FI': 'Voi veljet, miksi juuri teille myin nämä vehkeet?',
- 'he-IL': 'הקצינים צעקו: "כל הכבוד לצבא הצבאות!"',
- 'id-ID': 'Jangkrik itu memang enak, apalagi kalau digoreng',
- 'ms-MY': 'Muzik penyanyi wanita itu menggambarkan kehidupan yang penuh dengan duka nestapa',
- 'th-TH': 'เป็นไงบ้างครับ ผมชอบกินข้าวผัดกระเพราหมูกรอบ',
- 'vi-VN': 'Cô bé quàng khăn đỏ đang ngồi trên bãi cỏ xanh',
- 'ar-SA': 'أَبْجَدِيَّة عَرَبِيَّة',
- 'hi-IN': 'श्वेता ने श्वेता के श्वेते हाथों में श्वेता का श्वेता चावल पकड़ा',
- };
- const fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet';
-
- return previewStrings[lang] ?? fallbackPreview;
-}
-
-let ttsProviders = {
- ElevenLabs: ElevenLabsTtsProvider,
- Silero: SileroTtsProvider,
- XTTSv2: XTTSTtsProvider,
- System: SystemTtsProvider,
- Coqui: CoquiTtsProvider,
- Edge: EdgeTtsProvider,
- Novel: NovelTtsProvider,
- OpenAI: OpenAITtsProvider,
- AllTalk: AllTalkTtsProvider,
-};
-let ttsProvider;
-let ttsProviderName;
-
-let ttsLastMessage = null;
-
-async function onNarrateOneMessage() {
- audioElement.src = '/sounds/silence.mp3';
- const context = getContext();
- const id = $(this).closest('.mes').attr('mesid');
- const message = context.chat[id];
-
- if (!message) {
- return;
- }
-
- resetTtsPlayback();
- ttsJobQueue.push(message);
- moduleWorker();
-}
-
-async function onNarrateText(args, text) {
- if (!text) {
- return;
- }
-
- audioElement.src = '/sounds/silence.mp3';
-
- // To load all characters in the voice map, set unrestricted to true
- await initVoiceMap(true);
-
- const baseName = args?.voice || name2;
- const name = (baseName === 'SillyTavern System' ? DEFAULT_VOICE_MARKER : baseName) || DEFAULT_VOICE_MARKER;
-
- const voiceMapEntry = voiceMap[name] === DEFAULT_VOICE_MARKER
- ? voiceMap[DEFAULT_VOICE_MARKER]
- : voiceMap[name];
-
- if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) {
- toastr.info(`Specified voice for ${name} was not found. Check the TTS extension settings.`);
- return;
- }
-
- resetTtsPlayback();
- ttsJobQueue.push({ mes: text, name: name });
- await moduleWorker();
-
- // Return back to the chat voices
- await initVoiceMap(false);
-}
-
-async function moduleWorker() {
- // Primarily determining when to add new chat to the TTS queue
- const enabled = $('#tts_enabled').is(':checked');
- $('body').toggleClass('tts', enabled);
- if (!enabled) {
- return;
- }
-
- const context = getContext();
- const chat = context.chat;
-
- processTtsQueue();
- processAudioJobQueue();
- updateUiAudioPlayState();
-
- // Auto generation is disabled
- if (extension_settings.tts.auto_generation == false) {
- return;
- }
-
- // no characters or group selected
- if (!context.groupId && context.characterId === undefined) {
- return;
- }
-
- // Chat changed
- if (
- context.chatId !== lastChatId
- ) {
- currentMessageNumber = context.chat.length ? context.chat.length : 0;
- saveLastValues();
-
- // Force to speak on the first message in the new chat
- if (context.chat.length === 1) {
- lastMessageHash = -1;
- }
-
- return;
- }
-
- // take the count of messages
- let lastMessageNumber = context.chat.length ? context.chat.length : 0;
-
- // There's no new messages
- let diff = lastMessageNumber - currentMessageNumber;
- let hashNew = getStringHash((chat.length && chat[chat.length - 1].mes) ?? '');
-
- // if messages got deleted, diff will be < 0
- if (diff < 0) {
- // necessary actions will be taken by the onChatDeleted() handler
- return;
- }
-
- // if no new messages, or same message, or same message hash, do nothing
- if (diff == 0 && hashNew === lastMessageHash) {
- return;
- }
-
- // If streaming, wait for streaming to finish before processing new messages
- if (context.streamingProcessor && !context.streamingProcessor.isFinished) {
- return;
- }
-
- // clone message object, as things go haywire if message object is altered below (it's passed by reference)
- const message = structuredClone(chat[chat.length - 1]);
-
- // if last message within current message, message got extended. only send diff to TTS.
- if (ttsLastMessage !== null && message.mes.indexOf(ttsLastMessage) !== -1) {
- let tmp = message.mes;
- message.mes = message.mes.replace(ttsLastMessage, '');
- ttsLastMessage = tmp;
- } else {
- ttsLastMessage = message.mes;
- }
-
- // We're currently swiping. Don't generate voice
- if (!message || message.mes === '...' || message.mes === '') {
- return;
- }
-
- // Don't generate if message doesn't have a display text
- if (extension_settings.tts.narrate_translated_only && !(message?.extra?.display_text)) {
- return;
- }
-
- // Don't generate if message is a user message and user message narration is disabled
- if (message.is_user && !extension_settings.tts.narrate_user) {
- return;
- }
-
- // New messages, add new chat to history
- lastMessageHash = hashNew;
- currentMessageNumber = lastMessageNumber;
-
- console.debug(
- `Adding message from ${message.name} for TTS processing: "${message.mes}"`,
- );
- ttsJobQueue.push(message);
-}
-
-function talkingAnimation(switchValue) {
- if (!modules.includes('talkinghead')) {
- console.debug('Talking Animation module not loaded');
- return;
- }
-
- const apiUrl = getApiUrl();
- const animationType = switchValue ? 'start' : 'stop';
-
- if (switchValue !== storedvalue) {
- try {
- console.log(animationType + ' Talking Animation');
- doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`);
- storedvalue = switchValue; // Update the storedvalue to the current switchValue
- } catch (error) {
- // Handle the error here or simply ignore it to prevent logging
- }
- }
- updateUiAudioPlayState();
-}
-
-function resetTtsPlayback() {
- // Stop system TTS utterance
- cancelTtsPlay();
-
- // Clear currently processing jobs
- currentTtsJob = null;
- currentAudioJob = null;
-
- // Reset audio element
- audioElement.currentTime = 0;
- audioElement.src = '';
-
- // Clear any queue items
- ttsJobQueue.splice(0, ttsJobQueue.length);
- audioJobQueue.splice(0, audioJobQueue.length);
-
- // Set audio ready to process again
- audioQueueProcessorReady = true;
-}
-
-function isTtsProcessing() {
- let processing = false;
-
- // Check job queues
- if (ttsJobQueue.length > 0 || audioJobQueue.length > 0) {
- processing = true;
- }
- // Check current jobs
- if (currentTtsJob != null || currentAudioJob != null) {
- processing = true;
- }
- return processing;
-}
-
-function debugTtsPlayback() {
- console.log(JSON.stringify(
- {
- 'ttsProviderName': ttsProviderName,
- 'voiceMap': voiceMap,
- 'currentMessageNumber': currentMessageNumber,
- 'audioPaused': audioPaused,
- 'audioJobQueue': audioJobQueue,
- 'currentAudioJob': currentAudioJob,
- 'audioQueueProcessorReady': audioQueueProcessorReady,
- 'ttsJobQueue': ttsJobQueue,
- 'currentTtsJob': currentTtsJob,
- 'ttsConfig': extension_settings.tts,
- },
- ));
-}
-window.debugTtsPlayback = debugTtsPlayback;
-
-//##################//
-// Audio Control //
-//##################//
-
-let audioElement = new Audio();
-audioElement.id = 'tts_audio';
-audioElement.autoplay = true;
-
-let audioJobQueue = [];
-let currentAudioJob;
-let audioPaused = false;
-let audioQueueProcessorReady = true;
-
-async function playAudioData(audioBlob) {
- // Since current audio job can be cancelled, don't playback if it is null
- if (currentAudioJob == null) {
- console.log('Cancelled TTS playback because currentAudioJob was null');
- }
- const reader = new FileReader();
- reader.onload = function (e) {
- const srcUrl = e.target.result;
- audioElement.src = srcUrl;
- };
- reader.readAsDataURL(audioBlob);
- audioElement.addEventListener('ended', completeCurrentAudioJob);
- audioElement.addEventListener('canplay', () => {
- console.debug('Starting TTS playback');
- audioElement.play();
- });
-}
-
-window['tts_preview'] = function (id) {
- const audio = document.getElementById(id);
-
- if (audio && !$(audio).data('disabled')) {
- audio.play();
- }
- else {
- ttsProvider.previewTtsVoice(id);
- }
-};
-
-async function onTtsVoicesClick() {
- let popupText = '';
-
- try {
- const voiceIds = await ttsProvider.fetchTtsVoiceObjects();
-
- for (const voice of voiceIds) {
- popupText += `
-
- ${voice.lang || ''}
- ${voice.name}
-
-
`;
- if (voice.preview_url) {
- popupText += ``;
- }
- }
- } catch {
- popupText = 'Could not load voices list. Check your API key.';
- }
-
- callPopup(popupText, 'text');
-}
-
-function updateUiAudioPlayState() {
- if (extension_settings.tts.enabled == true) {
- $('#ttsExtensionMenuItem').show();
- let img;
- // Give user feedback that TTS is active by setting the stop icon if processing or playing
- if (!audioElement.paused || isTtsProcessing()) {
- img = 'fa-solid fa-stop-circle extensionsMenuExtensionButton';
- } else {
- img = 'fa-solid fa-circle-play extensionsMenuExtensionButton';
- }
- $('#tts_media_control').attr('class', img);
- } else {
- $('#ttsExtensionMenuItem').hide();
- }
-}
-
-function onAudioControlClicked() {
- audioElement.src = '/sounds/silence.mp3';
- let context = getContext();
- // Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful
- if (!audioElement.paused || isTtsProcessing()) {
- resetTtsPlayback();
- talkingAnimation(false);
- } else {
- // Default play behavior if not processing or playing is to play the last message.
- ttsJobQueue.push(context.chat[context.chat.length - 1]);
- }
- updateUiAudioPlayState();
-}
-
-function addAudioControl() {
-
- $('#extensionsMenu').prepend(`
-
-
- TTS Playback
-
`);
- $('#ttsExtensionMenuItem').attr('title', 'TTS play/pause').on('click', onAudioControlClicked);
- updateUiAudioPlayState();
-}
-
-function completeCurrentAudioJob() {
- audioQueueProcessorReady = true;
- currentAudioJob = null;
- talkingAnimation(false); //stop lip animation
- // updateUiPlayState();
-}
-
-/**
- * Accepts an HTTP response containing audio/mpeg data, and puts the data as a Blob() on the queue for playback
- * @param {Response} response
- */
-async function addAudioJob(response) {
- const audioData = await response.blob();
- if (!audioData.type.startsWith('audio/')) {
- throw `TTS received HTTP response with invalid data format. Expecting audio/*, got ${audioData.type}`;
- }
- audioJobQueue.push(audioData);
- console.debug('Pushed audio job to queue.');
-}
-
-async function processAudioJobQueue() {
- // Nothing to do, audio not completed, or audio paused - stop processing.
- if (audioJobQueue.length == 0 || !audioQueueProcessorReady || audioPaused) {
- return;
- }
- try {
- audioQueueProcessorReady = false;
- currentAudioJob = audioJobQueue.pop();
- playAudioData(currentAudioJob);
- talkingAnimation(true);
- } catch (error) {
- console.error(error);
- audioQueueProcessorReady = true;
- }
-}
-
-//################//
-// TTS Control //
-//################//
-
-let ttsJobQueue = [];
-let currentTtsJob; // Null if nothing is currently being processed
-let currentMessageNumber = 0;
-
-function completeTtsJob() {
- console.info(`Current TTS job for ${currentTtsJob?.name} completed.`);
- currentTtsJob = null;
-}
-
-function saveLastValues() {
- const context = getContext();
- lastChatId = context.chatId;
- lastMessageHash = getStringHash(
- (context.chat.length && context.chat[context.chat.length - 1].mes) ?? '',
- );
-}
-
-async function tts(text, voiceId, char) {
- let response = await ttsProvider.generateTts(text, voiceId);
-
- // RVC injection
- if (extension_settings.rvc.enabled && typeof window['rvcVoiceConversion'] === 'function')
- response = await window['rvcVoiceConversion'](response, char, text);
-
- addAudioJob(response);
- completeTtsJob();
-}
-
-async function processTtsQueue() {
- // Called each moduleWorker iteration to pull chat messages from queue
- if (currentTtsJob || ttsJobQueue.length <= 0 || audioPaused) {
- return;
- }
-
- console.debug('New message found, running TTS');
- currentTtsJob = ttsJobQueue.shift();
- let text = extension_settings.tts.narrate_translated_only ? (currentTtsJob?.extra?.display_text || currentTtsJob.mes) : currentTtsJob.mes;
-
- if (extension_settings.tts.skip_codeblocks) {
- text = text.replace(/^\s{4}.*$/gm, '').trim();
- text = text.replace(/```.*?```/gs, '').trim();
- }
-
- if (!extension_settings.tts.pass_asterisks) {
- text = extension_settings.tts.narrate_dialogues_only
- ? text.replace(/\*[^*]*?(\*|$)/g, '').trim() // remove asterisks content
- : text.replaceAll('*', '').trim(); // remove just the asterisks
- }
-
- if (extension_settings.tts.narrate_quoted_only) {
- const special_quotes = /[“”]/g; // Extend this regex to include other special quotes
- text = text.replace(special_quotes, '"');
- const matches = text.match(/".*?"/g); // Matches text inside double quotes, non-greedily
- const partJoiner = (ttsProvider?.separator || ' ... ');
- text = matches ? matches.join(partJoiner) : text;
- }
-
- if (typeof ttsProvider?.processText === 'function') {
- text = await ttsProvider.processText(text);
- }
-
- // Collapse newlines and spaces into single space
- text = text.replace(/\s+/g, ' ').trim();
-
- console.log(`TTS: ${text}`);
- const char = currentTtsJob.name;
-
- // Remove character name from start of the line if power user setting is disabled
- if (char && !power_user.allow_name2_display) {
- const escapedChar = escapeRegex(char);
- text = text.replace(new RegExp(`^${escapedChar}:`, 'gm'), '');
- }
-
- try {
- if (!text) {
- console.warn('Got empty text in TTS queue job.');
- completeTtsJob();
- return;
- }
-
- const voiceMapEntry = voiceMap[char] === DEFAULT_VOICE_MARKER ? voiceMap[DEFAULT_VOICE_MARKER] : voiceMap[char];
-
- if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) {
- throw `${char} not in voicemap. Configure character in extension settings voice map`;
- }
- const voice = await ttsProvider.getVoice(voiceMapEntry);
- const voiceId = voice.voice_id;
- if (voiceId == null) {
- toastr.error(`Specified voice for ${char} was not found. Check the TTS extension settings.`);
- throw `Unable to attain voiceId for ${char}`;
- }
- tts(text, voiceId, char);
- } catch (error) {
- console.error(error);
- currentTtsJob = null;
- }
-}
-
-// Secret function for now
-async function playFullConversation() {
- const context = getContext();
- const chat = context.chat;
- ttsJobQueue = chat;
-}
-window.playFullConversation = playFullConversation;
-
-//#############################//
-// Extension UI and Settings //
-//#############################//
-
-function loadSettings() {
- if (Object.keys(extension_settings.tts).length === 0) {
- Object.assign(extension_settings.tts, defaultSettings);
- }
- for (const key in defaultSettings) {
- if (!(key in extension_settings.tts)) {
- extension_settings.tts[key] = defaultSettings[key];
- }
- }
- $('#tts_provider').val(extension_settings.tts.currentProvider);
- $('#tts_enabled').prop(
- 'checked',
- extension_settings.tts.enabled,
- );
- $('#tts_narrate_dialogues').prop('checked', extension_settings.tts.narrate_dialogues_only);
- $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
- $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
- $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
- $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
- $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
- $('body').toggleClass('tts', extension_settings.tts.enabled);
-}
-
-const defaultSettings = {
- voiceMap: '',
- ttsEnabled: false,
- currentProvider: 'ElevenLabs',
- auto_generation: true,
- narrate_user: false,
-};
-
-function setTtsStatus(status, success) {
- $('#tts_status').text(status);
- if (success) {
- $('#tts_status').removeAttr('style');
- } else {
- $('#tts_status').css('color', 'red');
- }
-}
-
-function onRefreshClick() {
- Promise.all([
- ttsProvider.onRefreshClick(),
- // updateVoiceMap()
- ]).then(() => {
- extension_settings.tts[ttsProviderName] = ttsProvider.settings;
- saveSettingsDebounced();
- setTtsStatus('Successfully applied settings', true);
- console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`);
- initVoiceMap();
- updateVoiceMap();
- }).catch(error => {
- console.error(error);
- setTtsStatus(error, false);
- });
-}
-
-function onEnableClick() {
- extension_settings.tts.enabled = $('#tts_enabled').is(
- ':checked',
- );
- updateUiAudioPlayState();
- saveSettingsDebounced();
-}
-
-
-function onAutoGenerationClick() {
- extension_settings.tts.auto_generation = !!$('#tts_auto_generation').prop('checked');
- saveSettingsDebounced();
-}
-
-
-function onNarrateDialoguesClick() {
- extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
- saveSettingsDebounced();
-}
-
-function onNarrateUserClick() {
- extension_settings.tts.narrate_user = !!$('#tts_narrate_user').prop('checked');
- saveSettingsDebounced();
-}
-
-function onNarrateQuotedClick() {
- extension_settings.tts.narrate_quoted_only = !!$('#tts_narrate_quoted').prop('checked');
- saveSettingsDebounced();
-}
-
-
-function onNarrateTranslatedOnlyClick() {
- extension_settings.tts.narrate_translated_only = !!$('#tts_narrate_translated_only').prop('checked');
- saveSettingsDebounced();
-}
-
-function onSkipCodeblocksClick() {
- extension_settings.tts.skip_codeblocks = !!$('#tts_skip_codeblocks').prop('checked');
- saveSettingsDebounced();
-}
-
-function onPassAsterisksClick() {
- extension_settings.tts.pass_asterisks = !!$('#tts_pass_asterisks').prop('checked');
- saveSettingsDebounced();
-}
-
-//##############//
-// TTS Provider //
-//##############//
-
-async function loadTtsProvider(provider) {
- //Clear the current config and add new config
- $('#tts_provider_settings').html('');
-
- if (!provider) {
- return;
- }
-
- // Init provider references
- extension_settings.tts.currentProvider = provider;
- ttsProviderName = provider;
- ttsProvider = new ttsProviders[provider];
-
- // Init provider settings
- $('#tts_provider_settings').append(ttsProvider.settingsHtml);
- if (!(ttsProviderName in extension_settings.tts)) {
- console.warn(`Provider ${ttsProviderName} not in Extension Settings, initiatilizing provider in settings`);
- extension_settings.tts[ttsProviderName] = {};
- }
- await ttsProvider.loadSettings(extension_settings.tts[ttsProviderName]);
- await initVoiceMap();
-}
-
-function onTtsProviderChange() {
- const ttsProviderSelection = $('#tts_provider').val();
- extension_settings.tts.currentProvider = ttsProviderSelection;
- loadTtsProvider(ttsProviderSelection);
-}
-
-// Ensure that TTS provider settings are saved to extension settings.
-export function saveTtsProviderSettings() {
- extension_settings.tts[ttsProviderName] = ttsProvider.settings;
- updateVoiceMap();
- saveSettingsDebounced();
- console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`);
-}
-
-
-//###################//
-// voiceMap Handling //
-//###################//
-
-async function onChatChanged() {
- await resetTtsPlayback();
- const voiceMapInit = initVoiceMap();
- await Promise.race([voiceMapInit, delay(1000)]);
- ttsLastMessage = null;
-}
-
-async function onChatDeleted() {
- const context = getContext();
-
- // update internal references to new last message
- lastChatId = context.chatId;
- currentMessageNumber = context.chat.length ? context.chat.length : 0;
-
- // compare against lastMessageHash. If it's the same, we did not delete the last chat item, so no need to reset tts queue
- let messageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1].mes) ?? '');
- if (messageHash === lastMessageHash) {
- return;
- }
- lastMessageHash = messageHash;
- ttsLastMessage = (context.chat.length && context.chat[context.chat.length - 1].mes) ?? '';
-
- // stop any tts playback since message might not exist anymore
- await resetTtsPlayback();
-}
-
-/**
- * Get characters in current chat
- * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat.
- * @returns {string[]} - Array of character names
- */
-function getCharacters(unrestricted) {
- const context = getContext();
-
- if (unrestricted) {
- const names = context.characters.map(char => char.name);
- names.unshift(DEFAULT_VOICE_MARKER);
- return names;
- }
-
- let characters = [];
- if (context.groupId === null) {
- // Single char chat
- characters.push(DEFAULT_VOICE_MARKER);
- characters.push(context.name1);
- characters.push(context.name2);
- } else {
- // Group chat
- characters.push(DEFAULT_VOICE_MARKER);
- characters.push(context.name1);
- const group = context.groups.find(group => context.groupId == group.id);
- for (let member of group.members) {
- // Remove suffix
- if (member.endsWith('.png')) {
- member = member.slice(0, -4);
- }
- characters.push(member);
- }
- }
- return characters;
-}
-
-function sanitizeId(input) {
- // Remove any non-alphanumeric characters except underscore (_) and hyphen (-)
- let sanitized = input.replace(/[^a-zA-Z0-9-_]/g, '');
-
- // Ensure first character is always a letter
- if (!/^[a-zA-Z]/.test(sanitized)) {
- sanitized = 'element_' + sanitized;
- }
-
- return sanitized;
-}
-
-function parseVoiceMap(voiceMapString) {
- let parsedVoiceMap = {};
- for (const [charName, voiceId] of voiceMapString
- .split(',')
- .map(s => s.split(':'))) {
- if (charName && voiceId) {
- parsedVoiceMap[charName.trim()] = voiceId.trim();
- }
- }
- return parsedVoiceMap;
-}
-
-
-
-/**
- * Apply voiceMap based on current voiceMapEntries
- */
-function updateVoiceMap() {
- const tempVoiceMap = {};
- for (const voice of voiceMapEntries) {
- if (voice.voiceId === null) {
- continue;
- }
- tempVoiceMap[voice.name] = voice.voiceId;
- }
- if (Object.keys(tempVoiceMap).length !== 0) {
- voiceMap = tempVoiceMap;
- console.log(`Voicemap updated to ${JSON.stringify(voiceMap)}`);
- }
- if (!extension_settings.tts[ttsProviderName].voiceMap) {
- extension_settings.tts[ttsProviderName].voiceMap = {};
- }
- Object.assign(extension_settings.tts[ttsProviderName].voiceMap, voiceMap);
- saveSettingsDebounced();
-}
-
-class VoiceMapEntry {
- name;
- voiceId;
- selectElement;
- constructor(name, voiceId = DEFAULT_VOICE_MARKER) {
- this.name = name;
- this.voiceId = voiceId;
- this.selectElement = null;
- }
-
- addUI(voiceIds) {
- let sanitizedName = sanitizeId(this.name);
- let defaultOption = this.name === DEFAULT_VOICE_MARKER ?
- `` :
- ``;
- let template = `
-
- ${this.name}
-
-
- `;
- $('#tts_voicemap_block').append(template);
-
- // Populate voice ID select list
- for (const voiceId of voiceIds) {
- const option = document.createElement('option');
- option.innerText = voiceId.name;
- option.value = voiceId.name;
- $(`#tts_voicemap_char_${sanitizedName}_voice`).append(option);
- }
-
- this.selectElement = $(`#tts_voicemap_char_${sanitizedName}_voice`);
- this.selectElement.on('change', args => this.onSelectChange(args));
- this.selectElement.val(this.voiceId);
- }
-
- onSelectChange(args) {
- this.voiceId = this.selectElement.find(':selected').val();
- updateVoiceMap();
- }
-}
-
-/**
- * Init voiceMapEntries for character select list.
- * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat.
- */
-export async function initVoiceMap(unrestricted = false) {
- // Gate initialization if not enabled or TTS Provider not ready. Prevents error popups.
- const enabled = $('#tts_enabled').is(':checked');
- if (!enabled) {
- return;
- }
-
- // Keep errors inside extension UI rather than toastr. Toastr errors for TTS are annoying.
- try {
- await ttsProvider.checkReady();
- } catch (error) {
- const message = `TTS Provider not ready. ${error}`;
- setTtsStatus(message, false);
- return;
- }
-
- setTtsStatus('TTS Provider Loaded', true);
-
- // Clear existing voiceMap state
- $('#tts_voicemap_block').empty();
- voiceMapEntries = [];
-
- // Get characters in current chat
- const characters = getCharacters(unrestricted);
-
- // Get saved voicemap from provider settings, handling new and old representations
- let voiceMapFromSettings = {};
- if ('voiceMap' in extension_settings.tts[ttsProviderName]) {
- // Handle previous representation
- if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'string') {
- voiceMapFromSettings = parseVoiceMap(extension_settings.tts[ttsProviderName].voiceMap);
- // Handle new representation
- } else if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'object') {
- voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap;
- }
- }
-
- // Get voiceIds from provider
- let voiceIdsFromProvider;
- try {
- voiceIdsFromProvider = await ttsProvider.fetchTtsVoiceObjects();
- }
- catch {
- toastr.error('TTS Provider failed to return voice ids.');
- }
-
- // Build UI using VoiceMapEntry objects
- for (const character of characters) {
- if (character === 'SillyTavern System') {
- continue;
- }
- // Check provider settings for voiceIds
- let voiceId;
- if (character in voiceMapFromSettings) {
- voiceId = voiceMapFromSettings[character];
- } else if (character === DEFAULT_VOICE_MARKER) {
- voiceId = DISABLED_VOICE_MARKER;
- } else {
- voiceId = DEFAULT_VOICE_MARKER;
- }
- const voiceMapEntry = new VoiceMapEntry(character, voiceId);
- voiceMapEntry.addUI(voiceIdsFromProvider);
- voiceMapEntries.push(voiceMapEntry);
- }
- updateVoiceMap();
-}
-
-$(document).ready(function () {
- function addExtensionControls() {
- const settingsHtml = `
-
-
-
- TTS
-
-
-
-
-
- Select TTS Provider
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
- $('#extensions_settings').append(settingsHtml);
- $('#tts_refresh').on('click', onRefreshClick);
- $('#tts_enabled').on('click', onEnableClick);
- $('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick);
- $('#tts_narrate_quoted').on('click', onNarrateQuotedClick);
- $('#tts_narrate_translated_only').on('click', onNarrateTranslatedOnlyClick);
- $('#tts_skip_codeblocks').on('click', onSkipCodeblocksClick);
- $('#tts_pass_asterisks').on('click', onPassAsterisksClick);
- $('#tts_auto_generation').on('click', onAutoGenerationClick);
- $('#tts_narrate_user').on('click', onNarrateUserClick);
- $('#tts_voices').on('click', onTtsVoicesClick);
- for (const provider in ttsProviders) {
- $('#tts_provider').append($('').val(provider).text(provider));
- }
- $('#tts_provider').on('change', onTtsProviderChange);
- $(document).on('click', '.mes_narrate', onNarrateOneMessage);
- }
- addExtensionControls(); // No init dependencies
- loadSettings(); // Depends on Extension Controls and loadTtsProvider
- loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies
- addAudioControl(); // Depends on Extension Controls
- const wrapper = new ModuleWorkerWrapper(moduleWorker);
- setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
- eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
- eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
- eventSource.on(event_types.MESSAGE_DELETED, onChatDeleted);
- eventSource.on(event_types.GROUP_UPDATED, onChatChanged);
- registerSlashCommand('speak', onNarrateText, ['narrate', 'tts'], '(text) – narrate any text using currently selected character\'s voice. Use voice="Character Name" argument to set other voice from the voice map, example: /speak voice="Donald Duck" Quack!', true, true);
- document.body.appendChild(audioElement);
-});
+import { callPopup, cancelTtsPlay, eventSource, event_types, name2, saveSettingsDebounced } from '../../../script.js';
+import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules } from '../../extensions.js';
+import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '../../utils.js';
+import { EdgeTtsProvider } from './edge.js';
+import { ElevenLabsTtsProvider } from './elevenlabs.js';
+import { SileroTtsProvider } from './silerotts.js';
+import { CoquiTtsProvider } from './coqui.js';
+import { SystemTtsProvider } from './system.js';
+import { NovelTtsProvider } from './novel.js';
+import { power_user } from '../../power-user.js';
+import { registerSlashCommand } from '../../slash-commands.js';
+import { OpenAITtsProvider } from './openai.js';
+import { XTTSTtsProvider } from './xtts.js';
+import { AllTalkTtsProvider } from './alltalk.js';
+export { talkingAnimation };
+
+const UPDATE_INTERVAL = 1000;
+
+let voiceMapEntries = [];
+let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
+let storedvalue = false;
+let lastChatId = null;
+let lastMessageHash = null;
+
+const DEFAULT_VOICE_MARKER = '[Default Voice]';
+const DISABLED_VOICE_MARKER = 'disabled';
+
+export function getPreviewString(lang) {
+ const previewStrings = {
+ 'en-US': 'The quick brown fox jumps over the lazy dog',
+ 'en-GB': 'Sphinx of black quartz, judge my vow',
+ 'fr-FR': 'Portez ce vieux whisky au juge blond qui fume',
+ 'de-DE': 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich',
+ 'it-IT': 'Pranzo d\'acqua fa volti sghembi',
+ 'es-ES': 'Quiere la boca exhausta vid, kiwi, piña y fugaz jamón',
+ 'es-MX': 'Fabio me exige, sin tapujos, que añada cerveza al whisky',
+ 'ru-RU': 'В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!',
+ 'pt-BR': 'Vejo xá gritando que fez show sem playback.',
+ 'pt-PR': 'Todo pajé vulgar faz boquinha sexy com kiwi.',
+ 'uk-UA': 'Фабрикуймо гідність, лящім їжею, ґав хапаймо, з\'єднавці чаш!',
+ 'pl-PL': 'Pchnąć w tę łódź jeża lub ośm skrzyń fig',
+ 'cs-CZ': 'Příliš žluťoučký kůň úpěl ďábelské ódy',
+ 'sk-SK': 'Vyhŕňme si rukávy a vyprážajme čínske ryžové cestoviny',
+ 'hu-HU': 'Árvíztűrő tükörfúrógép',
+ 'tr-TR': 'Pijamalı hasta yağız şoföre çabucak güvendi',
+ 'nl-NL': 'De waard heeft een kalfje en een pinkje opgegeten',
+ 'sv-SE': 'Yxskaftbud, ge vårbygd, zinkqvarn',
+ 'da-DK': 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon',
+ 'ja-JP': 'いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす',
+ 'ko-KR': '가나다라마바사아자차카타파하',
+ 'zh-CN': '我能吞下玻璃而不伤身体',
+ 'ro-RO': 'Muzicologă în bej vând whisky și tequila, preț fix',
+ 'bg-BG': 'Щъркелите се разпръснаха по цялото небе',
+ 'el-GR': 'Ταχίστη αλώπηξ βαφής ψημένη γη, δρασκελίζει υπέρ νωθρού κυνός',
+ 'fi-FI': 'Voi veljet, miksi juuri teille myin nämä vehkeet?',
+ 'he-IL': 'הקצינים צעקו: "כל הכבוד לצבא הצבאות!"',
+ 'id-ID': 'Jangkrik itu memang enak, apalagi kalau digoreng',
+ 'ms-MY': 'Muzik penyanyi wanita itu menggambarkan kehidupan yang penuh dengan duka nestapa',
+ 'th-TH': 'เป็นไงบ้างครับ ผมชอบกินข้าวผัดกระเพราหมูกรอบ',
+ 'vi-VN': 'Cô bé quàng khăn đỏ đang ngồi trên bãi cỏ xanh',
+ 'ar-SA': 'أَبْجَدِيَّة عَرَبِيَّة',
+ 'hi-IN': 'श्वेता ने श्वेता के श्वेते हाथों में श्वेता का श्वेता चावल पकड़ा',
+ };
+ const fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet';
+
+ return previewStrings[lang] ?? fallbackPreview;
+}
+
+let ttsProviders = {
+ ElevenLabs: ElevenLabsTtsProvider,
+ Silero: SileroTtsProvider,
+ XTTSv2: XTTSTtsProvider,
+ System: SystemTtsProvider,
+ Coqui: CoquiTtsProvider,
+ Edge: EdgeTtsProvider,
+ Novel: NovelTtsProvider,
+ OpenAI: OpenAITtsProvider,
+ AllTalk: AllTalkTtsProvider,
+};
+let ttsProvider;
+let ttsProviderName;
+
+let ttsLastMessage = null;
+
+async function onNarrateOneMessage() {
+ audioElement.src = '/sounds/silence.mp3';
+ const context = getContext();
+ const id = $(this).closest('.mes').attr('mesid');
+ const message = context.chat[id];
+
+ if (!message) {
+ return;
+ }
+
+ resetTtsPlayback();
+ ttsJobQueue.push(message);
+ moduleWorker();
+}
+
+async function onNarrateText(args, text) {
+ if (!text) {
+ return;
+ }
+
+ audioElement.src = '/sounds/silence.mp3';
+
+ // To load all characters in the voice map, set unrestricted to true
+ await initVoiceMap(true);
+
+ const baseName = args?.voice || name2;
+ const name = (baseName === 'SillyTavern System' ? DEFAULT_VOICE_MARKER : baseName) || DEFAULT_VOICE_MARKER;
+
+ const voiceMapEntry = voiceMap[name] === DEFAULT_VOICE_MARKER
+ ? voiceMap[DEFAULT_VOICE_MARKER]
+ : voiceMap[name];
+
+ if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) {
+ toastr.info(`Specified voice for ${name} was not found. Check the TTS extension settings.`);
+ return;
+ }
+
+ resetTtsPlayback();
+ ttsJobQueue.push({ mes: text, name: name });
+ await moduleWorker();
+
+ // Return back to the chat voices
+ await initVoiceMap(false);
+}
+
+async function moduleWorker() {
+ // Primarily determining when to add new chat to the TTS queue
+ const enabled = $('#tts_enabled').is(':checked');
+ $('body').toggleClass('tts', enabled);
+ if (!enabled) {
+ return;
+ }
+
+ const context = getContext();
+ const chat = context.chat;
+
+ processTtsQueue();
+ processAudioJobQueue();
+ updateUiAudioPlayState();
+
+ // Auto generation is disabled
+ if (extension_settings.tts.auto_generation == false) {
+ return;
+ }
+
+ // no characters or group selected
+ if (!context.groupId && context.characterId === undefined) {
+ return;
+ }
+
+ // Chat changed
+ if (
+ context.chatId !== lastChatId
+ ) {
+ currentMessageNumber = context.chat.length ? context.chat.length : 0;
+ saveLastValues();
+
+ // Force to speak on the first message in the new chat
+ if (context.chat.length === 1) {
+ lastMessageHash = -1;
+ }
+
+ return;
+ }
+
+ // take the count of messages
+ let lastMessageNumber = context.chat.length ? context.chat.length : 0;
+
+ // There's no new messages
+ let diff = lastMessageNumber - currentMessageNumber;
+ let hashNew = getStringHash((chat.length && chat[chat.length - 1].mes) ?? '');
+
+ // if messages got deleted, diff will be < 0
+ if (diff < 0) {
+ // necessary actions will be taken by the onChatDeleted() handler
+ return;
+ }
+
+ // if no new messages, or same message, or same message hash, do nothing
+ if (diff == 0 && hashNew === lastMessageHash) {
+ return;
+ }
+
+ // If streaming, wait for streaming to finish before processing new messages
+ if (context.streamingProcessor && !context.streamingProcessor.isFinished) {
+ return;
+ }
+
+ // clone message object, as things go haywire if message object is altered below (it's passed by reference)
+ const message = structuredClone(chat[chat.length - 1]);
+
+ // if last message within current message, message got extended. only send diff to TTS.
+ if (ttsLastMessage !== null && message.mes.indexOf(ttsLastMessage) !== -1) {
+ let tmp = message.mes;
+ message.mes = message.mes.replace(ttsLastMessage, '');
+ ttsLastMessage = tmp;
+ } else {
+ ttsLastMessage = message.mes;
+ }
+
+ // We're currently swiping. Don't generate voice
+ if (!message || message.mes === '...' || message.mes === '') {
+ return;
+ }
+
+ // Don't generate if message doesn't have a display text
+ if (extension_settings.tts.narrate_translated_only && !(message?.extra?.display_text)) {
+ return;
+ }
+
+ // Don't generate if message is a user message and user message narration is disabled
+ if (message.is_user && !extension_settings.tts.narrate_user) {
+ return;
+ }
+
+ // New messages, add new chat to history
+ lastMessageHash = hashNew;
+ currentMessageNumber = lastMessageNumber;
+
+ console.debug(
+ `Adding message from ${message.name} for TTS processing: "${message.mes}"`,
+ );
+ ttsJobQueue.push(message);
+}
+
+function talkingAnimation(switchValue) {
+ if (!modules.includes('talkinghead')) {
+ console.debug('Talking Animation module not loaded');
+ return;
+ }
+
+ const apiUrl = getApiUrl();
+ const animationType = switchValue ? 'start' : 'stop';
+
+ if (switchValue !== storedvalue) {
+ try {
+ console.log(animationType + ' Talking Animation');
+ doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`);
+ storedvalue = switchValue; // Update the storedvalue to the current switchValue
+ } catch (error) {
+ // Handle the error here or simply ignore it to prevent logging
+ }
+ }
+ updateUiAudioPlayState();
+}
+
+function resetTtsPlayback() {
+ // Stop system TTS utterance
+ cancelTtsPlay();
+
+ // Clear currently processing jobs
+ currentTtsJob = null;
+ currentAudioJob = null;
+
+ // Reset audio element
+ audioElement.currentTime = 0;
+ audioElement.src = '';
+
+ // Clear any queue items
+ ttsJobQueue.splice(0, ttsJobQueue.length);
+ audioJobQueue.splice(0, audioJobQueue.length);
+
+ // Set audio ready to process again
+ audioQueueProcessorReady = true;
+}
+
+function isTtsProcessing() {
+ let processing = false;
+
+ // Check job queues
+ if (ttsJobQueue.length > 0 || audioJobQueue.length > 0) {
+ processing = true;
+ }
+ // Check current jobs
+ if (currentTtsJob != null || currentAudioJob != null) {
+ processing = true;
+ }
+ return processing;
+}
+
+function debugTtsPlayback() {
+ console.log(JSON.stringify(
+ {
+ 'ttsProviderName': ttsProviderName,
+ 'voiceMap': voiceMap,
+ 'currentMessageNumber': currentMessageNumber,
+ 'audioPaused': audioPaused,
+ 'audioJobQueue': audioJobQueue,
+ 'currentAudioJob': currentAudioJob,
+ 'audioQueueProcessorReady': audioQueueProcessorReady,
+ 'ttsJobQueue': ttsJobQueue,
+ 'currentTtsJob': currentTtsJob,
+ 'ttsConfig': extension_settings.tts,
+ },
+ ));
+}
+window.debugTtsPlayback = debugTtsPlayback;
+
+//##################//
+// Audio Control //
+//##################//
+
+let audioElement = new Audio();
+audioElement.id = 'tts_audio';
+audioElement.autoplay = true;
+
+let audioJobQueue = [];
+let currentAudioJob;
+let audioPaused = false;
+let audioQueueProcessorReady = true;
+
+async function playAudioData(audioBlob) {
+ // Since current audio job can be cancelled, don't playback if it is null
+ if (currentAudioJob == null) {
+ console.log('Cancelled TTS playback because currentAudioJob was null');
+ }
+ if (audioBlob instanceof Blob) {
+ const srcUrl = await getBase64Async(audioBlob);
+ audioElement.src = srcUrl;
+ } else if (typeof audioBlob === 'string') {
+ audioElement.src = audioBlob;
+ } else {
+ throw `TTS received invalid audio data type ${typeof audioBlob}`;
+ }
+ audioElement.addEventListener('ended', completeCurrentAudioJob);
+ audioElement.addEventListener('canplay', () => {
+ console.debug('Starting TTS playback');
+ audioElement.play();
+ });
+}
+
+window['tts_preview'] = function (id) {
+ const audio = document.getElementById(id);
+
+ if (audio && !$(audio).data('disabled')) {
+ audio.play();
+ }
+ else {
+ ttsProvider.previewTtsVoice(id);
+ }
+};
+
+async function onTtsVoicesClick() {
+ let popupText = '';
+
+ try {
+ const voiceIds = await ttsProvider.fetchTtsVoiceObjects();
+
+ for (const voice of voiceIds) {
+ popupText += `
+
+ ${voice.lang || ''}
+ ${voice.name}
+
+
`;
+ if (voice.preview_url) {
+ popupText += ``;
+ }
+ }
+ } catch {
+ popupText = 'Could not load voices list. Check your API key.';
+ }
+
+ callPopup(popupText, 'text');
+}
+
+function updateUiAudioPlayState() {
+ if (extension_settings.tts.enabled == true) {
+ $('#ttsExtensionMenuItem').show();
+ let img;
+ // Give user feedback that TTS is active by setting the stop icon if processing or playing
+ if (!audioElement.paused || isTtsProcessing()) {
+ img = 'fa-solid fa-stop-circle extensionsMenuExtensionButton';
+ } else {
+ img = 'fa-solid fa-circle-play extensionsMenuExtensionButton';
+ }
+ $('#tts_media_control').attr('class', img);
+ } else {
+ $('#ttsExtensionMenuItem').hide();
+ }
+}
+
+function onAudioControlClicked() {
+ audioElement.src = '/sounds/silence.mp3';
+ let context = getContext();
+ // Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful
+ if (!audioElement.paused || isTtsProcessing()) {
+ resetTtsPlayback();
+ talkingAnimation(false);
+ } else {
+ // Default play behavior if not processing or playing is to play the last message.
+ ttsJobQueue.push(context.chat[context.chat.length - 1]);
+ }
+ updateUiAudioPlayState();
+}
+
+function addAudioControl() {
+
+ $('#extensionsMenu').prepend(`
+
+
+ TTS Playback
+
`);
+ $('#ttsExtensionMenuItem').attr('title', 'TTS play/pause').on('click', onAudioControlClicked);
+ updateUiAudioPlayState();
+}
+
+function completeCurrentAudioJob() {
+ audioQueueProcessorReady = true;
+ currentAudioJob = null;
+ talkingAnimation(false); //stop lip animation
+ // updateUiPlayState();
+}
+
+/**
+ * Accepts an HTTP response containing audio/mpeg data, and puts the data as a Blob() on the queue for playback
+ * @param {Response} response
+ */
+async function addAudioJob(response) {
+ if (typeof response === 'string') {
+ audioJobQueue.push(response);
+ } else {
+ const audioData = await response.blob();
+ if (!audioData.type.startsWith('audio/')) {
+ throw `TTS received HTTP response with invalid data format. Expecting audio/*, got ${audioData.type}`;
+ }
+ audioJobQueue.push(audioData);
+ }
+ console.debug('Pushed audio job to queue.');
+}
+
+async function processAudioJobQueue() {
+ // Nothing to do, audio not completed, or audio paused - stop processing.
+ if (audioJobQueue.length == 0 || !audioQueueProcessorReady || audioPaused) {
+ return;
+ }
+ try {
+ audioQueueProcessorReady = false;
+ currentAudioJob = audioJobQueue.shift();
+ playAudioData(currentAudioJob);
+ talkingAnimation(true);
+ } catch (error) {
+ console.error(error);
+ audioQueueProcessorReady = true;
+ }
+}
+
+//################//
+// TTS Control //
+//################//
+
+let ttsJobQueue = [];
+let currentTtsJob; // Null if nothing is currently being processed
+let currentMessageNumber = 0;
+
+function completeTtsJob() {
+ console.info(`Current TTS job for ${currentTtsJob?.name} completed.`);
+ currentTtsJob = null;
+}
+
+function saveLastValues() {
+ const context = getContext();
+ lastChatId = context.chatId;
+ lastMessageHash = getStringHash(
+ (context.chat.length && context.chat[context.chat.length - 1].mes) ?? '',
+ );
+}
+
+async function tts(text, voiceId, char) {
+ async function processResponse(response) {
+ // RVC injection
+ if (extension_settings.rvc.enabled && typeof window['rvcVoiceConversion'] === 'function')
+ response = await window['rvcVoiceConversion'](response, char, text);
+
+ await addAudioJob(response);
+ }
+
+ let response = await ttsProvider.generateTts(text, voiceId);
+
+ // If async generator, process every chunk as it comes in
+ if (typeof response[Symbol.asyncIterator] === 'function') {
+ for await (const chunk of response) {
+ await processResponse(chunk);
+ }
+ } else {
+ await processResponse(response);
+ }
+
+ completeTtsJob();
+}
+
+async function processTtsQueue() {
+ // Called each moduleWorker iteration to pull chat messages from queue
+ if (currentTtsJob || ttsJobQueue.length <= 0 || audioPaused) {
+ return;
+ }
+
+ console.debug('New message found, running TTS');
+ currentTtsJob = ttsJobQueue.shift();
+ let text = extension_settings.tts.narrate_translated_only ? (currentTtsJob?.extra?.display_text || currentTtsJob.mes) : currentTtsJob.mes;
+
+ if (extension_settings.tts.skip_codeblocks) {
+ text = text.replace(/^\s{4}.*$/gm, '').trim();
+ text = text.replace(/```.*?```/gs, '').trim();
+ }
+
+ if (!extension_settings.tts.pass_asterisks) {
+ text = extension_settings.tts.narrate_dialogues_only
+ ? text.replace(/\*[^*]*?(\*|$)/g, '').trim() // remove asterisks content
+ : text.replaceAll('*', '').trim(); // remove just the asterisks
+ }
+
+ if (extension_settings.tts.narrate_quoted_only) {
+ const special_quotes = /[“”]/g; // Extend this regex to include other special quotes
+ text = text.replace(special_quotes, '"');
+ const matches = text.match(/".*?"/g); // Matches text inside double quotes, non-greedily
+ const partJoiner = (ttsProvider?.separator || ' ... ');
+ text = matches ? matches.join(partJoiner) : text;
+ }
+
+ if (typeof ttsProvider?.processText === 'function') {
+ text = await ttsProvider.processText(text);
+ }
+
+ // Collapse newlines and spaces into single space
+ text = text.replace(/\s+/g, ' ').trim();
+
+ console.log(`TTS: ${text}`);
+ const char = currentTtsJob.name;
+
+ // Remove character name from start of the line if power user setting is disabled
+ if (char && !power_user.allow_name2_display) {
+ const escapedChar = escapeRegex(char);
+ text = text.replace(new RegExp(`^${escapedChar}:`, 'gm'), '');
+ }
+
+ try {
+ if (!text) {
+ console.warn('Got empty text in TTS queue job.');
+ completeTtsJob();
+ return;
+ }
+
+ const voiceMapEntry = voiceMap[char] === DEFAULT_VOICE_MARKER ? voiceMap[DEFAULT_VOICE_MARKER] : voiceMap[char];
+
+ if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) {
+ throw `${char} not in voicemap. Configure character in extension settings voice map`;
+ }
+ const voice = await ttsProvider.getVoice(voiceMapEntry);
+ const voiceId = voice.voice_id;
+ if (voiceId == null) {
+ toastr.error(`Specified voice for ${char} was not found. Check the TTS extension settings.`);
+ throw `Unable to attain voiceId for ${char}`;
+ }
+ tts(text, voiceId, char);
+ } catch (error) {
+ console.error(error);
+ currentTtsJob = null;
+ }
+}
+
+// Secret function for now
+async function playFullConversation() {
+ const context = getContext();
+ const chat = context.chat;
+ ttsJobQueue = chat;
+}
+window.playFullConversation = playFullConversation;
+
+//#############################//
+// Extension UI and Settings //
+//#############################//
+
+function loadSettings() {
+ if (Object.keys(extension_settings.tts).length === 0) {
+ Object.assign(extension_settings.tts, defaultSettings);
+ }
+ for (const key in defaultSettings) {
+ if (!(key in extension_settings.tts)) {
+ extension_settings.tts[key] = defaultSettings[key];
+ }
+ }
+ $('#tts_provider').val(extension_settings.tts.currentProvider);
+ $('#tts_enabled').prop(
+ 'checked',
+ extension_settings.tts.enabled,
+ );
+ $('#tts_narrate_dialogues').prop('checked', extension_settings.tts.narrate_dialogues_only);
+ $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
+ $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
+ $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
+ $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
+ $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
+ $('body').toggleClass('tts', extension_settings.tts.enabled);
+}
+
+const defaultSettings = {
+ voiceMap: '',
+ ttsEnabled: false,
+ currentProvider: 'ElevenLabs',
+ auto_generation: true,
+ narrate_user: false,
+};
+
+function setTtsStatus(status, success) {
+ $('#tts_status').text(status);
+ if (success) {
+ $('#tts_status').removeAttr('style');
+ } else {
+ $('#tts_status').css('color', 'red');
+ }
+}
+
+function onRefreshClick() {
+ Promise.all([
+ ttsProvider.onRefreshClick(),
+ // updateVoiceMap()
+ ]).then(() => {
+ extension_settings.tts[ttsProviderName] = ttsProvider.settings;
+ saveSettingsDebounced();
+ setTtsStatus('Successfully applied settings', true);
+ console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`);
+ initVoiceMap();
+ updateVoiceMap();
+ }).catch(error => {
+ console.error(error);
+ setTtsStatus(error, false);
+ });
+}
+
+function onEnableClick() {
+ extension_settings.tts.enabled = $('#tts_enabled').is(
+ ':checked',
+ );
+ updateUiAudioPlayState();
+ saveSettingsDebounced();
+}
+
+
+function onAutoGenerationClick() {
+ extension_settings.tts.auto_generation = !!$('#tts_auto_generation').prop('checked');
+ saveSettingsDebounced();
+}
+
+
+function onNarrateDialoguesClick() {
+ extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
+ saveSettingsDebounced();
+}
+
+function onNarrateUserClick() {
+ extension_settings.tts.narrate_user = !!$('#tts_narrate_user').prop('checked');
+ saveSettingsDebounced();
+}
+
+function onNarrateQuotedClick() {
+ extension_settings.tts.narrate_quoted_only = !!$('#tts_narrate_quoted').prop('checked');
+ saveSettingsDebounced();
+}
+
+
+function onNarrateTranslatedOnlyClick() {
+ extension_settings.tts.narrate_translated_only = !!$('#tts_narrate_translated_only').prop('checked');
+ saveSettingsDebounced();
+}
+
+function onSkipCodeblocksClick() {
+ extension_settings.tts.skip_codeblocks = !!$('#tts_skip_codeblocks').prop('checked');
+ saveSettingsDebounced();
+}
+
+function onPassAsterisksClick() {
+ extension_settings.tts.pass_asterisks = !!$('#tts_pass_asterisks').prop('checked');
+ saveSettingsDebounced();
+ console.log("setting pass asterisks", extension_settings.tts.pass_asterisks)
+}
+
+//##############//
+// TTS Provider //
+//##############//
+
+async function loadTtsProvider(provider) {
+ //Clear the current config and add new config
+ $('#tts_provider_settings').html('');
+
+ if (!provider) {
+ return;
+ }
+
+ // Init provider references
+ extension_settings.tts.currentProvider = provider;
+ ttsProviderName = provider;
+ ttsProvider = new ttsProviders[provider];
+
+ // Init provider settings
+ $('#tts_provider_settings').append(ttsProvider.settingsHtml);
+ if (!(ttsProviderName in extension_settings.tts)) {
+ console.warn(`Provider ${ttsProviderName} not in Extension Settings, initiatilizing provider in settings`);
+ extension_settings.tts[ttsProviderName] = {};
+ }
+ await ttsProvider.loadSettings(extension_settings.tts[ttsProviderName]);
+ await initVoiceMap();
+}
+
+function onTtsProviderChange() {
+ const ttsProviderSelection = $('#tts_provider').val();
+ extension_settings.tts.currentProvider = ttsProviderSelection;
+ loadTtsProvider(ttsProviderSelection);
+}
+
+// Ensure that TTS provider settings are saved to extension settings.
+export function saveTtsProviderSettings() {
+ extension_settings.tts[ttsProviderName] = ttsProvider.settings;
+ updateVoiceMap();
+ saveSettingsDebounced();
+ console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`);
+}
+
+
+//###################//
+// voiceMap Handling //
+//###################//
+
+async function onChatChanged() {
+ await resetTtsPlayback();
+ const voiceMapInit = initVoiceMap();
+ await Promise.race([voiceMapInit, delay(1000)]);
+ ttsLastMessage = null;
+}
+
+async function onChatDeleted() {
+ const context = getContext();
+
+ // update internal references to new last message
+ lastChatId = context.chatId;
+ currentMessageNumber = context.chat.length ? context.chat.length : 0;
+
+ // compare against lastMessageHash. If it's the same, we did not delete the last chat item, so no need to reset tts queue
+ let messageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1].mes) ?? '');
+ if (messageHash === lastMessageHash) {
+ return;
+ }
+ lastMessageHash = messageHash;
+ ttsLastMessage = (context.chat.length && context.chat[context.chat.length - 1].mes) ?? '';
+
+ // stop any tts playback since message might not exist anymore
+ await resetTtsPlayback();
+}
+
+/**
+ * Get characters in current chat
+ * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat.
+ * @returns {string[]} - Array of character names
+ */
+function getCharacters(unrestricted) {
+ const context = getContext();
+
+ if (unrestricted) {
+ const names = context.characters.map(char => char.name);
+ names.unshift(DEFAULT_VOICE_MARKER);
+ return names.filter(onlyUnique);
+ }
+
+ let characters = [];
+ if (context.groupId === null) {
+ // Single char chat
+ characters.push(DEFAULT_VOICE_MARKER);
+ characters.push(context.name1);
+ characters.push(context.name2);
+ } else {
+ // Group chat
+ characters.push(DEFAULT_VOICE_MARKER);
+ characters.push(context.name1);
+ const group = context.groups.find(group => context.groupId == group.id);
+ for (let member of group.members) {
+ const character = context.characters.find(char => char.avatar == member);
+ if (character) {
+ characters.push(character.name);
+ }
+ }
+ }
+ return characters.filter(onlyUnique);
+}
+
+function sanitizeId(input) {
+ // Remove any non-alphanumeric characters except underscore (_) and hyphen (-)
+ let sanitized = input.replace(/[^a-zA-Z0-9-_]/g, '');
+
+ // Ensure first character is always a letter
+ if (!/^[a-zA-Z]/.test(sanitized)) {
+ sanitized = 'element_' + sanitized;
+ }
+
+ return sanitized;
+}
+
+function parseVoiceMap(voiceMapString) {
+ let parsedVoiceMap = {};
+ for (const [charName, voiceId] of voiceMapString
+ .split(',')
+ .map(s => s.split(':'))) {
+ if (charName && voiceId) {
+ parsedVoiceMap[charName.trim()] = voiceId.trim();
+ }
+ }
+ return parsedVoiceMap;
+}
+
+
+
+/**
+ * Apply voiceMap based on current voiceMapEntries
+ */
+function updateVoiceMap() {
+ const tempVoiceMap = {};
+ for (const voice of voiceMapEntries) {
+ if (voice.voiceId === null) {
+ continue;
+ }
+ tempVoiceMap[voice.name] = voice.voiceId;
+ }
+ if (Object.keys(tempVoiceMap).length !== 0) {
+ voiceMap = tempVoiceMap;
+ console.log(`Voicemap updated to ${JSON.stringify(voiceMap)}`);
+ }
+ if (!extension_settings.tts[ttsProviderName].voiceMap) {
+ extension_settings.tts[ttsProviderName].voiceMap = {};
+ }
+ Object.assign(extension_settings.tts[ttsProviderName].voiceMap, voiceMap);
+ saveSettingsDebounced();
+}
+
+class VoiceMapEntry {
+ name;
+ voiceId;
+ selectElement;
+ constructor(name, voiceId = DEFAULT_VOICE_MARKER) {
+ this.name = name;
+ this.voiceId = voiceId;
+ this.selectElement = null;
+ }
+
+ addUI(voiceIds) {
+ let sanitizedName = sanitizeId(this.name);
+ let defaultOption = this.name === DEFAULT_VOICE_MARKER ?
+ `` :
+ ``;
+ let template = `
+
+ ${this.name}
+
+
+ `;
+ $('#tts_voicemap_block').append(template);
+
+ // Populate voice ID select list
+ for (const voiceId of voiceIds) {
+ const option = document.createElement('option');
+ option.innerText = voiceId.name;
+ option.value = voiceId.name;
+ $(`#tts_voicemap_char_${sanitizedName}_voice`).append(option);
+ }
+
+ this.selectElement = $(`#tts_voicemap_char_${sanitizedName}_voice`);
+ this.selectElement.on('change', args => this.onSelectChange(args));
+ this.selectElement.val(this.voiceId);
+ }
+
+ onSelectChange(args) {
+ this.voiceId = this.selectElement.find(':selected').val();
+ updateVoiceMap();
+ }
+}
+
+/**
+ * Init voiceMapEntries for character select list.
+ * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat.
+ */
+export async function initVoiceMap(unrestricted = false) {
+ // Gate initialization if not enabled or TTS Provider not ready. Prevents error popups.
+ const enabled = $('#tts_enabled').is(':checked');
+ if (!enabled) {
+ return;
+ }
+
+ // Keep errors inside extension UI rather than toastr. Toastr errors for TTS are annoying.
+ try {
+ await ttsProvider.checkReady();
+ } catch (error) {
+ const message = `TTS Provider not ready. ${error}`;
+ setTtsStatus(message, false);
+ return;
+ }
+
+ setTtsStatus('TTS Provider Loaded', true);
+
+ // Clear existing voiceMap state
+ $('#tts_voicemap_block').empty();
+ voiceMapEntries = [];
+
+ // Get characters in current chat
+ const characters = getCharacters(unrestricted);
+
+ // Get saved voicemap from provider settings, handling new and old representations
+ let voiceMapFromSettings = {};
+ if ('voiceMap' in extension_settings.tts[ttsProviderName]) {
+ // Handle previous representation
+ if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'string') {
+ voiceMapFromSettings = parseVoiceMap(extension_settings.tts[ttsProviderName].voiceMap);
+ // Handle new representation
+ } else if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'object') {
+ voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap;
+ }
+ }
+
+ // Get voiceIds from provider
+ let voiceIdsFromProvider;
+ try {
+ voiceIdsFromProvider = await ttsProvider.fetchTtsVoiceObjects();
+ }
+ catch {
+ toastr.error('TTS Provider failed to return voice ids.');
+ }
+
+ // Build UI using VoiceMapEntry objects
+ for (const character of characters) {
+ if (character === 'SillyTavern System') {
+ continue;
+ }
+ // Check provider settings for voiceIds
+ let voiceId;
+ if (character in voiceMapFromSettings) {
+ voiceId = voiceMapFromSettings[character];
+ } else if (character === DEFAULT_VOICE_MARKER) {
+ voiceId = DISABLED_VOICE_MARKER;
+ } else {
+ voiceId = DEFAULT_VOICE_MARKER;
+ }
+ const voiceMapEntry = new VoiceMapEntry(character, voiceId);
+ voiceMapEntry.addUI(voiceIdsFromProvider);
+ voiceMapEntries.push(voiceMapEntry);
+ }
+ updateVoiceMap();
+}
+
+$(document).ready(function () {
+ function addExtensionControls() {
+ const settingsHtml = `
+