Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba1328311c | ||
|
|
9e6b9396bc | ||
|
|
2e40db5fe5 | ||
|
|
720bf8697f | ||
|
|
1893d1a130 | ||
|
|
8472baf031 | ||
|
|
6cab618706 | ||
|
|
ebf3b362f7 | ||
|
|
2dd303f53c | ||
|
|
df69fb0b07 | ||
|
|
a0d9771fa6 | ||
|
|
a71c46bb98 | ||
|
|
995f317b4f | ||
|
|
6ef2de2923 | ||
|
|
37c4e86566 | ||
|
|
11071b279b | ||
|
|
5ab7463f35 | ||
|
|
74c9238e3a | ||
|
|
e874e46304 |
No files matched your search
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"*.html": "jinja-html"
|
||||||
|
},
|
||||||
|
"emmet.includeLanguages": {
|
||||||
|
"jinja-html": "html",
|
||||||
|
"jinja2": "html"
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -69,17 +69,20 @@ Once the extension is loaded, please find all documentation and settings on the
|
|||||||
**Where to find voices** https://aiartes.com/voiceai or https://commons.wikimedia.org/ or interviews on youtube etc. Instructions on how to cut down and prepare a voice sample are within the built in documentation.
|
**Where to find voices** https://aiartes.com/voiceai or https://commons.wikimedia.org/ or interviews on youtube etc. Instructions on how to cut down and prepare a voice sample are within the built in documentation.
|
||||||
|
|
||||||
#### 🟩 The one thing I cant easily work around
|
#### 🟩 The one thing I cant easily work around
|
||||||
Narrator function specific - With a RP chat with your AI, **on your character card** `parameters menu` > `character tab` > `greeting` make sure that anything in there that is the **narrator is in asterisks** and anything **spoken is in double quotes**, then hit the `save` (💾) button. Greeting paragraphs/sentences are handled differently from how the AI sends text and so its difficut to account for them both.
|
Messages intended for the Narrator should be enclosed in asterisks `*` and those for the character inside quotation marks `"`. However, AI systems often deviate from these rules, resulting in text that is neither in quotes nor asterisks. Sometimes, text may appear with only a single asterisk, and AI models may vary their formatting mid-conversation. For example, they might use asterisks initially and then switch to unmarked text. A properly formatted line should look like this:
|
||||||
|
|
||||||
I could force a delimeter in at this stage, but I know it would/may affect things further down the line in the chat and I need a good think about that before just making a change. This issue **only** affects the greeting card/start of conversation and the "example" card that comes with text-generation-webui suffers this issue (if you want to try it for yourself). So you would put double quotes around like this (from the example card):
|
`"`Hey! I'm so excited to finally meet you. I've heard so many great things about you and I'm eager to pick your brain about computers.`"` `*`She walked across the room and picked up her cup of coffee`*`
|
||||||
|
|
||||||
`"`*Hey! I'm so excited to finally meet you. I've heard so many great things about you and I'm eager to pick your brain about computers. I'm sure you have a wealth of knowledge that I can learn from.*`"`
|
Most narrator/character systems switch voices upon encountering an asterisk or quotation marks, which is somewhat effective. AllTalk has undergone several revisions in its sentence splitting and identification methods. While some irregularities and AI deviations in message formatting are inevitable, any line beginning or ending with an asterisk should now be recognized as Narrator dialogue. Lines enclosed in double quotes are identified as Character dialogue. For any other text, you can choose how AllTalk handles it: whether it should be interpreted as Character or Narrator dialogue (most AI systems tend to lean more towards one format when generating text not enclosed in quotes or asterisks).
|
||||||
|
|
||||||
|
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
|
||||||
This is pretty much a repeat of the installation process.
|
This is pretty much a repeat of the installation process.
|
||||||
|
|
||||||
1) In a command prompt/terminal window you need to move into your Text generation webUI folder:<br><br>
|
1) In a command prompt/terminal window you need to move into your Text generation webUI folder:<br><br>
|
||||||
`cd text-generation-webui`
|
`cd text-generation-webui` and start the Python environment for your OS with whichever **one** of the below is correct for your OS:<br><br>
|
||||||
|
`cmd_windows.bat`, `./cmd_linux.sh`, `cmd_macos.sh` or `cmd_wsl.bat`<br>
|
||||||
|
|
||||||
2) Move into your extensions and alltalk_tts folder:<br><br>
|
2) Move into your extensions and alltalk_tts folder:<br><br>
|
||||||
`cd extensions` then `cd alltalk_tts`
|
`cd extensions` then `cd alltalk_tts`
|
||||||
@@ -276,7 +279,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:
|
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).
|
- 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).
|
||||||
- To install a few portions of the Nvidia CUDA 11.8 Toolkit (this will not impact text-generation-webui's cuda setup.
|
- To install a few portions of the Nvidia CUDA 11.8 Toolkit (this will not impact text-generation-webui's cuda setup.
|
||||||
- 18GB of disk space free (most of this is used temporarily)
|
- 18GB of disk space free (most of this is used temporarily)
|
||||||
- At least 2 minutes of good quality speech from your chosen speaker in mp3, wav or flacc format, in one or more files (have tested as far as 20 minutes worth of audio).
|
- At least 2 minutes of good quality speech from your chosen speaker in mp3, wav or flacc format, in one or more files (have tested as far as 20 minutes worth of audio).
|
||||||
@@ -691,11 +694,12 @@ The API returns a JSON object with the following properties:
|
|||||||
|
|
||||||
- **status** Indicates whether the generation was successful (generate-success) or failed (generate-failure).<br>
|
- **status** Indicates whether the generation was successful (generate-success) or failed (generate-failure).<br>
|
||||||
- **output_file_path** The on-disk location of the generated WAV file.<br>
|
- **output_file_path** The on-disk location of the generated WAV file.<br>
|
||||||
- **output_file_url** The HTTP location for accessing the generated WAV file.<br>
|
- **output_file_url** The HTTP location for accessing the generated WAV file for browser playback.<br>
|
||||||
|
- **output_cache_url** The HTTP location for accessing the generated WAV file as a pushed download.<br>
|
||||||
|
|
||||||
Example JSON TTS Generation Response:
|
Example JSON TTS Generation Response:
|
||||||
|
|
||||||
`{"status": "generate-success", "output_file_path": "C:\text-generation-webui\extensions\alltalk_tts\outputs\myoutputfile_1703149973.wav", "output_file_url": "http://127.0.0.1:7851/audio/myoutputfile_1703149973.wav"}`
|
`{"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"}`
|
||||||
|
|
||||||
### 🔴 Future to-do list
|
### 🔴 Future to-do list
|
||||||
- Voice output within the command prompt/terminal (TBD).
|
- Voice output within the command prompt/terminal (TBD).
|
||||||
|
|||||||
@@ -509,8 +509,53 @@ def voice_preview(string):
|
|||||||
return f"[{params['branding']}Server] Audio generation failed. Status: {generate_response.get('status')}"
|
return f"[{params['branding']}Server] Audio generation failed. Status: {generate_response.get('status')}"
|
||||||
|
|
||||||
|
|
||||||
def replace_asterisk(match):
|
def process_text(text):
|
||||||
return match.group(0) + '"<*'
|
# Normalize HTML encoded quotes
|
||||||
|
text = html.unescape(text)
|
||||||
|
|
||||||
|
# Replace ellipsis with a single dot
|
||||||
|
text = re.sub(r'\.{3,}', '.', text)
|
||||||
|
|
||||||
|
# Pattern to identify combined narrator and character speech
|
||||||
|
combined_pattern = r'(\*[^*"]+\*|"[^"*]+")'
|
||||||
|
|
||||||
|
# List to hold parts of speech along with their type
|
||||||
|
ordered_parts = []
|
||||||
|
|
||||||
|
# Track the start of the next segment
|
||||||
|
start = 0
|
||||||
|
|
||||||
|
# Find all matches
|
||||||
|
for match in re.finditer(combined_pattern, text):
|
||||||
|
# Add the text before the match, if any, as ambiguous
|
||||||
|
if start < match.start():
|
||||||
|
ambiguous_text = text[start:match.start()].strip()
|
||||||
|
if ambiguous_text:
|
||||||
|
ordered_parts.append(('ambiguous', ambiguous_text))
|
||||||
|
|
||||||
|
# Add the matched part as either narrator or character
|
||||||
|
matched_text = match.group(0)
|
||||||
|
if matched_text.startswith('*') and matched_text.endswith('*'):
|
||||||
|
ordered_parts.append(('narrator', matched_text.strip('*').strip()))
|
||||||
|
elif matched_text.startswith('"') and matched_text.endswith('"'):
|
||||||
|
ordered_parts.append(('character', matched_text.strip('"').strip()))
|
||||||
|
else:
|
||||||
|
# In case of mixed or improperly formatted parts
|
||||||
|
if '*' in matched_text:
|
||||||
|
ordered_parts.append(('narrator', matched_text.strip('*').strip('"')))
|
||||||
|
else:
|
||||||
|
ordered_parts.append(('character', matched_text.strip('"').strip('*')))
|
||||||
|
|
||||||
|
# Update the start of the next segment
|
||||||
|
start = match.end()
|
||||||
|
|
||||||
|
# Add any remaining text after the last match as ambiguous
|
||||||
|
if start < len(text):
|
||||||
|
ambiguous_text = text[start:].strip()
|
||||||
|
if ambiguous_text:
|
||||||
|
ordered_parts.append(('ambiguous', ambiguous_text))
|
||||||
|
|
||||||
|
return ordered_parts
|
||||||
|
|
||||||
#################################
|
#################################
|
||||||
#### TTS STANDARD GENERATION ####
|
#### TTS STANDARD GENERATION ####
|
||||||
@@ -520,6 +565,7 @@ def output_modifier(string, state):
|
|||||||
if not params["activate"]:
|
if not params["activate"]:
|
||||||
return string
|
return string
|
||||||
original_string = string
|
original_string = string
|
||||||
|
#print("ORIGINAL STRING IS:", original_string)
|
||||||
cleaned_string = before_audio_generation(string, params)
|
cleaned_string = before_audio_generation(string, params)
|
||||||
if cleaned_string is None:
|
if cleaned_string is None:
|
||||||
return
|
return
|
||||||
@@ -530,82 +576,55 @@ def output_modifier(string, state):
|
|||||||
if process_lock.acquire(blocking=False):
|
if process_lock.acquire(blocking=False):
|
||||||
try:
|
try:
|
||||||
if params["narrator_enabled"]:
|
if params["narrator_enabled"]:
|
||||||
# Do Some basic stripping and remove any double CR's
|
processed_parts = process_text(original_string)
|
||||||
processed_string = (
|
|
||||||
original_string
|
|
||||||
.replace("***", "*")
|
|
||||||
.replace("**", "*")
|
|
||||||
.replace("\n\n", "\n")
|
|
||||||
.replace("\n", "")
|
|
||||||
)
|
|
||||||
processed_string = re.sub(r'\.\*\n\*', '. ', processed_string)
|
|
||||||
processed_string = (processed_string .replace('"\n\n*', '"<*'))
|
|
||||||
processed_string = (processed_string .replace('"\n', '< '))
|
|
||||||
processed_string = (processed_string .replace("'", "'"))
|
|
||||||
processed_string = (processed_string .replace("\n", " "))
|
|
||||||
processed_string = (processed_string .replace('"', '"<'))
|
|
||||||
processed_string = (processed_string .replace('"', '"<'))
|
|
||||||
#pattern = re.compile(r'(?<=[a-zA-Z!])"<(?=[a-zA-Z])')
|
|
||||||
pattern = re.compile(r'(?<=[a-zA-Z!?\.])"<(?=[a-zA-Z])')
|
|
||||||
processed_string = pattern.sub('" ', processed_string)
|
|
||||||
pattern = re.compile(r'(?<=[a-zA-Z])\*(?=[a-zA-Z])')
|
|
||||||
processed_string = pattern.sub('*"', processed_string)
|
|
||||||
processed_string = processed_string.replace('"<. *', '"< *"')
|
|
||||||
processed_string = processed_string.replace('< *"', '< *')
|
|
||||||
processed_string = processed_string.replace('. *', '< *')
|
|
||||||
processed_string = processed_string.replace('! *', '"< *')
|
|
||||||
processed_string = processed_string.replace('? *', '< *')
|
|
||||||
processed_string = processed_string.replace('* ', '* "<')
|
|
||||||
processed_string = processed_string.replace('*. ', '* "<')
|
|
||||||
processed_string = re.sub(r'\*(?=[a-zA-Z])', replace_asterisk, processed_string)
|
|
||||||
# Set up a tracking of the individual wav files.
|
|
||||||
audio_files_all_paragraphs = []
|
audio_files_all_paragraphs = []
|
||||||
# Split the line using " and ".* " (so end of sentences, leaving special characters added to the start of all OTHER sentences, bar possibly the first one if its starting with a *
|
for part_type, part in processed_parts:
|
||||||
parts = re.split(r'"|\.\*', processed_string)
|
|
||||||
audio_files_paragraph = []
|
|
||||||
for i, part in enumerate(parts):
|
|
||||||
# Skip parts that are too short
|
# Skip parts that are too short
|
||||||
if len(part.strip()) <= 3:
|
if len(part.strip()) <= 3:
|
||||||
continue
|
continue
|
||||||
# Figure out which type of line it is, then replace characters as necessary to avoid TTS trying to pronunce them, htmlunescape after.
|
|
||||||
# Character will always be a < with a letter immediately after it
|
# Determine the voice to use based on the part type
|
||||||
if '<' in part and '< *' not in part and '<*' not in part and '< *' not in part and '< ' not in part and '< ' not in part:
|
if part_type == 'narrator':
|
||||||
cleaned_part = html.unescape(part.replace('<', ''))
|
|
||||||
voice_to_use = params["voice"]
|
|
||||||
#Narrator will always be an * or < with an * a position or two after it.
|
|
||||||
elif '<*' in part or '< *' in part or '< *' in part or '*' in part:
|
|
||||||
cleaned_part = html.unescape(part.replace('<*', '').replace('< *', '').replace('< *', '').replace('*', ''))
|
|
||||||
voice_to_use = params["narrator_voice"]
|
voice_to_use = params["narrator_voice"]
|
||||||
#If the other two dont capture it, aka, the AI gave no * or " on the line, use non_quoted_text_is aka user interface, user can choose Char or Narrator
|
print(f"[{params['branding']}TTSGen] \033[92mNarrator\033[0m") # Green
|
||||||
elif non_quoted_text_is:
|
elif part_type == 'character':
|
||||||
cleaned_part = html.unescape(part.replace('< ', '').replace('< ', '').replace('< ', ''))
|
|
||||||
voice_to_use = params["voice"]
|
voice_to_use = params["voice"]
|
||||||
|
print(f"[{params['branding']}TTSGen] \033[36mCharacter\033[0m") # Yellow
|
||||||
else:
|
else:
|
||||||
cleaned_part = html.unescape(part.replace('< ', '').replace('< ', '').replace('< ', ''))
|
# Handle ambiguous parts based on user preference
|
||||||
voice_to_use = params["narrator_voice"]
|
voice_to_use = params["voice"] if non_quoted_text_is else params["narrator_voice"]
|
||||||
# Generate that TTS and output to a file
|
voice_description = "\033[36mCharacter (Text-not-inside)\033[0m" if non_quoted_text_is else "\033[92mNarrator (Text-not-inside)\033[0m"
|
||||||
|
print(f"[{params['branding']}TTSGen] {voice_description}")
|
||||||
|
|
||||||
|
# Replace multiple exclamation marks, question marks, or other punctuation with a single instance
|
||||||
|
cleaned_part = re.sub(r'([!?.])\1+', r'\1', part)
|
||||||
|
# Further clean to remove any other unwanted characters
|
||||||
|
cleaned_part = re.sub(r'[^a-zA-Z0-9\s\.,;:!?\-\'"]', '', cleaned_part)
|
||||||
|
# Remove all newline characters (single or multiple)
|
||||||
|
cleaned_part = re.sub(r'\n+', ' ', cleaned_part)
|
||||||
|
|
||||||
|
# Generate TTS and output to a file
|
||||||
output_filename = get_output_filename(state)
|
output_filename = get_output_filename(state)
|
||||||
generate_response = send_generate_request(
|
generate_response = send_generate_request(
|
||||||
cleaned_part, voice_to_use, language_code, output_filename
|
cleaned_part, voice_to_use, language_code, output_filename
|
||||||
)
|
)
|
||||||
audio_path = generate_response.get("data", {}).get("audio_path")
|
audio_path = generate_response.get("data", {}).get("audio_path")
|
||||||
audio_files_paragraph.append(audio_path)
|
audio_files_all_paragraphs.append(audio_path)
|
||||||
# Accumulate audio files within the paragraph
|
|
||||||
audio_files_all_paragraphs.extend(audio_files_paragraph)
|
|
||||||
# Combine audio files across paragraphs
|
# Combine audio files across paragraphs
|
||||||
final_output_file = combine(
|
final_output_file = combine(
|
||||||
audio_files_all_paragraphs, params["output_folder_wav"], state
|
audio_files_all_paragraphs, params["output_folder_wav"], state
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
processed_string = (
|
# Decode HTML entities first
|
||||||
original_string
|
cleaned_part = html.unescape(original_string)
|
||||||
.replace("***", "")
|
# Replace multiple instances of certain punctuation marks with a single instance
|
||||||
.replace("**", "")
|
cleaned_part = re.sub(r'([!?.])\1+', r'\1', cleaned_part)
|
||||||
.replace("*", "")
|
# Further clean to remove any other unwanted characters
|
||||||
.replace("\n\n", "\n")
|
cleaned_part = re.sub(r'[^a-zA-Z0-9\s\.,;:!?\-\'"]', '', cleaned_part)
|
||||||
.replace("'", "'")
|
# Remove all newline characters (single or multiple)
|
||||||
)
|
cleaned_part = re.sub(r'\n+', ' ', cleaned_part)
|
||||||
cleaned_part = html.unescape(processed_string)
|
|
||||||
# Process the part and give it a non-character name if being used vai API or standalone.
|
# Process the part and give it a non-character name if being used vai API or standalone.
|
||||||
if "character_menu" in state:
|
if "character_menu" in state:
|
||||||
output_file = Path(f'{params["output_folder_wav"]}/{state["character_menu"]}_{int(time.time())}.wav')
|
output_file = Path(f'{params["output_folder_wav"]}/{state["character_menu"]}_{int(time.time())}.wav')
|
||||||
@@ -713,10 +732,6 @@ def input_modifier(string, state):
|
|||||||
shared.processing_message = "*Is recording a voice message...*"
|
shared.processing_message = "*Is recording a voice message...*"
|
||||||
return string
|
return string
|
||||||
|
|
||||||
def custom_css():
|
|
||||||
path_to_css = Path(f"{this_dir}/style.css")
|
|
||||||
return open(path_to_css, "r").read()
|
|
||||||
|
|
||||||
|
|
||||||
def ui():
|
def ui():
|
||||||
with gr.Accordion(params["branding"] + " TTS (XTTSv2)"):
|
with gr.Accordion(params["branding"] + " TTS (XTTSv2)"):
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
.SDAP .hires_opts input[type="number"] {
|
|
||||||
width: 6em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* silero_tts preview */
|
|
||||||
.form:has(> #silero_preview_text) {
|
|
||||||
min-width: 75%
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AllTalk TTS Generator</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body,
|
||||||
|
textarea,
|
||||||
|
select,
|
||||||
|
input[type="range"],
|
||||||
|
input[type="number"],
|
||||||
|
button {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 20px;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
max-width: 80%;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, .2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
/* Align items to the start */
|
||||||
|
align-items: center;
|
||||||
|
/* Align items vertically */
|
||||||
|
gap: 20px;
|
||||||
|
/* This creates a consistent gap between all child elements */
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
/* Space from bottom if wrapped */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* Removed flex property to stop forcing them to grow */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure select and input elements do not exceed the width of their parent */
|
||||||
|
select,
|
||||||
|
input[type="range"],
|
||||||
|
input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
/* They will fill the width of their .setting container */
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
|
||||||
|
select,
|
||||||
|
input[type="range"],
|
||||||
|
input[type="number"] {
|
||||||
|
width: auto;
|
||||||
|
/* Only auto width for larger screens */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode {
|
||||||
|
background-color: #121212;
|
||||||
|
color: #ffffff;
|
||||||
|
/* Text color for dark mode background */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .form-container {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modify the text color for elements within dark mode */
|
||||||
|
.dark-mode label,
|
||||||
|
.dark-mode textarea,
|
||||||
|
.dark-mode select,
|
||||||
|
.dark-mode input[type="range"],
|
||||||
|
.dark-mode input[type="number"],
|
||||||
|
.dark-mode button,
|
||||||
|
.dark-mode .error {
|
||||||
|
color: #ffffff;
|
||||||
|
/* Text color for dark mode elements */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modify text color for input elements, textarea, and dropdowns in dark mode */
|
||||||
|
.dark-mode input,
|
||||||
|
.dark-mode textarea,
|
||||||
|
.dark-mode select {
|
||||||
|
color: #000000;
|
||||||
|
/* Text color for input elements, textarea, and dropdowns in dark mode */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set background color for textarea in dark mode */
|
||||||
|
.dark-mode textarea {
|
||||||
|
background-color: #C0C0C0;
|
||||||
|
/* Light grey background color for textarea in dark mode */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set background color for option elements within select in dark mode */
|
||||||
|
.dark-mode select {
|
||||||
|
background-color: #C0C0C0;
|
||||||
|
/* Light grey background color for options in dark mode */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional styles if needed for fixed-width dropdowns */
|
||||||
|
.fixed-size-dropdown {
|
||||||
|
width: 130px;
|
||||||
|
/* Adjust the width as needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 5px solid #f3f3f3;
|
||||||
|
border-top: 5px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Add space between sliders */
|
||||||
|
.slider-setting {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Adjust the thumb appearance for a more standard line slider look */
|
||||||
|
input[type="range"]::-webkit-slider-thumb,
|
||||||
|
input[type="range"]::-moz-range-thumb,
|
||||||
|
input[type="range"]::-ms-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
/* Smaller width for a more standard thumb size */
|
||||||
|
height: 16px;
|
||||||
|
/* Smaller height for a more standard thumb size */
|
||||||
|
border-radius: 50%;
|
||||||
|
/* Keep it rounded, or set to 0 for square */
|
||||||
|
background: #3071a9;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: -7px;
|
||||||
|
/* Adjust if needed to align thumb with the track */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<h1>AllTalk TTS Generator</h1>
|
||||||
|
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="input-section">
|
||||||
|
<label for="text_input">Text Input:</label>
|
||||||
|
<textarea id="text_input" rows="8"></textarea> <!-- Removed cols attribute -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
|
||||||
|
<div class="setting voice-setting">
|
||||||
|
<label for="character_voice_gen">Character Voice:</label>
|
||||||
|
<select id="character_voice_gen"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting language-setting">
|
||||||
|
<label for="language">Language:</label>
|
||||||
|
<select id="language" name="language" class="fixed-size-dropdown">
|
||||||
|
<option value="ar">Arabic</option>
|
||||||
|
<option value="zh-cn">Chinese (Simplified)</option>
|
||||||
|
<option value="cs">Czech</option>
|
||||||
|
<option value="nl">Dutch</option>
|
||||||
|
<option value="en" selected>English</option>
|
||||||
|
<option value="fr">French</option>
|
||||||
|
<option value="de">German</option>
|
||||||
|
<option value="hu">Hungarian</option>
|
||||||
|
<option value="it">Italian</option>
|
||||||
|
<option value="ja">Japanese</option>
|
||||||
|
<option value="ko">Korean</option>
|
||||||
|
<option value="pl">Polish</option>
|
||||||
|
<option value="pt">Portuguese</option>
|
||||||
|
<option value="ru">Russian</option>
|
||||||
|
<option value="es">Spanish</option>
|
||||||
|
<option value="tr">Turkish</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="setting play-mode-setting">
|
||||||
|
<label for="playMode">Playback:</label>
|
||||||
|
<select id="playMode" name="playMode" class="fixed-size-dropdown">
|
||||||
|
<option value="local" selected>In Browser</option>
|
||||||
|
<option value="remote">On Server</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting theme-mode-setting">
|
||||||
|
<label for="themeMode">Theme:</label>
|
||||||
|
<select id="themeMode" name="themeMode" class="fixed-size-dropdown">
|
||||||
|
<option value="light" selected>Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting chunk-size-setting">
|
||||||
|
<label for="chunkSize">Sentence Chunk Size:</label>
|
||||||
|
<input type="range" id="chunkSize" name="chunkSize" class="slider-setting" min="1" max="10" value="4"
|
||||||
|
oninput="updateChunkSizeValue(this.value)">
|
||||||
|
<span id="chunkSizeValue">3</span> <!-- Display the value of the slider -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting volume-setting">
|
||||||
|
<label for="volume">Volume:</label>
|
||||||
|
<input type="range" id="volume" name="volume" class="slider-setting" min="0.1" max="1.0" step="0.1"
|
||||||
|
value="0.8" oninput="updateVolumeValue(this.value)">
|
||||||
|
<span id="volumeValue">0.8</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button onclick="generateTTS()">Generate TTS</button>
|
||||||
|
<button onclick="stopPlayback()">Stop Playback</button>
|
||||||
|
<button onclick="previewVoice()">Preview Voice</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="progressIndicator" class="progress-overlay" style="display: none;">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function fetchVoices() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("http://127.0.0.1:7851/api/voices");
|
||||||
|
const result = await response.json();
|
||||||
|
const characterVoiceSelect = document.getElementById("character_voice_gen");
|
||||||
|
|
||||||
|
result.voices.forEach(voice => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = voice;
|
||||||
|
option.text = voice;
|
||||||
|
characterVoiceSelect.add(option);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVolumeValue(value) {
|
||||||
|
document.getElementById("volumeValue").innerText = value;
|
||||||
|
|
||||||
|
// Set the volume for browser playback
|
||||||
|
audioElement.volume = parseFloat(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stopTTSGeneration = false;
|
||||||
|
let audioQueue = [];
|
||||||
|
let currentlyPlaying = false;
|
||||||
|
const audioElement = new Audio();
|
||||||
|
|
||||||
|
async function generateTTS() {
|
||||||
|
try {
|
||||||
|
// Reset the flag at the start
|
||||||
|
stopTTSGeneration = false;
|
||||||
|
|
||||||
|
const textInput = document.getElementById("text_input").value;
|
||||||
|
const language = document.getElementById("language").value;
|
||||||
|
const characterVoice = document.getElementById("character_voice_gen").value;
|
||||||
|
const volume = parseFloat(document.getElementById("volume").value);
|
||||||
|
const chunkSize = parseInt(document.getElementById("chunkSize").value);
|
||||||
|
// Retrieve the value of the selected option for playMode from the dropdown
|
||||||
|
const playModeValue = document.getElementById("playMode").value;
|
||||||
|
|
||||||
|
// Clean up the textInput
|
||||||
|
let cleanedText = textInput.replace(/ \- | \– /g, ' '); // Replaces " - " and " – " with " "
|
||||||
|
cleanedText = cleanedText.replace(/\.\s'/g, ".'"); // Correctly remove space between a period and a single quote
|
||||||
|
cleanedText = cleanedText.replace(/'/g, ""); // Remove all single quotes
|
||||||
|
|
||||||
|
// Split text into chunks based on the specified number of sentences
|
||||||
|
const sentences = cleanedText.match(/("[^"]+"|[^.!?]+[.!?]+)/g);
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < sentences.length; i += chunkSize) {
|
||||||
|
let chunk = sentences.slice(i, i + chunkSize).map(s => s.trim()).join(' ');
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use playModeValue for the conditional check
|
||||||
|
if (playModeValue === "remote") {
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
if (stopTTSGeneration) break; // Stop if flag is set
|
||||||
|
await generateAndPlayRemote(chunk, language, characterVoice, volume);
|
||||||
|
}
|
||||||
|
} else if (playModeValue === "local") {
|
||||||
|
generateAndPlayLocal(chunks, 0);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in generateTTS:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function generateAndPlayLocal(chunks, index) {
|
||||||
|
if (index >= chunks.length || stopTTSGeneration) {
|
||||||
|
return; // Stop if no more chunks or flag is set
|
||||||
|
}
|
||||||
|
if (index >= chunks.length) {
|
||||||
|
// No more chunks to process
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chunk = chunks[index];
|
||||||
|
const data = new URLSearchParams({
|
||||||
|
text_input: chunk.trim(),
|
||||||
|
text_filtering: "standard",
|
||||||
|
character_voice_gen: document.getElementById("character_voice_gen").value,
|
||||||
|
narrator_enabled: "false",
|
||||||
|
narrator_voice_gen: document.getElementById("character_voice_gen").value,
|
||||||
|
text_not_inside: "character",
|
||||||
|
language: document.getElementById("language").value,
|
||||||
|
output_file_name: "myoutputfile",
|
||||||
|
output_file_timestamp: "true",
|
||||||
|
autoplay: "false",
|
||||||
|
autoplay_volume: document.getElementById("volume").value
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch("http://127.0.0.1:7851/api/tts-generate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.output_file_url) {
|
||||||
|
audioQueue.push(result.output_file_url);
|
||||||
|
if (!currentlyPlaying) {
|
||||||
|
playNextLocalChunk();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively call the function with the next index
|
||||||
|
generateAndPlayLocal(chunks, index + 1);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in generateAndPlayLocal with chunk index", index, ":", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateAndPlayRemote(chunk, language, characterVoice, volume) {
|
||||||
|
try {
|
||||||
|
if (stopTTSGeneration) {
|
||||||
|
console.log("TTS generation stopped.");
|
||||||
|
return; // Early return if stop flag is set
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new URLSearchParams({
|
||||||
|
text_input: chunk.trim(),
|
||||||
|
text_filtering: "standard",
|
||||||
|
character_voice_gen: characterVoice,
|
||||||
|
narrator_enabled: "false",
|
||||||
|
narrator_voice_gen: characterVoice,
|
||||||
|
text_not_inside: "character",
|
||||||
|
language: language,
|
||||||
|
output_file_name: "myoutputfile",
|
||||||
|
output_file_timestamp: "true",
|
||||||
|
autoplay: "true",
|
||||||
|
autoplay_volume: volume.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch("http://127.0.0.1:7851/api/tts-generate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API error: ${result.detail || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("TTS generation complete for chunk:", chunk);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in generateAndPlayRemote:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function previewVoice() {
|
||||||
|
const characterVoice = document.getElementById("character_voice_gen").value;
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("voice", characterVoice);
|
||||||
|
|
||||||
|
const response = await fetch("http://127.0.0.1:7851/api/previewvoice/", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.status === "generate-success") {
|
||||||
|
// Play the preview audio
|
||||||
|
const audioPreview = new Audio(result.output_file_url);
|
||||||
|
audioPreview.play();
|
||||||
|
} else {
|
||||||
|
throw new Error('Preview generation failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in previewVoice:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playNextLocalChunk() {
|
||||||
|
if (audioQueue.length > 0) {
|
||||||
|
currentlyPlaying = true;
|
||||||
|
const audioUrl = audioQueue.shift();
|
||||||
|
audioElement.src = audioUrl;
|
||||||
|
audioElement.play();
|
||||||
|
|
||||||
|
audioElement.onended = () => {
|
||||||
|
currentlyPlaying = false;
|
||||||
|
playNextLocalChunk();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleThemeMode() {
|
||||||
|
// Retrieve the value of the selected option for themeMode from the dropdown
|
||||||
|
const themeModeValue = document.getElementById('themeMode').value;
|
||||||
|
|
||||||
|
if (themeModeValue === "dark") {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', 'enabled'); // Assuming saveDarkModePreference sets this
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', 'disabled'); // Assuming saveDarkModePreference sets this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChunkSizeValue(value) {
|
||||||
|
document.getElementById("chunkSizeValue").innerText = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally, save the dark mode preference in localStorage
|
||||||
|
function saveDarkModePreference(isDarkMode) {
|
||||||
|
localStorage.setItem('darkMode', isDarkMode ? 'enabled' : 'disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPlayback() {
|
||||||
|
// Stop current audio playback
|
||||||
|
audioElement.pause();
|
||||||
|
audioElement.currentTime = 0;
|
||||||
|
|
||||||
|
// Clear the audio queue, reset playback flag, and stop further TTS generation
|
||||||
|
audioQueue = [];
|
||||||
|
currentlyPlaying = false;
|
||||||
|
stopTTSGeneration = true; // Set flag to stop TTS generation
|
||||||
|
}
|
||||||
|
|
||||||
|
function showProgress() {
|
||||||
|
document.getElementById('progressIndicator').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideProgress() {
|
||||||
|
document.getElementById('progressIndicator').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayOutput(result) {
|
||||||
|
const outputContainer = document.getElementById("output");
|
||||||
|
outputContainer.innerHTML = `<p>Status: ${result.status}</p>`;
|
||||||
|
if (result.output_file_path) {
|
||||||
|
outputContainer.innerHTML += `<p>File Path: ${result.output_file_path}</p>`;
|
||||||
|
}
|
||||||
|
if (result.output_file_url) {
|
||||||
|
outputContainer.innerHTML += `<p>File URL: ${result.output_file_url}</p>`;
|
||||||
|
}
|
||||||
|
if (result.error) {
|
||||||
|
outputContainer.innerHTML += `<p class="error">Error: ${result.error}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
fetchVoices();
|
||||||
|
|
||||||
|
const isDarkModeEnabled = localStorage.getItem('darkMode') === 'enabled';
|
||||||
|
const themeModeSelect = document.getElementById('themeMode');
|
||||||
|
if (isDarkModeEnabled) {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
themeModeSelect.value = 'dark';
|
||||||
|
} else {
|
||||||
|
themeModeSelect.value = 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add change event listener to the themeMode dropdown
|
||||||
|
themeModeSelect.addEventListener('change', toggleThemeMode);
|
||||||
|
|
||||||
|
// Initialize other elements if necessary
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large.
Load diff
+211
-987
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user