diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..507675c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.associations": { + "*.html": "jinja-html" + }, + "emmet.includeLanguages": { + "jinja-html": "html", + "jinja2": "html" + }, +} \ No newline at end of file diff --git a/README.md b/README.md index 61bf387..1eece6d 100644 --- a/README.md +++ b/README.md @@ -69,11 +69,13 @@ 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. #### 🟩 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 This is pretty much a repeat of the installation process. diff --git a/script.py b/script.py index 3e6d98e..d2a42dd 100644 --- a/script.py +++ b/script.py @@ -509,11 +509,53 @@ def voice_preview(string): return f"[{params['branding']}Server] Audio generation failed. Status: {generate_response.get('status')}" -#def replace_asterisk(match): -# return match.group(0) + '"<*' +def process_text(text): + # Normalize HTML encoded quotes + text = html.unescape(text) -def replace_asterisk(match): - return '"<*' + # 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 #### @@ -534,86 +576,55 @@ def output_modifier(string, state): if process_lock.acquire(blocking=False): try: if params["narrator_enabled"]: - # Do Some basic stripping and remove any double CR's - 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) - #print("PROCESSED STRING IS:", processed_string) - # Set up a tracking of the individual wav files. + processed_parts = process_text(original_string) + 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 * - parts = re.split(r'"|\.\*', processed_string) - audio_files_paragraph = [] - for i, part in enumerate(parts): + for part_type, part in processed_parts: # Skip parts that are too short if len(part.strip()) <= 3: 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 - #print("PART IS:", part) - if '<' in part and '< *' not in part and '<*' not in part and '< *' not in part and '< ' not in part and '< ' not in part: - 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('*', '')) + + # Determine the voice to use based on the part type + if part_type == 'narrator': 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 - elif non_quoted_text_is: - #print("HIT ELIF CHAR VOICE") - cleaned_part = html.unescape(part.replace('< ', '').replace('< ', '').replace('< ', '')) + print(f"[{params['branding']}TTSGen] \033[92mNarrator\033[0m") # Green + elif part_type == 'character': voice_to_use = params["voice"] + print(f"[{params['branding']}TTSGen] \033[36mCharacter\033[0m") # Yellow else: - #print("HIT ELIF NARRATOR VOICE") - cleaned_part = html.unescape(part.replace('< ', '').replace('< ', '').replace('< ', '')) - voice_to_use = params["narrator_voice"] - # Generate that TTS and output to a file + # Handle ambiguous parts based on user preference + voice_to_use = params["voice"] if non_quoted_text_is else params["narrator_voice"] + 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) generate_response = send_generate_request( cleaned_part, voice_to_use, language_code, output_filename ) audio_path = generate_response.get("data", {}).get("audio_path") - audio_files_paragraph.append(audio_path) - # Accumulate audio files within the paragraph - audio_files_all_paragraphs.extend(audio_files_paragraph) + audio_files_all_paragraphs.append(audio_path) + # Combine audio files across paragraphs final_output_file = combine( audio_files_all_paragraphs, params["output_folder_wav"], state ) else: - processed_string = ( - original_string - .replace("***", "") - .replace("**", "") - .replace("*", "") - .replace("\n\n", "\n") - .replace("'", "'") - ) - cleaned_part = html.unescape(processed_string) + # Decode HTML entities first + cleaned_part = html.unescape(original_string) + # Replace multiple instances of certain punctuation marks with a single instance + cleaned_part = re.sub(r'([!?.])\1+', r'\1', cleaned_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) # Process the part and give it a non-character name if being used vai API or standalone. if "character_menu" in state: output_file = Path(f'{params["output_folder_wav"]}/{state["character_menu"]}_{int(time.time())}.wav') @@ -721,10 +732,6 @@ def input_modifier(string, state): shared.processing_message = "*Is recording a voice message...*" return string -def custom_css(): - path_to_css = Path(f"{this_dir}/style.css") - return open(path_to_css, "r").read() - def ui(): with gr.Accordion(params["branding"] + " TTS (XTTSv2)"): diff --git a/style.css b/style.css deleted file mode 100644 index 2330a74..0000000 --- a/style.css +++ /dev/null @@ -1,8 +0,0 @@ -.SDAP .hires_opts input[type="number"] { - width: 6em !important; -} - -/* silero_tts preview */ -.form:has(> #silero_preview_text) { - min-width: 75% -} diff --git a/templates/TTS_Input_Proof_of_concept.html b/templates/TTS_Input_Proof_of_concept.html new file mode 100644 index 0000000..0cb5333 --- /dev/null +++ b/templates/TTS_Input_Proof_of_concept.html @@ -0,0 +1,584 @@ + + + +
+ + +