hrlima commited on
Commit
bf96629
·
verified ·
1 Parent(s): 3d0e4a1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +61 -184
app.py CHANGED
@@ -1,10 +1,5 @@
1
  import os
2
  import json
3
- import base64
4
- import tempfile
5
- import requests
6
- import math
7
- import numpy as np
8
  import firebase_admin
9
  from firebase_admin import credentials, firestore
10
  from flask import Flask, request, jsonify
@@ -26,55 +21,23 @@ try:
26
  except Exception as e:
27
  print(f"❌ Erro ao inicializar Firebase: {e}")
28
 
29
- # ====== CONFIGS AJUSTÁVEIS (env vars) ======
30
- # Modelo recomendado para PT (substitua se quiser um checkpoint em inglês)
31
- AUDIO_SER_MODEL = os.getenv("AUDIO_SER_MODEL", "alefiury/wav2vec2-xls-r-300m-pt-br-spontaneous-speech-emotion-recognition")
32
- # Ex.: set ENABLE_ASR=true para ativar ASR (pode consumir muita memória)
33
- ENABLE_ASR = os.getenv("ENABLE_ASR", "false").lower() in ("1", "true", "yes")
34
- AUDIO_TOPK_RUNS = os.getenv("AUDIO_TOPK_RUNS", "10,15,20") # exemplo: "10,15,20"
35
- AUDIO_SOFTMAX_TEMP = float(os.getenv("AUDIO_SOFTMAX_TEMP", "0.6"))
36
- MIN_LABEL_PROB = float(os.getenv("MIN_LABEL_PROB", "0.02"))
37
- DEPRESSION_THRESHOLD = float(os.getenv("DEPRESSION_THRESHOLD", "0.92"))
38
-
39
- # ====== PIPELINE: modelo SER (wav2vec2 finetuned) ======
40
  try:
41
- audio_pipeline = pipeline(
42
- task="audio-classification",
43
- model=AUDIO_SER_MODEL
44
- )
45
- print(f"✅ audio_pipeline carregado: {AUDIO_SER_MODEL}")
46
  except Exception as e:
47
- print(f"❌ Erro ao carregar audio_pipeline ({AUDIO_SER_MODEL}): {e}")
48
- audio_pipeline = None
49
-
50
- # Opcional: ASR (desativado por padrão para economia de recursos)
51
- asr_pipeline = None
52
- if ENABLE_ASR:
53
- try:
54
- asr_pipeline = pipeline(
55
- task="automatic-speech-recognition",
56
- model="openai/whisper-large-v3"
57
- )
58
- print("✅ asr_pipeline carregado (ENABLE_ASR=true).")
59
- except Exception as e:
60
- print(f"⚠️ ASR indisponível: {e}")
61
- asr_pipeline = None
62
 
63
- # ====== MAPEAMENTO DE EMOÇÕES (ING->PT) ======
64
- # OBS: cada modelo pode usar rótulos ligeiramente diferentes; padronizamos para estes
65
  emotion_labels = {
66
- "angry": "raiva",
 
67
  "anger": "raiva",
68
- "disgust": "insegurança",
69
- "fearful": "ansiedade",
70
  "fear": "ansiedade",
71
- "happy": "alegria",
72
- "joy": "alegria",
73
- "neutral": "neutro",
74
- "sad": "tristeza",
75
- "sadness": "tristeza",
76
- "surprised": "surpreso",
77
- "surprise": "surpreso",
78
  }
79
 
80
  # ====== SUGESTÕES ======
@@ -91,19 +54,19 @@ def gerar_sugestao(emotion_pt):
91
  }
92
  return sugestoes.get(emotion_pt, "Mantenha o equilíbrio emocional e cuide de você mesmo.")
93
 
94
- # ====== FALLBACK POR TEXTO ======
95
  EMOTION_KEYWORDS = {
96
  "tristeza": ["triste","desanimado","melancólico","chateado","solitário","deprimido","abatido","infeliz","desmotivado"],
97
  "ansiedade": ["ansioso","preocupado","nervoso","tenso","inquieto","aflito","alarmado","sobrecarregado","inseguro","apreensivo"],
98
  "insegurança": ["inseguro","incerto","receoso","hesitante","duvidoso","apreensivo","desconfiado"],
99
  "raiva": ["irritado","zangado","raiva","furioso","ódio","revoltado","frustrado","indignado","hostil","bravo","enfurecido","irado"],
100
  "alegria": ["feliz","animado","contente","alegre","satisfeito","entusiasmado","radiante","orgulhoso","euforia"],
101
- "depressão": ["sem esperança","vazio","desesperado","sem vontade","cansado da vida","desamparado", "matar","depressivo"],
102
  "neutro": ["ok","normal","tranquilo","indiferente","equilibrado","estável"]
103
  }
