aspirant312 commited on
Commit
dde2f3d
·
0 Parent(s):

Initial commit: Parler TTS FastAPI with Docker

Browse files
Files changed (5) hide show
  1. .gitignore +13 -0
  2. Dockerfile +24 -0
  3. README.md +151 -0
  4. api.py +205 -0
  5. requirements.txt +20 -0
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .env
8
+ .venv
9
+ venv/
10
+ *.log
11
+ .DS_Store
12
+ .claude/
13
+ test_speech.wav
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM pytorch/pytorch:2.1.0-cuda11.8-runtime-ubuntu22.04
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ git \
8
+ libsndfile1 \
9
+ ffmpeg \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements and install dependencies
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+ RUN pip install --no-cache-dir uvicorn[standard]
16
+
17
+ # Copy application code
18
+ COPY api.py .
19
+
20
+ # Expose port
21
+ EXPOSE 7860
22
+
23
+ # Run the application
24
+ CMD ["python", "-m", "uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Parler TTS API
3
+ emoji: 🎙️
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_file: api.py
8
+ python_version: 3.10
9
+ ---
10
+
11
+ # Indic Parler-TTS API
12
+
13
+ FastAPI endpoint for Urdu Text-to-Speech using [ai4bharat/indic-parler-tts](https://huggingface.co/ai4bharat/indic-parler-tts).
14
+
15
+ ## API Endpoints
16
+
17
+ ### Health Check
18
+ ```
19
+ GET /
20
+ ```
21
+ Returns model status and available speakers.
22
+
23
+ **Response:**
24
+ ```json
25
+ {
26
+ "status": "ok",
27
+ "model": "Indic Parler-TTS",
28
+ "speakers": ["Divya", "Rani", "Rohit", "Aman", "Generic Female", "Generic Male"],
29
+ "sample_rate": 24000
30
+ }
31
+ ```
32
+
33
+ ### Generate Speech
34
+ ```
35
+ POST /tts
36
+ ```
37
+
38
+ **Request Body:**
39
+ ```json
40
+ {
41
+ "text": "السلام علیکم، میرا نام اردو ٹی ٹی ایس ہے۔",
42
+ "speaker": "Divya",
43
+ "pitch": "Moderate",
44
+ "rate": "Moderate",
45
+ "temperature": 0.8,
46
+ "do_sample": true
47
+ }
48
+ ```
49
+
50
+ **Parameters:**
51
+ - `text` (string, required): Urdu text to synthesize
52
+ - `speaker` (string, optional): Speaker name. Options: `Divya`, `Rani`, `Rohit`, `Aman`, `Generic Female`, `Generic Male`. Default: `Divya`
53
+ - `pitch` (string, optional): Voice pitch. Options: `High`, `Moderate`, `Low`. Default: `Moderate`
54
+ - `rate` (string, optional): Speaking rate. Options: `Slow`, `Moderate`, `Fast`. Default: `Moderate`
55
+ - `temperature` (float, optional): Sampling temperature (0.1-2.0). Default: `0.8`
56
+ - `do_sample` (boolean, optional): Use sampling vs greedy decoding. Default: `true`
57
+
58
+ **Response:**
59
+ - WAV audio file (audio/wav)
60
+
61
+ ### Get Available Speakers
62
+ ```
63
+ GET /speakers
64
+ ```
65
+
66
+ **Response:**
67
+ ```json
68
+ {
69
+ "speakers": ["Divya", "Rani", "Rohit", "Aman", "Generic Female", "Generic Male"]
70
+ }
71
+ ```
72
+
73
+ ## Example Usage
74
+
75
+ ### cURL
76
+ ```bash
77
+ curl -X POST http://localhost:7860/tts \
78
+ -H "Content-Type: application/json" \
79
+ -d '{
80
+ "text": "السلام علیکم",
81
+ "speaker": "Divya",
82
+ "pitch": "Moderate",
83
+ "rate": "Moderate"
84
+ }' \
85
+ --output speech.wav
86
+ ```
87
+
88
+ ### Python
89
+ ```python
90
+ import requests
91
+ import json
92
+
93
+ url = "http://localhost:7860/tts"
94
+ payload = {
95
+ "text": "السلام علیکم، میرا نام اردو ٹی ٹی ایس ہے۔",
96
+ "speaker": "Divya",
97
+ "pitch": "Moderate",
98
+ "rate": "Moderate",
99
+ "temperature": 0.8,
100
+ "do_sample": True
101
+ }
102
+
103
+ response = requests.post(url, json=payload)
104
+ if response.status_code == 200:
105
+ with open("speech.wav", "wb") as f:
106
+ f.write(response.content)
107
+ print("Audio saved!")
108
+ else:
109
+ print(f"Error: {response.status_code}")
110
+ print(response.text)
111
+ ```
112
+
113
+ ## Running Locally
114
+
115
+ ### With Docker
116
+ ```bash
117
+ docker build -t parler-tts-api .
118
+ docker run -p 7860:7860 --gpus all parler-tts-api
119
+ ```
120
+
121
+ ### Without Docker
122
+ ```bash
123
+ python3 -m venv venv
124
+ source venv/bin/activate
125
+ pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
126
+ pip install -r requirements.txt
127
+ pip install uvicorn[standard]
128
+ python api.py
129
+ ```
130
+
131
+ Then visit `http://localhost:7860/docs` for interactive API documentation.
132
+
133
+ ## Environment Variables
134
+
135
+ For HF Spaces deployment, set the following secret:
136
+ - `HF_TOKEN`: Your Hugging Face API token (required for gated model access)
137
+
138
+ ## Technical Details
139
+
140
+ - **Model**: Indic Parler-TTS (multi-speaker, multi-language)
141
+ - **Language**: Urdu (auto-detected from script)
142
+ - **Sample Rate**: 24 kHz
143
+ - **Audio Format**: WAV (16-bit PCM)
144
+ - **Framework**: FastAPI + PyTorch
145
+ - **Deployment**: HF Spaces Docker runtime
146
+
147
+ ### Quality Notes
148
+ - Language is auto-detected from Urdu script — do NOT mention language in voice descriptions
149
+ - Named speakers (Divya, Rohit, etc.) provide consistent voices
150
+ - Same random seed used across sentences for voice consistency within a generation
151
+ - Text cleaning removes Latin/English characters to prevent language mixing
api.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.responses import StreamingResponse
3
+ import torch
4
+ import numpy as np
5
+ import re
6
+ from io import BytesIO
7
+ import soundfile as sf
8
+ from pydantic import BaseModel
9
+ from parler_tts import ParlerTTSForConditionalGeneration
10
+ from transformers import AutoTokenizer
11
+
12
+ # Try to import spaces for HF Spaces deployment
13
+ try:
14
+ import spaces
15
+ HAS_SPACES = True
16
+ except ImportError:
17
+ HAS_SPACES = False
18
+ class _NoOpSpaces:
19
+ def GPU(self, *args, **kwargs):
20
+ def decorator(fn):
21
+ return fn
22
+ return decorator
23
+ spaces = _NoOpSpaces()
24
+
25
+ # --- Model Loading ---
26
+
27
+ MODEL_ID = "ai4bharat/indic-parler-tts"
28
+ DEVICE = "cpu"
29
+
30
+ print("Loading Indic Parler-TTS model...")
31
+ model = ParlerTTSForConditionalGeneration.from_pretrained(MODEL_ID).to(DEVICE)
32
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
33
+ description_tokenizer = AutoTokenizer.from_pretrained(model.config.text_encoder._name_or_path)
34
+ SAMPLE_RATE = model.config.sampling_rate
35
+ print("Model loaded!")
36
+
37
+ # Named speakers
38
+ SPEAKERS = {
39
+ "Divya": "Divya",
40
+ "Rani": "Rani",
41
+ "Rohit": "Rohit",
42
+ "Aman": "Aman",
43
+ "Generic Female": "",
44
+ "Generic Male": "",
45
+ }
46
+
47
+ app = FastAPI(title="Parler TTS API", version="1.0")
48
+
49
+
50
+ class TTSRequest(BaseModel):
51
+ text: str
52
+ speaker: str = "Divya"
53
+ pitch: str = "Moderate"
54
+ rate: str = "Moderate"
55
+ temperature: float = 0.8
56
+ do_sample: bool = True
57
+
58
+
59
+ def build_description(speaker_name, gender, pitch, rate):
60
+ """Build voice description prompt."""
61
+ if speaker_name:
62
+ return (
63
+ f"{speaker_name}'s voice delivers a slightly expressive speech "
64
+ f"with a {pitch.lower()} pitch and a {rate.lower()} speaking rate. "
65
+ f"The recording is of very high quality, with the speaker's voice sounding clear "
66
+ f"and very close up. Very clear audio."
67
+ )
68
+ else:
69
+ return (
70
+ f"A {gender.lower()} speaker delivers a slightly expressive and clear speech "
71
+ f"with a {pitch.lower()} pitch and a {rate.lower()} speaking rate. "
72
+ f"The recording is of very high quality, with the speaker's voice sounding clear "
73
+ f"and very close up. Very clear audio."
74
+ )
75
+
76
+
77
+ def split_sentences(text):
78
+ """Split Urdu text into sentences."""
79
+ sentences = re.split(r'[۔।\.\!\?]+', text)
80
+ return [s.strip() for s in sentences if s.strip()]
81
+
82
+
83
+ def clean_urdu_text(text):
84
+ """Clean and normalize Urdu text."""
85
+ text = re.sub(r'[a-zA-Z]+', '', text)
86
+ text = re.sub(r'\s+', ' ', text)
87
+ text = text.strip()
88
+ if text and text[-1] not in '۔.!?،':
89
+ text += '۔'
90
+ return text
91
+
92
+
93
+ @spaces.GPU()
94
+ def generate_speech_internal(text, speaker, pitch, rate, temperature, do_sample):
95
+ """Internal function for speech generation."""
96
+ if not text.strip():
97
+ return None
98
+
99
+ try:
100
+ text = clean_urdu_text(text)
101
+ speaker_name = SPEAKERS.get(speaker, "")
102
+ gender = "female" if "Female" in speaker or speaker in ["Divya", "Rani"] else "male"
103
+ description = build_description(speaker_name, gender, pitch, rate)
104
+ model.to("cuda")
105
+
106
+ sentences = split_sentences(text)
107
+ if not sentences:
108
+ sentences = [text.strip()]
109
+
110
+ all_audio = []
111
+ seed = torch.randint(0, 2**32, (1,)).item()
112
+
113
+ for sentence in sentences:
114
+ desc_tokens = description_tokenizer(description, return_tensors="pt")
115
+ prompt_tokens = tokenizer(sentence, return_tensors="pt")
116
+
117
+ torch.manual_seed(seed)
118
+ if torch.cuda.is_available():
119
+ torch.cuda.manual_seed(seed)
120
+
121
+ with torch.no_grad():
122
+ generation = model.generate(
123
+ input_ids=desc_tokens.input_ids.to("cuda"),
124
+ attention_mask=desc_tokens.attention_mask.to("cuda"),
125
+ prompt_input_ids=prompt_tokens.input_ids.to("cuda"),
126
+ prompt_attention_mask=prompt_tokens.attention_mask.to("cuda"),
127
+ do_sample=do_sample,
128
+ temperature=temperature if do_sample else 1.0,
129
+ min_new_tokens=10,
130
+ )
131
+
132
+ audio_chunk = generation.cpu().numpy().squeeze()
133
+ audio_chunk = (audio_chunk * 32767).astype(np.int16)
134
+ all_audio.append(audio_chunk)
135
+
136
+ silence = np.zeros(int(SAMPLE_RATE * 0.3), dtype=np.int16)
137
+ all_audio.append(silence)
138
+
139
+ model.to("cpu")
140
+
141
+ if not all_audio:
142
+ return None
143
+
144
+ audio = np.concatenate(all_audio)
145
+ return audio
146
+
147
+ except Exception as e:
148
+ print(f"Error generating speech: {e}")
149
+ model.to("cpu")
150
+ return None
151
+
152
+
153
+ @app.get("/")
154
+ async def root():
155
+ """Health check endpoint."""
156
+ return {
157
+ "status": "ok",
158
+ "model": "Indic Parler-TTS",
159
+ "speakers": list(SPEAKERS.keys()),
160
+ "sample_rate": SAMPLE_RATE
161
+ }
162
+
163
+
164
+ @app.post("/tts")
165
+ async def text_to_speech(request: TTSRequest):
166
+ """Generate speech from Urdu text."""
167
+ if not request.text.strip():
168
+ raise HTTPException(status_code=400, detail="Text cannot be empty")
169
+
170
+ if request.speaker not in SPEAKERS:
171
+ raise HTTPException(status_code=400, detail=f"Invalid speaker. Choose from: {list(SPEAKERS.keys())}")
172
+
173
+ audio = generate_speech_internal(
174
+ request.text,
175
+ request.speaker,
176
+ request.pitch,
177
+ request.rate,
178
+ request.temperature,
179
+ request.do_sample
180
+ )
181
+
182
+ if audio is None:
183
+ raise HTTPException(status_code=500, detail="Failed to generate speech")
184
+
185
+ # Convert to WAV in memory
186
+ audio_buffer = BytesIO()
187
+ sf.write(audio_buffer, audio, SAMPLE_RATE, format='WAV')
188
+ audio_buffer.seek(0)
189
+
190
+ return StreamingResponse(
191
+ iter([audio_buffer.getvalue()]),
192
+ media_type="audio/wav",
193
+ headers={"Content-Disposition": "attachment; filename=speech.wav"}
194
+ )
195
+
196
+
197
+ @app.get("/speakers")
198
+ async def get_speakers():
199
+ """Get list of available speakers."""
200
+ return {"speakers": list(SPEAKERS.keys())}
201
+
202
+
203
+ if __name__ == "__main__":
204
+ import uvicorn
205
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Shared ---
2
+ torch>=2.1
3
+ numpy
4
+ gradio
5
+ soundfile
6
+
7
+ # --- Transformers ecosystem ---
8
+ transformers
9
+ accelerate
10
+ huggingface_hub
11
+
12
+ # --- For Orpheus Urdu TTS (app_orpheus.py) ---
13
+ snac
14
+ bitsandbytes
15
+
16
+ # --- For Indic Parler-TTS (app.py) ---
17
+ git+https://github.com/huggingface/parler-tts.git
18
+
19
+ # --- For MMS-TTS Urdu (app_mms.py) ---
20
+ # uses transformers only (already included above)