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("""
Go beyond simple positive/negative sentiment. Identify granular emotional triggers—Joy, Sadness, Anger, Fear, and Surprise—within literary drafts, political speeches, or customer opinions.