104
 
105
  def fallback_emotion(text):
106
- text_lower = (text or "").lower()
107
  match_counts = {k: sum(1 for w in v if w in text_lower) for k, v in EMOTION_KEYWORDS.items()}
108
  emotion = max(match_counts, key=match_counts.get)
109
  if match_counts[emotion] == 0:
@@ -117,161 +80,75 @@ def fallback_emotion(text):
117
  "debug": "Fallback ativado"
118
  }
119
 
120
- # ====== UTIL: softmax temperado ======
121
- def tempered_softmax(scores_dict, temperature=1.0):
122
- labels = list(scores_dict.keys())
123
- vals = np.array([scores_dict[l] for l in labels], dtype=float)
124
- vals = np.clip(vals, 1e-8, 1-1e-8)
125
- logits = np.log(vals / (1 - vals))
126
- scaled = logits / max(temperature, 1e-6)
127
- exps = np.exp(scaled - np.max(scaled))
128
- probs = exps / np.sum(exps)
129
- return dict(zip(labels, probs))
130
 
131
- def average_probabilities(list_of_prob_dicts):
132
- all_keys = set()
133
- for d in list_of_prob_dicts:
134
- all_keys.update(d.keys())
135
- if not all_keys:
136
- return {}
137
- avg = {k: 0.0 for k in all_keys}
138
- for d in list_of_prob_dicts:
139
- for k in all_keys:
140
- avg[k] += d.get(k, 0.0)
141
- n = len(list_of_prob_dicts)
142
- for k in avg:
143
- avg[k] /= n
144
- total = sum(avg.values()) or 1.0
145
- for k in avg:
146
- avg[k] /= total
147
- return avg
148
 
149
- # ====== HELPERS ÁUDIO ======
150
- def save_bytes_to_tempfile(bbytes, suffix=".wav"):
151
- fd, path = tempfile.mkstemp(suffix=suffix)
152
- os.close(fd)
153
- with open(path, "wb") as f:
154
- f.write(bbytes)
155
- return path
156
 
157
- def fetch_url_to_tempfile(url):
158
- r = requests.get(url, timeout=15)
159
- r.raise_for_status()
160
- content_type = r.headers.get("content-type", "")
161
- suffix = ".wav"
162
- if "mpeg" in content_type or "mp3" in content_type:
163
- suffix = ".mp3"
164
- return save_bytes_to_tempfile(r.content, suffix=suffix)
 
165
 
166
- # ====== ROTA /analyze ======
167
  @app.route("/analyze", methods=["POST"])
168
  def analyze():
169
  try:
170
- audio_path = None
171
- audio_bytes = None
172
- data = None
173
 
174
- # receber multipart/file ou json
175
- if "file" in request.files:
176
- f = request.files["file"]
177
- audio_bytes = f.read()
178
- else:
179
- try:
180
- data = request.get_json(silent=True)
181
- except Exception:
182
- data = None
183
- if data:
184
- if "audio_base64" in data:
185
- audio_bytes = base64.b64decode(data["audio_base64"])
186
- elif "audio_url" in data:
187
- audio_path = fetch_url_to_tempfile(data["audio_url"])
188
- elif "text" in data and (not audio_bytes and not audio_path):
189
- return jsonify(fallback_emotion(data["text"]))
190
 
191
- if audio_bytes:
192
- audio_path = save_bytes_to_tempfile(audio_bytes, suffix=".wav")
193
 
