Spaces:
Sleeping
Sleeping
update TTS deployment fixes
Browse files- Dockerfile +10 -2
- app.py +65 -48
- requirements.txt +0 -1
Dockerfile
CHANGED
|
@@ -4,20 +4,28 @@ FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04
|
|
| 4 |
# Set non-interactive to avoid prompts during apt-get
|
| 5 |
ENV DEBIAN_FRONTEND=noninteractive
|
| 6 |
|
| 7 |
-
# Install Python 3.10, pip, and
|
| 8 |
RUN apt-get update && apt-get install -y \
|
| 9 |
python3 \
|
| 10 |
python3-pip \
|
| 11 |
ffmpeg \
|
|
|
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
| 14 |
# Set the working directory
|
| 15 |
WORKDIR /app
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
# Copy the requirements file into the container
|
| 18 |
COPY requirements.txt .
|
| 19 |
|
| 20 |
-
# Install dependencies
|
| 21 |
RUN pip3 install --no-cache-dir -r requirements.txt
|
| 22 |
|
| 23 |
# Copy the rest of the application code
|
|
|
|
| 4 |
# Set non-interactive to avoid prompts during apt-get
|
| 5 |
ENV DEBIAN_FRONTEND=noninteractive
|
| 6 |
|
| 7 |
+
# Install Python 3.10, pip, ffmpeg, and libsndfile1 for audio processing stability
|
| 8 |
RUN apt-get update && apt-get install -y \
|
| 9 |
python3 \
|
| 10 |
python3-pip \
|
| 11 |
ffmpeg \
|
| 12 |
+
libsndfile1 \
|
| 13 |
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
|
| 15 |
# Set the working directory
|
| 16 |
WORKDIR /app
|
| 17 |
|
| 18 |
+
# Upgrade pip for better dependency resolution
|
| 19 |
+
RUN pip3 install --no-cache-dir --upgrade pip
|
| 20 |
+
|
| 21 |
+
# Install PyTorch explicitly for CUDA 11.8 to ensure full RTX 3090 / Vast.ai compatibility
|
| 22 |
+
# (Done before other requirements to ensure correct indexing)
|
| 23 |
+
RUN pip3 install --no-cache-dir torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
|
| 24 |
+
|
| 25 |
# Copy the requirements file into the container
|
| 26 |
COPY requirements.txt .
|
| 27 |
|
| 28 |
+
# Install dependencies
|
| 29 |
RUN pip3 install --no-cache-dir -r requirements.txt
|
| 30 |
|
| 31 |
# Copy the rest of the application code
|
app.py
CHANGED
|
@@ -8,8 +8,12 @@ import asyncio
|
|
| 8 |
from typing import Optional
|
| 9 |
from pathlib import Path
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
from utils.config import (
|
| 15 |
DIR_TEMP, DIR_OUTPUT, DEFAULT_LANG, DEFAULT_VOICE_ID,
|
|
@@ -24,23 +28,23 @@ app = FastAPI(
|
|
| 24 |
version="1.0.0"
|
| 25 |
)
|
| 26 |
|
| 27 |
-
# Startup event to clear temp and output directories and
|
| 28 |
@app.on_event("startup")
|
| 29 |
async def startup_event():
|
| 30 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
for directory in [DIR_TEMP, DIR_OUTPUT]:
|
| 32 |
for item in directory.glob("*"):
|
| 33 |
-
|
| 34 |
-
item.
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
print(f"CUDA is available: {torch.cuda.get_device_name(0)}")
|
| 41 |
-
torch.cuda.empty_cache()
|
| 42 |
-
else:
|
| 43 |
-
print("CUDA is not available. Running on CPU (edge-tts is API based).")
|
| 44 |
|
| 45 |
class TTSRequest(BaseModel):
|
| 46 |
text: str = Field(..., description="Text to synthesize into speech")
|
|
@@ -48,13 +52,15 @@ class TTSRequest(BaseModel):
|
|
| 48 |
voice_id: str = Field(DEFAULT_VOICE_ID, description="Voice ID from registry")
|
| 49 |
custom_voice_name: Optional[str] = Field(None, description="Direct Voice name, e.g. 'en-US-GuyNeural'")
|
| 50 |
|
| 51 |
-
def remove_file(path: Path):
|
| 52 |
-
"""Background task to remove a file after returning it."""
|
|
|
|
| 53 |
try:
|
| 54 |
if path.exists():
|
| 55 |
path.unlink()
|
|
|
|
| 56 |
except Exception as e:
|
| 57 |
-
|
| 58 |
|
| 59 |
@app.post("/generate", summary="Generate TTS Audio from Text")
|
| 60 |
async def generate_audio(request: TTSRequest, background_tasks: BackgroundTasks):
|
|
@@ -73,36 +79,50 @@ async def generate_audio(request: TTSRequest, background_tasks: BackgroundTasks)
|
|
| 73 |
output_wav = DIR_OUTPUT / f"output_{req_id}.wav"
|
| 74 |
|
| 75 |
try:
|
| 76 |
-
#
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
if not chunks:
|
| 82 |
-
raise HTTPException(status_code=400, detail="No valid text found after cleaning.")
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
chunk_wav_paths.append(chunk_wav)
|
| 91 |
-
tasks.append(tts_to_wav(chunk, chunk_wav, voice_name))
|
| 92 |
-
|
| 93 |
-
# Run all edge-tts requests concurrently
|
| 94 |
-
await asyncio.gather(*tasks)
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
else:
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
-
#
|
| 104 |
-
if
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
# Schedule cleanup of the output file after it's returned
|
| 108 |
background_tasks.add_task(remove_file, output_wav)
|
|
@@ -122,7 +142,4 @@ async def generate_audio(request: TTSRequest, background_tasks: BackgroundTasks)
|
|
| 122 |
if output_wav.exists():
|
| 123 |
output_wav.unlink()
|
| 124 |
|
| 125 |
-
|
| 126 |
-
torch.cuda.empty_cache()
|
| 127 |
-
|
| 128 |
-
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
| 8 |
from typing import Optional
|
| 9 |
from pathlib import Path
|
| 10 |
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
# Configure basic logging
|
| 14 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 15 |
+
|
| 16 |
+
# GPU logic removed as edge-tts operates via API
|
| 17 |
|
| 18 |
from utils.config import (
|
| 19 |
DIR_TEMP, DIR_OUTPUT, DEFAULT_LANG, DEFAULT_VOICE_ID,
|
|
|
|
| 28 |
version="1.0.0"
|
| 29 |
)
|
| 30 |
|
| 31 |
+
# Startup event to clear temp and output directories and ensure they exist
|
| 32 |
@app.on_event("startup")
|
| 33 |
async def startup_event():
|
| 34 |
+
# Ensure directories always exist
|
| 35 |
+
for directory in [DIR_TEMP, DIR_OUTPUT]:
|
| 36 |
+
directory.mkdir(parents=True, exist_ok=True)
|
| 37 |
+
|
| 38 |
+
# Cleanup previous runs safely
|
| 39 |
for directory in [DIR_TEMP, DIR_OUTPUT]:
|
| 40 |
for item in directory.glob("*"):
|
| 41 |
+
try:
|
| 42 |
+
if item.is_file():
|
| 43 |
+
item.unlink()
|
| 44 |
+
elif item.is_dir():
|
| 45 |
+
shutil.rmtree(item)
|
| 46 |
+
except Exception as e:
|
| 47 |
+
logging.error(f"Cleanup error on {item}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
class TTSRequest(BaseModel):
|
| 50 |
text: str = Field(..., description="Text to synthesize into speech")
|
|
|
|
| 52 |
voice_id: str = Field(DEFAULT_VOICE_ID, description="Voice ID from registry")
|
| 53 |
custom_voice_name: Optional[str] = Field(None, description="Direct Voice name, e.g. 'en-US-GuyNeural'")
|
| 54 |
|
| 55 |
+
async def remove_file(path: Path):
|
| 56 |
+
"""Background task to safely remove a file after returning it with a 30-second delay."""
|
| 57 |
+
await asyncio.sleep(30) # Safe async delay to guarantee response finishes streaming before deletion
|
| 58 |
try:
|
| 59 |
if path.exists():
|
| 60 |
path.unlink()
|
| 61 |
+
logging.info(f"Safely deleted output file: {path}")
|
| 62 |
except Exception as e:
|
| 63 |
+
logging.error(f"Failed to delete {path}: {e}")
|
| 64 |
|
| 65 |
@app.post("/generate", summary="Generate TTS Audio from Text")
|
| 66 |
async def generate_audio(request: TTSRequest, background_tasks: BackgroundTasks):
|
|
|
|
| 79 |
output_wav = DIR_OUTPUT / f"output_{req_id}.wav"
|
| 80 |
|
| 81 |
try:
|
| 82 |
+
# 1. Chunk Text
|
| 83 |
+
chunks = chunk_text(request.text, lang=request.lang, max_chars=MAX_CHARS, min_chars=MIN_CHARS)
|
| 84 |
+
|
| 85 |
+
if not chunks:
|
| 86 |
+
raise HTTPException(status_code=400, detail="No valid text found after cleaning.")
|
|
|
|
|
|
|
| 87 |
|
| 88 |
+
# 2. Generate Audio for each chunk asynchronously with concurrency limit
|
| 89 |
+
sem = asyncio.Semaphore(5) # Max 5 concurrent connections to edge-tts to prevent rate limits
|
| 90 |
+
|
| 91 |
+
async def bounded_tts(chunk_text, chunk_wav_path, voice):
|
| 92 |
+
async with sem:
|
| 93 |
+
return await tts_to_wav(chunk_text, chunk_wav_path, voice)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
+
tasks = []
|
| 96 |
+
chunk_wav_paths = []
|
| 97 |
+
|
| 98 |
+
for i, chunk in enumerate(chunks):
|
| 99 |
+
chunk_wav = req_temp_dir / f"chunk_{i:04d}.wav"
|
| 100 |
+
chunk_wav_paths.append(chunk_wav)
|
| 101 |
+
tasks.append(bounded_tts(chunk, chunk_wav, voice_name))
|
| 102 |
+
|
| 103 |
+
# Run edge-tts requests concurrently with a limit and catch partial failures safely
|
| 104 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
| 105 |
+
|
| 106 |
+
successful_wav_paths = []
|
| 107 |
+
for path, result in zip(chunk_wav_paths, results):
|
| 108 |
+
if isinstance(result, Exception):
|
| 109 |
+
logging.error(f"Failed to generate TTS for chunk {path}: {result}")
|
| 110 |
else:
|
| 111 |
+
successful_wav_paths.append(path)
|
| 112 |
+
|
| 113 |
+
if not successful_wav_paths:
|
| 114 |
+
logging.error(f"All TTS chunks failed for request {req_id}")
|
| 115 |
+
raise HTTPException(
|
| 116 |
+
status_code=500,
|
| 117 |
+
detail="All TTS generation chunks failed due to API errors. Please verify the text content or try again later."
|
| 118 |
+
)
|
| 119 |
|
| 120 |
+
# 3. Concatenate
|
| 121 |
+
if len(successful_wav_paths) == 1:
|
| 122 |
+
# If only one chunk, just move it to output
|
| 123 |
+
shutil.move(str(successful_wav_paths[0]), str(output_wav))
|
| 124 |
+
else:
|
| 125 |
+
await concat_wavs_by_timeline(successful_wav_paths, output_wav)
|
| 126 |
|
| 127 |
# Schedule cleanup of the output file after it's returned
|
| 128 |
background_tasks.add_task(remove_file, output_wav)
|
|
|
|
| 142 |
if output_wav.exists():
|
| 143 |
output_wav.unlink()
|
| 144 |
|
| 145 |
+
raise HTTPException(status_code=500, detail=f"TTS Generation failed: {str(e)}")
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -4,5 +4,4 @@ edge-tts==6.1.9
|
|
| 4 |
nest_asyncio==1.5.8
|
| 5 |
tqdm==4.66.1
|
| 6 |
unidecode==1.3.7
|
| 7 |
-
torch==2.1.0 # Added for GPU support and optimization readiness
|
| 8 |
python-multipart==0.0.6
|
|
|
|
| 4 |
nest_asyncio==1.5.8
|
| 5 |
tqdm==4.66.1
|
| 6 |
unidecode==1.3.7
|
|
|
|
| 7 |
python-multipart==0.0.6
|