Add files via upload

This commit is contained in:
erew123 authored and GitHub committed 2024-01-01 18:10:14 +00:00
1 parent df69fb0b07
commit 2dd303f53c
4 files changed
+1886 -782

No files matched your search

+78 -67
View File
@@ -509,11 +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) + '&quot;<*' # Normalize HTML encoded quotes
text = html.unescape(text)
def replace_asterisk(match): # Replace ellipsis with a single dot
return '&quot;<*' 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 ####
@@ -534,86 +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('&quot;\n\n*', '&quot;<*'))
processed_string = (processed_string .replace('&quot;\n', '< '))
processed_string = (processed_string .replace("&#x27;", "'"))
processed_string = (processed_string .replace("\n", " "))
processed_string = (processed_string .replace('&quot;', '&quot;<'))
processed_string = (processed_string .replace('"', '&quot;<'))
#pattern = re.compile(r'(?<=[a-zA-Z!])&quot;<(?=[a-zA-Z])')
pattern = re.compile(r'(?<=[a-zA-Z!?\.])&quot;<(?=[a-zA-Z])')
processed_string = pattern.sub('&quot; ', processed_string)
pattern = re.compile(r'(?<=[a-zA-Z])\*(?=[a-zA-Z])')
processed_string = pattern.sub('*&quot;', processed_string)
processed_string = processed_string.replace('&quot;<. *', '&quot;< *"')
processed_string = processed_string.replace('< *"', '< *')
processed_string = processed_string.replace('. *', '< *')
processed_string = processed_string.replace('! *', '&quot;< *')
processed_string = processed_string.replace('? *', '< *')
processed_string = processed_string.replace('* ', '* &quot;<')
processed_string = processed_string.replace('*. ', '* &quot;<')
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.
audio_files_all_paragraphs = [] audio_files_all_paragraphs = []
# Split the line using &quot; 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'&quot;|\.\*', 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
#print("PART IS:", part) if part_type == 'narrator':
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('*', ''))
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 &quot; 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':
#print("HIT ELIF CHAR VOICE")
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:
#print("HIT ELIF NARRATOR VOICE") # Handle ambiguous parts based on user preference
cleaned_part = html.unescape(part.replace('< ', '').replace('< ', '').replace('< ', '')) voice_to_use = params["voice"] if non_quoted_text_is else params["narrator_voice"]
voice_to_use = 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"
# Generate that TTS and output to a file 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("&#x27;", "'") # 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')
+584
View File
@@ -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>
+1094 -635
View File
File diff suppressed because it is too large. Load diff
+130 -80
View File
@@ -18,8 +18,10 @@ from fastapi import (
Request, Request,
Response, Response,
Depends, Depends,
HTTPException,
) )
from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse, FileResponse, StreamingResponse from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse, FileResponse, StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@@ -120,7 +122,14 @@ async def startup_shutdown(no_actual_value_it_demanded_something_be_here):
# Create FastAPI app with lifespan # Create FastAPI app with lifespan
app = FastAPI(lifespan=startup_shutdown) app = FastAPI(lifespan=startup_shutdown)
# Allow all origins, and set other CORS options
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Set this to the specific origins you want to allow
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
##################################### #####################################
#### MODEL LOADING AND UNLOADING #### #### MODEL LOADING AND UNLOADING ####
@@ -480,29 +489,32 @@ async def generate_audio_internal(text, voice, language, output_file, streaming)
sound_norm_refs=model.config.sound_norm_refs, sound_norm_refs=model.config.sound_norm_refs,
) )
# If the user has requested streaming, then use the TTS.inference_stream function, # Common arguments for both functions
# otherwise use TTS.inference common_args = {
"text": text,
"language": language,
"gpt_cond_latent": gpt_cond_latent,
"speaker_embedding": speaker_embedding,
"temperature": float(params["local_temperature"]),
"length_penalty": float(model.config.length_penalty),
"repetition_penalty": float(params["local_repetition_penalty"]),
"top_k": int(model.config.top_k),
"top_p": float(model.config.top_p),
"enable_text_splitting": True
}
# Determine the correct inference function and add streaming specific argument if needed
inference_func = model.inference_stream if streaming else model.inference inference_func = model.inference_stream if streaming else model.inference
output = inference_func(
text,
language,
gpt_cond_latent=gpt_cond_latent,
speaker_embedding=speaker_embedding,
stream_chunk_size=20 if streaming else None,
temperature=float(params["local_temperature"]),
length_penalty=float(model.config.length_penalty),
repetition_penalty=float(params["local_repetition_penalty"]),
top_k=int(model.config.top_k), # Convert to int if necessary
top_p=float(model.config.top_p),
enable_text_splitting=True,
)
if streaming: if streaming:
# If streaming, then yield each chunk of audio as it is generated. We also collect each common_args["stream_chunk_size"] = 20
# chunk in a list so that we can save the full audio to a file once the stream is finished.
file_chunks = []
# Create and yield a wav file with no data to start the stream # Call the appropriate function
output = inference_func(**common_args)
# Process the output based on streaming or non-streaming
if streaming:
# Streaming-specific operations
file_chunks = []
wav_buf = io.BytesIO() wav_buf = io.BytesIO()
with wave.open(wav_buf, "wb") as vfout: with wave.open(wav_buf, "wb") as vfout:
vfout.setnchannels(1) vfout.setnchannels(1)
@@ -512,24 +524,20 @@ async def generate_audio_internal(text, voice, language, output_file, streaming)
wav_buf.seek(0) wav_buf.seek(0)
yield wav_buf.read() yield wav_buf.read()
# Yield each chunk of audio as it is generated
for i, chunk in enumerate(output): for i, chunk in enumerate(output):
# Append the chunk to later save to a file
file_chunks.append(chunk) file_chunks.append(chunk)
# Process the wav data
if isinstance(chunk, list): if isinstance(chunk, list):
chunk = torch.cat(chunk, dim=0) chunk = torch.cat(chunk, dim=0)
chunk = chunk.clone().detach().cpu().numpy() chunk = chunk.clone().detach().cpu().numpy()
chunk = chunk[None, : int(chunk.shape[0])] chunk = chunk[None, : int(chunk.shape[0])]
chunk = np.clip(chunk, -1, 1) chunk = np.clip(chunk, -1, 1)
chunk = (chunk * 32767).astype(np.int16) chunk = (chunk * 32767).astype(np.int16)
# Then yield the chunk to be sent to the client
yield chunk.tobytes() yield chunk.tobytes()
else: else:
# If not streaming, then save to the file on disk # Non-streaming-specific operation
torchaudio.save(output_file, torch.tensor(output["wav"]).unsqueeze(0), 24000) torchaudio.save(output_file, torch.tensor(output["wav"]).unsqueeze(0), 24000)
# API LOCAL Methods # API LOCAL Methods
elif params["tts_method_api_local"]: elif params["tts_method_api_local"]:
# Streaming only allowed for XTTSv2 local # Streaming only allowed for XTTSv2 local
@@ -586,7 +594,7 @@ async def generate(request: Request):
voice = data["voice"] voice = data["voice"]
language = data["language"] language = data["language"]
output_file = data["output_file"] output_file = data["output_file"]
streaming = data["streaming"] or False streaming = False
# Generation logic # Generation logic
response = await generate_audio(text, voice, language, output_file, streaming) response = await generate_audio(text, voice, language, output_file, streaming)
if streaming: if streaming:
@@ -727,6 +735,22 @@ async def get_audio(filename: str):
audio_path = this_dir / "outputs" / filename audio_path = this_dir / "outputs" / filename
return FileResponse(audio_path) return FileResponse(audio_path)
@app.get("/audiocache/{filename}")
async def get_audio(filename: str):
audio_path = Path("outputs") / filename
if not audio_path.is_file():
raise HTTPException(status_code=404, detail="File not found")
response = FileResponse(
path=audio_path,
media_type='audio/wav',
filename=filename
)
# Set caching headers
response.headers["Cache-Control"] = "public, max-age=604800" # Cache for one week
response.headers["ETag"] = str(audio_path.stat().st_mtime) # Use the file's last modified time as a simple ETag
return response
######################### #########################
#### VOICES LIST API #### #### VOICES LIST API ####
@@ -840,6 +864,45 @@ class TTSGenerator:
except ValidationError as e: except ValidationError as e:
return str(e) return str(e)
def process_text(text):
# 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
def standard_filtering(text_input): def standard_filtering(text_input):
text_output = (text_input text_output = (text_input
.replace("***", "") .replace("***", "")
@@ -850,20 +913,6 @@ def standard_filtering(text_input):
) )
return text_output return text_output
def narrator_filtering(narrator_text_input):
processed_string = re.sub(r'\.\*\n\*', '. ', narrator_text_input)
processed_string = (
processed_string
.replace("\n", " ")
.replace('&quot;', '&quot;<')
.replace('"', '&quot;<')
)
processed_string = processed_string.replace('&quot;<. *', '&quot;< *"')
processed_string = processed_string.replace('< *"', '< *')
processed_string = processed_string.replace('. *', '< *')
text_output = html.unescape(processed_string)
return text_output
def combine(output_file_timestamp, output_file_name, audio_files): def combine(output_file_timestamp, output_file_name, audio_files):
audio = np.array([]) audio = np.array([])
sample_rate = None sample_rate = None
@@ -884,9 +933,11 @@ def combine(output_file_timestamp, output_file_name, audio_files):
timestamp = int(time.time()) timestamp = int(time.time())
output_file_path = os.path.join(this_dir / "outputs" / f'{output_file_name}_{timestamp}_combined.wav') output_file_path = os.path.join(this_dir / "outputs" / f'{output_file_name}_{timestamp}_combined.wav')
output_file_url = f'http://{params["ip_address"]}:{params["port_number"]}/audio/{output_file_name}_{timestamp}_combined.wav' output_file_url = f'http://{params["ip_address"]}:{params["port_number"]}/audio/{output_file_name}_{timestamp}_combined.wav'
output_cache_url = f'http://{params["ip_address"]}:{params["port_number"]}/audiocache/{output_file_name}_{timestamp}_combined.wav'
else: else:
output_file_path = os.path.join(this_dir / "outputs" / f'{output_file_name}_combined.wav') output_file_path = os.path.join(this_dir / "outputs" / f'{output_file_name}_combined.wav')
output_file_url = f'http://{params["ip_address"]}:{params["port_number"]}/audio/{output_file_name}.wav' output_file_url = f'http://{params["ip_address"]}:{params["port_number"]}/audio/{output_file_name}.wav'
output_cache_url = f'http://{params["ip_address"]}:{params["port_number"]}/audiocache/{output_file_name}.wav'
try: try:
sf.write(output_file_path, audio, samplerate=sample_rate) sf.write(output_file_path, audio, samplerate=sample_rate)
# Clean up unnecessary files # Clean up unnecessary files
@@ -895,7 +946,7 @@ def combine(output_file_timestamp, output_file_name, audio_files):
except Exception as e: except Exception as e:
# Handle exceptions (e.g., failed to write output file) # Handle exceptions (e.g., failed to write output file)
return None, None return None, None
return output_file_path, output_file_url return output_file_path, output_file_url, output_cache_url
# Generation API (separate from text-generation-webui) # Generation API (separate from text-generation-webui)
@app.post("/api/tts-generate", response_class=JSONResponse) @app.post("/api/tts-generate", response_class=JSONResponse)
@@ -934,61 +985,60 @@ async def tts_generate(
else: else:
return JSONResponse(content={"error": JSONresult}, status_code=400) return JSONResponse(content={"error": JSONresult}, status_code=400)
if narrator_enabled: if narrator_enabled:
#print("ORIGINAL UNTOUCHED STRING IS:", text_input,"\n") processed_parts = process_text(text_input)
if text_filtering in ["standard", "none"]:
cleaned_string = (
text_input
.replace('"', '"<')
.replace('*', '<*')
)
parts = re.split(r'(?<=<\*\s)|(?<=\.\s)|(?<=\."\<)|(?<=\."<)', cleaned_string)
parts = list(filter(lambda x: x.strip(), parts))
elif text_filtering == "html":
cleaned_string = standard_filtering(text_input)
cleaned_string = narrator_filtering(cleaned_string)
parts = re.split(r'&quot;|\.\*', cleaned_string)
audio_files_all_paragraphs = [] audio_files_all_paragraphs = []
audio_files_paragraph = [] for part_type, part in processed_parts:
for i, part in enumerate(parts): # Skip parts that are too short
if len(part.strip()) <= 1: 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. # Determine the voice to use based on the part type
# Character will always be a < with a letter immediately after it if part_type == 'narrator':
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 = character_voice_gen
#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('*', '').replace('<. ', ''))
voice_to_use = narrator_voice_gen voice_to_use = narrator_voice_gen
#If the other two dont capture it, aka, the AI gave no * or &quot; 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 text_not_inside == "character": elif part_type == 'character':
cleaned_part = html.unescape(part.replace('< ', '').replace('< ', '').replace('< ', ''))
voice_to_use = character_voice_gen voice_to_use = character_voice_gen
elif text_not_inside == "narrator": print(f"[{params['branding']}TTSGen] \033[36mCharacter\033[0m") # Yellow
cleaned_part = html.unescape(part.replace('< ', '').replace('< ', '').replace('< ', '')) else:
voice_to_use = narrator_voice_gen # Handle ambiguous parts based on user preference
output_file = this_dir / "outputs" / f"{output_file_name}_{uuid.uuid4()}_{int(time.time())}_{i}.wav" voice_to_use = character_voice_gen if text_not_inside == "character" else narrator_voice_gen
voice_description = "\033[36mCharacter (Text-not-inside)\033[0m" if text_not_inside == "character" 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)
output_file = this_dir / "outputs" / f"{output_file_name}_{uuid.uuid4()}_{int(time.time())}.wav"
output_file_str = output_file.as_posix() output_file_str = output_file.as_posix()
response = await generate_audio(cleaned_part, voice_to_use, language, output_file_str, streaming) response = await generate_audio(cleaned_part, voice_to_use, language, output_file_str, streaming)
audio_path = output_file_str audio_path = output_file_str
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
output_file_path, output_file_url = combine(output_file_timestamp, output_file_name, audio_files_all_paragraphs) output_file_path, output_file_url, output_cache_url = combine(output_file_timestamp, output_file_name, audio_files_all_paragraphs)
else: else:
if output_file_timestamp: if output_file_timestamp:
timestamp = int(time.time()) timestamp = int(time.time())
output_file_path = this_dir / "outputs" / f"{output_file_name}_{timestamp}.wav" output_file_path = this_dir / "outputs" / f"{output_file_name}_{timestamp}.wav"
output_file_url = f'http://{params["ip_address"]}:{params["port_number"]}/audio/{output_file_name}_{timestamp}.wav' output_file_url = f'http://{params["ip_address"]}:{params["port_number"]}/audio/{output_file_name}_{timestamp}.wav'
output_cache_url = f'http://{params["ip_address"]}:{params["port_number"]}/audiocache/{output_file_name}_{timestamp}.wav'
else: else:
output_file_path = this_dir / "outputs" / f"{output_file_name}.wav" output_file_path = this_dir / "outputs" / f"{output_file_name}.wav"
output_file_url = f'http://{params["ip_address"]}:{params["port_number"]}/audio/{output_file_name}.wav' output_file_url = f'http://{params["ip_address"]}:{params["port_number"]}/audio/{output_file_name}.wav'
output_cache_url = f'http://{params["ip_address"]}:{params["port_number"]}/audiocache/{output_file_name}_{timestamp}.wav'
if text_filtering == "html": if text_filtering == "html":
cleaned_string = html.unescape(standard_filtering(text_input)) cleaned_string = html.unescape(standard_filtering(text_input))
cleaned_string = re.sub(r'([!?.])\1+', r'\1', text_input)
# Further clean to remove any other unwanted characters
cleaned_string = re.sub(r'[^a-zA-Z0-9\s\.,;:!?\-\'"]', '', cleaned_string)
# Remove all newline characters (single or multiple)
cleaned_string = re.sub(r'\n+', ' ', cleaned_string)
elif text_filtering == "standard": elif text_filtering == "standard":
cleaned_string = standard_filtering(text_input) cleaned_string = re.sub(r'([!?.])\1+', r'\1', text_input)
# Further clean to remove any other unwanted characters
cleaned_string = re.sub(r'[^a-zA-Z0-9\s\.,;:!?\-\'"]', '', cleaned_string)
# Remove all newline characters (single or multiple)
cleaned_string = re.sub(r'\n+', ' ', cleaned_string)
else: else:
cleaned_string = text_input cleaned_string = text_input
response = await generate_audio(cleaned_string, character_voice_gen, language, output_file_path, streaming) response = await generate_audio(cleaned_string, character_voice_gen, language, output_file_path, streaming)
@@ -998,7 +1048,7 @@ async def tts_generate(
play_audio(output_file_path, autoplay_volume) play_audio(output_file_path, autoplay_volume)
if streaming: if streaming:
return StreamingResponse(response, media_type="audio/wav") return StreamingResponse(response, media_type="audio/wav")
return JSONResponse(content={"status": "generate-success", "output_file_path": str(output_file_path), "output_file_url": str(output_file_url)}, status_code=200) return JSONResponse(content={"status": "generate-success", "output_file_path": str(output_file_path), "output_file_url": str(output_file_url), "output_cache_url": str(output_cache_url)}, status_code=200)
except Exception as e: except Exception as e:
return JSONResponse(content={"status": "generate-failure", "error": "An error occurred"}, status_code=500) return JSONResponse(content={"status": "generate-failure", "error": "An error occurred"}, status_code=500)