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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +78 -97
app.py CHANGED
@@ -26,27 +26,55 @@ try:
26
  except Exception as e:
27
  print(f"❌ Erro ao inicializar Firebase: {e}")
28
 
29
- # ====== PIPELINE: Apenas o modelo de áudio (Whisper fine-tuned) ======
 
 
 
 
 
 
 
 
 
 
30
  try:
31
  audio_pipeline = pipeline(
32
  task="audio-classification",
33
- model="firdhokk/speech-emotion-recognition-with-openai-whisper-large-v3"
34
  )
35
- print("✅ audio_pipeline carregado.")
36
  except Exception as e:
37
- print(f"❌ Erro ao carregar audio_pipeline: {e}")
38
  audio_pipeline = None
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  # ====== MAPEAMENTO DE EMOÇÕES (ING->PT) ======
 
41
  emotion_labels = {
42
  "angry": "raiva",
 
43
  "disgust": "insegurança",
44
  "fearful": "ansiedade",
45
  "fear": "ansiedade",
46
  "happy": "alegria",
 
47
  "neutral": "neutro",
48
  "sad": "tristeza",
 
49
  "surprised": "surpreso",
 
50
  }
51
 
52
  # ====== SUGESTÕES ======
@@ -63,7 +91,7 @@ def gerar_sugestao(emotion_pt):
63
  }
64
  return sugestoes.get(emotion_pt, "Mantenha o equilíbrio emocional e cuide de você mesmo.")
65
 
66
- # ====== FALLBACK APRIMORADO COM PALAVRAS-CHAVE (mantido) ======
67
  EMOTION_KEYWORDS = {
68
  "tristeza": ["triste","desanimado","melancólico","chateado","solitário","deprimido","abatido","infeliz","desmotivado"],
69
  "ansiedade": ["ansioso","preocupado","nervoso","tenso","inquieto","aflito","alarmado","sobrecarregado","inseguro","apreensivo"],
@@ -89,52 +117,7 @@ def fallback_emotion(text):
89
  "debug": "Fallback ativado"
90
  }
91
 
92
- # ====== AJUSTE HÍBRIDO (mantido) ======
93
- def hybrid_emotion(text, result):
94
- text_lower = (text or "").lower()
95
- detected = result.get("emotion", "neutro")
96
- max_matches = 0
97
-
98
- for emo, keywords in EMOTION_KEYWORDS.items():
99
- matches = sum(2 for w in keywords if w in text_lower)
100
- if matches > max_matches:
101
- max_matches = matches
102
- if emo != detected:
103
- detected = emo
104
-
105
- confidence = result.get("confidence", 0.0)
106
- if detected != result.get("emotion"):
107
- confidence = 0.7 + max_matches * 0.05
108
- confidence = min(confidence, 1.0)
109
-
110
- return {
111
- "status": "ok",
112
- "emotion": detected,
113
- "emode": [detected],
114
- "confidence": round(confidence, 2),
115
- "probabilities": result.get("probabilities", {detected: 1.0}),
116
- "suggestion": result.get("suggestion", gerar_sugestao(detected)),
117
- "debug": result.get("debug", "Híbrido aplicado")
118
- }
119
-
120
- # ====== HELPERS PARA ÁUDIO ======
121
- def save_bytes_to_tempfile(bbytes, suffix=".wav"):
122
- fd, path = tempfile.mkstemp(suffix=suffix)
123
- os.close(fd)
124
- with open(path, "wb") as f:
125
- f.write(bbytes)
126
- return path
127
-
128
- def fetch_url_to_tempfile(url):
129
- r = requests.get(url, timeout=15)
130
- r.raise_for_status()
131
- content_type = r.headers.get("content-type", "")
132
- suffix = ".wav"
133
- if "mpeg" in content_type or "mp3" in content_type:
134
- suffix = ".mp3"
135
- return save_bytes_to_tempfile(r.content, suffix=suffix)
136
-
137
- # ====== UTIL: Softmax com temperatura para calibrar probabilidades ======
138
  def tempered_softmax(scores_dict, temperature=1.0):
139
  labels = list(scores_dict.keys())
140
  vals = np.array([scores_dict[l] for l in labels], dtype=float)
@@ -145,7 +128,6 @@ def tempered_softmax(scores_dict, temperature=1.0):
145
  probs = exps / np.sum(exps)
146
  return dict(zip(labels, probs))
147
 
148
- # ====== UTIL: média/união de probabilidades ======
149
  def average_probabilities(list_of_prob_dicts):
150
  all_keys = set()
151
  for d in list_of_prob_dicts:
@@ -158,13 +140,30 @@ def average_probabilities(list_of_prob_dicts):
158
  avg[k] += d.get(k, 0.0)
159
  n = len(list_of_prob_dicts)
160
  for k in avg:
161
- avg[k] = avg[k] / n
162
  total = sum(avg.values()) or 1.0
163
  for k in avg:
164
- avg[k] = avg[k] / total
165
  return avg
166
 
167
- # ====== ROTA DE ANÁLISE (apenas modelo firdhokk, precisão aumentada por ensemble interno) ======
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  @app.route("/analyze", methods=["POST"])
169
  def analyze():
170
  try:
@@ -172,7 +171,7 @@ def analyze():
172
  audio_bytes = None
173
  data = None
174
 
175
- # prioridade: arquivo multipart 'file'
176
  if "file" in request.files:
