Jomaric's picture
feat: initial app release
9c57c1d
Raw
History Blame Contribute Delete
11.5 kB
import os
import gradio as gr
import pandas as pd
import numpy as np
import re
import plotly.express as px
import plotly.graph_objects as go
from huggingface_hub import InferenceClient
def load_data(file_obj):
"""Safely loads CSV, Excel, or TXT file into a Pandas DataFrame."""
if file_obj is None:
return None, gr.update(choices=[], visible=False), "Please upload a file."
file_path = file_obj.name
ext = os.path.splitext(file_path)[1].lower()
try:
if ext == '.csv':
df = pd.read_csv(file_path)
elif ext in ['.xls', '.xlsx']:
df = pd.read_excel(file_path)
elif ext == '.txt':
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
df = pd.DataFrame({'text': [content]})
else:
return None, gr.update(choices=[], visible=False), "Unsupported file format. Please upload .csv, .xlsx, or .txt."
string_cols = [col for col in df.columns if df[col].dtype == 'object' or df[col].astype(str).str.len().mean() > 5]
if not string_cols:
string_cols = list(df.columns)
return df, gr.update(choices=string_cols, value=string_cols[0], visible=True), f"Successfully loaded dataset with {len(df)} rows."
except Exception as e:
return None, gr.update(choices=[], visible=False), f"Error loading file: {str(e)}"
# Precompiled local micro-lexicon of major emotion keywords
EMOTION_LEXICON = {
"Joy": ["happy", "glad", "joy", "cheerful", "delight", "love", "smile", "laugh", "great", "excellent", "wonderful", "celebrate", "proud", "excited", "peace"],
"Sadness": ["sad", "gloomy", "cry", "grief", "sorrow", "pain", "unhappy", "depressed", "lonely", "tear", "hurt", "loss", "mourn", "disappointed", "empty"],
"Anger": ["angry", "mad", "furious", "hate", "rage", "irritated", "annoyed", "outrage", "hostile", "bitter", "spite", "offended", "resent", "aggression", "clash"],
"Fear": ["fear", "scared", "afraid", "terrified", "panic", "worry", "dread", "anxious", "horror", "threat", "danger", "frightened", "nervous", "coward", "unsafe"],
"Surprise": ["surprise", "shock", "amazed", "astonish", "sudden", "unexpected", "startle", "unbelievable", "wonder", "incredible", "reveal", "discovery"]
}
def run_local_emotion(text):
"""Calculates local lexicon-based emotional scoring."""
words = re.findall(r'\b[a-zA-Z]{3,}\b', text.lower())
scores = {"Joy": 0.0, "Sadness": 0.0, "Anger": 0.0, "Fear": 0.0, "Surprise": 0.0}
if not words:
return scores
for w in words:
for emotion, keywords in EMOTION_LEXICON.items():
if w in keywords:
scores[emotion] += 1.0
# Normalize by total words to get intensities
total = sum(scores.values())
if total > 0:
for k in scores:
scores[k] = round(scores[k] / total, 4)
return scores
def run_neural_emotion(text, hf_token, model_name):
"""Uses advanced sequence-classification pipeline to detect multi-label emotions."""
if not hf_token:
raise ValueError("Hugging Face Access Token is required for Transformers mode.")
client = InferenceClient(token=hf_token)
try:
# returns list of dicts: [{'label': 'joy', 'score': 0.99}, ...]
resp = client.text_classification(text, model=model_name)
# Standardize labels
scores = {}
for item in resp:
label = item["label"].capitalize()
# Map neutral/disgust back or display directly
if label == "Neutral":
continue
scores[label] = round(item["score"], 4)
return scores
except Exception as e:
raise RuntimeError(f"Hugging Face API error: {str(e)}")
def analyze_emotion(text_input, file_obj, text_col, method, hf_token, hf_model):
docs = []
if file_obj is not None:
df, _, _ = load_data(file_obj)
if df is not None and text_col in df.columns:
docs = df[text_col].astype(str).fillna("").tolist()
elif text_input and text_input.strip():
docs = [text_input]
if not docs:
return None, None, None, "Please enter text or upload a valid dataset first."
try:
results = []
# In bulk mode, we run emotion scoring for every row
for doc_idx, doc_text in enumerate(docs):
if method == "Local Lexicon-Based (CPU & Fast)":
scores = run_local_emotion(doc_text)
else:
scores = run_neural_emotion(doc_text, hf_token, hf_model)
row_data = {"Doc_Num": doc_idx + 1, "Dominant_Emotion": max(scores, key=scores.get) if sum(scores.values()) > 0 else "Neutral"}
for k, v in scores.items():
row_data[k] = v
results.append(row_data)
df_res = pd.DataFrame(results)
# 1. Visualization format for the first document
first_doc_scores = {k: v for k, v in results[0].items() if k not in ["Doc_Num", "Dominant_Emotion"]}
# Plotly Radar (Spider) Chart
categories = list(first_doc_scores.keys())
values = list(first_doc_scores.values())
# Radar chart needs to close the loop
categories.append(categories[0])
values.append(values[0])
fig = go.Figure()
fig.add_trace(go.Scatterpolar(
r=values,
theta=categories,
fill='toself',
fillcolor='rgba(99, 102, 241, 0.3)',
line=dict(color='#6366f1', width=3),
name='First Doc Emotions'
))
fig.update_layout(
polar=dict(
radialaxis=dict(visible=True, range=[0, 1]),
bgcolor='#0f172a'
),
template="plotly_dark",
title="Emotional Intensity Fingerprint",
height=400,
margin=dict(l=40, r=40, t=50, b=40)
)
# Export CSV
csv_path = "emotion_detector_report.csv"
df_res.to_csv(csv_path, index=False)
status_md = f"Successfully analyzed **{len(df_res)}** documents. Dominant sentiment: **{df_res['Dominant_Emotion'].mode()[0]}**."
return df_res, fig, csv_path, status_md
except Exception as e:
return None, None, None, f"Execution failed: {str(e)}"
custom_css = """
body {
background-color: #0b0f19;
color: #f3f4f6;
}
.gradio-container {
font-family: 'Inter', sans-serif !important;
}
h1, h2 {
color: #6366f1 !important;
}
"""
with gr.Blocks(theme=gr.themes.Default(primary_hue="indigo", secondary_hue="slate"), css=custom_css) as demo:
df_state = gr.State()
gr.HTML("""
<div style="text-align: center; margin-bottom: 2rem;">
<h1 style="font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem; background: linear-gradient(to right, #6366f1, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Interactive Emotion Detector</h1>
<p style="font-size: 1.1rem; color: #94a3b8; max-width: 800px; margin: 0 auto;">
Go beyond simple positive/negative sentiment. Identify granular emotional triggers—Joy, Sadness, Anger, Fear, and Surprise—within literary drafts, political speeches, or customer opinions.
</p>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 1. Upload Source Text")
with gr.Tabs():
with gr.TabItem("Paste Raw Text"):
text_input = gr.Textbox(
label="Source Text",
placeholder="Paste your text draft here to reveal emotional signatures...",
lines=12
)
with gr.TabItem("Upload Dataset File"):
file_input = gr.File(label="Upload (.csv, .xlsx, .txt)", file_types=[".csv", ".xlsx", ".txt"])
text_column_selector = gr.Dropdown(
label="Target Text Column",
choices=[],
visible=False,
interactive=True
)
status_text = gr.Markdown("No file uploaded yet.")
gr.Markdown("### 2. Configure Model")
method_selector = gr.Radio(
choices=["Local Lexicon-Based (CPU & Fast)", "Transformers (API Mode)"],
value="Local Lexicon-Based (CPU & Fast)",
label="Emotion Detector Model"
)
with gr.Group() as token_group:
hf_token_input = gr.Textbox(
label="Hugging Face API Token",
placeholder="hf_...",
type="password",
visible=False,
info="Required to call advanced emotion classification. Get one free at huggingface.co."
)
hf_model_input = gr.Dropdown(
choices=[
"j-hartmann/emotion-english-distilroberta-base",
"bhadresh-savani/distilbert-base-uncased-emotion"
],
value="j-hartmann/emotion-english-distilroberta-base",
label="Transformer Model (HF API)",
visible=False
)
run_btn = gr.Button("Extract Emotions", variant="primary")
with gr.Column(scale=2):
gr.Markdown("### 3. Emotional Signature Results")
status_markdown = gr.Markdown("Enter text and click 'Extract Emotions' to run.")
with gr.Tabs():
with gr.TabItem("Spider/Radar Intensity Chart"):
chart_output = gr.Plot(label="Emotion Spider Plot")
with gr.TabItem("Granular Scores Table"):
table_output = gr.Dataframe(
headers=["Doc_Num", "Dominant_Emotion", "Joy", "Sadness", "Anger", "Fear", "Surprise"],
datatype=["number", "str", "number", "number", "number", "number", "number"],
interactive=False,
wrap=True
)
gr.Markdown("### 4. Export")
download_csv = gr.File(label="Download Emotions Report (CSV)")
# Show/hide token field depending on model
def toggle_method_fields(method):
if method == "Transformers (API Mode)":
return gr.update(visible=True), gr.update(visible=True)
else:
return gr.update(visible=False), gr.update(visible=False)
method_selector.change(
fn=toggle_method_fields,
inputs=method_selector,
outputs=[hf_token_input, hf_model_input]
)
file_input.change(
fn=load_data,
inputs=file_input,
outputs=[df_state, text_column_selector, status_text]
)
run_btn.click(
fn=analyze_emotion,
inputs=[text_input, file_input, text_column_selector, method_selector, hf_token_input, hf_model_input],
outputs=[table_output, chart_output, download_csv, status_markdown]
)
if __name__ == "__main__":
demo.launch()