Bgk Injector SqLi commited on
Commit
fa547a0
·
1 Parent(s): 84f49d6

Deploy Wami Dioula STT & TTS API

Browse files

- FastAPI app with Speech-to-Text and Text-to-Speech
- Support for Dioula language (facebook/mms models)
- Docker configuration for HF Spaces
- CORS enabled for public API access
- Interactive documentation (Swagger + ReDoc)
- HF_TOKEN configured in Dockerfile

Files changed (5) hide show
  1. .dockerignore +14 -0
  2. Dockerfile +21 -0
  3. README.md +87 -8
  4. app.py +281 -0
  5. requirements.txt +8 -0
.dockerignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .pytest_cache/
6
+ .venv/
7
+ venv/
8
+ ENV/
9
+ .git/
10
+ .gitignore
11
+ *.md
12
+ !README.md
13
+ test_api.py
14
+ *.log
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Installer ffmpeg pour la conversion audio
6
+ RUN apt-get update && apt-get install -y \
7
+ ffmpeg \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copier les fichiers
11
+ COPY requirements.txt .
12
+ COPY app.py .
13
+
14
+ # Installer les dépendances Python
15
+ RUN pip install --no-cache-dir -r requirements.txt
16
+
17
+ # Port pour Hugging Face Spaces
18
+ EXPOSE 7860
19
+
20
+ # Lancer l'application
21
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,12 +1,91 @@
1
  ---
2
- title: Wami
3
- emoji: 🏢
4
- colorFrom: yellow
5
- colorTo: purple
6
  sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- short_description: 'wami lingual '
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Wami - Dioula STT & TTS API
3
+ emoji: 🎙️
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 7860
 
 
8
  ---
9
 