194
- if not audio_path:
195
- return jsonify({"error": "Nenhum áudio foi enviado. Envie 'file', 'audio_base64' ou 'audio_url', ou 'text' para fallback."}), 400
196
-
197
- if not audio_pipeline:
198
- if data and "text" in data:
199
- return jsonify(fallback_emotion(data["text"]))
200
- return jsonify({"error": "Modelo de áudio indisponível no momento."}), 500
201
-
202
- # ----- Ensemble interno: múltiplas runs com diferentes top_k -----
203
- topk_list = [int(x) for x in AUDIO_TOPK_RUNS.split(",") if x.strip().isdigit()]
204
- if not topk_list:
205
- topk_list = [10, 15, 20]
206
-
207
- run_probs = []
208
- raw_runs = []
209
-
210
- for topk in topk_list:
211
- try:
212
- raw_result = audio_pipeline(audio_path, top_k=topk)
213
- probs = {}
214
- # raw_result é lista de dicts
215
- for item in raw_result:
216
- lbl = item.get("label", "").lower()
217
- if lbl == "fear":
218
- lbl = "fearful"
219
- probs[lbl] = float(item.get("score", 0.0))
220
- if probs:
221
- run_probs.append(probs)
222
- raw_runs.append({"top_k": topk, "raw": raw_result})
223
- except Exception as e:
224
- print(f"⚠️ audio_pipeline falhou top_k={topk}: {e}")
225
-
226
- if not run_probs:
227
- return jsonify({"error": "Modelo não retornou rótulos em nenhuma tentativa."}), 500
228
-
229
- avg_probs = average_probabilities(run_probs)
230
-
231
- # recalibrar com temperatura (mais baixa => mais confiante)
232
- calibrated = tempered_softmax(avg_probs, temperature=AUDIO_SOFTMAX_TEMP)
233
-
234
- # filtrar rótulos fracos
235
- filtered = {k: (v if v >= MIN_LABEL_PROB else 0.0) for k, v in calibrated.items()}
236
- totalf = sum(filtered.values()) or 1.0
237
- normalized = {k: (v / totalf) for k, v in filtered.items()}
238
-
239
- top_label = max(normalized, key=normalized.get)
240
- top_score = normalized[top_label]
241
 
 
 
 
242
  emotion_pt = emotion_labels.get(top_label, "desconhecido")
243
- if emotion_pt == "tristeza" and top_score >= DEPRESSION_THRESHOLD:
244
- emotion_pt = "depressão"
245
 
246
- probabilities_pt = { emotion_labels.get(k, k): round(float(v), 3) for k, v in normalized.items() }
 
 
247
 
248
  base_result = {
249
  "status": "ok",
250
  "emotion": emotion_pt,
251
  "emode": [emotion_pt],
252
- "confidence": round(float(top_score), 3),
253
- "probabilities": probabilities_pt,
254
- "suggestion": gerar_sugestao(emotion_pt),
255
- "debug": {
256
- "model": AUDIO_SER_MODEL,
257
- "runs": raw_runs,
258
- "avg_probs": {k: round(float(v), 4) for k, v in avg_probs.items()},
259
- "calibrated": {k: round(float(v), 4) for k, v in calibrated.items()},
260
- "normalized": {k: round(float(v), 4) for k, v in normalized.items()}
261
- }
262
  }
263
 
264
- return jsonify(base_result)
 
 
265
 
266
  except Exception as e:
267
- print(f"❌ Erro na rota /analyze: {e}")
268
  return jsonify({"error": str(e)}), 500
269
- finally:
270
- try:
271
- if 'audio_path' in locals() and audio_path and os.path.exists(audio_path):
272
- os.remove(audio_path)
273
- except Exception:
274
- pass
275
 
276
  if __name__ == "__main__":
277
- app.run(host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
 
1
  import os
2
  import json
 
 
 
 
 
3
  import firebase_admin
4
  from firebase_admin import credentials, firestore
5
  from flask import Flask, request, jsonify
 
21
  except Exception as e:
22
  print(f"❌ Erro ao inicializar Firebase: {e}")
23
 
24
+ # ====== MODELO ======
 
 
 
 
 
 
 
 
 
 
25
  try:
26
+ model_pipeline = pipeline("text-classification", model="pysentimiento/robertuito-emotion-analysis")
27
+ print("✅ Modelo carregado com sucesso!")
 
 
 
28
  except Exception as e:
29
+ print(f"❌ Erro ao carregar modelo: {e}")
30
+ model_pipeline = None
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
+ # ====== MAPEAMENTO DE EMOÇÕES ======
 
33
  emotion_labels = {
34
+ "sadness": "tristeza",
35
+ "joy": "alegria",
36
  "anger": "raiva",
 
 
37
  "fear": "ansiedade",
38
+ "disgust": "insegurança",
39
+ "surprise": "alegria",
40
+ "others": "neutro"
 
 
 
 
41
  }
42
 
43
  # ====== SUGESTÕES ======
 
54
  }
55
  return sugestoes.get(emotion_pt, "Mantenha o equilíbrio emocional e cuide de você mesmo.")
56
 
