Virtual-Kimi / kimi-js /kimi-emotion-system.js
VirtualKimi's picture
Upload 38 files
798bcc6 verified
raw
history blame
22.9 kB
// ===== 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;
};