Spaces:
Running
Running
// ===== 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; | |
}; | |