57
+ # ====== FALLBACK APRIMORADO COM PALAVRAS-CHAVE ======
58
  EMOTION_KEYWORDS = {
59
  "tristeza": ["triste","desanimado","melancólico","chateado","solitário","deprimido","abatido","infeliz","desmotivado"],
60
  "ansiedade": ["ansioso","preocupado","nervoso","tenso","inquieto","aflito","alarmado","sobrecarregado","inseguro","apreensivo"],
61
  "insegurança": ["inseguro","incerto","receoso","hesitante","duvidoso","apreensivo","desconfiado"],
62
  "raiva": ["irritado","zangado","raiva","furioso","ódio","revoltado","frustrado","indignado","hostil","bravo","enfurecido","irado"],
63
  "alegria": ["feliz","animado","contente","alegre","satisfeito","entusiasmado","radiante","orgulhoso","euforia"],
64
+ "depressão": ["sem esperança","vazio","desesperado","sem vontade","cansado da vida","desamparado"],
65
  "neutro": ["ok","normal","tranquilo","indiferente","equilibrado","estável"]
66
  }
67
 
68
  def fallback_emotion(text):
69
+ text_lower = text.lower()
70
  match_counts = {k: sum(1 for w in v if w in text_lower) for k, v in EMOTION_KEYWORDS.items()}
71
  emotion = max(match_counts, key=match_counts.get)
72
  if match_counts[emotion] == 0:
 
80
  "debug": "Fallback ativado"
81
  }
82
 
83
+ # ====== AJUSTE HÍBRIDO ======
84
+ def hybrid_emotion(text, result):
85
+ text_lower = text.lower()
86
+ detected = result.get("emotion", "neutro")
87
+ max_matches = 0
 
 
 
 
 
88
 
89
+ for emo, keywords in EMOTION_KEYWORDS.items():
90
+ matches = sum(2 for w in keywords if w in text_lower)
91
+ if matches > max_matches:
92
+ max_matches = matches
93
+ if emo != detected:
94
+ detected = emo
 
 
 
 
 
 
 
 
 
 
 
95
 
96
+ confidence = result.get("confidence", 0.0)
97
+ if detected != result.get("emotion"):
98
+ confidence = 0.7 + max_matches * 0.05
99
+ confidence = min(confidence, 1.0)
 
 
 
100
 
101
+ return {
102
+ "status": "ok",
103
+ "emotion": detected,
104
+ "emode": [detected],
105
+ "confidence": round(confidence, 2),
106
+ "probabilities": result.get("probabilities", {detected: 1.0}),
107
+ "suggestion": result.get("suggestion", gerar_sugestao(detected)),
108
+ "debug": result.get("debug", "Híbrido aplicado")
109
+ }
110
 
111
+ # ====== ROTA DE ANÁLISE ======
112
  @app.route("/analyze", methods=["POST"])
113
  def analyze():
114
  try:
115
+ data = request.get_json()
116
+ if not data or "text" not in data:
117
+ return jsonify({"error": "Campo 'text' é obrigatório."}), 400
118
 
119
+ text = data["text"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
+ if not model_pipeline:
122
+ return jsonify(fallback_emotion(text))
123
 
124
+ result = model_pipeline(text, return_all_scores=True)
125
+ if not result or len(result) == 0:
126
+ return jsonify(fallback_emotion(text))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
+ scores = {r["label"]: r["score"] for r in result[0]}
129
+ top_label = max(scores, key=scores.get)
130
+ confidence = round(scores[top_label], 2)
131
  emotion_pt = emotion_labels.get(top_label, "desconhecido")
 
 
132
 
133
+ # Ajuste especial para "tristeza" muito forte
134
+ if emotion_pt == "tristeza" and confidence >= 0.9:
135
+ emotion_pt = "depressão"
136
 
137
  base_result = {
138
  "status": "ok",
139
  "emotion": emotion_pt,
140
  "emode": [emotion_pt],
141
+ "confidence": confidence,
142
+ "probabilities": {emotion_labels.get(k, k): round(v,3) for k,v in scores.items()},
143
+ "suggestion": gerar_sugestao(emotion_pt)
 
 
 
 
 
 
 
144
  }
145
 
146
+ # Aplica lógica híbrida com fallback de palavras-chave
147
+ final_result = hybrid_emotion(text, base_result)
148
+ return jsonify(final_result)
149
 
150
  except Exception as e:
 
151
  return jsonify({"error": str(e)}), 500
 
 
 
 
 
 
152
 
153
  if __name__ == "__main__":
154
+ app.run(host="0.0.0.0", port=7860)