Skip to main content
Glama
discrete-emotion-classifier.ts12.8 kB
/** * Discrete Emotion Classifier * * Classifies text into 11 discrete emotion types with intensity scoring, * confidence assessment, and evidence extraction. * * Requirements: 9.1, 9.2, 9.3, 9.4, 9.5 */ import type { EmotionClassification, EmotionModel, EmotionType } from "./types"; /** * Emotion lexicons for each discrete emotion type */ const JOY_WORDS = new Set([ "happy", "happiness", "joy", "joyful", "joyous", "delighted", "pleased", "glad", "cheerful", "thrilled", "ecstatic", "elated", "blissful", "content", "satisfied", "wonderful", "great", "excellent", "fantastic", "amazing", "love", "loving", "loved", "excited", "excitement", "enthusiastic", "optimistic", "hopeful", "merry", "jubilant", ]); const SADNESS_WORDS = new Set([ "sad", "sadness", "unhappy", "miserable", "depressed", "depression", "disappointed", "disappointing", "disappointment", "upset", "down", "gloomy", "melancholy", "sorrowful", "heartbroken", "devastated", "grief", "mourning", "dejected", "despondent", "disheartened", "forlorn", "woeful", "tearful", "crying", "weeping", "feel", // Weak indicator for ambiguous emotional states ]); const ANGER_WORDS = new Set([ "angry", "anger", "mad", "furious", "enraged", "outraged", "frustrated", "frustration", "annoyed", "irritated", "irritation", "infuriated", "livid", "irate", "wrathful", "hostile", "resentful", "bitter", "indignant", "exasperated", "aggravated", "provoked", "incensed", "fuming", "seething", ]); const FEAR_WORDS = new Set([ "scared", "afraid", "fear", "fearful", "frightened", "terrified", "horrified", "panicked", "panic", "anxious", "anxiety", "worried", "worry", "nervous", "apprehensive", "alarmed", "dread", "dreading", "uneasy", "tense", "threatened", "intimidated", "petrified", "spooked", "startled", ]); const DISGUST_WORDS = new Set([ "disgusted", "disgusting", "disgust", "repulsed", "repulsive", "revolted", "revolting", "nauseated", "nauseous", "sickened", "gross", "yuck", "ew", "nasty", "foul", "vile", "loathsome", "abhorrent", "detestable", "offensive", "repugnant", "distasteful", ]); const SURPRISE_WORDS = new Set([ "surprised", "surprise", "shocking", "shocked", "astonished", "amazed", "astounded", "stunned", "startled", "unexpected", "wow", "unbelievable", "incredible", "remarkable", "extraordinary", "bewildered", "dumbfounded", "flabbergasted", "speechless", "awestruck", ]); const PRIDE_WORDS = new Set([ "proud", "pride", "accomplished", "achievement", "successful", "triumph", "triumphant", "victorious", "honored", "distinguished", "dignified", "confident", "self-assured", "capable", "competent", "worthy", "deserving", "earned", "satisfied", "fulfilled", ]); const SHAME_WORDS = new Set([ "ashamed", "shame", "embarrassed", "embarrassment", "humiliated", "humiliation", "mortified", "disgraced", "dishonored", "degraded", "belittled", "inferior", "inadequate", "unworthy", "regretful", "remorseful", "chagrined", "abashed", "sheepish", ]); const GUILT_WORDS = new Set([ "guilty", "guilt", "regret", "regretful", "remorse", "remorseful", "sorry", "apologetic", "contrite", "penitent", "repentant", "culpable", "blameworthy", "responsible", "fault", "wrongdoing", "conscience", "self-reproach", ]); const GRATITUDE_WORDS = new Set([ "grateful", "gratitude", "thankful", "thanks", "thank", "appreciated", "appreciation", "appreciative", "indebted", "obliged", "blessed", "fortunate", "lucky", "gracious", "beholden", "recognize", "acknowledge", ]); const AWE_WORDS = new Set([ "awe", "awestruck", "awe-inspiring", "breathtaking", "magnificent", "majestic", "spectacular", "stunning", "overwhelming", "wondrous", "sublime", "transcendent", "profound", "reverent", "venerable", "impressive", "grand", "epic", "monumental", ]); /** * Intensity amplifiers */ const INTENSITY_AMPLIFIERS = new Set([ "very", "extremely", "absolutely", "completely", "totally", "utterly", "incredibly", "remarkably", "exceptionally", "extraordinarily", "so", "really", "truly", "deeply", "profoundly", ]); /** * Intensity reducers */ const INTENSITY_REDUCERS = new Set([ "a bit", "bit", "slightly", "somewhat", "kind of", "sort of", "rather", "fairly", "moderately", "mildly", "little", ]); /** * Emotion detection result with evidence */ interface EmotionDetectionResult { emotion: EmotionType; intensity: number; confidence: number; evidence: string[]; wordCount: number; } /** * DiscreteEmotionClassifier * * Classifies text into discrete emotion categories using lexicon-based approach. */ export class DiscreteEmotionClassifier { private readonly cache: Map<string, EmotionClassification[]>; private readonly maxCacheSize: number = 1000; constructor(_model: EmotionModel) { // Model parameter kept for future extensibility this.cache = new Map(); } /** * Classify text into discrete emotions * @param text - Text to classify * @returns Array of detected emotions with intensity and confidence */ classify(text: string): EmotionClassification[] { // Check cache const cached = this.cache.get(text); if (cached) { return cached; } // Detect all emotions const results = this.detectAllEmotions(text, false); // Filter out very low-intensity emotions (threshold: 0.15) // Lower threshold to catch ambiguous emotional signals const filtered = results .filter((r) => r.intensity > 0.15) .map((r) => ({ emotion: r.emotion, intensity: r.intensity, confidence: r.confidence, })); // Cache result this.cacheResult(text, filtered); return filtered; } /** * Classify text with evidence extraction * @param text - Text to classify * @returns Array of detected emotions with evidence words */ classifyWithEvidence(text: string): EmotionClassification[] { // Detect all emotions with evidence const results = this.detectAllEmotions(text, true); // Filter out low-intensity emotions return results .filter((r) => r.intensity > 0.2) .map((r) => ({ emotion: r.emotion, intensity: r.intensity, confidence: r.confidence, evidence: r.evidence, })); } /** * Detect all emotions in text * @param text - Text to analyze * @param includeEvidence - Whether to include evidence words * @returns Array of emotion detection results */ private detectAllEmotions(text: string, includeEvidence: boolean): EmotionDetectionResult[] { if (!text || text.trim().length === 0) { return []; } const normalized = this.normalizeText(text); const words = normalized.split(/\s+/).filter((w) => w.length > 0); // Count intensity markers const intensityMarkers = this.countIntensityMarkers(text); const amplifierCount = this.countAmplifiers(words); const reducerCount = this.countReducers(words); const results: EmotionDetectionResult[] = []; const context = { intensityMarkers, amplifierCount, reducerCount, includeEvidence }; // Detect each emotion type results.push(this.detectEmotion("joy", JOY_WORDS, words, context)); results.push(this.detectEmotion("sadness", SADNESS_WORDS, words, context)); results.push(this.detectEmotion("anger", ANGER_WORDS, words, context)); results.push(this.detectEmotion("fear", FEAR_WORDS, words, context)); results.push(this.detectEmotion("disgust", DISGUST_WORDS, words, context)); results.push(this.detectEmotion("surprise", SURPRISE_WORDS, words, context)); results.push(this.detectEmotion("pride", PRIDE_WORDS, words, context)); results.push(this.detectEmotion("shame", SHAME_WORDS, words, context)); results.push(this.detectEmotion("guilt", GUILT_WORDS, words, context)); results.push(this.detectEmotion("gratitude", GRATITUDE_WORDS, words, context)); results.push(this.detectEmotion("awe", AWE_WORDS, words, context)); return results; } /** * Detect a specific emotion in text */ private detectEmotion( emotion: EmotionType, lexicon: Set<string>, words: string[], context: { intensityMarkers: number; amplifierCount: number; reducerCount: number; includeEvidence: boolean; } ): EmotionDetectionResult { const evidence: string[] = []; let matchCount = 0; // Find matching words for (const word of words) { if (lexicon.has(word)) { matchCount++; if (context.includeEvidence) { evidence.push(word); } } } // Calculate base intensity let intensity = 0; if (matchCount > 0) { // Base intensity from match count (not ratio to avoid over-scaling short text) // Use logarithmic scaling for more nuanced intensity intensity = Math.min(0.3 + matchCount * 0.25, 0.9); // Apply amplifiers and reducers const netAmplification = context.amplifierCount - context.reducerCount; if (netAmplification > 0) { intensity = Math.min(intensity + netAmplification * 0.15, 1.0); } else if (netAmplification < 0) { intensity = Math.max(intensity + netAmplification * 0.15, 0.2); } // Apply intensity markers (exclamation marks, caps) const markerBoost = Math.min(context.intensityMarkers * 0.1, 0.3); intensity = Math.min(intensity + markerBoost, 1.0); } // Calculate confidence const confidence = this.calculateConfidence(matchCount, words.length, intensity); return { emotion, intensity: Math.max(0, Math.min(1, intensity)), confidence: Math.max(0, Math.min(1, confidence)), evidence, wordCount: matchCount, }; } /** * Calculate confidence in emotion detection */ private calculateConfidence(matchCount: number, totalWords: number, intensity: number): number { if (matchCount === 0) { return 0; } // Confidence based on: // 1. Number of matching words (more matches = higher confidence) // 2. Intensity of the emotion // 3. Text length (longer text with matches = higher confidence) const matchFactor = Math.min(matchCount / 3, 1.0); // 3+ matches = full confidence const intensityFactor = intensity; const lengthFactor = Math.min(totalWords / 10, 1.0); // 10+ words = full confidence const confidence = matchFactor * 0.5 + intensityFactor * 0.3 + lengthFactor * 0.2; // Minimum confidence for any detection return Math.max(confidence, 0.3); } /** * Normalize text for analysis */ private normalizeText(text: string): string { return text .toLowerCase() .replace(/[\u{1F600}-\u{1F64F}]/gu, "") // Remove emoticons .replace(/[\u{1F300}-\u{1F5FF}]/gu, "") // Remove misc symbols .replace(/[\u{1F680}-\u{1F6FF}]/gu, "") // Remove transport symbols .replace(/[\u{2600}-\u{26FF}]/gu, "") // Remove misc symbols .replace(/[^\w\s]/g, " ") // Remove punctuation .replace(/\s+/g, " ") .trim(); } /** * Count intensity markers in text */ private countIntensityMarkers(text: string): number { let count = 0; // Count exclamation marks count += (text.match(/!/g) ?? []).length; // Count question marks (can indicate intensity) count += (text.match(/\?/g) ?? []).length * 0.5; // Count capital letters (excluding first letter of sentences) const words = text.split(/\s+/); for (const word of words) { if (word.length > 1 && word === word.toUpperCase() && /[A-Z]/.test(word)) { count += 1; } } return count; } /** * Count amplifier words */ private countAmplifiers(words: string[]): number { return words.filter((w) => INTENSITY_AMPLIFIERS.has(w)).length; } /** * Count reducer words */ private countReducers(words: string[]): number { return words.filter((w) => INTENSITY_REDUCERS.has(w)).length; } /** * Cache classification result */ private cacheResult(text: string, result: EmotionClassification[]): void { // Implement LRU eviction if (this.cache.size >= this.maxCacheSize) { const firstKey = this.cache.keys().next().value; if (firstKey !== undefined) { this.cache.delete(firstKey); } } this.cache.set(text, result); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ThoughtMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server