Spaces:
Running
Running
| """ | |
| Sistema Avançado de Análise de Sentimentos | |
| Versão melhorada com mais modelos e melhor cálculo de confiança | |
| """ | |
| import gradio as gr | |
| import torch | |
| from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification | |
| import numpy as np | |
| from collections import Counter | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| # Modelos de moderação - MAIS MODELOS | |
| MODERATION_MODELS = [ | |
| "citizenlab/distilbert-base-multilingual-cased-toxicity", | |
| "unitary/toxic-bert", | |
| "martin-ha/toxic-comment-model", | |
| "facebook/roberta-hate-speech-dynabench-r4-target", | |
| "Hate-speech-CNERG/dehatebert-mono-portuguese", | |
| ] | |
| print("Carregando sistema de moderação...") | |
| moderators = [] | |
| for model_name in MODERATION_MODELS: | |
| try: | |
| print(f"Moderador: {model_name.split('/')[-1]}...", end=" ") | |
| if "dehatebert" in model_name or "roberta-hate" in model_name: | |
| tokenizer = AutoTokenizer.from_pretrained(model_name) | |
| model = AutoModelForSequenceClassification.from_pretrained(model_name) | |
| moderator = pipeline( | |
| "text-classification", | |
| model=model, | |
| tokenizer=tokenizer, | |
| device=0 if torch.cuda.is_available() else -1 | |
| ) | |
| else: | |
| moderator = pipeline( | |
| "text-classification", | |
| model=model_name, | |
| device=0 if torch.cuda.is_available() else -1 | |
| ) | |
| moderators.append(moderator) | |
| print("OK") | |
| except Exception as e: | |
| print(f"FALHA") | |
| continue | |
| print(f"Moderadores ativos: {len(moderators)}") | |
| # MAIS MODELOS DE SENTIMENTO - Expandido de 12 para 18 | |
| SENTIMENT_MODELS = [ | |
| # Português específico (prioritários) | |
| "neuralmind/bert-base-portuguese-cased", | |
| "neuralmind/bert-large-portuguese-cased", | |
| "rufimelo/bert-large-portuguese-cased-finetuned-with-yelp-reviews", | |
| # XLM-RoBERTa (excelentes para multilíngue) | |
| "cardiffnlp/twitter-xlm-roberta-base-sentiment", | |
| "cardiffnlp/twitter-xlm-roberta-base-sentiment-multilingual", | |
| "citizenlab/twitter-xlm-roberta-base-sentiment-finetunned", | |
| # BERT Multilíngue | |
| "lxyuan/distilbert-base-multilingual-cased-sentiments-student", | |
| "nlptown/bert-base-multilingual-uncased-sentiment", | |
| # RoBERTa variants | |
| "finiteautomata/bertweet-base-sentiment-analysis", | |
| "siebert/sentiment-roberta-large-english", | |
| "cardiffnlp/twitter-roberta-base-sentiment-latest", | |
| "cardiffnlp/twitter-roberta-base-sentiment", | |
| # DistilBERT variants | |
| "distilbert-base-uncased-finetuned-sst-2-english", | |
| "bhadresh-savani/distilbert-base-uncased-emotion", | |
| # Emotion models (mapeados para sentimento) | |
| "j-hartmann/emotion-english-distilroberta-base", | |
| "arpanghoshal/EmoRoBERTa", | |
| # Modelos adicionais especializados | |
| "michellejieli/emotion_text_classifier", | |
| "mrm8488/distilroberta-finetuned-financial-news-sentiment-analysis", | |
| ] | |
| print("\nCarregando modelos de análise de sentimentos...") | |
| classifiers = [] | |
| for idx, model_name in enumerate(SENTIMENT_MODELS, 1): | |
| try: | |
| print(f"[{idx}/{len(SENTIMENT_MODELS)}] {model_name.split('/')[-1]}...", end=" ") | |
| if "neuralmind" in model_name or "emotion" in model_name or "Emo" in model_name: | |
| tokenizer = AutoTokenizer.from_pretrained(model_name) | |
| model = AutoModelForSequenceClassification.from_pretrained(model_name) | |
| classifier = pipeline( | |
| "sentiment-analysis" if "sentiment" in model_name else "text-classification", | |
| model=model, | |
| tokenizer=tokenizer, | |
| device=0 if torch.cuda.is_available() else -1 | |
| ) | |
| else: | |
| classifier = pipeline( | |
| "sentiment-analysis", | |
| model=model_name, | |
| device=0 if torch.cuda.is_available() else -1 | |
| ) | |
| classifiers.append(classifier) | |
| print("OK") | |
| except Exception as e: | |
| print("FALHA") | |
| continue | |
| print(f"\n{'='*60}") | |
| print(f"Sistema completo:") | |
| print(f"- Analisadores: {len(classifiers)}") | |
| print(f"- Moderadores: {len(moderators)}") | |
| print(f"{'='*60}\n") | |
| # Limiar AUMENTADO para evitar falsos positivos | |
| TOXICITY_THRESHOLD = 0.80 # Aumentado para reduzir falsos positivos | |
| # Mapeamento expandido de labels | |
| LABEL_MAPPING = { | |
| # Sentimento padrão | |
| 'NEGATIVE': 'Negativo', 'negative': 'Negativo', 'NEG': 'Negativo', | |
| 'NEUTRAL': 'Neutro', 'neutral': 'Neutro', 'NEU': 'Neutro', | |
| 'POSITIVE': 'Positivo', 'positive': 'Positivo', 'POS': 'Positivo', | |
| 'LABEL_0': 'Negativo', 'LABEL_1': 'Neutro', 'LABEL_2': 'Positivo', | |
| # Estrelas | |
| '1 star': 'Negativo', '2 stars': 'Negativo', | |
| '3 stars': 'Neutro', | |
| '4 stars': 'Positivo', '5 stars': 'Positivo', | |
| # Emoções -> Sentimentos | |
| 'anger': 'Negativo', 'disgust': 'Negativo', 'fear': 'Negativo', | |
| 'sadness': 'Negativo', 'surprise': 'Neutro', | |
| 'joy': 'Positivo', 'love': 'Positivo', 'admiration': 'Positivo', | |
| # Outros formatos | |
| 'neg': 'Negativo', 'neu': 'Neutro', 'pos': 'Positivo', | |
| } | |
| def verificar_linguagem(texto): | |
| """ | |
| Verifica linguagem imprópria com MAIS modelos e threshold MAIOR | |
| Com interpretação melhorada de labels | |
| """ | |
| if not moderators or len(texto.strip()) < 3: | |
| return False, 0.0 | |
| scores_toxicos = [] | |
| for moderator in moderators: | |
| try: | |
| resultado = moderator(texto[:512])[0] | |
| label = resultado['label'].lower() | |
| score = resultado['score'] | |
| # Interpretar labels com MAIS cuidado | |
| # Labels que indicam TOXICIDADE | |
| toxic_keywords = ['toxic', 'hate', 'offensive', 'hateful', 'obscene', 'threat', 'insult'] | |
| # Labels que indicam NORMALIDADE | |
| normal_keywords = ['not', 'normal', 'neutral', 'clean'] | |
| is_toxic_label = any(word in label for word in toxic_keywords) | |
| is_normal_label = any(word in label for word in normal_keywords) | |
| # Calcular toxicity score com lógica melhorada | |
| if is_toxic_label and not is_normal_label: | |
| # Label diz que é tóxico | |
| toxicity = score | |
| elif is_normal_label or 'not' in label: | |
| # Label diz que NÃO é tóxico | |
| toxicity = 1 - score | |
| else: | |
| # Label ambíguo, assumir score direto se alto | |
| toxicity = score if score > 0.5 else 1 - score | |
| scores_toxicos.append(toxicity) | |
| except: | |
| continue | |
| if not scores_toxicos: | |
| return False, 0.0 | |
| # Média dos scores | |
| toxicity_score = np.mean(scores_toxicos) | |
| # Threshold MAIOR para reduzir falsos positivos | |
| has_improper = toxicity_score > TOXICITY_THRESHOLD | |
| return has_improper, toxicity_score | |
| def normalizar_label(label): | |
| """Normaliza labels""" | |
| label_upper = label.upper() if isinstance(label, str) else str(label) | |
| return LABEL_MAPPING.get(label, LABEL_MAPPING.get(label_upper, 'Neutro')) | |
| def analisar_texto(texto): | |
| """ | |
| Análise com MELHOR cálculo de confiança | |
| """ | |
| if not texto or len(texto.strip()) < 3: | |
| return "Aguardando texto para análise", {}, "-", "-", "-" | |
| # ANÁLISE DE SENTIMENTO | |
| texto_processado = texto[:512] | |
| predicoes = [] | |
| scores_brutos = [] # Para melhor cálculo | |
| scores_por_classe = { | |
| 'Negativo': [], | |
| 'Neutro': [], | |
| 'Positivo': [] | |
| } | |
| modelos_usados = 0 | |
| for classifier in classifiers: | |
| try: | |
| resultado = classifier(texto_processado)[0] | |
| label_norm = normalizar_label(resultado['label']) | |
| score = resultado['score'] | |
| predicoes.append(label_norm) | |
| scores_brutos.append(score) | |
| modelos_usados += 1 | |
| # Distribuição mais conservadora | |
| if label_norm == 'Negativo': | |
| scores_por_classe['Negativo'].append(score) | |
| remaining = 1 - score | |
| scores_por_classe['Neutro'].append(remaining * 0.4) | |
| scores_por_classe['Positivo'].append(remaining * 0.6) | |
| elif label_norm == 'Neutro': | |
| scores_por_classe['Neutro'].append(score) | |
| remaining = 1 - score | |
| scores_por_classe['Negativo'].append(remaining * 0.5) | |
| scores_por_classe['Positivo'].append(remaining * 0.5) | |
| else: # Positivo | |
| scores_por_classe['Positivo'].append(score) | |
| remaining = 1 - score | |
| scores_por_classe['Negativo'].append(remaining * 0.6) | |
| scores_por_classe['Neutro'].append(remaining * 0.4) | |
| except: | |
| continue | |
| if not predicoes or modelos_usados == 0: | |
| return "Erro no processamento", {}, "-", "-", "-" | |
| # Voting majoritário | |
| contagem = Counter(predicoes) | |
| classificacao = contagem.most_common(1)[0][0] | |
| votos = contagem[classificacao] | |
| # MELHOR cálculo de probabilidades | |
| probs = {} | |
| for classe in ['Negativo', 'Neutro', 'Positivo']: | |
| scores = scores_por_classe[classe] | |
| if scores: | |
| # Usar mediana ao invés de média para reduzir outliers | |
| probs[classe] = float(np.median(scores)) | |
| else: | |
| probs[classe] = 0.0 | |
| # Normalizar | |
| total = sum(probs.values()) | |
| if total > 0: | |
| probs = {k: v/total for k, v in probs.items()} | |
| # Confiança baseada em voting + score | |
| confianca_voting = votos / modelos_usados | |
| confianca_score = probs[classificacao] | |
| # Confiança final = média ponderada (60% voting, 40% score) | |
| confianca_final = (confianca_voting * 0.6) + (confianca_score * 0.4) | |
| # Consistência | |
| scores_final = scores_por_classe[classificacao] | |
| if len(scores_final) > 1: | |
| desvio = np.std(scores_final) | |
| nivel = "Alta" if desvio < 0.1 else "Média" if desvio < 0.2 else "Baixa" | |
| else: | |
| desvio = 0 | |
| nivel = "N/A" | |
| # VERIFICAR LINGUAGEM (com threshold mais alto) | |
| has_improper, improper_score = verificar_linguagem(texto) | |
| # LÓGICA INTELIGENTE: Se Positivo com boa confiança, provavelmente não é ofensivo | |
| if classificacao == 'Positivo' and confianca_final > 0.70: | |
| has_improper = False # Ignora alerta para textos claramente positivos | |
| # Se Neutro ou Negativo, ainda verifica normalmente | |
| # Formatar resultado | |
| if has_improper: | |
| resultado_texto = f"""**{classificacao}** | |
| ⚠️ **Alerta de Conteúdo** | |
| Detectada possível linguagem imprópria (confiança: {improper_score:.1%}). | |
| Recomendamos evitar: | |
| • Discurso de ódio | |
| • Termos discriminatórios | |
| • Linguagem ofensiva | |
| O sentimento foi analisado normalmente.""" | |
| else: | |
| resultado_texto = f"**{classificacao}**" | |
| confianca_texto = f"{confianca_final:.1%}" | |
| consenso_texto = f"{votos}/{modelos_usados} modelos ({(votos/modelos_usados)*100:.0f}%)" | |
| consistencia_texto = f"{nivel} (σ={desvio:.3f})" if desvio > 0 else "N/A" | |
| return resultado_texto, probs, confianca_texto, consenso_texto, consistencia_texto | |
| # Casos de teste variados | |
| casos_teste = [ | |
| ["Este produto superou todas as minhas expectativas. Qualidade excepcional!"], | |
| ["Experiência extremamente negativa. Produto defeituoso e atendimento péssimo."], | |
| ["Produto normal. Atende o básico sem grandes destaques ou problemas."], | |
| ["Recomendo! Excelente custo-benefício e entrega rápida."], | |
| ["Satisfatório. Funciona conforme descrito, nada além disso."], | |
| ["Produto horrível, péssima qualidade, muito ruim, não recomendo."], | |
| ["Maravilhoso! Adorei cada detalhe, perfeito em todos os aspectos!"], | |
| ["Decepcionante. Não corresponde à descrição e apresenta defeitos graves."], | |
| ] | |
| # Interface | |
| with gr.Blocks(title="Análise de Sentimentos Avançada") as demo: | |
| gr.Markdown( | |
| f""" | |
| # Sistema Avançado de Análise de Sentimentos | |
| Análise por ensemble de **{len(classifiers)} modelos** especializados. | |
| **Sistema de verificação:** {len(moderators)} moderadores detectam linguagem imprópria. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| texto_input = gr.Textbox( | |
| label="Texto para Análise", | |
| placeholder="Digite ou cole o texto aqui (até 512 caracteres)...", | |
| lines=5, | |
| max_lines=10 | |
| ) | |
| with gr.Row(): | |
| btn_analisar = gr.Button("Analisar", variant="primary", size="lg") | |
| btn_limpar = gr.Button("Limpar", size="lg") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| resultado_output = gr.Markdown(label="Classificação") | |
| confianca_output = gr.Textbox(label="Confiança", interactive=False) | |
| consenso_output = gr.Textbox(label="Consenso", interactive=False) | |
| consistencia_output = gr.Textbox(label="Consistência", interactive=False) | |
| with gr.Column(scale=1): | |
| probs_output = gr.Label( | |
| label="Distribuição de Probabilidades", | |
| num_top_classes=3 | |
| ) | |
| gr.Markdown("### Casos de Teste") | |
| gr.Examples( | |
| examples=casos_teste, | |
| inputs=texto_input, | |
| outputs=[resultado_output, probs_output, confianca_output, consenso_output, consistencia_output], | |
| fn=analisar_texto, | |
| cache_examples=False | |
| ) | |
| gr.Markdown( | |
| f""" | |
| --- | |
| ## Especificações do Sistema | |
| ### Análise de Sentimento | |
| **Modelos Ativos:** {len(classifiers)} / {len(SENTIMENT_MODELS)} | |
| **Arquitetura:** | |
| - BERTimbau (português específico) | |
| - XLM-RoBERTa (multilíngue) | |
| - BERT e DistilBERT | |
| - RoBERTa especializados | |
| - Modelos de emoção | |
| **Método:** | |
| - Voting majoritário | |
| - Agregação por mediana (reduz outliers) | |
| - Confiança combinada (voting + score) | |
| ### Verificação de Linguagem | |
| **Moderadores Ativos:** {len(moderators)} / {len(MODERATION_MODELS)} | |
| **Threshold:** {TOXICITY_THRESHOLD*100:.0f}% (mais alto para evitar falsos positivos) | |
| **Lógica Inteligente:** | |
| - Textos claramente positivos (>70% confiança) não geram alertas | |
| - Foco em detectar problemas reais | |
| **Modelos:** | |
| - DistilBERT Toxicity | |
| - Toxic-BERT (Unitary) | |
| - Toxic Comment Model | |
| - RoBERTa Hate Speech | |
| - DeHateBERT Portuguese | |
| ### Melhorias Implementadas | |
| ✅ **Mais modelos** ({len(classifiers)} analisadores, {len(moderators)} moderadores) | |
| ✅ **Melhor confiança** (combina voting + probabilidades) | |
| ✅ **Menos falsos positivos** (threshold aumentado de 70% → 75%) | |
| ✅ **Agregação robusta** (mediana ao invés de média) | |
| ✅ **Distribuição conservadora** (scores mais equilibrados) | |
| ### Fluxo de Processamento | |
| 1. **Análise paralela** por todos os modelos | |
| 2. **Voting majoritário** determina classificação | |
| 3. **Agregação por mediana** calcula probabilidades | |
| 4. **Confiança combinada** (60% voting + 40% score) | |
| 5. **Verificação de linguagem** com threshold elevado | |
| 6. **Resultado final** com métricas de qualidade | |
| """ | |
| ) | |
| btn_analisar.click( | |
| fn=analisar_texto, | |
| inputs=texto_input, | |
| outputs=[resultado_output, probs_output, confianca_output, consenso_output, consistencia_output] | |
| ) | |
| btn_limpar.click( | |
| fn=lambda: ("", "", "", "", "", {}), | |
| inputs=None, | |
| outputs=[texto_input, resultado_output, confianca_output, consenso_output, consistencia_output, probs_output] | |
| ) | |
| texto_input.submit( | |
| fn=analisar_texto, | |
| inputs=texto_input, | |
| outputs=[resultado_output, probs_output, confianca_output, consenso_output, consistencia_output] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |