Spaces:
Sleeping
Sleeping
ajout du code necessaire sans voice
Browse files- .gitignore +5 -0
- __init__.py +1 -0
- sentiment_space/README.fr.md +312 -0
- sentiment_space/app.py +59 -0
- sentiment_space/auth.py +26 -0
- sentiment_space/bassa_analyzer.py +533 -0
- sentiment_space/requirements.txt +15 -0
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
text
|
| 3 |
+
__pycache__
|
| 4 |
+
|
| 5 |
+
|
__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# This file makes the ia directory a Python package
|
sentiment_space/README.fr.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Module d'Analyse de Sentiments Multilingue (Bassa, Français, Anglais)
|
| 2 |
+
|
| 3 |
+
Ce document fournit les instructions nécessaires pour intégrer et utiliser le module d'analyse de sentiments. Le système est conçu pour fonctionner 100% hors ligne, sans dépendre d'APIs externes.
|
| 4 |
+
|
| 5 |
+
## 1. Schéma d'Architecture
|
| 6 |
+
|
| 7 |
+
Le système est composé de trois parties principales : un frontend React.js, un backend Node.js, et un service d'IA Python autonome.
|
| 8 |
+
|
| 9 |
+
```
|
| 10 |
+
+-----------------------+ (JSON via HTTP POST) +-------------------------+
|
| 11 |
+
| | <------------------------> | |
|
| 12 |
+
| Frontend (React.js) | /api/analyze | Backend (Node.js) |
|
| 13 |
+
| | | - Express/Fastify |
|
| 14 |
+
| - Enregistre/saisit | | - Gestion des routes |
|
| 15 |
+
| laudio ou le texte | | - Logique métier |
|
| 16 |
+
| - Envoie en Base64 | | |
|
| 17 |
+
+-----------------------+ +------------+------------+
|
| 18 |
+
| (Appel HTTP)
|
| 19 |
+
|
|
| 20 |
+
+-----------------------v-----------------------+
|
| 21 |
+
| |
|
| 22 |
+
| Service IA (Python) |
|
| 23 |
+
| - FastAPI |
|
| 24 |
+
| - bassa_analyzer.py |
|
| 25 |
+
| - Modèles IA (chargés localement) |
|
| 26 |
+
| - Whisper (Transcription) |
|
| 27 |
+
| - XLM-Roberta (Analyse Sentiment) |
|
| 28 |
+
| - Références Bassa (Audio Embeddings) |
|
| 29 |
+
| |
|
| 30 |
+
+-------------------------------------------------+
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
##2rvice d'IA (Python)
|
| 34 |
+
|
| 35 |
+
Le cœur de l'analyse est contenu dans le fichier `bassa_analyzer.py` et exposé via l'API FastAPI dans `api_server.py`.
|
| 36 |
+
|
| 37 |
+
### Installation
|
| 38 |
+
|
| 39 |
+
Assurez-vous que votre environnement virtuel est activé, puis installez les dépendances requises :
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
pip install -r requirements.txt
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
La liste des dépendances inclut `torch`, `transformers`, `soundfile`, `pandas`, `scikit-learn`, `langdetect`, `fastapi`, `uvicorn`, et `torchaudio`.
|
| 46 |
+
|
| 47 |
+
### Lancement du Service d'IA
|
| 48 |
+
|
| 49 |
+
Depuis le dossier `ia`, lancez le service avec Uvicorn :
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
uvicorn app:app --host 0.0.0.0 --port 8000
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
- `--host 0.0.0.0` : service accessible sur votre réseau local (nécessaire pour que le backend Node.js puisse s'y connecter).
|
| 56 |
+
- `--port 8000` est le port d'écoute. Vous pouvez le changer si nécessaire.
|
| 57 |
+
|
| 58 |
+
Le service d'IA est maintenant en cours d'exécution. Vous pouvez accéder à la documentation interactive de l'API (générée automatiquement) à l'adresse [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs).
|
| 59 |
+
|
| 60 |
+
### Endpoint de Santé
|
| 61 |
+
|
| 62 |
+
Pour vérifier que le service d'IA est opérationnel, utilisez :
|
| 63 |
+
|
| 64 |
+
```
|
| 65 |
+
GET /health
|
| 66 |
+
```
|
| 67 |
+
Réponse :
|
| 68 |
+
```json
|
| 69 |
+
{"status": ok,message": "API opérationnelle"}
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
### Utilisation du Service d'IA
|
| 73 |
+
|
| 74 |
+
Le service expose un endpoint principal :
|
| 75 |
+
|
| 76 |
+
```
|
| 77 |
+
POST /analyze
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
#### Format de la Requête
|
| 81 |
+
|
| 82 |
+
```json[object Object]input_type:audio" | text",
|
| 83 |
+
content: .." // Base64 pour audio, texte brut sinon
|
| 84 |
+
}
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
#### Format de la Réponse
|
| 88 |
+
|
| 89 |
+
```json
|
| 90 |
+
[object Object] langue_detectee:fr,//bss,fr, ou "en"
|
| 91 |
+
texte_original: rvice est vraiment exceptionnel, je suis ravi.",
|
| 92 |
+
traduction_anglaise": "[Translation to be implemented]",
|
| 93 |
+
"sentiment: ositif, // positif",neutre", ou negatif
|
| 94 |
+
confiance:00.9999 methode_utilisee": nlptown_bert_multilingual", // ou base_referencee"
|
| 95 |
+
processing_time": 1.23 // Temps de traitement en secondes
|
| 96 |
+
}
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
## 3. Intégration Backend (Node.js)
|
| 100 |
+
|
| 101 |
+
Le backend Node.js communique avec le service dIAPython via des appels HTTP.
|
| 102 |
+
|
| 103 |
+
### Exemple d'Appel depuis le Backend Node.js
|
| 104 |
+
|
| 105 |
+
Voici comment le service backend Node.js peut appeler le service d'IA Python en utilisant `node-fetch` (ou un autre client HTTP comme `axios`).
|
| 106 |
+
|
| 107 |
+
```javascript
|
| 108 |
+
const fetch = require('node-fetch');
|
| 109 |
+
const AI_SERVICE_URL = http://127018000lyze';
|
| 110 |
+
|
| 111 |
+
// Exemple de fonction dans votre service Node.js
|
| 112 |
+
async function analyserContenu(inputType, content) {
|
| 113 |
+
try[object Object] console.log(`Envoi de la requête au service d'IA: ${AI_SERVICE_URL}`);
|
| 114 |
+
|
| 115 |
+
const response = await fetch(AI_SERVICE_URL, {
|
| 116 |
+
method: 'POST',
|
| 117 |
+
headers:[object Object]
|
| 118 |
+
Content-Type':application/json,
|
| 119 |
+
Accept':application/json'
|
| 120 |
+
},
|
| 121 |
+
body: JSON.stringify({
|
| 122 |
+
input_type: inputType,
|
| 123 |
+
content: content
|
| 124 |
+
})
|
| 125 |
+
});
|
| 126 |
+
|
| 127 |
+
if (!response.ok) {
|
| 128 |
+
throw new Error(`Erreur HTTP: ${response.status}`);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
const result = await response.json();
|
| 132 |
+
console.log('Résultat de l\analyse:', result);
|
| 133 |
+
return result;
|
| 134 |
+
|
| 135 |
+
} catch (error)[object Object] console.error('Erreur lors de l\'appel au service d\'IA:, error);
|
| 136 |
+
throw error;
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// Exemple d'utilisation
|
| 141 |
+
app.post('/api/analyze, async (req, res) => {
|
| 142 |
+
try[object Object] const { input_type, content } = req.body;
|
| 143 |
+
const result = await analyserContenu(input_type, content);
|
| 144 |
+
res.json(result);
|
| 145 |
+
} catch (error) {
|
| 146 |
+
res.status(500.json({ error:Erreur lors de l\'analyse' });
|
| 147 |
+
}
|
| 148 |
+
});
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
## Exemple d'appel API depuis un backend Node.js
|
| 152 |
+
|
| 153 |
+
Voici comment votre équipe backend Node.js peut appeler le service d'IA Python via HTTP, en utilisant `node-fetch` (ou `axios`).
|
| 154 |
+
|
| 155 |
+
### Exemple de code Node.js (en français)
|
| 156 |
+
|
| 157 |
+
```javascript
|
| 158 |
+
const fetch = require('node-fetch');
|
| 159 |
+
const URL_IA = 'http://127.0.0.1:8000/analyze'; // Adapter l'URL si besoin
|
| 160 |
+
|
| 161 |
+
// Fonction pour analyser un texte ou un audio (en Base64)
|
| 162 |
+
async function analyserAvecIA(inputType, contenu) {
|
| 163 |
+
try {
|
| 164 |
+
const reponse = await fetch(URL_IA, {
|
| 165 |
+
method: 'POST',
|
| 166 |
+
headers: {
|
| 167 |
+
'Content-Type': 'application/json',
|
| 168 |
+
'Accept': 'application/json'
|
| 169 |
+
},
|
| 170 |
+
body: JSON.stringify({
|
| 171 |
+
input_type: inputType, // 'text' ou 'audio'
|
| 172 |
+
content: contenu
|
| 173 |
+
})
|
| 174 |
+
});
|
| 175 |
+
if (!reponse.ok) {
|
| 176 |
+
throw new Error(`Erreur HTTP: ${reponse.status}`);
|
| 177 |
+
}
|
| 178 |
+
const resultat = await reponse.json();
|
| 179 |
+
console.log('Résultat IA:', resultat);
|
| 180 |
+
return resultat;
|
| 181 |
+
} catch (err) {
|
| 182 |
+
console.error('Erreur lors de l\'appel à l\'IA:', err);
|
| 183 |
+
throw err;
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
// Exemple d'utilisation pour du texte :
|
| 188 |
+
(async () => {
|
| 189 |
+
const texte = "Je me sens très fatigué depuis quelques jours.";
|
| 190 |
+
const resultat = await analyserAvecIA('text', texte);
|
| 191 |
+
console.log(resultat);
|
| 192 |
+
})();
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
### Format attendu de la requête
|
| 196 |
+
|
| 197 |
+
```json
|
| 198 |
+
{
|
| 199 |
+
"input_type": "text", // ou "audio"
|
| 200 |
+
"content": "..." // texte brut ou audio encodé en Base64
|
| 201 |
+
}
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
### Exemple de réponse JSON
|
| 205 |
+
|
| 206 |
+
```json
|
| 207 |
+
{
|
| 208 |
+
"sentiment": "negatif",
|
| 209 |
+
"score_sentiment": 0.87,
|
| 210 |
+
"theme": "fatigue",
|
| 211 |
+
"langue_detectee": "fr",
|
| 212 |
+
"texte_original": "Je me sens très fatigué depuis quelques jours."
|
| 213 |
+
}
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
- `sentiment` : positif, neutre ou negatif
|
| 217 |
+
- `score_sentiment` : score de confiance du modèle (0 à 1)
|
| 218 |
+
- `theme` : thème médical détecté (en français)
|
| 219 |
+
- `langue_detectee` : langue détectée (fr, en, bss)
|
| 220 |
+
- `texte_original` : texte analysé ou transcription
|
| 221 |
+
|
| 222 |
+
Votre backend Node.js peut ainsi relayer ce résultat à l'admin ou au frontend.
|
| 223 |
+
|
| 224 |
+
## 4. Intégration Frontend (React.js)
|
| 225 |
+
|
| 226 |
+
Le frontend communique avec le backend Node.js via un endpoint dAPI (par exemple, `/api/analyze`).
|
| 227 |
+
|
| 228 |
+
### Envoi d'un Fichier Audio
|
| 229 |
+
|
| 230 |
+
L'utilisateur enregistre ou sélectionne un fichier audio. Le fichier (Blob) doit être converti en chaîne Base64 avant dêtre envoyé.
|
| 231 |
+
|
| 232 |
+
```typescript
|
| 233 |
+
// Fonction pour convertir un Blob en Base64
|
| 234 |
+
const convertBlobToBase64ob: Blob): Promise<string> => {
|
| 235 |
+
return new Promise((resolve, reject) => {
|
| 236 |
+
const reader = new FileReader();
|
| 237 |
+
reader.onerror = reject;
|
| 238 |
+
reader.onload = () => {
|
| 239 |
+
// Retourne seulement la partie Base64ta URL
|
| 240 |
+
const dataUrl = reader.result as string;
|
| 241 |
+
const base64= dataUrl.split(',)[1]; resolve(base64);
|
| 242 |
+
};
|
| 243 |
+
reader.readAsDataURL(blob);
|
| 244 |
+
});
|
| 245 |
+
};
|
| 246 |
+
|
| 247 |
+
// Fonction pour appeler l'API
|
| 248 |
+
const analyserAudio = async (audioBlob: Blob) => {
|
| 249 |
+
try {
|
| 250 |
+
const base64Audio = await convertBlobToBase64(audioBlob);
|
| 251 |
+
|
| 252 |
+
const response = await fetch('/api/analyze', { // Votre endpoint Node.js
|
| 253 |
+
method: 'POST',
|
| 254 |
+
headers:[object Object]
|
| 255 |
+
Content-Type':application/json, },
|
| 256 |
+
body: JSON.stringify({
|
| 257 |
+
input_type: "audio, content: base64o
|
| 258 |
+
}),
|
| 259 |
+
});
|
| 260 |
+
|
| 261 |
+
if (!response.ok) {
|
| 262 |
+
throw new Error(`Erreur HTTP: ${response.status}`);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
return await response.json();
|
| 266 |
+
} catch (error)[object Object] console.error(Erreur lors de l'analyse audio:, error);
|
| 267 |
+
return { error: Impossible d'analyser laudio." };
|
| 268 |
+
}
|
| 269 |
+
};
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
### Envoi de Texte
|
| 273 |
+
|
| 274 |
+
L'envoi de texte est plus direct.
|
| 275 |
+
|
| 276 |
+
```typescript
|
| 277 |
+
const analyserTexte = async (texte: string) => {
|
| 278 |
+
try {
|
| 279 |
+
const response = await fetch('/api/analyze', { // Votre endpoint Node.js
|
| 280 |
+
method: 'POST',
|
| 281 |
+
headers:[object Object]
|
| 282 |
+
Content-Type':application/json, },
|
| 283 |
+
body: JSON.stringify({
|
| 284 |
+
input_type: "text, content: texte
|
| 285 |
+
}),
|
| 286 |
+
});
|
| 287 |
+
|
| 288 |
+
if (!response.ok) {
|
| 289 |
+
throw new Error(`Erreur HTTP: ${response.status}`);
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
return await response.json();
|
| 293 |
+
} catch (error)[object Object] console.error(Erreur lors de l'analyse de texte:, error);
|
| 294 |
+
return { error: Impossible danalyser le texte." };
|
| 295 |
+
}
|
| 296 |
+
};
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
## 5Architecture d'Intégration
|
| 300 |
+
|
| 301 |
+
Cette architecture découplée est plus robuste, plus facile à maintenir et à mettre à l'échelle :
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
1. **React -> Node.js** : Le frontend envoie la requête (audio/texte) à votre backend principal Node.js.
|
| 305 |
+
2. **Node.js -> Python** : Le backend Node.js relaie la requête au service dIA Python.
|
| 306 |
+
3. **Python -> Node.js** : Le service d'IA retourne le résultat JSON de l'analyse.
|
| 307 |
+
4. **Node.js -> React** : Le backend Node.js renvoie la réponse finale au frontend.
|
| 308 |
+
|
| 309 |
+
## 6. Conseils pour la Rapidité et la Robustesse
|
| 310 |
+
|
| 311 |
+
- **Les modèles sont chargés une seule fois au démarrage du service d'IA** pour garantir des réponses rapides.
|
| 312 |
+
|
sentiment_space/app.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uvicorn
|
| 2 |
+
from fastapi import FastAPI, HTTPException
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from bassa_analyzer import analyze, load_models_and_references
|
| 5 |
+
import time
|
| 6 |
+
|
| 7 |
+
# --- API Setup ---
|
| 8 |
+
app = FastAPI(
|
| 9 |
+
title="Sentiment Analysis API",
|
| 10 |
+
description="API rapide pour l'analyse de sentiment à partir de texte ou d'audio (Bassa, Français, Anglais).",
|
| 11 |
+
version="1.1.0"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
# --- Load models at startup for speed ---
|
| 15 |
+
@app.on_event("startup")
|
| 16 |
+
def startup_event():
|
| 17 |
+
print("[API] Chargement des modèles IA...")
|
| 18 |
+
load_models_and_references(offline=True)
|
| 19 |
+
print("[API] Modèles chargés. API prête !")
|
| 20 |
+
|
| 21 |
+
# --- Request & Response Models ---
|
| 22 |
+
class AnalysisRequest(BaseModel):
|
| 23 |
+
input_type: str
|
| 24 |
+
content: str
|
| 25 |
+
|
| 26 |
+
class AnalysisResponse(BaseModel):
|
| 27 |
+
sentiment: str
|
| 28 |
+
score_sentiment: float
|
| 29 |
+
theme: str
|
| 30 |
+
langue_detectee: str
|
| 31 |
+
texte_original: str
|
| 32 |
+
|
| 33 |
+
# --- Health Check Endpoint ---
|
| 34 |
+
@app.get("/health")
|
| 35 |
+
def health():
|
| 36 |
+
return {"status": "ok", "message": "API opérationnelle"}
|
| 37 |
+
|
| 38 |
+
# --- API Endpoint ---
|
| 39 |
+
@app.post("/analyze", response_model=AnalysisResponse)
|
| 40 |
+
def handle_analysis(request: AnalysisRequest):
|
| 41 |
+
"""
|
| 42 |
+
Reçoit une requête d'analyse, traite avec bassa_analyzer,
|
| 43 |
+
et retourne le résultat.
|
| 44 |
+
"""
|
| 45 |
+
start = time.time()
|
| 46 |
+
try:
|
| 47 |
+
result = analyze(input_type=request.input_type, content=request.content)
|
| 48 |
+
if "error" in result:
|
| 49 |
+
raise HTTPException(status_code=400, detail=result["error"])
|
| 50 |
+
return result
|
| 51 |
+
except ValueError as e:
|
| 52 |
+
raise HTTPException(status_code=422, detail=str(e))
|
| 53 |
+
except Exception as e:
|
| 54 |
+
raise HTTPException(status_code=500, detail=f"Erreur interne: {e}")
|
| 55 |
+
|
| 56 |
+
# --- How to Run ---
|
| 57 |
+
# uvicorn app:app --host 0.0.0.0 --port 8000
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
sentiment_space/auth.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from huggingface_hub import login, HfFolder
|
| 3 |
+
from getpass import getpass
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
load_dotenv() # Loads from .env
|
| 6 |
+
def configure_auth():
|
| 7 |
+
"""Safe token handling"""
|
| 8 |
+
# 1. Check for existing token
|
| 9 |
+
token = os.getenv("HF_TOKEN") or HfFolder.get_token()
|
| 10 |
+
|
| 11 |
+
# 2. Prompt if no token found
|
| 12 |
+
if not token:
|
| 13 |
+
print("Enter Hugging Face token (will be hidden):")
|
| 14 |
+
token = getpass("> ")
|
| 15 |
+
|
| 16 |
+
# Verify token format
|
| 17 |
+
if not token.startswith("hf_"):
|
| 18 |
+
raise ValueError("Invalid token format. Must start with 'hf_'")
|
| 19 |
+
|
| 20 |
+
# 3. Securely store for session
|
| 21 |
+
os.environ["HF_TOKEN"] = token
|
| 22 |
+
login(token=token)
|
| 23 |
+
print("✅ Authentication successful - token stored in memory")
|
| 24 |
+
|
| 25 |
+
if __name__ == "__main__":
|
| 26 |
+
configure_auth()
|
sentiment_space/bassa_analyzer.py
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import io
|
| 3 |
+
import os
|
| 4 |
+
import torch
|
| 5 |
+
import soundfile as sf
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import numpy as np
|
| 8 |
+
from transformers import (
|
| 9 |
+
pipeline,
|
| 10 |
+
WhisperForConditionalGeneration,
|
| 11 |
+
WhisperProcessor,
|
| 12 |
+
AutoTokenizer,
|
| 13 |
+
AutoModelForSequenceClassification,
|
| 14 |
+
AutoModelForSeq2SeqLM
|
| 15 |
+
)
|
| 16 |
+
from langdetect import detect, detect_langs, DetectorFactory
|
| 17 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 18 |
+
import traceback
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# --- Configuration ---
|
| 22 |
+
DATASETS_DIR = os.path.dirname(__file__)
|
| 23 |
+
REFERENCE_CSV_PATH = os.path.join(DATASETS_DIR, 'reference.csv')
|
| 24 |
+
SIMILARITY_THRESHOLD = 0.85 # Minimum similarity score to trust the reference-based analysis
|
| 25 |
+
|
| 26 |
+
# --- Global Variables for Models ---
|
| 27 |
+
whisper_model = None
|
| 28 |
+
whisper_processor = None
|
| 29 |
+
sentiment_pipeline_model = None
|
| 30 |
+
reference_embeddings = []
|
| 31 |
+
translator_model = None
|
| 32 |
+
translator_tokenizer = None
|
| 33 |
+
langid_model = None # SpeechBrain language ID model
|
| 34 |
+
|
| 35 |
+
def get_audio_embedding(audio_data, processor, model):
|
| 36 |
+
"""Generates a feature embedding from audio data using the Whisper encoder."""
|
| 37 |
+
input_features = processor(audio_data, sampling_rate=16000, return_tensors="pt").input_features
|
| 38 |
+
# Use the encoder to get a representative embedding
|
| 39 |
+
with torch.no_grad():
|
| 40 |
+
embedding = model.get_encoder()(input_features).last_hidden_state.mean(dim=1)
|
| 41 |
+
return embedding.cpu().numpy()
|
| 42 |
+
|
| 43 |
+
def load_models_and_references(offline=False):
|
| 44 |
+
"""Loads all models and reference data into memory. Called once on startup."""
|
| 45 |
+
global whisper_model, whisper_processor, sentiment_pipeline_model, reference_embeddings, translator_model, translator_tokenizer, langid_model
|
| 46 |
+
|
| 47 |
+
print(f"Initializing models in {'OFFLINE' if offline else 'ONLINE'} mode...")
|
| 48 |
+
|
| 49 |
+
# 0. Set device
|
| 50 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 51 |
+
print(f"Device set to use {device}")
|
| 52 |
+
|
| 53 |
+
# 1. Load Whisper model (for audio transcription and embeddings)
|
| 54 |
+
whisper_model_name = "openai/whisper-medium" # switched to medium for efficiency
|
| 55 |
+
try:
|
| 56 |
+
whisper_processor = WhisperProcessor.from_pretrained(whisper_model_name, local_files_only=offline)
|
| 57 |
+
whisper_model = WhisperForConditionalGeneration.from_pretrained(
|
| 58 |
+
whisper_model_name,
|
| 59 |
+
local_files_only=offline
|
| 60 |
+
).to(device)
|
| 61 |
+
# Freeze model parameters to save memory
|
| 62 |
+
whisper_model.eval()
|
| 63 |
+
for param in whisper_model.parameters():
|
| 64 |
+
param.requires_grad = False
|
| 65 |
+
except Exception as e:
|
| 66 |
+
print(f"Error loading Whisper model: {e}")
|
| 67 |
+
if offline:
|
| 68 |
+
print("Hint: Run this script once with an internet connection to download the necessary models.")
|
| 69 |
+
raise
|
| 70 |
+
|
| 71 |
+
# 2. Load Sentiment Analysis Model
|
| 72 |
+
sentiment_model_name = "nlptown/bert-base-multilingual-uncased-sentiment"
|
| 73 |
+
try:
|
| 74 |
+
sentiment_tokenizer = AutoTokenizer.from_pretrained(sentiment_model_name, local_files_only=offline)
|
| 75 |
+
sentiment_model = AutoModelForSequenceClassification.from_pretrained(
|
| 76 |
+
sentiment_model_name,
|
| 77 |
+
local_files_only=offline
|
| 78 |
+
).to(device)
|
| 79 |
+
sentiment_pipeline_model = pipeline(
|
| 80 |
+
"sentiment-analysis",
|
| 81 |
+
model=sentiment_model,
|
| 82 |
+
tokenizer=sentiment_tokenizer,
|
| 83 |
+
device=0 if device == "cuda" else -1
|
| 84 |
+
)
|
| 85 |
+
except Exception as e:
|
| 86 |
+
print(f"Error loading sentiment pipeline: {e}")
|
| 87 |
+
if offline:
|
| 88 |
+
print("Hint: Run this script once with an internet connection to download the necessary models.")
|
| 89 |
+
raise
|
| 90 |
+
|
| 91 |
+
# 3. Load Translation Model (French to English)
|
| 92 |
+
try:
|
| 93 |
+
translator_model_name = "Helsinki-NLP/opus-mt-fr-en"
|
| 94 |
+
translator_tokenizer = AutoTokenizer.from_pretrained(
|
| 95 |
+
translator_model_name,
|
| 96 |
+
local_files_only=offline
|
| 97 |
+
)
|
| 98 |
+
translator_model = AutoModelForSeq2SeqLM.from_pretrained(
|
| 99 |
+
translator_model_name,
|
| 100 |
+
local_files_only=offline
|
| 101 |
+
).to(device)
|
| 102 |
+
translator_model.eval()
|
| 103 |
+
except Exception as e:
|
| 104 |
+
print(f"Warning: Could not load translation model. French-to-English translation will be unavailable. Error: {e}")
|
| 105 |
+
translator_model = None
|
| 106 |
+
translator_tokenizer = None
|
| 107 |
+
|
| 108 |
+
# 4. Load and process reference audios
|
| 109 |
+
try:
|
| 110 |
+
df = pd.read_csv(REFERENCE_CSV_PATH)
|
| 111 |
+
reference_embeddings = []
|
| 112 |
+
|
| 113 |
+
for index, row in df.iterrows():
|
| 114 |
+
audio_path = os.path.join(DATASETS_DIR, row['audio_path'])
|
| 115 |
+
try:
|
| 116 |
+
# Load audio file
|
| 117 |
+
audio_data, orig_sr = sf.read(audio_path)
|
| 118 |
+
|
| 119 |
+
# Apply the same preprocessing as input audio: mono, 16kHz, noise-reduced, trimmed
|
| 120 |
+
audio_data = preprocess_audio(audio_data, orig_sr)
|
| 121 |
+
|
| 122 |
+
# Get audio embedding using the same preprocessing as input
|
| 123 |
+
with torch.no_grad():
|
| 124 |
+
input_features = whisper_processor(
|
| 125 |
+
audio_data,
|
| 126 |
+
sampling_rate=16000,
|
| 127 |
+
return_tensors="pt"
|
| 128 |
+
).input_features.to(whisper_model.device)
|
| 129 |
+
embedding = whisper_model.get_encoder()(input_features).last_hidden_state.mean(dim=1).cpu().numpy()
|
| 130 |
+
|
| 131 |
+
# Store reference data
|
| 132 |
+
reference_embeddings.append({
|
| 133 |
+
'sentiment': row['sentiment'],
|
| 134 |
+
'embedding': embedding,
|
| 135 |
+
'audio_path': row['audio_path']
|
| 136 |
+
})
|
| 137 |
+
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f"Warning: Could not process reference audio {row['audio_path']}: {e}")
|
| 140 |
+
|
| 141 |
+
print(f"Loaded {len(reference_embeddings)} reference audio embeddings.")
|
| 142 |
+
|
| 143 |
+
except Exception as e:
|
| 144 |
+
print(f"Error loading reference audios: {e}")
|
| 145 |
+
if not os.path.exists(REFERENCE_CSV_PATH):
|
| 146 |
+
print(f"Error: Reference CSV file not found at {REFERENCE_CSV_PATH}")
|
| 147 |
+
reference_embeddings = []
|
| 148 |
+
|
| 149 |
+
def preprocess_audio(audio_data, orig_sr):
|
| 150 |
+
"""Ensure audio is mono, 16kHz, normalized, noise-reduced, and trimmed of silence."""
|
| 151 |
+
import torchaudio.transforms as T
|
| 152 |
+
import torch
|
| 153 |
+
import numpy as np
|
| 154 |
+
|
| 155 |
+
# Convert to mono if needed
|
| 156 |
+
if len(audio_data.shape) > 1:
|
| 157 |
+
audio_data = audio_data.mean(axis=0)
|
| 158 |
+
|
| 159 |
+
# Convert numpy to torch tensor if needed
|
| 160 |
+
if not isinstance(audio_data, torch.Tensor):
|
| 161 |
+
audio_data = torch.tensor(audio_data, dtype=torch.float32)
|
| 162 |
+
|
| 163 |
+
# Resample if needed
|
| 164 |
+
if orig_sr != 16000:
|
| 165 |
+
resampler = T.Resample(orig_sr, 16000)
|
| 166 |
+
audio_data = resampler(audio_data)
|
| 167 |
+
|
| 168 |
+
# Normalize
|
| 169 |
+
audio_data = audio_data / (audio_data.abs().max() + 1e-8)
|
| 170 |
+
|
| 171 |
+
# Simple noise reduction using spectral gating
|
| 172 |
+
try:
|
| 173 |
+
# Convert to frequency domain
|
| 174 |
+
stft = torch.stft(audio_data, n_fft=1024, hop_length=256, return_complex=True)
|
| 175 |
+
|
| 176 |
+
# Calculate noise floor (using first 0.5 seconds as noise estimate)
|
| 177 |
+
noise_samples = min(int(0.5 * 16000), len(audio_data))
|
| 178 |
+
noise_stft = torch.stft(audio_data[:noise_samples], n_fft=1024, hop_length=256, return_complex=True)
|
| 179 |
+
noise_floor = torch.mean(torch.abs(noise_stft), dim=1, keepdim=True)
|
| 180 |
+
|
| 181 |
+
# Apply spectral gating
|
| 182 |
+
magnitude = torch.abs(stft)
|
| 183 |
+
phase = torch.angle(stft)
|
| 184 |
+
|
| 185 |
+
# Gate threshold (adjust this value for more/less aggressive noise reduction)
|
| 186 |
+
gate_threshold = 2.0 * noise_floor
|
| 187 |
+
|
| 188 |
+
# Apply gating
|
| 189 |
+
gated_magnitude = torch.where(magnitude > gate_threshold, magnitude, magnitude * 0.1)
|
| 190 |
+
|
| 191 |
+
# Reconstruct signal
|
| 192 |
+
stft_cleaned = gated_magnitude * torch.exp(1j * phase)
|
| 193 |
+
audio_data = torch.istft(stft_cleaned, n_fft=1024, hop_length=256)
|
| 194 |
+
|
| 195 |
+
except Exception as e:
|
| 196 |
+
print(f"Warning: Noise reduction failed, using original audio: {e}")
|
| 197 |
+
# If noise reduction fails, continue with original audio
|
| 198 |
+
|
| 199 |
+
# Trim silence using VAD
|
| 200 |
+
try:
|
| 201 |
+
audio_data = T.Vad(sample_rate=16000)(audio_data.unsqueeze(0)).squeeze(0)
|
| 202 |
+
except Exception as e:
|
| 203 |
+
print(f"Warning: VAD failed, using original audio: {e}")
|
| 204 |
+
|
| 205 |
+
# Final normalization
|
| 206 |
+
audio_data = audio_data / (audio_data.abs().max() + 1e-8)
|
| 207 |
+
|
| 208 |
+
return audio_data.numpy()
|
| 209 |
+
|
| 210 |
+
def translate_fr_to_en(text):
|
| 211 |
+
"""Translate French text to English using the loaded model."""
|
| 212 |
+
global translator_model, translator_tokenizer
|
| 213 |
+
|
| 214 |
+
if translator_model is None or translator_tokenizer is None:
|
| 215 |
+
return "[Translation not available]"
|
| 216 |
+
|
| 217 |
+
try:
|
| 218 |
+
# Tokenize the input text
|
| 219 |
+
inputs = translator_tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=512)
|
| 220 |
+
|
| 221 |
+
# Move to the same device as the model
|
| 222 |
+
inputs = {k: v.to(translator_model.device) for k, v in inputs.items()}
|
| 223 |
+
|
| 224 |
+
# Generate translation
|
| 225 |
+
with torch.no_grad():
|
| 226 |
+
translated = translator_model.generate(**inputs)
|
| 227 |
+
|
| 228 |
+
# Decode and clean up the output
|
| 229 |
+
translation = translator_tokenizer.decode(translated[0], skip_special_tokens=True)
|
| 230 |
+
return translation
|
| 231 |
+
except Exception as e:
|
| 232 |
+
print(f"Translation error: {e}")
|
| 233 |
+
return f"[Translation error: {str(e)}]"
|
| 234 |
+
|
| 235 |
+
def validate_text_semantics(text, language):
|
| 236 |
+
"""Check if text makes semantic sense for the given language."""
|
| 237 |
+
if not text or len(text.strip()) < 3:
|
| 238 |
+
return False
|
| 239 |
+
|
| 240 |
+
text_lower = text.lower().strip()
|
| 241 |
+
|
| 242 |
+
# Common meaningful words for each language
|
| 243 |
+
french_words = {
|
| 244 |
+
'je', 'tu', 'il', 'elle', 'nous', 'vous', 'ils', 'elles', 'suis', 'es', 'est', 'sommes', 'êtes', 'sont',
|
| 245 |
+
'le', 'la', 'les', 'un', 'une', 'des', 'ce', 'cette', 'ces', 'mon', 'ma', 'mes', 'ton', 'ta', 'tes',
|
| 246 |
+
'et', 'ou', 'mais', 'avec', 'sans', 'pour', 'dans', 'sur', 'sous', 'devant', 'derrière', 'entre',
|
| 247 |
+
'bon', 'bonne', 'mauvais', 'mauvaise', 'grand', 'grande', 'petit', 'petite', 'ouveau', 'nouvelle',
|
| 248 |
+
'content', 'heureux', 'triste', 'fatigué', 'malade', 'fort', 'faible', 'riche', 'pauvre',
|
| 249 |
+
'maison', 'voiture', 'travail', 'famille', 'ami', 'temps', 'jour', 'nuit', 'matin', 'soir',
|
| 250 |
+
'manger', 'boire', 'dormir', 'parler', 'écouter', 'voir', 'regarder', 'penser', 'avoir'
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
english_words = {
|
| 254 |
+
'i', 'you', 'he', 'she', 'it', 'we', 'they', 'am', 'is', 'are', 'was', 'were', 'be', 'been',
|
| 255 |
+
'the', 'a', 'an', 'this', 'that', 'these', 'those', 'my', 'your', 'his', 'her', 'their', 'our',
|
| 256 |
+
'and', 'or', 'but', 'with', 'without', 'for', 'in', 'on', 'under', 'over', 'between', 'among',
|
| 257 |
+
'good', 'bad', 'big', 'small', 'old', 'young', 'happy', 'sad', 'tired', 'sick', 'strong', 'weak',
|
| 258 |
+
'house', 'car', 'work', 'family', 'friend', 'time', 'day', 'night', 'morning', 'evening',
|
| 259 |
+
'eat', 'drink', 'sleep', 'talk', 'listen', 'see', 'watch', 'think', 'know'
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
# Count meaningful words
|
| 263 |
+
words = text_lower.split()
|
| 264 |
+
if language == 'fr':
|
| 265 |
+
meaningful_words = sum(1 for word in words if word in french_words)
|
| 266 |
+
else: # english
|
| 267 |
+
meaningful_words = sum(1 for word in words if word in english_words)
|
| 268 |
+
|
| 269 |
+
# Check if at least 30% of words are meaningful
|
| 270 |
+
if len(words) > 0:
|
| 271 |
+
meaningful_ratio = meaningful_words / len(words)
|
| 272 |
+
return meaningful_ratio >= 0.3
|
| 273 |
+
return False
|
| 274 |
+
|
| 275 |
+
def infer_medical_theme(text, sentiment):
|
| 276 |
+
"""Infère un thème médical (en français) à partir du texte et du sentiment."""
|
| 277 |
+
text = text.lower() if text else ""
|
| 278 |
+
themes = [
|
| 279 |
+
("douleur", ["douleur", "mal", "souffrance", "tête", "ventre", "blessure", "brûlure", "crampe", "migraine", "a mal", "j'ai mal", "je souffre", "courbature", "arthrose", "lombalgie", "cervicalgie", "fracture", "entorse"]),
|
| 280 |
+
("anxiété", ["anxiété", "peur", "angoisse", "stress", "inquiet", "inquiétude", "panique", "crainte", "phobie", "appréhension"]),
|
| 281 |
+
("satisfaction", ["satisfait", "content", "heureux", "bien", "amélioration", "guéri", "merci", "soulagement", "rassuré", "confiance", "espoir"]),
|
| 282 |
+
("traitement", ["traitement", "médicament", "opération", "chirurgie", "soin", "injection", "piqûre", "prise en charge", "thérapie", "rééducation", "soins intensifs", "hospitalisation", "transfusion"]),
|
| 283 |
+
("diagnostic", ["diagnostic", "maladie", "symptôme", "fièvre", "infection", "test", "examen", "analyse", "bilan", "scanner", "irm", "radio", "prise de sang"]),
|
| 284 |
+
("fatigue", ["fatigue", "épuisé", "fatigué", "lassitude", "sommeil", "insomnie", "épuisement", "endormi", "sommeiller"]),
|
| 285 |
+
("sommeil", ["sommeil", "dormir", "insomnie", "réveil", "cauchemar", "sommeiller", "nuit blanche"]),
|
| 286 |
+
("appétit", ["appétit", "manger", "faim", "perte d'appétit", "anorexie", "boulimie", "nourriture", "aliment"]),
|
| 287 |
+
("mobilité", ["marcher", "mobilité", "déplacement", "fauteuil", "béquille", "boiter", "paralysie", "immobilisation", "chute"]),
|
| 288 |
+
("respiration", ["respirer", "respiration", "essoufflé", "asthme", "bronchite", "poumon", "dyspnée", "toux", "apnée"]),
|
| 289 |
+
("humeur", ["humeur", "dépression", "triste", "déprimé", "moral", "colère", "irritable", "pleurer", "joie", "motivation"]),
|
| 290 |
+
("isolement", ["seul", "isolement", "solitude", "abandonné", "délaissé", "rejeté"]),
|
| 291 |
+
("famille", ["famille", "enfant", "fils", "fille", "mari", "épouse", "parents", "proche", "visite"]),
|
| 292 |
+
("médicaments", ["médicament", "comprimé", "pilule", "ordonnance", "pharmacie", "dose", "posologie"]),
|
| 293 |
+
("suivi", ["suivi", "contrôle", "consultation", "rendez-vous", "visite", "bilan", "monitoring"]),
|
| 294 |
+
("guérison", ["guérison", "guéri", "rétabli", "rémission", "amélioration", "progression"]),
|
| 295 |
+
("rechute", ["rechute", "récidive", "retour", "aggravation"]),
|
| 296 |
+
("prévention", ["prévention", "vaccin", "vaccination", "protection", "dépistage"]),
|
| 297 |
+
("urgence", ["urgence", "urgence médicale", "samu", "pompiers", "urgence vitale", "urgence absolue"]),
|
| 298 |
+
]
|
| 299 |
+
for theme, keywords in themes:
|
| 300 |
+
for kw in keywords:
|
| 301 |
+
if kw in text:
|
| 302 |
+
return theme
|
| 303 |
+
if sentiment == "negatif":
|
| 304 |
+
return "douleur"
|
| 305 |
+
elif sentiment == "positif":
|
| 306 |
+
return "satisfaction"
|
| 307 |
+
elif sentiment == "neutre":
|
| 308 |
+
return "traitement"
|
| 309 |
+
else:
|
| 310 |
+
return "traitement"
|
| 311 |
+
|
| 312 |
+
def analyze(input_type: str, content: str) -> dict:
|
| 313 |
+
"""
|
| 314 |
+
Analyzes sentiment from an audio or text input in Bassa, French, or English.
|
| 315 |
+
"""
|
| 316 |
+
if not content:
|
| 317 |
+
raise ValueError("content cannot be empty")
|
| 318 |
+
|
| 319 |
+
text_original = ""
|
| 320 |
+
audio_embedding = None
|
| 321 |
+
methode_utilisee = "bert_fallback"
|
| 322 |
+
sentiment, confiance = None, 0.0
|
| 323 |
+
lang = None
|
| 324 |
+
|
| 325 |
+
# --- Step 1: Process Input ---
|
| 326 |
+
if input_type == "audio":
|
| 327 |
+
try:
|
| 328 |
+
audio_bytes = base64.b64decode(content)
|
| 329 |
+
audio_data, samplerate = sf.read(io.BytesIO(audio_bytes))
|
| 330 |
+
# Preprocess audio: mono, 16kHz, normalized, trimmed
|
| 331 |
+
audio_data = preprocess_audio(audio_data, samplerate)
|
| 332 |
+
samplerate = 16000
|
| 333 |
+
|
| 334 |
+
if whisper_processor is None or whisper_model is None:
|
| 335 |
+
return {"error": "Whisper model or processor is not loaded. Please check initialization."}
|
| 336 |
+
|
| 337 |
+
detected_lang = None
|
| 338 |
+
processed_input = whisper_processor(audio_data, sampling_rate=16000, return_tensors="pt")
|
| 339 |
+
input_features = processed_input.input_features.to(whisper_model.device)
|
| 340 |
+
attention_mask = processed_input.get("attention_mask")
|
| 341 |
+
|
| 342 |
+
# First, try automatic language detection without forcing any language
|
| 343 |
+
output_auto = whisper_model.generate(
|
| 344 |
+
input_features,
|
| 345 |
+
attention_mask=attention_mask,
|
| 346 |
+
return_dict_in_generate=True,
|
| 347 |
+
output_scores=True
|
| 348 |
+
)
|
| 349 |
+
trans_auto = whisper_processor.batch_decode(output_auto.sequences, skip_special_tokens=True)[0]
|
| 350 |
+
|
| 351 |
+
def avg_logprob(output):
|
| 352 |
+
if output.scores and len(output.scores) > 0:
|
| 353 |
+
return torch.cat([s for s in output.scores]).mean().item()
|
| 354 |
+
return float('-inf')
|
| 355 |
+
|
| 356 |
+
score_auto = avg_logprob(output_auto)
|
| 357 |
+
len_auto = len(trans_auto.strip())
|
| 358 |
+
|
| 359 |
+
print(f"[DEBUG] Auto transcription: '{trans_auto}' (score: {score_auto:.4f})")
|
| 360 |
+
|
| 361 |
+
# Check if auto transcription is meaningful
|
| 362 |
+
if len_auto >= 3:
|
| 363 |
+
try:
|
| 364 |
+
lang_auto = detect(trans_auto)
|
| 365 |
+
print(f"[DEBUG] Auto langdetect: {lang_auto}")
|
| 366 |
+
|
| 367 |
+
# If auto detection is confident French or English, use it directly
|
| 368 |
+
if lang_auto in ['fr', 'en']:
|
| 369 |
+
lang = lang_auto
|
| 370 |
+
text_original = trans_auto
|
| 371 |
+
print(f"[DEBUG] Auto detection confident: {lang_auto}, proceeding directly")
|
| 372 |
+
else:
|
| 373 |
+
# Not French or English, likely Bassa - will check references
|
| 374 |
+
lang = 'bss'
|
| 375 |
+
text_original = "[Bassa audio, no transcription]"
|
| 376 |
+
print(f"[DEBUG] Auto detection not French/English, checking Bassa references")
|
| 377 |
+
except:
|
| 378 |
+
# Langdetect failed, likely Bassa
|
| 379 |
+
lang = 'bss'
|
| 380 |
+
text_original = "[Bassa audio, no transcription]"
|
| 381 |
+
print(f"[DEBUG] Langdetect failed, defaulting to Bassa")
|
| 382 |
+
else:
|
| 383 |
+
# Auto transcription too short, likely Bassa
|
| 384 |
+
lang = 'bss'
|
| 385 |
+
text_original = "[Bassa audio, no transcription]"
|
| 386 |
+
print(f"[DEBUG] Auto transcription too short, defaulting to Bassa")
|
| 387 |
+
|
| 388 |
+
# For Bassa detection, prioritize reference-based analysis over forced decoding
|
| 389 |
+
# Only use forced decoding if auto detection is very confident about French/English
|
| 390 |
+
if lang == 'bss' and len_auto >= 5: # Auto transcription is substantial
|
| 391 |
+
print(f"[DEBUG] Auto detection suggests Bassa, checking reference similarity...")
|
| 392 |
+
|
| 393 |
+
# First, check if this audio matches any Bassa reference
|
| 394 |
+
if audio_embedding is not None and reference_embeddings:
|
| 395 |
+
similarities = [cosine_similarity(audio_embedding, ref['embedding'])[0][0] for ref in reference_embeddings]
|
| 396 |
+
max_similarity = max(similarities)
|
| 397 |
+
print(f"[DEBUG] Max reference similarity: {max_similarity:.4f}")
|
| 398 |
+
|
| 399 |
+
if max_similarity > SIMILARITY_THRESHOLD:
|
| 400 |
+
print(f"[DEBUG] High similarity with Bassa reference, confirming Bassa")
|
| 401 |
+
# Keep as Bassa, reference-based analysis will be used
|
| 402 |
+
else:
|
| 403 |
+
print(f"[DEBUG] Low similarity with Bassa references, trying forced decoding...")
|
| 404 |
+
|
| 405 |
+
# Only try forced decoding if reference similarity is low
|
| 406 |
+
# Use Whisper forced decoding for both French and English, pick best
|
| 407 |
+
forced_decoder_ids_fr = whisper_processor.get_decoder_prompt_ids(language="french", task="transcribe")
|
| 408 |
+
forced_decoder_ids_en = whisper_processor.get_decoder_prompt_ids(language="english", task="transcribe")
|
| 409 |
+
output_fr = whisper_model.generate(
|
| 410 |
+
input_features,
|
| 411 |
+
attention_mask=attention_mask,
|
| 412 |
+
forced_decoder_ids=forced_decoder_ids_fr,
|
| 413 |
+
return_dict_in_generate=True,
|
| 414 |
+
output_scores=True
|
| 415 |
+
)
|
| 416 |
+
output_en = whisper_model.generate(
|
| 417 |
+
input_features,
|
| 418 |
+
attention_mask=attention_mask,
|
| 419 |
+
forced_decoder_ids=forced_decoder_ids_en,
|
| 420 |
+
return_dict_in_generate=True,
|
| 421 |
+
output_scores=True
|
| 422 |
+
)
|
| 423 |
+
trans_fr = whisper_processor.batch_decode(output_fr.sequences, skip_special_tokens=True)[0]
|
| 424 |
+
trans_en = whisper_processor.batch_decode(output_en.sequences, skip_special_tokens=True)[0]
|
| 425 |
+
|
| 426 |
+
score_fr = avg_logprob(output_fr)
|
| 427 |
+
score_en = avg_logprob(output_en)
|
| 428 |
+
|
| 429 |
+
print(f"[DEBUG] Forced French: '{trans_fr}' (score: {score_fr:.4f})")
|
| 430 |
+
print(f"[DEBUG] Forced English: '{trans_en}' (score: {score_en:.4f})")
|
| 431 |
+
|
| 432 |
+
# Only switch to French/English if forced decoding produces significantly better results
|
| 433 |
+
# and the text makes clear semantic sense
|
| 434 |
+
len_fr = len(trans_fr.strip())
|
| 435 |
+
len_en = len(trans_en.strip())
|
| 436 |
+
|
| 437 |
+
if len_fr >= 5 and score_fr > score_auto + 1.0 and validate_text_semantics(trans_fr, 'fr'):
|
| 438 |
+
lang = 'fr'
|
| 439 |
+
text_original = trans_fr
|
| 440 |
+
print(f"[DEBUG] Forced French significantly better and makes sense")
|
| 441 |
+
elif len_en >= 5 and score_en > score_auto + 1.0 and validate_text_semantics(trans_en, 'en'):
|
| 442 |
+
lang = 'en'
|
| 443 |
+
text_original = trans_en
|
| 444 |
+
print(f"[DEBUG] Forced English significantly better and makes sense")
|
| 445 |
+
else:
|
| 446 |
+
# Keep as Bassa
|
| 447 |
+
print(f"[DEBUG] Forced decoding didn't produce better results, keeping as Bassa")
|
| 448 |
+
print(f"[DEBUG] No reference embeddings available, keeping as Bassa")
|
| 449 |
+
|
| 450 |
+
# Always compute embedding for reference-based analysis
|
| 451 |
+
with torch.no_grad():
|
| 452 |
+
audio_embedding = whisper_model.get_encoder()(input_features, attention_mask=attention_mask).last_hidden_state.mean(dim=1).cpu().numpy()
|
| 453 |
+
|
| 454 |
+
except Exception as e:
|
| 455 |
+
# Print the full traceback to the console for detailed debugging
|
| 456 |
+
print(f"--- DETAILED ERROR TRACEBACK ---")
|
| 457 |
+
traceback.print_exc()
|
| 458 |
+
print(f"----------------------------------")
|
| 459 |
+
return {"error": f"Failed to process audio file: {e}"}
|
| 460 |
+
else:
|
| 461 |
+
text_original = content
|
| 462 |
+
|
| 463 |
+
if not text_original.strip() and lang != 'bss':
|
| 464 |
+
return {"error": "Transcription failed or text is empty."}
|
| 465 |
+
|
| 466 |
+
# --- Step 2: Language Detection ---
|
| 467 |
+
if lang is None:
|
| 468 |
+
try:
|
| 469 |
+
lang = detect(text_original)
|
| 470 |
+
if lang not in ['fr', 'en']:
|
| 471 |
+
lang = 'bss'
|
| 472 |
+
except:
|
| 473 |
+
lang = 'bss'
|
| 474 |
+
|
| 475 |
+
# --- Step 3: Sentiment Analysis ---
|
| 476 |
+
# A. Bassa Reference-Based Analysis (if applicable)
|
| 477 |
+
if lang == 'bss' and audio_embedding is not None and reference_embeddings:
|
| 478 |
+
similarities = [cosine_similarity(audio_embedding, ref['embedding'])[0][0] for ref in reference_embeddings]
|
| 479 |
+
max_similarity = max(similarities)
|
| 480 |
+
if max_similarity > SIMILARITY_THRESHOLD:
|
| 481 |
+
best_match_index = np.argmax(similarities)
|
| 482 |
+
sentiment = reference_embeddings[best_match_index]['sentiment']
|
| 483 |
+
confiance = max_similarity
|
| 484 |
+
methode_utilisee = "base_referencee"
|
| 485 |
+
# B. Fallback to General Model or Mark as Uncertain
|
| 486 |
+
if sentiment is None:
|
| 487 |
+
if lang == 'bss':
|
| 488 |
+
sentiment = 'neutre'
|
| 489 |
+
confiance = 0.0
|
| 490 |
+
methode_utilisee = "reference_miss"
|
| 491 |
+
else:
|
| 492 |
+
try:
|
| 493 |
+
sentiment_result = sentiment_pipeline_model(text_original)[0]
|
| 494 |
+
star_rating = int(sentiment_result['label'].split()[0])
|
| 495 |
+
if star_rating <= 2:
|
| 496 |
+
sentiment = 'negatif'
|
| 497 |
+
elif star_rating == 3:
|
| 498 |
+
sentiment = 'neutre'
|
| 499 |
+
else:
|
| 500 |
+
sentiment = 'positif'
|
| 501 |
+
confiance = sentiment_result['score']
|
| 502 |
+
methode_utilisee = "nlptown_bert_multilingual"
|
| 503 |
+
except Exception as e:
|
| 504 |
+
print(f"Sentiment analysis error: {e}")
|
| 505 |
+
sentiment = 'neutre'
|
| 506 |
+
confiance = 0.0
|
| 507 |
+
methode_utilisee = "sentiment_error"
|
| 508 |
+
|
| 509 |
+
# --- Step 4: Translation ---
|
| 510 |
+
traduction_anglaise = "[Translation disabled: sentencepiece not installed]"
|
| 511 |
+
|
| 512 |
+
# --- Step 5: Format Output ---
|
| 513 |
+
theme = infer_medical_theme(text_original, sentiment)
|
| 514 |
+
return {
|
| 515 |
+
"sentiment": sentiment,
|
| 516 |
+
"score_sentiment": round(float(confiance), 4),
|
| 517 |
+
"theme": theme,
|
| 518 |
+
"langue_detectee": lang,
|
| 519 |
+
"texte_original": text_original
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
# --- Initialization ---
|
| 523 |
+
# Set offline=False for the very first run to download models.
|
| 524 |
+
# Set offline=True for all subsequent runs to work without internet.
|
| 525 |
+
load_models_and_references(offline=False)
|
| 526 |
+
|
| 527 |
+
if __name__ == '__main__':
|
| 528 |
+
print("\n--- Running Test Analysis ---")
|
| 529 |
+
test_text = "Je suis très content de ce produit, c'est formidable!"
|
| 530 |
+
result = analyze(input_type="text", content=test_text)
|
| 531 |
+
print(f"Analysis for text: '{test_text}'")
|
| 532 |
+
import json
|
| 533 |
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
sentiment_space/requirements.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transformers
|
| 2 |
+
huggingface_hub
|
| 3 |
+
datasets[audio]
|
| 4 |
+
accelerate
|
| 5 |
+
torch
|
| 6 |
+
torchaudio
|
| 7 |
+
soundfile
|
| 8 |
+
librosa
|
| 9 |
+
pandas
|
| 10 |
+
scikit-learn
|
| 11 |
+
langdetect
|
| 12 |
+
fastapi
|
| 13 |
+
uvicorn[standard]
|
| 14 |
+
protobuf
|
| 15 |
+
sentencepiece
|