Dama03 commited on
Commit
7b2fa2c
·
1 Parent(s): 8b16f1e

ajout du code necessaire sans voice

Browse files
.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