177
  f = request.files["file"]
178
  audio_bytes = f.read()
@@ -181,14 +180,12 @@ def analyze():
181
  data = request.get_json(silent=True)
182
  except Exception:
183
  data = None
184
-
185
  if data:
186
  if "audio_base64" in data:
187
  audio_bytes = base64.b64decode(data["audio_base64"])
188
  elif "audio_url" in data:
189
  audio_path = fetch_url_to_tempfile(data["audio_url"])
190
  elif "text" in data and (not audio_bytes and not audio_path):
191
- # apenas texto -> fallback textual
192
  return jsonify(fallback_emotion(data["text"]))
193
 
194
  if audio_bytes:
@@ -202,60 +199,50 @@ def analyze():
202
  return jsonify(fallback_emotion(data["text"]))
203
  return jsonify({"error": "Modelo de áudio indisponível no momento."}), 500
204
 
205
- # -------------------------
206
- # EXECUTAR VÁRIAS PASSAGENS (ensemble interno)
207
- # -------------------------
208
- # lista de top_k para executar o pipeline (captura incertezas)
209
- topk_list = [10, 15, 20]
210
- run_probs = [] # armazenará dicts label->score para cada run (antes de softmax)
211
- raw_runs = [] # debug: guardar raw_result para inspeção
212
 
213
  for topk in topk_list:
214
  try:
215
  raw_result = audio_pipeline(audio_path, top_k=topk)
216
- # normalizar formato: raw_result é lista de dicts [{'label':..., 'score':...}, ...]
217
  probs = {}
 
218
  for item in raw_result:
219
- label = item.get("label", "").lower()
220
- if label == "fear":
221
- label = "fearful"
222
- probs[label] = float(item.get("score", 0.0))
223
  if probs:
224
  run_probs.append(probs)
225
  raw_runs.append({"top_k": topk, "raw": raw_result})
226
  except Exception as e:
227
- # log e seguir para próximas tentativas (não interromper totalmente)
228
- print(f"⚠️ audio_pipeline falhou no top_k={topk}: {e}")
229
 
230
  if not run_probs:
231
  return jsonify({"error": "Modelo não retornou rótulos em nenhuma tentativa."}), 500
232
 
233
- # 1) média das probabilidades (por rótulo) entre as execuções
234
  avg_probs = average_probabilities(run_probs)
235
 
236
- # 2) recalibrar com temperatura (temperatura menor -> mais "afiado")
237
- temp = float(os.getenv("AUDIO_SOFTMAX_TEMP", 0.6)) # default 0.6 para maior precisão
238
- calibrated_probs = tempered_softmax(avg_probs, temperature=temp)
239
 
240
- # 3) opcional: aplicar pequena regra de confiança mínima para reduzir rótulos com prob insignificante
241
- # (zero out labels abaixo threshold then renormalize)
242
- min_prob_threshold = float(os.getenv("MIN_LABEL_PROB", 0.02)) # 2% por padrão
243
- filtered = {k: v if v >= min_prob_threshold else 0.0 for k, v in calibrated_probs.items()}
244
  totalf = sum(filtered.values()) or 1.0
245
  normalized = {k: (v / totalf) for k, v in filtered.items()}
246
 
247
- # escolher rótulo final
248
  top_label = max(normalized, key=normalized.get)
249
  top_score = normalized[top_label]
250
 
251
- # map to portuguese
252
  emotion_pt = emotion_labels.get(top_label, "desconhecido")
253
-
254
- # regra de negócio: tristeza muito forte -> depressão
255
- if emotion_pt == "tristeza" and top_score >= float(os.getenv("DEPRESSION_THRESHOLD", 0.92)):
256
  emotion_pt = "depressão"
257
 
258
- # montar probabilidades para output (mapeadas p/ pt)
259
  probabilities_pt = { emotion_labels.get(k, k): round(float(v), 3) for k, v in normalized.items() }
260
 
261
  base_result = {
@@ -266,21 +253,15 @@ def analyze():
266
  "probabilities": probabilities_pt,
267
  "suggestion": gerar_sugestao(emotion_pt),
268
  "debug": {
 
269
  "runs": raw_runs,
270
  "avg_probs": {k: round(float(v), 4) for k, v in avg_probs.items()},
271
- "calibrated_probs": {k: round(float(v), 4) for k, v in calibrated_probs.items()},
272
- "normalized_probs": {k: round(float(v), 4) for k, v in normalized.items()}
273
  }
274
  }
275
 
276
- # permitir que cliente envie 'text' (override/híbrido) — mantido como opção leve
277
- text_for_hybrid = None
278
- if data and "text" in data:
279
- text_for_hybrid = data["text"]
280
-
281
- final_result = hybrid_emotion(text_for_hybrid, base_result) if text_for_hybrid else base_result
282
-
283
- return jsonify(final_result)
284
 
285
  except Exception as e:
286
  print(f"❌ Erro na rota /analyze: {e}")
@@ -293,4 +274,4 @@ def analyze():
293
  pass
294
 
295
  if __name__ == "__main__":
296
- app.run(host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
 
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
  }
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"],
 
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)
 
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:
 
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:
 
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()
 
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:
 
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 = {
 
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}")
 
274
  pass
275
 
276
  if __name__ == "__main__":
277
+ app.run(host="0.0.0.0", port=int(os.getenv("PORT", 7860)))