tassid's picture
Update app.py
26bb094 verified
raw
history blame
16.2 kB
"""
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()