Spaces:
Paused
Paused
| 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() | |