// ===== KIMI UNIFIED EMOTION SYSTEM ===== // Centralizes all emotion analysis, personality updates, and validation class KimiEmotionSystem { constructor(database = null) { this.db = database; this.negativeStreaks = {}; // Unified emotion mappings this.EMOTIONS = { // Base emotions POSITIVE: "positive", NEGATIVE: "negative", NEUTRAL: "neutral", // Specific emotions ROMANTIC: "romantic", DANCING: "dancing", LISTENING: "listening", LAUGHING: "laughing", SURPRISE: "surprise", CONFIDENT: "confident", SHY: "shy", FLIRTATIOUS: "flirtatious", KISS: "kiss", GOODBYE: "goodbye" }; // Unified video context mapping this.emotionToVideoCategory = { positive: "speakingPositive", negative: "speakingNegative", neutral: "neutral", dancing: "dancing", listening: "listening", romantic: "speakingPositive", laughing: "speakingPositive", surprise: "speakingPositive", confident: "speakingPositive", shy: "neutral", flirtatious: "speakingPositive", kiss: "speakingPositive", goodbye: "neutral" }; // Unified trait defaults - More balanced for progressive experience this.TRAIT_DEFAULTS = { affection: 65, // Reduced from 80 - starts neutral, grows with interaction playfulness: 55, // Reduced from 70 - more reserved initially intelligence: 70, // Reduced from 85 - still competent but not overwhelming empathy: 75, // Reduced from 90 - caring but not overly so humor: 60, // Reduced from 75 - develops sense of humor over time romance: 50 // Significantly reduced from 95 - romance must be earned! }; } // ===== UNIFIED EMOTION ANALYSIS ===== analyzeEmotion(text, lang = "auto") { if (!text || typeof text !== "string") return this.EMOTIONS.NEUTRAL; const lowerText = text.toLowerCase(); // Auto-detect language let detectedLang = this._detectLanguage(text, lang); // Get language-specific keywords const positiveWords = window.KIMI_CONTEXT_POSITIVE?.[detectedLang] || window.KIMI_CONTEXT_POSITIVE?.en || ["happy", "good", "great", "love"]; const negativeWords = window.KIMI_CONTEXT_NEGATIVE?.[detectedLang] || window.KIMI_CONTEXT_NEGATIVE?.en || ["sad", "bad", "angry", "hate"]; const emotionKeywords = window.KIMI_CONTEXT_KEYWORDS?.[detectedLang] || window.KIMI_CONTEXT_KEYWORDS?.en || {}; // Priority order for emotion detection const emotionChecks = [ // Listening intent (user asks to talk or indicates speaking/listening) { emotion: this.EMOTIONS.LISTENING, keywords: emotionKeywords.listening || [ "listen", "listening", "écoute", "ecoute", "écouter", "parle", "speak", "talk", "question", "ask" ] }, { emotion: this.EMOTIONS.DANCING, keywords: emotionKeywords.dancing || ["dance", "dancing"] }, { emotion: this.EMOTIONS.ROMANTIC, keywords: emotionKeywords.romantic || ["love", "romantic"] }, { emotion: this.EMOTIONS.LAUGHING, keywords: emotionKeywords.laughing || ["laugh", "funny"] }, { emotion: this.EMOTIONS.SURPRISE, keywords: emotionKeywords.surprise || ["wow", "surprise"] }, { emotion: this.EMOTIONS.CONFIDENT, keywords: emotionKeywords.confident || ["confident", "strong"] }, { emotion: this.EMOTIONS.SHY, keywords: emotionKeywords.shy || ["shy", "embarrassed"] }, { emotion: this.EMOTIONS.FLIRTATIOUS, keywords: emotionKeywords.flirtatious || ["flirt", "tease"] }, { emotion: this.EMOTIONS.KISS, keywords: emotionKeywords.kiss || ["kiss", "embrace"] }, { emotion: this.EMOTIONS.GOODBYE, keywords: emotionKeywords.goodbye || ["goodbye", "bye"] } ]; // Check for specific emotions first, applying sensitivity weights per language const sensitivity = (window.KIMI_EMOTION_SENSITIVITY && (window.KIMI_EMOTION_SENSITIVITY[detectedLang] || window.KIMI_EMOTION_SENSITIVITY.default)) || { listening: 1, dancing: 1, romantic: 1, laughing: 1, surprise: 1, confident: 1, shy: 1, flirtatious: 1, kiss: 1, goodbye: 1, positive: 1, negative: 1 }; let bestEmotion = null; let bestScore = 0; for (const check of emotionChecks) { const hits = check.keywords.reduce((acc, word) => acc + (lowerText.includes(word.toLowerCase()) ? 1 : 0), 0); if (hits > 0) { const key = check.emotion; const weight = sensitivity[key] != null ? sensitivity[key] : 1; const score = hits * weight; if (score > bestScore) { bestScore = score; bestEmotion = check.emotion; } } } if (bestEmotion) return bestEmotion; // Fall back to positive/negative analysis const hasPositive = positiveWords.some(word => lowerText.includes(word.toLowerCase())); const hasNegative = negativeWords.some(word => lowerText.includes(word.toLowerCase())); if (hasPositive && !hasNegative) { // Apply sensitivity for base polarity if ((sensitivity.positive || 1) >= (sensitivity.negative || 1)) return this.EMOTIONS.POSITIVE; // If negative is favored, still fall back to positive since no negative hit return this.EMOTIONS.POSITIVE; } if (hasNegative && !hasPositive) { if ((sensitivity.negative || 1) >= (sensitivity.positive || 1)) return this.EMOTIONS.NEGATIVE; return this.EMOTIONS.NEGATIVE; } return this.EMOTIONS.NEUTRAL; } // ===== UNIFIED PERSONALITY SYSTEM ===== async updatePersonalityFromEmotion(emotion, text, character = null) { if (!this.db) { console.warn("Database not available for personality updates"); return; } const selectedCharacter = character || (await this.db.getSelectedCharacter()); const traits = await this.db.getAllPersonalityTraits(selectedCharacter); const safe = (v, def) => (typeof v === "number" && isFinite(v) ? v : def); let affection = safe(traits?.affection, this.TRAIT_DEFAULTS.affection); let romance = safe(traits?.romance, this.TRAIT_DEFAULTS.romance); let empathy = safe(traits?.empathy, this.TRAIT_DEFAULTS.empathy); let playfulness = safe(traits?.playfulness, this.TRAIT_DEFAULTS.playfulness); let humor = safe(traits?.humor, this.TRAIT_DEFAULTS.humor); let intelligence = safe(traits?.intelligence, this.TRAIT_DEFAULTS.intelligence); // Unified adjustment functions - More gradual progression for balanced experience const adjustUp = (val, amount) => { // Slower progression as values get higher - make romance and affection harder to max out if (val >= 95) return val + amount * 0.1; // Very slow near max if (val >= 85) return val + amount * 0.3; // Slow progression at high levels if (val >= 70) return val + amount * 0.6; // Moderate progression at medium levels if (val >= 50) return val + amount * 0.8; // Normal progression above average return val + amount; // Normal progression below average }; const adjustDown = (val, amount) => { // Faster decline at higher values - easier to lose than to gain if (val >= 80) return val - amount * 1.2; // Faster loss at high levels if (val >= 60) return val - amount; // Normal loss at medium levels if (val >= 40) return val - amount * 0.8; // Slower loss at low-medium levels if (val <= 20) return val - amount * 0.4; // Very slow loss at low levels return val - amount * 0.6; // Moderate loss between 20-40 }; // Unified emotion-based adjustments - More balanced and realistic progression const gainCfg = window.KIMI_TRAIT_ADJUSTMENT || { globalGain: 1, globalLoss: 1, emotionGain: {}, traitGain: {}, traitLoss: {} }; const emoGain = emotion && gainCfg.emotionGain ? gainCfg.emotionGain[emotion] || 1 : 1; const GGAIN = (gainCfg.globalGain || 1) * emoGain; const GLOSS = gainCfg.globalLoss || 1; // Helpers to apply trait-specific scaling const scaleGain = (traitName, baseDelta) => { const t = gainCfg.traitGain && (gainCfg.traitGain[traitName] || 1); return baseDelta * GGAIN * t; }; const scaleLoss = (traitName, baseDelta) => { const t = gainCfg.traitLoss && (gainCfg.traitLoss[traitName] || 1); return baseDelta * GLOSS * t; }; switch (emotion) { case this.EMOTIONS.POSITIVE: affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.4))); // Slightly more affection gain empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.2))); playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.2))); humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.2))); romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.1))); // Romance grows very slowly break; case this.EMOTIONS.NEGATIVE: affection = Math.max(0, adjustDown(affection, scaleLoss("affection", 0.6))); // Affection drops faster on negative empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.3))); // Empathy still grows (understanding pain) break; case this.EMOTIONS.ROMANTIC: romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.6))); // Reduced from 0.8 - romance should be earned affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.3))); // Reduced from 0.4 break; case this.EMOTIONS.LAUGHING: humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.8))); // Humor grows with laughter playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.4))); // Increased playfulness connection affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.2))); // Small affection boost from shared laughter break; case this.EMOTIONS.DANCING: playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 1.2))); // Dancing = maximum playfulness boost affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.3))); // Affection from shared activity break; case this.EMOTIONS.SHY: affection = Math.max(0, adjustDown(affection, scaleLoss("affection", 0.1))); // Small affection loss romance = Math.max(0, adjustDown(romance, scaleLoss("romance", 0.2))); // Shyness reduces romance more break; case this.EMOTIONS.CONFIDENT: affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.3))); // Reduced from 0.4 intelligence = Math.min(100, adjustUp(intelligence, scaleGain("intelligence", 0.1))); // Slight intelligence boost break; case this.EMOTIONS.FLIRTATIOUS: romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.5))); // Reduced from 0.6 playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.3))); // Reduced from 0.4 affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.2))); // Small affection boost break; } // Content-based adjustments (unified) await this._analyzeTextContent( text, traits => { if (typeof traits.romance !== "undefined") romance = traits.romance; if (typeof traits.affection !== "undefined") affection = traits.affection; if (typeof traits.humor !== "undefined") humor = traits.humor; if (typeof traits.playfulness !== "undefined") playfulness = traits.playfulness; }, adjustUp ); // Preserve fractional progress to allow gradual visible changes const to2 = v => Number(Number(v).toFixed(2)); const clamp = v => Math.max(0, Math.min(100, v)); const updatedTraits = { affection: to2(clamp(affection)), romance: to2(clamp(romance)), empathy: to2(clamp(empathy)), playfulness: to2(clamp(playfulness)), humor: to2(clamp(humor)), intelligence: to2(clamp(intelligence)) }; // Save to database await this.db.setPersonalityBatch(updatedTraits, selectedCharacter); return updatedTraits; } // ===== UNIFIED LLM PERSONALITY ANALYSIS ===== async updatePersonalityFromConversation(userMessage, kimiResponse, character = null) { if (!this.db) return; const lowerUser = userMessage ? userMessage.toLowerCase() : ""; const lowerKimi = (kimiResponse || "").toLowerCase(); const traits = (await this.db.getAllPersonalityTraits(character)) || {}; const selectedLanguage = await this.db.getPreference("selectedLanguage", "en"); // Use unified keyword system const getPersonalityWords = (trait, type) => { if (window.KIMI_PERSONALITY_KEYWORDS && window.KIMI_PERSONALITY_KEYWORDS[selectedLanguage]) { return window.KIMI_PERSONALITY_KEYWORDS[selectedLanguage][trait]?.[type] || []; } return this._getFallbackKeywords(trait, type); }; for (const trait of ["humor", "intelligence", "romance", "affection", "playfulness", "empathy"]) { const posWords = getPersonalityWords(trait, "positive"); const negWords = getPersonalityWords(trait, "negative"); let value = typeof traits[trait] === "number" && isFinite(traits[trait]) ? traits[trait] : this.TRAIT_DEFAULTS[trait]; // Count occurrences with proper weighting let posCount = 0; let negCount = 0; for (const w of posWords) { posCount += (lowerUser.match(new RegExp(w, "g")) || []).length * 1.0; posCount += (lowerKimi.match(new RegExp(w, "g")) || []).length * 0.3; } for (const w of negWords) { negCount += (lowerUser.match(new RegExp(w, "g")) || []).length * 1.0; negCount += (lowerKimi.match(new RegExp(w, "g")) || []).length * 0.3; } const delta = (posCount - negCount) * 0.3; // Reduced from 0.4 - slower LLM-based progression // Apply streak logic if (!this.negativeStreaks[trait]) this.negativeStreaks[trait] = 0; if (negCount > 0 && posCount === 0) { this.negativeStreaks[trait]++; if (this.negativeStreaks[trait] >= 3) { value = Math.max(0, Math.min(100, value + delta - 1)); } else { value = Math.max(0, Math.min(100, value + delta)); } } else if (posCount > 0) { this.negativeStreaks[trait] = 0; value = Math.max(0, Math.min(100, value + delta)); } if (delta !== 0) { await this.db.setPersonalityTrait(trait, value, character); } } } // ===== UNIFIED VIDEO CONTEXT MAPPING ===== mapEmotionToVideoCategory(emotion) { return this.emotionToVideoCategory[emotion] || "neutral"; } // ===== VALIDATION SYSTEM ===== validateEmotion(emotion) { const validEmotions = Object.values(this.EMOTIONS); if (!validEmotions.includes(emotion)) { console.warn(`Invalid emotion detected: ${emotion}, falling back to neutral`); return this.EMOTIONS.NEUTRAL; } return emotion; } validatePersonalityTrait(trait, value) { if (typeof value !== "number" || value < 0 || value > 100) { console.warn(`Invalid trait value for ${trait}: ${value}, using default`); return this.TRAIT_DEFAULTS[trait] || 50; } return value; } // ===== UTILITY METHODS ===== _detectLanguage(text, lang) { if (lang !== "auto") return lang; if (/[àâäéèêëîïôöùûüÿç]/i.test(text)) return "fr"; else if (/[äöüß]/i.test(text)) return "de"; else if (/[ñáéíóúü]/i.test(text)) return "es"; else if (/[àèìòù]/i.test(text)) return "it"; else if (/[\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]/i.test(text)) return "ja"; else if (/[\u4e00-\u9fff]/i.test(text)) return "zh"; return "en"; } async _analyzeTextContent(text, callback, adjustUp) { if (!this.db) return; const selectedLanguage = await this.db.getPreference("selectedLanguage", "en"); const romanticWords = window.KIMI_CONTEXT_KEYWORDS?.[selectedLanguage]?.romantic || window.KIMI_CONTEXT_KEYWORDS?.en?.romantic || ["love", "romantic", "kiss"]; const humorWords = window.KIMI_CONTEXT_KEYWORDS?.[selectedLanguage]?.laughing || window.KIMI_CONTEXT_KEYWORDS?.en?.laughing || ["joke", "funny", "lol"]; const romanticPattern = new RegExp(`(${romanticWords.join("|")})`, "i"); const humorPattern = new RegExp(`(${humorWords.join("|")})`, "i"); const traits = {}; if (text.match(romanticPattern)) { traits.romance = adjustUp(traits.romance || this.TRAIT_DEFAULTS.romance, 0.5); traits.affection = adjustUp(traits.affection || this.TRAIT_DEFAULTS.affection, 0.5); } if (text.match(humorPattern)) { traits.humor = adjustUp(traits.humor || this.TRAIT_DEFAULTS.humor, 2); traits.playfulness = adjustUp(traits.playfulness || this.TRAIT_DEFAULTS.playfulness, 1); } callback(traits); } _getFallbackKeywords(trait, type) { const fallbackKeywords = { humor: { positive: ["funny", "hilarious", "joke", "laugh", "amusing"], negative: ["boring", "sad", "serious", "cold", "dry"] }, intelligence: { positive: ["intelligent", "smart", "brilliant", "logical", "clever"], negative: ["stupid", "dumb", "foolish", "slow", "naive"] }, romance: { positive: ["cuddle", "love", "romantic", "kiss", "tenderness"], negative: ["cold", "distant", "indifferent", "rejection"] }, affection: { positive: ["affection", "tenderness", "close", "warmth", "kind"], negative: ["mean", "cold", "indifferent", "distant", "rejection"] }, playfulness: { positive: ["play", "game", "tease", "mischievous", "fun"], negative: ["serious", "boring", "strict", "rigid"] }, empathy: { positive: ["listen", "understand", "empathy", "support", "help"], negative: ["indifferent", "cold", "selfish", "ignore"] } }; return fallbackKeywords[trait]?.[type] || []; } // ===== PERSONALITY CALCULATION ===== calculatePersonalityAverage(traits) { const keys = ["affection", "romance", "empathy", "playfulness", "humor"]; let sum = 0; let count = 0; keys.forEach(key => { if (typeof traits[key] === "number") { sum += traits[key]; count++; } }); return count > 0 ? sum / count : 50; } getMoodCategoryFromPersonality(traits) { const avg = this.calculatePersonalityAverage(traits); if (avg >= 80) return "speakingPositive"; if (avg >= 60) return "neutral"; if (avg >= 40) return "neutral"; if (avg >= 20) return "speakingNegative"; return "speakingNegative"; } } window.KimiEmotionSystem = KimiEmotionSystem; export default KimiEmotionSystem; // ===== BACKWARD COMPATIBILITY LAYER ===== // Replace the old kimiAnalyzeEmotion function window.kimiAnalyzeEmotion = function (text, lang = "auto") { if (!window.kimiEmotionSystem) { window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB); } return window.kimiEmotionSystem.analyzeEmotion(text, lang); }; // Replace the old updatePersonalityTraitsFromEmotion function window.updatePersonalityTraitsFromEmotion = async function (emotion, text) { if (!window.kimiEmotionSystem) { window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB); } const updatedTraits = await window.kimiEmotionSystem.updatePersonalityFromEmotion(emotion, text); return updatedTraits; }; // Replace getPersonalityAverage function window.getPersonalityAverage = function (traits) { if (!window.kimiEmotionSystem) { window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB); } return window.kimiEmotionSystem.calculatePersonalityAverage(traits); }; // Unified trait defaults accessor window.getTraitDefaults = function () { if (window.kimiEmotionSystem) return window.kimiEmotionSystem.TRAIT_DEFAULTS; const temp = new KimiEmotionSystem(window.kimiDB); return temp.TRAIT_DEFAULTS; };