""" 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()