diff --git a/templates/admin.html b/templates/admin.html
index 2508fe2..02a756d 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -2,6 +2,7 @@
+
AllTalk TTS for Text generation webUI
@@ -108,6 +109,12 @@
gap: 4px;
}
+ #outputFileContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
label {
font-weight: bold;
font-size: 14px;
@@ -257,41 +264,95 @@
-
-
+
+
+
+
+
+
+
+
{{ output_file_path }}
-
+
Back to top of page
diff --git a/tts_server.py b/tts_server.py
index 10d69af..809489c 100644
--- a/tts_server.py
+++ b/tts_server.py
@@ -6,6 +6,8 @@ import torch
import torchaudio
from TTS.tts.configs.xtts_config import XttsConfig
from TTS.tts.models.xtts import Xtts
+import io
+import wave
##########################
#### Webserver Imports####
@@ -17,10 +19,9 @@ from fastapi import (
Response,
Depends,
)
-from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse, FileResponse
+from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse, FileResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
-from jinja2 import Template
from contextlib import asynccontextmanager
###########################
@@ -452,12 +453,23 @@ async def deepspeed(request: Request, new_deepspeed_value: bool):
########################
#### TTS GENERATION ####
########################
+
# TTS VOICE GENERATION METHODS (called from voice_preview and output_modifer)
-async def generate_audio(text, voice, language, output_file):
+async def generate_audio(text, voice, language, output_file, streaming=False):
+ # Get the async generator from the internal function
+ response = generate_audio_internal(text, voice, language, output_file, streaming)
+ # If streaming, then return the generator as-is, otherwise just exhaust it and return
+ if streaming:
+ return response
+ async for _ in response:
+ pass
+
+async def generate_audio_internal(text, voice, language, output_file, streaming):
global model
if params["low_vram"] and device == "cpu":
await switch_device()
generate_start_time = time.time() # Record the start time of generating TTS
+
# XTTSv2 LOCAL & Xttsv2 FT Method
if params["tts_method_xtts_local"] or tts_method_xtts_ft:
print(f"[{params['branding']}TTSGen] {text}")
@@ -467,11 +479,16 @@ async def generate_audio(text, voice, language, output_file):
max_ref_length=model.config.max_ref_len,
sound_norm_refs=model.config.sound_norm_refs,
)
- out = model.inference(
+
+ # If the user has requested streaming, then use the TTS.inference_stream function,
+ # otherwise use TTS.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"]),
@@ -479,9 +496,46 @@ async def generate_audio(text, voice, language, output_file):
top_p=float(model.config.top_p),
enable_text_splitting=True,
)
- torchaudio.save(output_file, torch.tensor(out["wav"]).unsqueeze(0), 24000)
+
+ if streaming:
+ # If streaming, then yield each chunk of audio as it is generated. We also collect each
+ # 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
+ wav_buf = io.BytesIO()
+ with wave.open(wav_buf, "wb") as vfout:
+ vfout.setnchannels(1)
+ vfout.setsampwidth(2)
+ vfout.setframerate(24000)
+ vfout.writeframes(b"")
+ wav_buf.seek(0)
+ yield wav_buf.read()
+
+ # Yield each chunk of audio as it is generated
+ for i, chunk in enumerate(output):
+ # Append the chunk to later save to a file
+ file_chunks.append(chunk)
+ # Process the wav data
+ if isinstance(chunk, list):
+ chunk = torch.cat(chunk, dim=0)
+ chunk = chunk.clone().detach().cpu().numpy()
+ chunk = chunk[None, : int(chunk.shape[0])]
+ chunk = np.clip(chunk, -1, 1)
+ chunk = (chunk * 32767).astype(np.int16)
+ # Then yield the chunk to be sent to the client
+ yield chunk.tobytes()
+
+ else:
+ # If not streaming, then save to the file on disk
+ torchaudio.save(output_file, torch.tensor(output["wav"]).unsqueeze(0), 24000)
+
# API LOCAL Methods
elif params["tts_method_api_local"]:
+ # Streaming only allowed for XTTSv2 local
+ if streaming:
+ raise ValueError("Streaming is only supported in XTTSv2 local")
+
# Set the correct output path (different from the if statement)
print(f"[{params['branding']}TTSGen] Using API Local")
model.tts_to_file(
@@ -495,8 +549,13 @@ async def generate_audio(text, voice, language, output_file):
top_k=model.config.top_k,
top_p=model.config.top_p,
)
+
# API TTS
elif params["tts_method_api_tts"]:
+ # Streaming only allowed for XTTSv2 local
+ if streaming:
+ raise ValueError("Streaming is only supported in XTTSv2 local")
+
print(f"[{params['branding']}TTSGen] Using API TTS")
model.tts_to_file(
text=text,
@@ -504,6 +563,7 @@ async def generate_audio(text, voice, language, output_file):
speaker_wav=[f"{this_dir}/voices/{voice}"],
language=language,
)
+
# Print Generation time and settings
generate_end_time = time.time() # Record the end time to generate TTS
generate_elapsed_time = generate_end_time - generate_start_time
@@ -526,8 +586,11 @@ async def generate(request: Request):
voice = data["voice"]
language = data["language"]
output_file = data["output_file"]
+ streaming = data["streaming"] or False
# Generation logic
- await generate_audio(text, voice, language, output_file)
+ response = await generate_audio(text, voice, language, output_file, streaming)
+ if streaming:
+ return StreamingResponse(response, media_type="audio/wav")
return JSONResponse(
content={"status": "generate-success", "data": {"audio_path": output_file}}
)
@@ -637,14 +700,25 @@ async def update_settings(
##################################
#### SETTINGS PAGE DEMO VOICE ####
##################################
-# Submit demo post here
+
+@app.get("/tts-demo-request", response_class=StreamingResponse)
+async def tts_demo_request_streaming(text: str, voice: str, language: str, output_file: str):
+ try:
+ output_file_path = this_dir / "outputs" / output_file
+ stream = await generate_audio(text, voice, language, output_file_path, streaming=True)
+ return StreamingResponse(stream, media_type="audio/wav")
+ except Exception as e:
+ print(f"An error occurred: {e}")
+ return JSONResponse(content={"error": "An error occurred"}, status_code=500)
+
@app.post("/tts-demo-request", response_class=JSONResponse)
async def tts_demo_request(request: Request, text: str = Form(...), voice: str = Form(...), language: str = Form(...), output_file: str = Form(...)):
try:
output_file_path = this_dir / "outputs" / output_file
- await generate_audio(text, voice, language, output_file_path)
+ await generate_audio(text, voice, language, output_file_path, streaming=False)
return JSONResponse(content={"output_file_path": str(output_file)}, status_code=200)
except Exception as e:
+ print(f"An error occurred: {e}")
return JSONResponse(content={"error": "An error occurred"}, status_code=500)
# Gives web access to the output files
@@ -682,7 +756,7 @@ async def preview_voice(request: Request, voice: str = Form(...)):
# Generate the audio
output_file_path = this_dir / "outputs" / f"{output_file_name}.wav"
- await generate_audio(text, voice, language, output_file_path)
+ await generate_audio(text, voice, language, output_file_path, streaming=False)
# Generate the URL
output_file_url = f'http://{params["ip_address"]}:{params["port_number"]}/audio/{output_file_name}.wav'
@@ -837,6 +911,7 @@ async def tts_generate(
output_file_timestamp: bool = Form(...),
autoplay: bool = Form(...),
autoplay_volume: float = Form(...),
+ streaming: bool = Form(False),
):
try:
json_input_data = {
@@ -851,6 +926,7 @@ async def tts_generate(
"output_file_timestamp": output_file_timestamp,
"autoplay": autoplay,
"autoplay_volume": autoplay_volume,
+ "streaming": streaming,
}
JSONresult = TTSGenerator.validate_json_input(json_input_data)
if JSONresult is None:
@@ -894,7 +970,7 @@ async def tts_generate(
voice_to_use = narrator_voice_gen
output_file = this_dir / "outputs" / f"{output_file_name}_{uuid.uuid4()}_{int(time.time())}_{i}.wav"
output_file_str = output_file.as_posix()
- await generate_audio(cleaned_part, voice_to_use, language, output_file_str)
+ response = await generate_audio(cleaned_part, voice_to_use, language, output_file_str, streaming)
audio_path = output_file_str
audio_files_paragraph.append(audio_path)
# Accumulate audio files within the paragraph
@@ -915,11 +991,13 @@ async def tts_generate(
cleaned_string = standard_filtering(text_input)
else:
cleaned_string = text_input
- await generate_audio(cleaned_string, character_voice_gen, language, output_file_path)
- if sounddevice_installed == False:
+ response = await generate_audio(cleaned_string, character_voice_gen, language, output_file_path, streaming)
+ if sounddevice_installed == False or streaming == True:
autoplay = False
if autoplay:
play_audio(output_file_path, autoplay_volume)
+ if streaming:
+ 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)
except Exception as e:
return JSONResponse(content={"status": "generate-failure", "error": "An error occurred"}, status_code=500)