Spaces:
Running
Running
| """ | |
| Sistema Avançado de Análise de Sentimentos com Moderação em Português | |
| Ensemble de modelos + Detecção de discurso de ódio em PT-BR | |
| """ | |
| 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 ESPECÍFICOS para PORTUGUÊS | |
| MODERATION_MODELS = [ | |
| # Modelos brasileiros de detecção de ódio | |
| "citizenlab/distilbert-base-multilingual-cased-toxicity", # Multilíngue mas funciona bem em PT | |
| "francisco-perez-sorrosal/distilbert-base-uncased-finetuned-with-hateoffensive", | |
| "Hate-speech-CNERG/dehatebert-mono-portuguese", # Específico PT! | |
| "neuralmind/bert-base-portuguese-cased", # BERTimbau adaptado | |
| ] | |
| print("Carregando sistema de moderação em português...") | |
| moderators = [] | |
| moderator_names = [] | |
| for model_name in MODERATION_MODELS: | |
| try: | |
| print(f"Carregando: {model_name.split('/')[-1]}...", end=" ") | |
| # Carregar com configuração específica | |
| if "dehatebert" 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 | |
| ) | |
| elif "neuralmind" in model_name: | |
| # BERTimbau precisa ser adaptado para classificação | |
| tokenizer = AutoTokenizer.from_pretrained(model_name) | |
| # Usar modelo base e adaptar | |
| moderator = None # Pular por enquanto, precisa fine-tuning específico | |
| print("PULADO (precisa adaptação)") | |
| continue | |
| else: | |
| moderator = pipeline( | |
| "text-classification", | |
| model=model_name, | |
| device=0 if torch.cuda.is_available() else -1 | |
| ) | |
| moderators.append(moderator) | |
| moderator_names.append(model_name.split('/')[-1]) | |
| print("OK") | |
| except Exception as e: | |
| print(f"FALHA ({str(e)[:40]}...)") | |
| continue | |
| print(f"Moderadores ativos: {len(moderators)}") | |
| # Modelos de análise de sentimentos | |
| SENTIMENT_MODELS = [ | |
| # Modelos em português prioritários | |
| "neuralmind/bert-base-portuguese-cased", | |
| "neuralmind/bert-large-portuguese-cased", | |
| # XLM-RoBERTa (excelentes para PT) | |
| "cardiffnlp/twitter-xlm-roberta-base-sentiment", | |
| "cardiffnlp/twitter-xlm-roberta-base-sentiment-multilingual", | |
| "citizenlab/twitter-xlm-roberta-base-sentiment-finetunned", | |
| # Multilíngues | |
| "lxyuan/distilbert-base-multilingual-cased-sentiments-student", | |
| "nlptown/bert-base-multilingual-uncased-sentiment", | |
| # Modelos adicionais | |
| "finiteautomata/bertweet-base-sentiment-analysis", | |
| "siebert/sentiment-roberta-large-english", | |
| "distilbert-base-uncased-finetuned-sst-2-english", | |
| ] | |
| 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: | |
| tokenizer = AutoTokenizer.from_pretrained(model_name) | |
| model = AutoModelForSequenceClassification.from_pretrained(model_name) | |
| classifier = pipeline( | |
| "sentiment-analysis", | |
| 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: | |
| print("FALHA") | |
| continue | |
| print(f"\n{'='*60}") | |
| print(f"Sistema completo:") | |
| print(f"- Moderadores: {len(moderators)}") | |
| print(f"- Analisadores: {len(classifiers)}") | |
| print(f"{'='*60}\n") | |
| # Limiar para detecção | |
| TOXICITY_THRESHOLD = 0.65 | |
| # Palavras-chave de alerta (backup em português) | |
| PALAVRAS_ALERTA = [ | |
| # Racismo | |
| 'preto', 'negro', 'macaco', 'escuro', | |
| # Homofobia | |
| 'gay', 'viado', 'bicha', 'sapatao', | |
| # Sexismo | |
| 'vadia', 'puta', 'vagabunda', | |
| # Xenofobia | |
| 'nordestino', 'baiano', 'paraiba', | |
| # Outros | |
| 'lixo', 'merda', 'idiota', 'burro' | |
| ] | |
| def verificar_palavras_suspeitas(texto): | |
| """ | |
| Verificação adicional por palavras-chave (backup) | |
| Retorna número de palavras suspeitas encontradas | |
| """ | |
| texto_lower = texto.lower() | |
| count = 0 | |
| for palavra in PALAVRAS_ALERTA: | |
| if palavra in texto_lower: | |
| count += 1 | |
| return count | |
| # Mapeamento de labels | |
| LABEL_MAPPING = { | |
| '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', | |
| '1 star': 'Negativo', '2 stars': 'Negativo', '3 stars': 'Neutro', | |
| '4 stars': 'Positivo', '5 stars': 'Positivo', | |
| # Labels específicos de hate speech | |
| 'hate': 'Tóxico', 'offensive': 'Tóxico', 'toxic': 'Tóxico', | |
| 'NOT': 'Normal', 'normal': 'Normal', 'neutral': 'Normal', | |
| } | |
| def verificar_conteudo(texto): | |
| """ | |
| Verifica conteúdo inadequado usando modelos + palavras-chave | |
| Retorna: (is_toxic, confidence, details) | |
| """ | |
| if not moderators: | |
| # Fallback: verificação por palavras-chave | |
| palavras_suspeitas = verificar_palavras_suspeitas(texto) | |
| if palavras_suspeitas >= 2: | |
| return True, 0.75, "Detecção por palavras-chave" | |
| return False, 0.0, "Sem moderadores ativos" | |
| scores_toxicos = [] | |
| detalhes = [] | |
| for idx, moderator in enumerate(moderators): | |
| try: | |
| resultado = moderator(texto[:512])[0] | |
| label = resultado['label'].lower() | |
| score = resultado['score'] | |
| # Interpretar resultado | |
| is_toxic_label = any(word in label for word in ['toxic', 'hate', 'offensive', 'negative']) | |
| if is_toxic_label: | |
| toxicity = score | |
| else: | |
| toxicity = 1 - score | |
| scores_toxicos.append(toxicity) | |
| detalhes.append(f"Modelo {idx+1}: {toxicity:.1%}") | |
| except: | |
| continue | |
| if not scores_toxicos: | |
| # Fallback para palavras-chave | |
| palavras_suspeitas = verificar_palavras_suspeitas(texto) | |
| if palavras_suspeitas >= 2: | |
| return True, 0.75, "Detecção por palavras-chave" | |
| return False, 0.0, "Erro na moderação" | |
| # Média dos scores | |
| toxicity_score = np.mean(scores_toxicos) | |
| # Verificação adicional por palavras | |
| palavras_suspeitas = verificar_palavras_suspeitas(texto) | |
| if palavras_suspeitas >= 3: | |
| toxicity_score = max(toxicity_score, 0.8) | |
| is_toxic = toxicity_score > TOXICITY_THRESHOLD | |
| return is_toxic, toxicity_score, " | ".join(detalhes) | |
| 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 moderação em português | |
| """ | |
| if not texto or len(texto.strip()) < 3: | |
| return "Aguardando texto para análise", {}, "-", "-", "-" | |
| # MODERAÇÃO | |
| is_toxic, toxicity_score, detalhes_mod = verificar_conteudo(texto) | |
| if is_toxic: | |
| mensagem_recusa = f""" | |
| **⚠️ Conteúdo Inadequado Detectado** | |
| Este sistema não analisa textos que contenham: | |
| • Discurso de ódio | |
| • Racismo ou discriminação racial | |
| • Homofobia ou LGBTfobia | |
| • Sexismo ou misoginia | |
| • Xenofobia | |
| • Linguagem ofensiva ou discriminatória | |
| **Por favor, reformule o texto de forma respeitosa.** | |
| *Nível de inadequação detectado: {toxicity_score:.1%}* | |
| """ | |
| info_moderacao = { | |
| 'Inadequado': toxicity_score, | |
| 'Adequado': 1 - toxicity_score | |
| } | |
| return mensagem_recusa, info_moderacao, f"{toxicity_score:.1%}", "Bloqueado", "Moderação" | |
| # ANÁLISE DE SENTIMENTO | |
| texto_processado = texto[:512] | |
| predicoes = [] | |
| 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) | |
| modelos_usados += 1 | |
| if label_norm == 'Negativo': | |
| scores_por_classe['Negativo'].append(score) | |
| scores_por_classe['Neutro'].append((1-score) * 0.3) | |
| scores_por_classe['Positivo'].append((1-score) * 0.7) | |
| elif label_norm == 'Neutro': | |
| scores_por_classe['Neutro'].append(score) | |
| scores_por_classe['Negativo'].append((1-score) * 0.5) | |
| scores_por_classe['Positivo'].append((1-score) * 0.5) | |
| else: | |
| scores_por_classe['Positivo'].append(score) | |
| scores_por_classe['Negativo'].append((1-score) * 0.7) | |
| scores_por_classe['Neutro'].append((1-score) * 0.3) | |
| except: | |
| continue | |
| if not predicoes or modelos_usados == 0: | |
| return "Erro no processamento", {}, "-", "-", "-" | |
| # Voting | |
| contagem = Counter(predicoes) | |
| classificacao = contagem.most_common(1)[0][0] | |
| votos = contagem[classificacao] | |
| # Probabilidades | |
| probs = {k: np.mean(v) if v else 0.0 for k, v in scores_por_classe.items()} | |
| total = sum(probs.values()) | |
| if total > 0: | |
| probs = {k: v/total for k, v in probs.items()} | |
| confianca = probs[classificacao] | |
| # 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" | |
| resultado_texto = f"{classificacao}" | |
| confianca_texto = f"{confianca:.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 | |
| casos_teste = [ | |
| ["Este produto superou minhas expectativas. Qualidade excelente e entrega rápida."], | |
| ["Experiência muito negativa. O produto apresentou defeitos e o atendimento foi inadequado."], | |
| ["Produto atende o esperado. Funcionalidades básicas dentro do padrão da categoria."], | |
| ["Recomendo fortemente. Excelente custo-benefício e durabilidade comprovada."], | |
| ["Satisfatório. Cumpre o prometido sem grandes destaques."], | |
| ] | |
| # Interface | |
| with gr.Blocks(title="Análise de Sentimentos") as demo: | |
| gr.Markdown( | |
| f""" | |
| # Sistema de Análise de Sentimentos com Moderação | |
| Análise por ensemble de {len(classifiers)} modelos com moderação de conteúdo em português. | |
| **Sistema de proteção:** Detecta automaticamente discurso de ódio, racismo, homofobia e conteúdo discriminatório. | |
| """ | |
| ) | |
| 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="Nível de Confiança", interactive=False) | |
| consenso_output = gr.Textbox(label="Consenso entre Modelos", 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""" | |
| --- | |
| ## Sobre o Sistema | |
| ### Moderação de Conteúdo | |
| O sistema verifica automaticamente e bloqueia: | |
| • Discurso de ódio e intolerância | |
| • Racismo e discriminação racial | |
| • Homofobia e LGBTfobia | |
| • Sexismo e misoginia | |
| • Xenofobia e regionalismo | |
| • Linguagem ofensiva ou discriminatória | |
| **Método:** Ensemble de modelos especializados + verificação por palavras-chave | |
| **Limiar:** {TOXICITY_THRESHOLD*100:.0f}% de confiança para bloqueio | |
| ### Análise de Sentimentos | |
| **Modelos Ativos:** {len(classifiers)} | |
| **Moderadores Ativos:** {len(moderators)} | |
| **Método:** Voting majoritário com agregação probabilística | |
| **Classes:** Negativo, Neutro, Positivo | |
| **Idioma Principal:** Português Brasileiro | |
| ### Fluxo de Processamento | |
| 1. **Recepção** do texto | |
| 2. **Moderação** por modelos especializados | |
| 3. **Verificação adicional** por palavras-chave | |
| 4. **Bloqueio** se inadequado ou **Análise** se adequado | |
| 5. **Resultado** com métricas de qualidade | |
| ### Modelos Utilizados | |
| **Moderação em Português:** | |
| - DistilBERT Toxicity Multilingual | |
| - HateOffensive Detection | |
| - DeHateBERT Portuguese | |
| - Verificação por palavras-chave em PT-BR | |
| **Análise de Sentimentos:** | |
| - BERTimbau (2 variantes) - Português BR | |
| - XLM-RoBERTa (3 variantes) - Multilíngue | |
| - BERT e DistilBERT Multilingual | |
| - Modelos especializados adicionais | |
| ### Política de Uso Responsável | |
| Este sistema foi desenvolvido para análise técnica de sentimentos em conteúdos respeitosos. | |
| Não tolera e não processa qualquer forma de discriminação ou discurso de ódio. | |
| **Compromisso:** Promover análise técnica mantendo respeito à dignidade humana e aos direitos fundamentais. | |
| --- | |
| **Nota Técnica:** O sistema utiliza múltiplas camadas de verificação para maximizar | |
| a detecção de conteúdo inadequado, incluindo modelos de IA e verificação por padrões linguísticos. | |
| """ | |
| ) | |
| 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() |