']:
continue
# Clean token
clean_token = token.replace('â', '').replace('##', '').strip()
if not clean_token:
continue
# Create masked input
masked_input_ids = input_ids.clone()
masked_input_ids[0, i] = tokenizer.unk_token_id
# Get prediction without this token
with torch.no_grad():
outputs = model(input_ids=masked_input_ids, attention_mask=attention_mask)
logits = outputs.logits
probs = torch.sigmoid(logits)
masked_prob = torch.max(probs).item()
# Directional importance
importance = baseline_prob - masked_prob
# Accumulate importance
if clean_token.lower() in word_importances:
if abs(importance) > abs(word_importances[clean_token.lower()]):
word_importances[clean_token.lower()] = importance
elif abs(importance) > 0.001:
if importance * word_importances[clean_token.lower()] >= 0:
word_importances[clean_token.lower()] += importance
else:
if abs(importance) > abs(word_importances[clean_token.lower()]):
word_importances[clean_token.lower()] = importance
else:
word_importances[clean_token.lower()] = importance
return word_importances
except Exception as e:
st.warning(f"Fallback explanation error: {str(e)}")
return None
def visualize_shap_explanation(text, word_importances):
"""Create beautiful HTML visualization of word importance"""
if word_importances is None or len(word_importances) == 0:
return None
try:
# Get words and their importances
words = text.split()
highlighted_words = []
for word in words:
word_clean = word.lower().strip('.,!?;:"()[]{}')
# Check if this word is in our top toxic words
if word_clean in word_importances:
# Highlight the toxic word
highlighted_words.append(f'{html.escape(word)}')
else:
# Normal text - not toxic
highlighted_words.append(f'{html.escape(word)}')
html_content = f'''
{" ".join(highlighted_words)}
Legend:
đĨ Toxic Words
âĒ Neutral Words
'''
return html_content
except Exception as e:
st.warning(f"Visualization error: {str(e)}")
return None
# Voice Input Functions
@st.cache_resource
def get_speech_recognizer():
"""Initialize and cache the speech recognizer"""
if not SPEECH_AVAILABLE:
return None
try:
r = sr.Recognizer()
return r
except Exception as e:
st.error(f"Failed to initialize speech recognizer: {str(e)}")
return None
def speech_to_text():
"""Convert speech to text using microphone"""
if not SPEECH_AVAILABLE:
return None
recognizer = get_speech_recognizer()
if recognizer is None:
return None
try:
with sr.Microphone() as source:
st.info("đ¤ Listening... Speak now!")
# Adjust for ambient noise
recognizer.adjust_for_ambient_noise(source, duration=0.5)
# Listen for audio
audio = recognizer.listen(source, timeout=5, phrase_time_limit=10)
st.info("đ Processing audio...")
# Recognize using Google Speech Recognition
text = recognizer.recognize_google(audio)
return text
except sr.WaitTimeoutError:
st.warning("âąī¸ No speech detected. Please try again.")
return None
except sr.UnknownValueError:
st.warning("â Could not understand the audio. Please try again.")
return None
except sr.RequestError as e:
st.error(f"đ´ Speech recognition service error: {str(e)}")
return None
except Exception as e:
st.error(f"đ´ Error during speech recognition: {str(e)}")
return None
def text_to_speech(text):
"""Convert text to speech"""
if not SPEECH_AVAILABLE:
return
try:
engine = pyttsx3.init()
# Set speech properties
engine.setProperty('rate', 150) # Speed of speech
engine.setProperty('volume', 0.9) # Volume level
# Speak the text
engine.say(text)
engine.runAndWait()
except Exception as e:
st.warning(f"Text-to-speech unavailable: {str(e)}")
# Combined Voice Analysis Function
def voice_analysis_pipeline(spoken_text: str, distilbert_tokenizer, distilbert_model, device) -> Dict[str, any]:
"""
Complete voice analysis pipeline:
1. Convert speech to text (already done)
2. Detect toxicity using multilingual pipeline
3. Detect sentiment using RoBERTa
4. Return structured results
Args:
spoken_text: Transcribed text from speech-to-text
distilbert_tokenizer: Tokenizer for toxicity model
distilbert_model: Toxicity model
device: Device for inference
Returns:
Dictionary with toxicity and sentiment results
"""
try:
# Step 1: Already have spoken_text
# Step 2: Detect language and get toxicity prediction
detected_lang = detect_language_robust(spoken_text)
# Get toxicity results
if detected_lang != 'en':
multilingual_result = predict_toxicity_multilingual(spoken_text, distilbert_tokenizer, distilbert_model, device)
toxicity_label = multilingual_result.get('toxicity_label', 'Unknown')
toxicity_confidence = multilingual_result.get('confidence', 0.0)
text_for_sentiment = multilingual_result.get('translated_text') or spoken_text
else:
results = predict_toxicity(distilbert_model, distilbert_tokenizer, spoken_text, device)
max_score = max(results.values())
toxicity_confidence = max_score
toxicity_label = "Toxic" if max_score >= 0.5 else "Non-toxic"
text_for_sentiment = spoken_text
# Step 3: Detect sentiment
sentiment_result = analyze_sentiment(text_for_sentiment)
if sentiment_result:
# Extract just the emotion part (e.g., "Angry/Disappointed" from "đ Angry/Disappointed")
emotion_full = sentiment_result['emotion']
sentiment_label = ' '.join(emotion_full.split()[1:]) if len(emotion_full.split()) > 1 else emotion_full
sentiment_confidence = sentiment_result.get('confidence', 0.0)
else:
sentiment_label = "Unknown"
sentiment_confidence = 0.0
# Return structured results
return {
"input_text": spoken_text,
"language": detected_lang,
"toxicity_label": toxicity_label,
"toxicity_confidence": toxicity_confidence,
"sentiment_label": sentiment_label,
"sentiment_confidence": sentiment_confidence,
"success": True
}
except Exception as e:
st.error(f"Voice analysis pipeline error: {str(e)}")
return {
"input_text": spoken_text,
"toxicity_label": "Error",
"toxicity_confidence": 0.0,
"sentiment_label": "Error",
"sentiment_confidence": 0.0,
"success": False,
"error": str(e)
}
# Streamlit UI
def main():
# Header
st.markdown('đŦ CleanSpeak: AI Toxic Comment Detector âĄ
',
unsafe_allow_html=True)
# Sidebar information
with st.sidebar:
st.title("đ About CleanSpeak")
# Navigation
st.markdown("### đ§ Navigation")
page = st.selectbox("Choose a page:", ["đ Toxicity Detector", "đ Dataset Visualization"])
st.markdown("---")
if page == "đ Dataset Visualization":
# Display visualization page
if VIS_AVAILABLE:
visualization_page()
else:
st.error("Visualization module not available. Please install matplotlib, seaborn, and wordcloud.")
st.stop()
st.markdown("""
**CleanSpeak** is an AI-driven toxicity detector powered by BERT.
### Features:
- đ Real-time detection
- đ§Š Multi-label classification
- đ¨ Beautiful gradient UI
- đŦ Word highlighting
- ⥠Fast & lightweight
### Toxicity Types:
- **Toxic**: General toxicity
- **Severe Toxic**: Extreme toxicity
- **Obscene**: Profane language
- **Threat**: Threatening language
- **Insult**: Insulting content
- **Identity Hate**: Hate speech
""")
st.markdown("---")
st.markdown("**Model:** DistilBERT (Locally Trained)")
st.markdown("**Device:** GPU (MPS) / CPU")
if not SPEECH_AVAILABLE:
st.info("đĄ **Voice Features:** Install speechrecognition, pyttsx3, and pyaudio for voice input/output")
# Load model
tokenizer, model, device = load_model()
if tokenizer is None or model is None or device is None:
st.error("Failed to load the model. Please check your internet connection.")
return
# Main content area
col1, col2 = st.columns([2, 1])
with col1:
st.markdown("### đ Enter a Comment to Analyze")
# Text input with voice button
col_input, col_voice = st.columns([5, 1])
with col_input:
# Initialize session state for voice input
if 'voice_text' not in st.session_state:
st.session_state['voice_text'] = ""
user_input = st.text_area(
"Type or paste a comment here:",
value=st.session_state.get('voice_text', ''),
placeholder="Example: This is a test comment...",
height=150,
label_visibility="collapsed"
)
# Clear voice_text after use
if st.session_state.get('voice_text'):
st.session_state['voice_text'] = ""
with col_voice:
if SPEECH_AVAILABLE:
if st.button("đ¤", use_container_width=True, help="Click to speak your comment"):
spoken_text = speech_to_text()
if spoken_text:
st.session_state['voice_text'] = spoken_text
st.rerun()
# New: Voice Analysis button (auto-analyze after recording)
if SPEECH_AVAILABLE:
if st.button("đ¤đ Voice Analysis (Speak & Auto-Analyze)", use_container_width=True):
with st.spinner("đ¤ Listening..."):
spoken_text = speech_to_text()
if spoken_text:
with st.spinner("đ Analyzing toxicity and sentiment..."):
# Run voice analysis pipeline
analysis_result = voice_analysis_pipeline(spoken_text, tokenizer, model, device)
if analysis_result.get('success'):
# Display transcribed text
st.markdown("### đ¤ Recognized Speech")
st.text_area("Transcribed Text:", value=analysis_result['input_text'], height=100, key="transcribed", label_visibility="collapsed")
# Display language if not English
if analysis_result.get('language') != 'en':
lang_display = LANGUAGE_NAMES.get(analysis_result['language'], analysis_result['language'].upper())
st.info(f"đ **Detected Language:** {lang_display} ({analysis_result['language'].upper()})")
# Display Toxicity Result
st.markdown("### đ§ Toxicity Analysis")
toxicity_label = analysis_result['toxicity_label']
toxicity_conf = analysis_result['toxicity_confidence']
if toxicity_label == "Toxic":
st.markdown(f"""
đ´ Toxic
Confidence: {toxicity_conf:.1%}
""", unsafe_allow_html=True)
voice_feedback = "Your message sounds harsh. Please reconsider."
else:
st.markdown(f"""
đĸ Non-toxic
Confidence: {toxicity_conf:.1%}
""", unsafe_allow_html=True)
voice_feedback = "Your message sounds kind and positive!"
# Display Sentiment Result
st.markdown("### đŦ Sentiment Analysis")
sentiment_label = analysis_result['sentiment_label']
sentiment_conf = analysis_result['sentiment_confidence']
# Determine color for sentiment
if "Positive" in sentiment_label or "Happy" in sentiment_label:
sent_color = "#4caf50"
sent_bg = "#e8f5e9"
sent_emoji = "đĸ"
elif "Negative" in sentiment_label or "Angry" in sentiment_label or "Disappointed" in sentiment_label:
sent_color = "#f44336"
sent_bg = "#ffebee"
sent_emoji = "đ´"
else: # Neutral
sent_color = "#ff9800"
sent_bg = "#fff3e0"
sent_emoji = "đĄ"
st.markdown(f"""
{sent_emoji} {sentiment_label}
Confidence: {sentiment_conf:.1%}
""", unsafe_allow_html=True)
# Voice feedback button
if st.button("đ Listen to Voice Feedback", use_container_width=True):
text_to_speech(voice_feedback)
else:
st.error("Voice analysis failed. Please try again.")
# Detect button
if st.button("đ Detect Toxicity", use_container_width=True):
if user_input.strip():
with st.spinner("đ Analyzing toxicity..."):
# Detect language first with robust detection
detected_lang = detect_language_robust(user_input)
# Use multilingual function if not English
text_for_explanation = user_input # Default to original text
multilingual_result = None # Initialize to track if translation happened
if detected_lang != 'en':
# Use multilingual prediction
multilingual_result = predict_toxicity_multilingual(user_input, tokenizer, model, device)
# Display language info
lang_display = LANGUAGE_NAMES.get(detected_lang, detected_lang.upper())
st.info(f"đ **Detected Language:** {lang_display} ({detected_lang.upper()})")
if multilingual_result.get('translated_text'):
st.info(f"đ **Translation:** {multilingual_result['translated_text']}")
text_for_explanation = multilingual_result['translated_text'] # Use translated text for explanation
st.info(f"đ¤ **Model Used:** {multilingual_result['model_used']}")
# Get results from multilingual prediction
results = multilingual_result.get('all_scores', {})
is_toxic = multilingual_result.get('toxicity_label') == 'Toxic'
# Create verdict
if is_toxic:
# Get active labels from scores if available
if results:
active_labels = [label for label, score in results.items() if score >= 0.5]
if active_labels:
verdict = f"Yes - {', '.join([LABEL_EMOJIS.get(l, '') + ' ' + l.replace('_', ' ').title() for l in active_labels])}"
else:
verdict = "Yes (General Toxicity)"
else:
verdict = f"Yes (Confidence: {multilingual_result['confidence']:.2%})"
else:
verdict = "No"
else:
# English text - use regular prediction
results = predict_toxicity(model, tokenizer, user_input, device)
# Get verdict
verdict, is_toxic = get_toxicity_verdict(results, threshold=0.5)
# Display verdict prominently
if is_toxic:
st.error(f"### đ¨ Toxicity Detected: **{verdict}**")
else:
st.success(f"### â
Toxicity Status: **{verdict}**")
# Sentiment Analysis
st.markdown("### đ Emotional Sentiment Analysis")
with st.spinner("Analyzing emotions..."):
try:
sentiment_result = analyze_sentiment(text_for_explanation)
if sentiment_result:
# Display sentiment
sentiment_cols = st.columns(3)
with sentiment_cols[0]:
st.markdown(f"""
{sentiment_result['emotion']}
{sentiment_result['confidence']:.1%}
""", unsafe_allow_html=True)
# Display all sentiment scores
with sentiment_cols[1]:
for sentiment, score in sentiment_result['all_scores'].items():
emoji = 'đ ' if sentiment == 'negative' else 'đ' if sentiment == 'neutral' else 'đ'
st.metric(f"{emoji} {sentiment.capitalize()}", f"{score:.1%}")
except Exception as e:
st.warning("Sentiment analysis unavailable")
# Voice feedback button
if SPEECH_AVAILABLE:
feedback_cols = st.columns(3)
with feedback_cols[0]:
if st.button("đ Listen to Feedback", use_container_width=True):
feedback_text = f"Toxicity status: {verdict}. "
if sentiment_result and sentiment_result.get('emotion'):
feedback_text += f"Emotional sentiment: {sentiment_result['emotion'].split()[1] if len(sentiment_result['emotion'].split()) > 1 else sentiment_result['emotion']}. "
text_to_speech(feedback_text)
# Display detailed results
st.markdown("### đ Detailed Toxicity Breakdown")
# Create columns for metrics
cols = st.columns(2)
for idx, (label, score) in enumerate(results.items()):
col_idx = idx % 2
emoji = LABEL_EMOJIS.get(label, 'đ')
display_name = label.replace('_', ' ').title()
with cols[col_idx]:
# Custom metric card
st.markdown(f"""
{emoji} {display_name}
{score*100:.1f}%
""", unsafe_allow_html=True)
# SHAP Explanation - always show directly under results (use translated text for non-English)
with st.spinner("đ Generating word importance explanation..."):
try:
word_importances = generate_shap_explanation(text_for_explanation, model, tokenizer, device)
if word_importances is not None:
shap_html = visualize_shap_explanation(text_for_explanation, word_importances)
if shap_html:
st.markdown("### đ¯ Word Importance Explanation")
# Add note for non-English texts
if detected_lang != 'en' and multilingual_result and multilingual_result.get('translated_text'):
st.info("âšī¸ *Word importance analysis is based on the English translation above.*")
st.markdown(shap_html, unsafe_allow_html=True)
else:
st.info("Could not generate visualization.")
else:
st.warning("Explanation generation failed.")
except Exception as e:
st.error(f"Error generating explanation: {str(e)}")
# Tip
if is_toxic:
st.markdown("""
â
Tip: Try rephrasing harsh words for a kinder comment :)
""", unsafe_allow_html=True)
else:
st.warning("â ī¸ Please enter a comment to analyze.")
with col2:
st.markdown("### đ¯ Quick Stats")
st.info("""
**Analyze any comment:**
1. Type or paste text
2. Click Detect
3. Get instant results
**The model checks for:**
- General toxicity
- Severe toxicity
- Obscenity
- Threats
- Insults
- Identity-based hate
""")
st.markdown("---")
# Example button
if st.button("đ Try Example", use_container_width=True):
example = "I love this product! It works amazingly well."
st.session_state['example_text'] = example
st.rerun()
def visualization_page():
"""Display the dataset visualization page"""
st.markdown('đ Dataset Visualization Dashboard
',
unsafe_allow_html=True)
st.markdown("""
### Welcome to the Dataset Analysis Dashboard! đ
This page provides insights into the Jigsaw Toxic Comment Classification dataset.
Explore the distribution of toxicity types, word clouds, and label overlaps.
""")
# Check if train.csv exists
if not os.path.exists('train.csv'):
st.error("â train.csv not found in the current directory.")
st.info("đĄ Please make sure train.csv is in the same directory as app.py")
return
# Generate visualizations
with st.spinner("đ¨ Generating visualizations..."):
try:
fig1, fig2, fig3, fig4, fig5 = vis.main_visualization('train.csv')
except Exception as e:
st.error(f"Error generating visualizations: {str(e)}")
return
if fig1 is None:
st.error("Failed to generate visualizations")
return
# Display visualizations
st.markdown("---")
# Label frequency chart
st.markdown("### đ Label Distribution")
st.markdown("This chart shows how many comments belong to each toxicity category.")
st.pyplot(fig1)
plt.close(fig1)
# Pie chart
st.markdown("### đ§Š Toxic vs Non-Toxic Distribution")
st.markdown("Overall distribution of toxic and non-toxic comments in the dataset.")
st.pyplot(fig4)
plt.close(fig4)
# Heatmap
st.markdown("### đĨ Label Co-occurrence Heatmap")
st.markdown("This heatmap shows which toxicity labels often appear together in the same comment.")
st.pyplot(fig5)
plt.close(fig5)
# Word clouds
st.markdown("---")
st.markdown("### đŦ Word Clouds")
st.markdown("""
**Word clouds** visualize the most frequent words in toxic vs non-toxic comments.
Larger words appear more frequently in that category.
""")
col1, col2 = st.columns(2)
with col1:
st.markdown("#### đ´ Toxic Comments")
st.pyplot(fig2)
plt.close(fig2)
with col2:
st.markdown("#### đĸ Non-Toxic Comments")
st.pyplot(fig3)
plt.close(fig3)
# Regenerate button
st.markdown("---")
if st.button("đ Regenerate All Visualizations", use_container_width=True):
st.rerun()
# Footer
st.markdown("---")
st.markdown("""
đ Dataset: Jigsaw Toxic Comment Classification Challenge
đ ī¸ Tools: Pandas, Matplotlib, Seaborn, WordCloud
""", unsafe_allow_html=True)
def init_session_state():
"""Initialize session state"""
if 'example_text' not in st.session_state:
st.session_state.example_text = ""
if __name__ == "__main__":
init_session_state()
main()