hrlima commited on
Commit
a44c51b
·
verified ·
1 Parent(s): 79f2787

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +59 -193
app.py CHANGED
@@ -26,8 +26,7 @@ try:
26
  except Exception as e:
27
  print(f"❌ Erro ao inicializar Firebase: {e}")
28
 
29
- # ====== PIPELINES ======
30
- # 1) Pipeline de classificação de áudio (modelo Whisper fine-tuned)
31
  try:
32
  audio_pipeline = pipeline(
33
  task="audio-classification",
@@ -38,30 +37,6 @@ except Exception as e:
38
  print(f"❌ Erro ao carregar audio_pipeline: {e}")
39
  audio_pipeline = None
40
 
41
- # 2) Pipeline ASR (transcrição) - usar Whisper para obter texto que ajudará no texto-classifier
42
- # Note: dependendo do ambiente, carregar whisper-large-v3 pode ser pesado.
43
- try:
44
- asr_pipeline = pipeline(
45
- task="automatic-speech-recognition",
46
- model="openai/whisper-large-v3"
47
- )
48
- print("✅ asr_pipeline carregado.")
49
- except Exception as e:
50
- print(f"⚠️ ASR indisponível: {e}")
51
- asr_pipeline = None
52
-
53
- # 3) Pipeline de classificação de texto (para multimodal ensemble)
54
- try:
55
- text_pipeline = pipeline(
56
- task="text-classification",
57
- model="pysentimiento/robertuito-emotion-analysis",
58
- return_all_scores=True
59
- )
60
- print("✅ text_pipeline carregado.")
61
- except Exception as e:
62
- print(f"⚠️ text_pipeline indisponível: {e}")
63
- text_pipeline = None
64
-
65
  # ====== MAPEAMENTO DE EMOÇÕES (ING->PT) ======
66
  emotion_labels = {
67
  "angry": "raiva",
@@ -72,7 +47,6 @@ emotion_labels = {
72
  "neutral": "neutro",
73
  "sad": "tristeza",
74
  "surprised": "surpreso",
75
- # fallback caso o label seja diferente
76
  }
77
 
78
  # ====== SUGESTÕES ======
@@ -89,7 +63,7 @@ def gerar_sugestao(emotion_pt):
89
  }
90
  return sugestoes.get(emotion_pt, "Mantenha o equilíbrio emocional e cuide de você mesmo.")
91
 
92
- # ====== FALLBACK APRIMORADO COM PALAVRAS-CHAVE ======
93
  EMOTION_KEYWORDS = {
94
  "tristeza": ["triste","desanimado","melancólico","chateado","solitário","deprimido","abatido","infeliz","desmotivado"],
95
  "ansiedade": ["ansioso","preocupado","nervoso","tenso","inquieto","aflito","alarmado","sobrecarregado","inseguro","apreensivo"],
@@ -162,42 +136,35 @@ def fetch_url_to_tempfile(url):
162
 
163
  # ====== UTIL: Softmax com temperatura para calibrar probabilidades ======
164
  def tempered_softmax(scores_dict, temperature=1.0):
165
- # scores_dict: {label: score} (scores raw in [0..1] but we re-calibrate)
166
- # convert to logit-like by -log(1-score) as proxy if scores are probs; fallback simple rescale
167
  labels = list(scores_dict.keys())
168
  vals = np.array([scores_dict[l] for l in labels], dtype=float)
169
- # small smoothing to avoid zeros
170
  vals = np.clip(vals, 1e-8, 1-1e-8)
171
- # convert probabilities -> logits approximately
172
  logits = np.log(vals / (1 - vals))
173
  scaled = logits / max(temperature, 1e-6)
174
  exps = np.exp(scaled - np.max(scaled))
175
  probs = exps / np.sum(exps)
176
  return dict(zip(labels, probs))
177
 
178
- # ====== UTIL: média de probabilidades de várias predições (normalização) ======
179
  def average_probabilities(list_of_prob_dicts):
180
- # all dicts share same keys (or not) - unify keys
181
  all_keys = set()
182
  for d in list_of_prob_dicts:
183
  all_keys.update(d.keys())
 
 
184
  avg = {k: 0.0 for k in all_keys}
185
  for d in list_of_prob_dicts:
186
- # treat missing as 0
187
  for k in all_keys:
188
  avg[k] += d.get(k, 0.0)
189
  n = len(list_of_prob_dicts)
190
- if n == 0:
191
- return avg
192
  for k in avg:
193
  avg[k] = avg[k] / n
194
- # normalize
195
  total = sum(avg.values()) or 1.0
196
  for k in avg:
197
  avg[k] = avg[k] / total
198
  return avg
199
 
200
- # ====== ROTA DE ANÁLISE (melhorias de precisão multimodal) ======
201
  @app.route("/analyze", methods=["POST"])
202
  def analyze():
203
  try:
@@ -235,160 +202,62 @@ def analyze():
235
  return jsonify(fallback_emotion(data["text"]))
236
  return jsonify({"error": "Modelo de áudio indisponível no momento."}), 500
237
 
238
- # ====== 1) Classificação de áudio (obter top_k mais completo) ======
239
- # aumentamos top_k para capturar incertezas e depois re-calibramos
240
- raw_result = audio_pipeline(audio_path, top_k=15)
241
- # raw_result geralmente é lista de dicts: [{'label': 'Happy', 'score': 0.9}, ...]
242
- audio_scores = {}
243
- for item in raw_result:
244
- label = item.get("label", "").lower()
245
- if label == "fear":
246
- label = "fearful"
247
- # some models return labels like 'Happy' or 'HAPPY' etc.
248
- audio_scores[label] = float(item.get("score", 0.0))
249
-
250
- if not audio_scores:
251
- return jsonify({"error": "Nenhum rótulo retornado pelo modelo de áudio."}), 500
252
-
253
- # ====== 2) Calibrar probabilidades de áudio com temperatura (ajustável) ======
254
- # temperatura menor -> mais confiante; ajustar conforme necessidade (ex.: 0.7)
255
- temp = float(os.getenv("AUDIO_SOFTMAX_TEMP", 0.7))
256
- calibrated_audio_probs = tempered_softmax(audio_scores, temperature=temp)
257
-
258
- # ====== 3) Tentar transcrever (ASR) e classificar texto (se disponível) ======
259
- text_probs_list = []
260
- transcription = None
261
- if asr_pipeline:
262
  try:
263
- asr_out = asr_pipeline(audio_path)
264
- # asr_out pode ser string ou dict dependendo da versão da pipeline
265
- if isinstance(asr_out, dict):
266
- transcription = asr_out.get("text", "") or asr_out.get("transcription", "")
267
- else:
268
- transcription = str(asr_out)
269
- transcription = (transcription or "").strip()
270
- # split into sentences for per-sentence classification (if long)
271
- if transcription:
272
- sentences = [s.strip() for s in transcription.replace("\n", " ").split(".") if s.strip()]
273
- # limit to first N sentences to avoid long processing
274
- max_sentences = 6
275
- for s in sentences[:max_sentences]:
276
- if text_pipeline:
277
- text_scores = text_pipeline(s, return_all_scores=True)
278
- # text_scores often returns a list with one element (list of label/score)
279
- if isinstance(text_scores, list) and len(text_scores) > 0:
280
- scores_list = text_scores[0]
281
- # convert to map label->score
282
- tmap = {}
283
- for it in scores_list:
284
- lbl = it.get("label", "").lower()
285
- # map textual labels to our english subset if needed
286
- tmap[lbl] = float(it.get("score", 0.0))
287
- # normalize softmax (already probs, but ensure normalization and map labels to english keys)
288
- # keep original labels (e.g., 'joy','sadness','anger','fear','others')
289
- text_probs_list.append(tmap)
290
- # if no sentences or classifier missing, attempt single-shot classify entire transcription
291
- if not text_probs_list and text_pipeline and transcription:
292
- text_scores = text_pipeline(transcription, return_all_scores=True)
293
- if isinstance(text_scores, list) and len(text_scores) > 0:
294
- scores_list = text_scores[0]
295
- tmap = {}
296
- for it in scores_list:
297
- tmap[it.get("label", "").lower()] = float(it.get("score", 0.0))
298
- text_probs_list.append(tmap)
299
  except Exception as e:
300
- # ASR failing shouldn't break the pipeline; apenas logar e seguir com áudio
301
- print(f"⚠️ ASR falhou: {e}")
302
-
303
- # agregue as probabilidades de texto (média)
304
- combined_text_probs = {}
305
- if text_probs_list:
306
- combined_text_probs = average_probabilities(text_probs_list)
307
- # dobrar a confiabilidade de texto se houver muitas sentenças -> confiabilidade maior
308
- # map text labels (example: pysentimiento uses 'joy','sadness','anger','fear','others')
309
- # convert to our english labels set used in audio if possible
310
- # build a mapped version of text probs to common labels
311
- text_to_common = {}
312
- for k, v in combined_text_probs.items():
313
- kl = k.lower()
314
- # tenta mapear palavras comuns
315
- if "joy" in kl or "happy" in kl or "alegr" in kl:
316
- text_to_common["happy"] = v
317
- elif "sad" in kl or "sadness" in kl:
318
- text_to_common["sad"] = v
319
- elif "anger" in kl or "angry" in kl:
320
- text_to_common["angry"] = v
321
- elif "fear" in kl or "anx" in kl:
322
- text_to_common["fearful"] = v
323
- elif "disgust" in kl:
324
- text_to_common["disgust"] = v
325
- elif "others" in kl or "neutral" in kl:
326
- text_to_common["neutral"] = v
327
- else:
328
- # keep as-is for potential mapping later
329
- text_to_common[kl] = v
330
-
331
- # normalize mapped text_to_common
332
- if text_to_common:
333
- total = sum(text_to_common.values()) or 1.0
334
- for k in list(text_to_common.keys()):
335
- text_to_common[k] = text_to_common[k] / total
336
-
337
- # ====== 4) Ensemble multimodal: combinar probabilidades de áudio e texto
338
- # pesos base — ajustar conforme experimento (audio tende a carregar sinal prosódico)
339
- base_weight_audio = float(os.getenv("WEIGHT_AUDIO", 0.65))
340
- base_weight_text = float(os.getenv("WEIGHT_TEXT", 0.35))
341
-
342
- # ajustar pesos dinamicamente pela confiança: se ASR/text forte -> aumentar peso text
343
- # compute confidence proxies
344
- audio_conf_proxy = max(calibrated_audio_probs.values()) # [0..1]
345
- text_conf_proxy = max(text_to_common.values()) if text_to_common else 0.0
346
-
347
- # scale weights
348
- # quanto maior a confiança relativa, maior o peso
349
- if (audio_conf_proxy + text_conf_proxy) > 0:
350
- weight_audio = base_weight_audio * (audio_conf_proxy / (audio_conf_proxy + text_conf_proxy))
351
- weight_text = base_weight_text * (text_conf_proxy / (audio_conf_proxy + text_conf_proxy))
352
- # renormalize to sum to 1 if both non-zero, otherwise fallback
353
- s = weight_audio + weight_text
354
- if s > 0:
355
- weight_audio = weight_audio / s
356
- weight_text = weight_text / s
357
- else:
358
- # fallback para pesos base
359
- weight_audio = base_weight_audio
360
- weight_text = base_weight_text
361
-
362
- # Build unified set of labels
363
- all_labels = set(list(calibrated_audio_probs.keys()) + list(text_to_common.keys()))
364
- merged_probs = {}
365
- for lbl in all_labels:
366
- a = calibrated_audio_probs.get(lbl, 0.0)
367
- t = text_to_common.get(lbl, 0.0)
368
- merged = a * weight_audio + t * weight_text
369
- merged_probs[lbl] = merged
370
-
371
- # normalize merged
372
- total_m = sum(merged_probs.values()) or 1.0
373
- for k in merged_probs:
374
- merged_probs[k] = merged_probs[k] / total_m
375
-
376
- # ====== 5) Escolher rótulo final e montar resposta ======
377
- top_label = max(merged_probs, key=merged_probs.get)
378
- top_score = merged_probs[top_label]
379
  # map to portuguese
380
  emotion_pt = emotion_labels.get(top_label, "desconhecido")
381
 
382
- # ajuste para tristeza muito forte
383
- if emotion_pt == "tristeza" and top_score >= 0.92:
384
  emotion_pt = "depressão"
385
 
386
- # montar probabilidades mapeadas para pt (mantendo somente rótulos conhecidos)
387
- probabilities_pt = {}
388
- for k, v in merged_probs.items():
389
- probabilities_pt[emotion_labels.get(k, k)] = round(float(v), 3)
390
 
391
- # construir resultado base
392
  base_result = {
393
  "status": "ok",
394
  "emotion": emotion_pt,
@@ -397,15 +266,14 @@ def analyze():
397
  "probabilities": probabilities_pt,
398
  "suggestion": gerar_sugestao(emotion_pt),
399
  "debug": {
400
- "audio_raw": audio_scores,
401
- "audio_calibrated": {k: round(float(v), 3) for k, v in calibrated_audio_probs.items()},
402
- "text_transcription": transcription,
403
- "text_mapped_probs": {k: round(float(v), 3) for k, v in text_to_common.items()},
404
- "weights": {"audio": round(weight_audio, 3), "text": round(weight_text, 3)}
405
  }
406
  }
407
 
408
- # aplicar híbrido com fallback textual se houver 'text' no JSON
409
  text_for_hybrid = None
410
  if data and "text" in data:
411
  text_for_hybrid = data["text"]
@@ -418,7 +286,6 @@ def analyze():
418
  print(f"❌ Erro na rota /analyze: {e}")
419
  return jsonify({"error": str(e)}), 500
420
  finally:
421
- # limpar tempfiles (se existirem)
422
  try:
423
  if 'audio_path' in locals() and audio_path and os.path.exists(audio_path):
424
  os.remove(audio_path)
@@ -426,5 +293,4 @@ def analyze():
426
  pass
427
 
428
  if __name__ == "__main__":
429
- # porta padrão ou PORT env var
430
  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
+ # ====== PIPELINE: Apenas o modelo de áudio (Whisper fine-tuned) ======
 
30
  try:
31
  audio_pipeline = pipeline(
32
  task="audio-classification",
 
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",
 
47
  "neutral": "neutro",
48
  "sad": "tristeza",
49
  "surprised": "surpreso",
 
50
  }
51
 
52
  # ====== SUGESTÕES ======
 
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"],
 
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)
 
141
  vals = np.clip(vals, 1e-8, 1-1e-8)
 
142
  logits = np.log(vals / (1 - vals))
143
  scaled = logits / max(temperature, 1e-6)
144
  exps = np.exp(scaled - np.max(scaled))
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:
152
  all_keys.update(d.keys())
153
+ if not all_keys:
154
+ return {}
155
  avg = {k: 0.0 for k in all_keys}
156
  for d in list_of_prob_dicts:
 
157
  for k in all_keys:
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:
 
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 = {
262
  "status": "ok",
263
  "emotion": emotion_pt,
 
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"]
 
286
  print(f"❌ Erro na rota /analyze: {e}")
287
  return jsonify({"error": str(e)}), 500
288
  finally:
 
289
  try:
290
  if 'audio_path' in locals() and audio_path and os.path.exists(audio_path):
291
  os.remove(audio_path)
 
293
  pass
294
 
295
  if __name__ == "__main__":
 
296
  app.run(host="0.0.0.0", port=int(os.getenv("PORT", 7860)))