USF00 commited on
Commit
c1acbd7
·
1 Parent(s): 493b3af

update TTS deployment fixes

Browse files
Files changed (3) hide show
  1. Dockerfile +10 -2
  2. app.py +65 -48
  3. 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 ffmpeg
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 (including torch for potential GPU inference)
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
- # GPU Optimization Imports (as per user request)
12
- import torch
 
 
 
 
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 initialize GPU
28
  @app.on_event("startup")
29
  async def startup_event():
30
- # Cleanup previous runs
 
 
 
 
31
  for directory in [DIR_TEMP, DIR_OUTPUT]:
32
  for item in directory.glob("*"):
33
- if item.is_file():
34
- item.unlink()
35
- elif item.is_dir():
36
- shutil.rmtree(item)
37
-
38
- # Optional GPU Warmup/Initialization
39
- if torch.cuda.is_available():
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
- print(f"Failed to delete {path}: {e}")
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
- # Wrap inference simulation in no_grad for GPU memory optimization requirements
77
- with torch.no_grad():
78
- # 1. Chunk Text
79
- chunks = chunk_text(request.text, lang=request.lang, max_chars=MAX_CHARS, min_chars=MIN_CHARS)
80
-
81
- if not chunks:
82
- raise HTTPException(status_code=400, detail="No valid text found after cleaning.")
83
 
84
- # 2. Generate Audio for each chunk asynchronously
85
- tasks = []
86
- chunk_wav_paths = []
87
-
88
- for i, chunk in enumerate(chunks):
89
- chunk_wav = req_temp_dir / f"chunk_{i:04d}.wav"
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
- # 3. Concatenate
97
- if len(chunk_wav_paths) == 1:
98
- # If only one chunk, just move it to output
99
- shutil.move(str(chunk_wav_paths[0]), str(output_wav))
 
 
 
 
 
 
 
 
 
 
 
100
  else:
101
- await concat_wavs_by_timeline(chunk_wav_paths, output_wav)
 
 
 
 
 
 
 
102
 
103
- # Ensure CUDA cache is cleared after generation to prevent memory leaks
104
- if torch.cuda.is_available():
105
- torch.cuda.empty_cache()
 
 
 
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
- if torch.cuda.is_available():
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