10
+ # Wami - API Dioula STT & TTS
11
+
12
+ API de reconnaissance vocale (Speech-to-Text) et synthèse vocale (Text-to-Speech) en langue Dioula.
13
+
14
+ ## 🚀 Utilisation
15
+
16
+ ### Endpoints disponibles
17
+
18
+ #### 1. Speech-to-Text (STT)
19
+
20
+ Transcrit un fichier audio en texte Dioula.
21
+
22
+ ```bash
23
+ curl -X POST https://votre-space-name.hf.space/api/stt \
24
+ -F "audio=@recording.wav"
25
+ ```
26
+
27
+ **Réponse:**
28
+ ```json
29
+ {
30
+ "transcription": "texte transcrit en dioula"
31
+ }
32
+ ```
33
+
34
+ #### 2. Text-to-Speech (TTS)
35
+
36
+ Génère un audio en Dioula depuis du texte.
37
+
38
+ ```bash
39
+ curl -X POST https://votre-space-name.hf.space/api/tts \
40
+ -F "text=na an be do minkɛ" \
41
+ -o output.wav
42
+ ```
43
+
44
+ **Réponse:** Fichier audio WAV
45
+
46
+ #### 3. Health Check
47
+
48
+ Vérifie le statut de l'API.
49
+
50
+ ```bash
51
+ curl https://votre-space-name.hf.space/health
52
+ ```
53
+
54
+ **Réponse:**
55
+ ```json
56
+ {
57
+ "status": "healthy",
58
+ "device": "cuda",
59
+ "models_loaded": {
60
+ "stt": true,
61
+ "tts": true
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## 📖 Documentation interactive
67
+
68
+ - **Swagger UI:** `https://votre-space-name.hf.space/docs`
69
+ - **ReDoc:** `https://votre-space-name.hf.space/redoc`
70
+
71
+ ## 🔧 Modèles utilisés
72
+
73
+ - **STT:** [facebook/mms-1b-all](https://huggingface.co/facebook/mms-1b-all) (adapter Dioula)
74
+ - **TTS:** [facebook/mms-tts-dyu](https://huggingface.co/facebook/mms-tts-dyu)
75
+
76
+ ## 💻 Déploiement local
77
+
78
+ ```bash
79
+ pip install -r requirements.txt
80
+ python app.py
81
+ ```
82
+
83
+ Ouvrez [http://localhost:7860](http://localhost:7860)
84
+
85
+ ## 🌍 À propos du Dioula
86
+
87
+ Le Dioula (code langue: `dyu`) est une langue mandée parlée principalement en Côte d'Ivoire, au Burkina Faso et au Mali.
88
+
89
+ ## 📝 Licence
90
+
91
+ Les modèles utilisés sont sous licence Apache 2.0. Voir les pages des modèles pour plus de détails.
app.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import scipy.io.wavfile
8
+ import soundfile as sf
9
+ import torch
10
+ import torchaudio
11
+ from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
12
+ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+
15
+ app = FastAPI(
16
+ title="Wami - Dioula STT & TTS API",
17
+ description="API de reconnaissance vocale (STT) et synthèse vocale (TTS) en Dioula",
18
+ version="1.0.0"
19
+ )
20
+
21
+ # CORS pour permettre les appels depuis n'importe quel domaine
22
+ app.add_middleware(
23
+ CORSMiddleware,
24
+ allow_origins=["*"],
25
+ allow_credentials=True,
26
+ allow_methods=["*"],
27
+ allow_headers=["*"],
28
+ )
29
+
30
+ # Gestionnaires d'erreur globaux
31
+ @app.exception_handler(HTTPException)
32
+ async def http_exception_handler(request: Request, exc: HTTPException):
33
+ return JSONResponse(
34
+ status_code=exc.status_code,
35
+ content={"error": exc.detail}
36
+ )
37
+
38
+ @app.exception_handler(Exception)
39
+ async def global_exception_handler(request: Request, exc: Exception):
40
+ return JSONResponse(
41
+ status_code=500,
42
+ content={"error": f"Erreur serveur: {str(exc)}"}
43
+ )
44
+
45
+ # Globals
46
+ stt_processor = None
47
+ stt_model = None
48
+ tts_tokenizer = None
49
+ tts_model = None
50
+ device = "cpu"
51
+
52
+ @app.on_event("startup")
53
+ def load_models():
54
+ global stt_processor, stt_model, tts_tokenizer, tts_model, device
55
+
56
+ device = "cuda" if torch.cuda.is_available() else "cpu"
57
+ print(f"🚀 Device: {device}")
58
+
59
+ # STT
60
+ from transformers import AutoProcessor, Wav2Vec2ForCTC
61
+ print("⏳ Chargement du modèle STT (Dioula)...")
62
+ stt_processor = AutoProcessor.from_pretrained("facebook/mms-1b-all", target_lang="dyu")
63
+ stt_model = Wav2Vec2ForCTC.from_pretrained(
64
+ "facebook/mms-1b-all",
65
+ target_lang="dyu",
66
+ ignore_mismatched_sizes=True
67
+ )
68
+ stt_model.load_adapter("dyu")
69
+ stt_model.to(device)
70
+ print("✅ STT prêt!")
71
+
72
+ # TTS
73
+ from transformers import AutoTokenizer, VitsModel
74
+ print("⏳ Chargement du modèle TTS (Dioula)...")
75
+ tts_tokenizer = AutoTokenizer.from_pretrained("facebook/mms-tts-dyu")
76
+ tts_model = VitsModel.from_pretrained("facebook/mms-tts-dyu").to(device)
77
+ print("✅ TTS prêt!")
78
+
79
+ # Page d'accueil avec documentation
80
+ @app.get("/", response_class=HTMLResponse)
81
+ def home():
82
+ return """
83
+ <!DOCTYPE html>
84
+ <html lang="fr">
85
+ <head>
86
+ <meta charset="UTF-8">
87
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
88
+ <title>Wami - API Dioula STT & TTS</title>
89
+ <style>
90
+ body { font-family: system-ui; max-width: 800px; margin: 40px auto; padding: 20px; line-height: 1.6; }
91
+ h1 { color: #2563eb; }
92
+ h2 { color: #1e40af; margin-top: 30px; }
93
+ code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; }
94
+ pre { background: #0f172a; color: #e2e8f0; padding: 16px; border-radius: 8px; overflow-x: auto; }
95
+ .endpoint { background: #f8fafc; padding: 16px; border-left: 4px solid #3b82f6; margin: 16px 0; }
96
+ .method { display: inline-block; padding: 4px 8px; border-radius: 4px; font-weight: bold; margin-right: 8px; }
97
+ .post { background: #10b981; color: white; }
98
+ .get { background: #3b82f6; color: white; }
99
+ </style>
100
+ </head>
101
+ <body>
102
+ <h1>🎙️ Wami - API Dioula STT & TTS</h1>
103
+ <p>API de reconnaissance vocale (Speech-to-Text) et synthèse vocale (Text-to-Speech) en Dioula.</p>
104
+
105
+ <h2>📖 Endpoints</h2>
106
+
107
+ <div class="endpoint">
108
+ <p><span class="method get">GET</span> <code>/</code></p>
109
+ <p>Cette page de documentation</p>
110
+ </div>
111
+
112
+ <div class="endpoint">
113
+ <p><span class="method get">GET</span> <code>/health</code></p>
114
+ <p>Statut de l'API et des modèles</p>
115
+ </div>
116
+
117
+ <div class="endpoint">
118
+ <p><span class="method post">POST</span> <code>/api/stt</code></p>
119
+ <p><strong>Speech-to-Text</strong> - Transcrit un fichier audio en texte Dioula</p>
120
+ <p><strong>Entrée:</strong> Fichier audio (WebM, WAV, MP3)</p>
121
+ <p><strong>Sortie:</strong> <code>{"transcription": "texte en dioula"}</code></p>
122
+ <pre>curl -X POST https://votre-space.hf.space/api/stt \\
123
+ -F "audio=@recording.wav"</pre>
124
+ </div>
125
+
126
+ <div class="endpoint">
127
+ <p><span class="method post">POST</span> <code>/api/tts</code></p>
128
+ <p><strong>Text-to-Speech</strong> - Génère un audio en Dioula depuis du texte</p>
129
+ <p><strong>Entrée:</strong> Texte en Dioula (paramètre <code>text</code>)</p>
130
+ <p><strong>Sortie:</strong> Fichier WAV</p>
131
+ <pre>curl -X POST https://votre-space.hf.space/api/tts \\
132
+ -F "text=na an be do minkɛ" \\
133
+ -o output.wav</pre>
134
+ </div>
135
+
136
+ <h2>🔗 Documentation interactive</h2>
137
+ <p>
138
+ <a href="/docs">Swagger UI</a> |
139
+ <a href="/redoc">ReDoc</a>
140
+ </p>
141
+
142
+ <h2>ℹ️ Modèles</h2>
143
+ <ul>
144
+ <li><strong>STT:</strong> facebook/mms-1b-all (adapter Dioula)</li>
145
+ <li><strong>TTS:</strong> facebook/mms-tts-dyu</li>
146
+ </ul>
147
+ </body>
148
+ </html>
149
+ """
150
+
151
+ @app.get("/health")
152
+ def health_check():
153
+ """Vérifie le statut de l'API et des modèles"""
154
+ return {
155
+ "status": "healthy",
156
+ "device": device,
157
+ "models_loaded": {
158
+ "stt": stt_model is not None,
159
+ "tts": tts_model is not None
160
+ }
161
+ }
162
+
163
+ @app.post("/api/stt")
164
+ async def speech_to_text(audio: UploadFile = File(...)):
165
+ """
166
+ Transcrit un fichier audio en texte Dioula
167
+
168
+ - **audio**: Fichier audio (WebM, WAV, MP3, etc.)
169
+ """
170
+ tmp_input = None
171
+ tmp_wav = None
172
+
173
+ try:
174
+ audio_bytes = await audio.read()
175
+
176
+ # Déterminer l'extension
177
+ content_type = audio.content_type or ""
178
+ if "webm" in content_type:
179
+ suffix = ".webm"
180
+ elif "wav" in content_type:
181
+ suffix = ".wav"
182
+ elif "mp3" in content_type:
183
+ suffix = ".mp3"
184
+ else:
185
+ suffix = ".webm"
186
+
187
+ # Sauvegarder temporairement
188
+ tmp_input = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
189
+ tmp_input.write(audio_bytes)
190
+ tmp_input.close()
191
+
192
+ # Convertir en WAV si nécessaire
193
+ if suffix != ".wav":
194
+ try:
195
+ audio_data, sample_rate = sf.read(tmp_input.name)
196
+ tmp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
197
+ tmp_wav.close()
198
+ sf.write(tmp_wav.name, audio_data, sample_rate)
199
+ audio_path = tmp_wav.name
200
+ except Exception as e:
201
+ raise HTTPException(
202
+ status_code=400,
203
+ detail=f"Impossible de lire l'audio. Format non supporté. Erreur: {str(e)}"
204
+ )
205
+ else:
206
+ audio_path = tmp_input.name
207
+
208
+ # Charger avec torchaudio
209
+ audio_input, sample_rate = torchaudio.load(audio_path)
210
+
211
+ # Mono
212
+ if audio_input.shape[0] > 1:
213
+ audio_input = torch.mean(audio_input, dim=0, keepdim=True)
214
+
215
+ # Resample à 16 kHz
216
+ if sample_rate != 16000:
217
+ resampler = torchaudio.transforms.Resample(sample_rate, 16000)
218
+ audio_input = resampler(audio_input)
219
+
220
+ audio_input = audio_input.squeeze()
221
+
222
+ # Inférence
223
+ inputs = stt_processor(audio_input, sampling_rate=16000, return_tensors="pt")
224
+ inputs = {k: v.to(device) for k, v in inputs.items()}
225
+
226
+ with torch.no_grad():
227
+ logits = stt_model(**inputs).logits
228
+
229
+ predicted_ids = torch.argmax(logits, dim=-1)
230
+ transcription = stt_processor.batch_decode(predicted_ids)[0]
231
+
232
+ return {"transcription": transcription}
233
+
234
+ except HTTPException:
235
+ raise
236
+ except Exception as e:
237
+ print(f"Erreur STT: {e}")
238
+ raise HTTPException(status_code=500, detail=f"Erreur lors de la transcription: {str(e)}")
239
+ finally:
240
+ if tmp_input and Path(tmp_input.name).exists():
241
+ Path(tmp_input.name).unlink(missing_ok=True)
242
+ if tmp_wav and Path(tmp_wav.name).exists():
243
+ Path(tmp_wav.name).unlink(missing_ok=True)
244
+
245
+ @app.post("/api/tts")
246
+ async def text_to_speech(text: str = Form(...)):
247
+ """
248
+ Génère un audio en Dioula depuis du texte
249
+
250
+ - **text**: Texte en Dioula à synthétiser
251
+ """
252
+ try:
253
+ if not text.strip():
254
+ raise HTTPException(status_code=400, detail="Le texte ne peut pas être vide")
255
+
256
+ inputs = tts_tokenizer(text, return_tensors="pt").to(device)
257
+
258
+ with torch.no_grad():
259
+ waveform = tts_model(**inputs).waveform
260
+
261
+ audio_data = waveform[0].cpu().numpy()
262
+ sample_rate = tts_model.config.sampling_rate
263
+
264
+ tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
265
+ scipy.io.wavfile.write(tmp.name, rate=sample_rate, data=audio_data)
266
+ tmp.close()
267
+
268
+ return FileResponse(
269
+ tmp.name,
270
+ media_type="audio/wav",
271
+ filename="tts_dioula.wav"
272
+ )
273
+ except HTTPException:
274
+ raise
275
+ except Exception as e:
276
+ print(f"Erreur TTS: {e}")
277
+ raise HTTPException(status_code=500, detail=f"Erreur lors de la génération audio: {str(e)}")
278
+
279
+ if __name__ == "__main__":
280
+ import uvicorn
281
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.115.0
2
+ uvicorn[standard]>=0.34.0
3
+ python-multipart>=0.0.18
4
+ scipy>=1.14.0
5
+ soundfile>=0.12.0
6
+ torch>=2.5.0
7
+ torchaudio>=2.5.0
8
+ transformers>=4.40.0