VirtualKimi commited on
Commit
6236c66
Β·
verified Β·
1 Parent(s): dfd7665

Upload 38 files

Browse files
CHANGELOG.md CHANGED
@@ -1,40 +1,5 @@
1
  # Virtual Kimi App Changelog
2
 
3
- # [1.1.4] - 2025-09-01
4
-
5
- ### Added
6
-
7
- - Added two persistent traits: `trust` and `intimacy`.
8
- - Added an ephemeral relational state `warmth`. It decays over time and can be raised. Events: `relationship:trustChanged`, `relationship:intimacyChanged`, `relationship:warmthChanged`.
9
- - Auto-store short-term relationship memories on strong love declarations (`relationship:affirmation`, 6h cooldown).
10
- - Added EN/FR keyword lists for `trust`, `intimacy`, and `boundary` to drive conversation-based changes.
11
-
12
- ### Improvements
13
-
14
- - Conversation drift now covers `trust`, `intimacy`, and `boundary`.
15
- - Boundary changes update trust, empathy, and intimacy with scaled effects.
16
- - `warmth` now affects speaking video selection: high warmth favors positive clips and suppresses negative ones.
17
- - Treats affectionate profanity (tender words mixed with mild swears) as romantic: small, safe boosts to affection, romance, trust/intimacy, plus a warmth pulse.
18
- - Words like "chaos" or "rebelle" raise playfulness and give a mild warmth boost.
19
- - Memory scoring: relationship/boundary/stage memories get higher relevance, influenced by current warmth.
20
- - Warmth amplification runs after base trait changes to scale final affection/romance/trust/intimacy values.
21
-
22
- ### Safeguards
23
-
24
- - Limits per-message relational changes to avoid large spikes (soft scaling then hard cap).
25
- - Dampens repeated keyword hits (sqrt aggregation and per-word caps) to reduce farming.
26
-
27
- ### Technical
28
-
29
- - Added configurable `WARMTH_CFG` for decay and amplification.
30
- - Centralized special-case handling to avoid double-counting (affectionate profanity, chaotic lexicon, romantic pulse).
31
- - Integrates with existing persistence smoothing and drift tracking; avoids duplicate writes.
32
-
33
- ### Notes
34
-
35
- - `boundary` is currently stored as a trait. It may become a meta-signal in a future update.
36
- - Anti-spam cooldowns for repeated romantic bursts are planned but not yet implemented.
37
-
38
  # [1.1.3] - 2025-09-01
39
 
40
  ### Bug Fixes
 
1
  # Virtual Kimi App Changelog
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  # [1.1.3] - 2025-09-01
4
 
5
  ### Bug Fixes
index.html CHANGED
@@ -140,9 +140,6 @@
140
  <button class="mic-button" id="mic-button" aria-label="Start Listening">
141
  <i class="fas fa-microphone"></i>
142
  </button>
143
- <span id="asr-lang-badge"
144
- style="display:none;align-self:center;font-size:11px;padding:2px 6px;border-radius:12px;background:#b34747;color:#fff;font-weight:600"
145
- title="ASR fallback language differs from UI language">ASR</span>
146
  <button class="control-button-unified" id="settings-button" aria-label="Settings">
147
  <i class="fas fa-cog"></i>
148
  </button>
@@ -1089,10 +1086,7 @@
1089
  </div>
1090
 
1091
  <script src="dexie.min.js"></script>
1092
- <script src="kimi-locale/i18n.js"></script>
1093
- <script type="module" src="kimi-js/kimi-event-bus.js"></script>
1094
- <script type="module" src="kimi-js/kimi-emotion-config.js"></script>
1095
- <script type="module" src="kimi-js/kimi-trait-sim.js"></script>
1096
  <script type="module" src="kimi-js/kimi-personality-utils.js"></script>
1097
  <script type="module" src="kimi-js/kimi-utils.js"></script>
1098
  <script type="module" src="kimi-js/kimi-main.js"></script>
 
140
  <button class="mic-button" id="mic-button" aria-label="Start Listening">
141
  <i class="fas fa-microphone"></i>
142
  </button>
 
 
 
143
  <button class="control-button-unified" id="settings-button" aria-label="Settings">
144
  <i class="fas fa-cog"></i>
145
  </button>
 
1086
  </div>
1087
 
1088
  <script src="dexie.min.js"></script>
1089
+ <script src="kimi-locale/i18n.js" defer></script>
 
 
 
1090
  <script type="module" src="kimi-js/kimi-personality-utils.js"></script>
1091
  <script type="module" src="kimi-js/kimi-utils.js"></script>
1092
  <script type="module" src="kimi-js/kimi-main.js"></script>
kimi-css/kimi-style.css CHANGED
@@ -1756,7 +1756,7 @@ button:focus,
1756
  }
1757
 
1758
  .progress-container {
1759
- height: 12px;
1760
  }
1761
 
1762
  .mic-button {
 
1756
  }
1757
 
1758
  .progress-container {
1759
+ height: 10px;
1760
  }
1761
 
1762
  .mic-button {
kimi-js/kimi-constants.js CHANGED
@@ -111,21 +111,7 @@ window.KIMI_CONTEXT_KEYWORDS = {
111
  laughing: ["haha", "mdr", "rire", "drΓ΄le", "hilarant", "mort de rire", "ptdr", "rigole", "sourit", "tu plaisantes"],
112
  shy: ["timide", "gΓͺnΓ©", "rougir", "honteux", "intimidΓ©", "mal Γ  l’aise", "rΓ©servΓ©", "introverti", "timiditΓ©"],
113
  confident: ["confiance", "fier", "sΓ»r", "fort", "dΓ©terminΓ©", "assurΓ©", "audacieux", "leader", "sans peur", "affirmΓ©"],
114
- romantic: [
115
- "amour",
116
- "romantique",
117
- "tendre",
118
- "cΓ’lin",
119
- "bisou",
120
- "mon cΕ“ur",
121
- "chΓ©ri",
122
- "ma belle",
123
- "ma femme",
124
- "merveilleuse",
125
- "merveilleux",
126
- "passionnΓ©",
127
- "adorΓ©"
128
- ],
129
  flirtatious: [
130
  "flirt",
131
  "taquin",
@@ -227,19 +213,7 @@ window.KIMI_CONTEXT_KEYWORDS = {
227
 
228
  window.KIMI_CONTEXT_POSITIVE = {
229
  en: ["happy", "joy", "great", "awesome", "perfect", "excellent", "magnificent", "lovely", "nice"],
230
- fr: [
231
- "heureux",
232
- "joie",
233
- "gΓ©nial",
234
- "parfait",
235
- "excellent",
236
- "magnifique",
237
- "super",
238
- "chouette",
239
- "formidable",
240
- "merveilleuse",
241
- "merveilleux"
242
- ],
243
  es: ["feliz", "alegrΓ­a", "genial", "perfecto", "excelente", "magnΓ­fico", "estupendo", "maravilloso"],
244
  de: ["glΓΌcklich", "freude", "toll", "perfekt", "ausgezeichnet", "großartig", "wunderbar", "herrlich"],
245
  it: ["felice", "gioia", "fantastico", "perfetto", "eccellente", "magnifico", "meraviglioso", "ottimo"],
@@ -398,18 +372,6 @@ window.KIMI_PERSONALITY_KEYWORDS = {
398
  empathy: {
399
  positive: ["listen", "understand", "empathy", "support", "help", "comfort", "compassion", "caring", "kindness"],
400
  negative: ["indifferent", "cold", "selfish", "ignore", "despise", "hostile", "uncaring"]
401
- },
402
- trust: {
403
- positive: ["trust", "honest", "truth", "loyal", "loyalty", "reliable", "dependable", "safe"],
404
- negative: ["lie", "lying", "betray", "betrayal", "cheat", "cheating", "unfaithful", "dishonest"]
405
- },
406
- intimacy: {
407
- positive: ["intimate", "close", "deep", "vulnerable", "tender", "touch", "caress", "cuddle"],
408
- negative: ["distant", "cold", "closed", "blocked", "awkward", "uncomfortable"]
409
- },
410
- boundary: {
411
- positive: ["consent", "respect", "limit", "safe word", "ok with", "are you fine", "are you okay"],
412
- negative: ["force", "coerce", "push you", "ignore your no", "against your will"]
413
  }
414
  },
415
  fr: {
@@ -448,20 +410,7 @@ window.KIMI_PERSONALITY_KEYWORDS = {
448
  ]
449
  },
450
  romance: {
451
- positive: [
452
- "cΓ’lin",
453
- "amour",
454
- "romantique",
455
- "bisou",
456
- "tendresse",
457
- "passion",
458
- "sΓ©duisant",
459
- "charmant",
460
- "adorable",
461
- "merveilleuse",
462
- "merveilleux",
463
- "ma femme"
464
- ],
465
  negative: [
466
  "froid",
467
  "froide",
@@ -486,10 +435,7 @@ window.KIMI_PERSONALITY_KEYWORDS = {
486
  "cΓ’lin",
487
  "aimer",
488
  "adorer",
489
- "adorable",
490
- "merveilleuse",
491
- "merveilleux",
492
- "ma femme"
493
  ],
494
  negative: [
495
  "mΓ©chant",
@@ -507,14 +453,8 @@ window.KIMI_PERSONALITY_KEYWORDS = {
507
  "idiote",
508
  "stupide",
509
  "con",
510
- "conne",
511
  "connard",
512
- "connasse",
513
- "connasses",
514
- "salope",
515
- "pute",
516
- "putain",
517
- "poufiasse"
518
  ]
519
  },
520
  playfulness: {
@@ -545,18 +485,6 @@ window.KIMI_PERSONALITY_KEYWORDS = {
545
  "bienveillance"
546
  ],
547
  negative: ["indiffΓ©rent", "indiffΓ©rente", "froid", "froide", "Γ©goΓ―ste", "ignorer", "mΓ©priser", "dΓ©nigrer", "hostile"]
548
- },
549
- trust: {
550
- positive: ["confiance", "honnΓͺte", "fidΓ¨le", "fiable", "loyal", "sincΓ¨re", "sΓ©curitΓ©"],
551
- negative: ["mensonge", "menti", "trahi", "trahison", "tromper", "infidΓ¨le", "malhonnΓͺte"]
552
- },
553
- intimacy: {
554
- positive: ["intime", "proche", "profond", "vulnΓ©rable", "tendre", "toucher", "caresse", "cΓ’lin"],
555
- negative: ["distant", "froide", "fermΓ©", "bloquΓ©", "mal Γ  l'aise"]
556
- },
557
- boundary: {
558
- positive: ["consentement", "respect", "limite", "d'accord", "ok pour", "Γ§a te va"],
559
- negative: ["forcer", "te pousser", "ignorer ton non", "contre ta volontΓ©"]
560
  }
561
  },
562
  es: {
 
111
  laughing: ["haha", "mdr", "rire", "drΓ΄le", "hilarant", "mort de rire", "ptdr", "rigole", "sourit", "tu plaisantes"],
112
  shy: ["timide", "gΓͺnΓ©", "rougir", "honteux", "intimidΓ©", "mal Γ  l’aise", "rΓ©servΓ©", "introverti", "timiditΓ©"],
113
  confident: ["confiance", "fier", "sΓ»r", "fort", "dΓ©terminΓ©", "assurΓ©", "audacieux", "leader", "sans peur", "affirmΓ©"],
114
+ romantic: ["amour", "romantique", "tendre", "cΓ’lin", "bisou", "mon cΕ“ur", "chΓ©ri", "ma belle", "passionnΓ©", "adorΓ©"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  flirtatious: [
116
  "flirt",
117
  "taquin",
 
213
 
214
  window.KIMI_CONTEXT_POSITIVE = {
215
  en: ["happy", "joy", "great", "awesome", "perfect", "excellent", "magnificent", "lovely", "nice"],
216
+ fr: ["heureux", "joie", "gΓ©nial", "parfait", "excellent", "magnifique", "super", "chouette"],
 
 
 
 
 
 
 
 
 
 
 
 
217
  es: ["feliz", "alegrΓ­a", "genial", "perfecto", "excelente", "magnΓ­fico", "estupendo", "maravilloso"],
218
  de: ["glΓΌcklich", "freude", "toll", "perfekt", "ausgezeichnet", "großartig", "wunderbar", "herrlich"],
219
  it: ["felice", "gioia", "fantastico", "perfetto", "eccellente", "magnifico", "meraviglioso", "ottimo"],
 
372
  empathy: {
373
  positive: ["listen", "understand", "empathy", "support", "help", "comfort", "compassion", "caring", "kindness"],
374
  negative: ["indifferent", "cold", "selfish", "ignore", "despise", "hostile", "uncaring"]
 
 
 
 
 
 
 
 
 
 
 
 
375
  }
376
  },
377
  fr: {
 
410
  ]
411
  },
412
  romance: {
413
+ positive: ["cΓ’lin", "amour", "romantique", "bisou", "tendresse", "passion", "sΓ©duisant", "charmant", "adorable"],
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  negative: [
415
  "froid",
416
  "froide",
 
435
  "cΓ’lin",
436
  "aimer",
437
  "adorer",
438
+ "adorable"
 
 
 
439
  ],
440
  negative: [
441
  "mΓ©chant",
 
453
  "idiote",
454
  "stupide",
455
  "con",
 
456
  "connard",
457
+ "salope"
 
 
 
 
 
458
  ]
459
  },
460
  playfulness: {
 
485
  "bienveillance"
486
  ],
487
  negative: ["indiffΓ©rent", "indiffΓ©rente", "froid", "froide", "Γ©goΓ―ste", "ignorer", "mΓ©priser", "dΓ©nigrer", "hostile"]
 
 
 
 
 
 
 
 
 
 
 
 
488
  }
489
  },
490
  es: {
kimi-js/kimi-emotion-system.js CHANGED
@@ -9,20 +9,18 @@ class KimiEmotionSystem {
9
  * - Each delta passes through adjustUp / adjustDown with global + per-trait multipliers
10
  * (window.KIMI_TRAIT_ADJUSTMENT) for consistent scaling.
11
  * 2. Content keyword analysis (_analyzeTextContent) may override interim trait values (explicit matches).
12
- * 3. Cross-trait modifiers (_applyCrossTraitModifiers) apply ALL synergy / balancing rules (single location to avoid double application).
13
  * 4. Conversation-based drift (updatePersonalityFromConversation) uses TRAIT_KEYWORD_MODEL:
14
  * - Counts positive/negative keyword hits (user weighted 1.0, model weighted 0.5).
15
  * - Computes rawDelta = posHits*posFactor - negHits*negFactor.
16
  * - Applies sustained negativity amplification after streakPenaltyAfter.
17
  * - Clamps magnitude to maxStep per trait, then applies directly with bounds [0,100].
18
  * 5. Persistence: _preparePersistTrait decides threshold & smoothing before batch write.
19
- * 6. Global personality average (UI) = mean of six core traits. This class is the single source of truth; external helpers now delegate.
20
  * NOTE: Affection is fully independent (no derived average). All adjustments centralized here to avoid duplication.
21
  */
22
  this.db = database;
23
  this.negativeStreaks = {};
24
- // Accumulated micro-changes not yet persisted (trait -> float)
25
- this._pendingDrift = {};
26
 
27
  // Unified emotion mappings
28
  this.EMOTIONS = {
@@ -68,29 +66,25 @@ class KimiEmotionSystem {
68
  intelligence: 70, // Competent baseline intellect
69
  empathy: 75, // Warm & caring baseline
70
  humor: 60, // Mild sense of humor baseline
71
- romance: 50, // Neutral romance baseline (earned over time)
72
- trust: 50, // Trust starts neutral
73
- intimacy: 45 // Intimacy builds slower than trust/romance
74
  };
75
 
76
  // Central emotion -> trait base deltas (pre global multipliers & gainCfg scaling)
77
  // Positive numbers increase trait, negative decrease.
78
  // Keep values small; final effect passes through adjustUp/adjustDown and global multipliers.
79
- // Rebalanced: keep relative ordering but narrow spread to avoid runaway traits.
80
- // Target typical per-event magnitude range ~0.1 - 0.5.
81
  this.EMOTION_TRAIT_EFFECTS = {
82
- positive: { affection: 0.35, empathy: 0.18, playfulness: 0.2, humor: 0.22 },
83
- negative: { affection: -0.55, empathy: 0.22 },
84
- romantic: { romance: 0.55, affection: 0.45, empathy: 0.14 },
85
- flirtatious: { romance: 0.45, playfulness: 0.38, affection: 0.2 },
86
- laughing: { humor: 0.6, playfulness: 0.4, affection: 0.2 },
87
- dancing: { playfulness: 0.55, affection: 0.35 },
88
- surprise: { intelligence: 0.1, empathy: 0.1 },
89
- shy: { romance: -0.25, affection: -0.1 },
90
- confident: { intelligence: 0.13, affection: 0.45 },
91
- listening: { empathy: 0.45, intelligence: 0.2 },
92
- kiss: { romance: 0.65, affection: 0.55 },
93
- goodbye: { affection: -0.12, empathy: 0.08 }
94
  };
95
 
96
  // Trait keyword scaling model for conversation analysis (per-message delta shaping)
@@ -100,40 +94,8 @@ class KimiEmotionSystem {
100
  empathy: { posFactor: 0.4, negFactor: 0.5, streakPenaltyAfter: 3, maxStep: 1.5 },
101
  playfulness: { posFactor: 0.45, negFactor: 0.4, streakPenaltyAfter: 4, maxStep: 1.4 },
102
  humor: { posFactor: 0.55, negFactor: 0.45, streakPenaltyAfter: 4, maxStep: 1.6 },
103
- intelligence: { posFactor: 0.35, negFactor: 0.55, streakPenaltyAfter: 2, maxStep: 1.2 },
104
- trust: { posFactor: 0.45, negFactor: 0.9, streakPenaltyAfter: 2, maxStep: 1.2 },
105
- intimacy: { posFactor: 0.35, negFactor: 0.6, streakPenaltyAfter: 2, maxStep: 1.0 }
106
  };
107
-
108
- // Ephemeral relational warmth (short-term amplifier damped over time)
109
- this._warmth = 0; // range suggestion: -50..+50 (internally clamped)
110
- this._lastWarmthDecay = Date.now();
111
- this.WARMTH_CFG = Object.assign(
112
- {
113
- decayPerMinute: 2.5, // linear decay toward 0
114
- maxAbs: 50,
115
- affectionAmplifierAtMax: 0.25, // +25% affection delta at max warmth
116
- romanceAmplifierAtMax: 0.2,
117
- trustAmplifierAtMax: 0.22,
118
- negativeMultiplier: 1.2 // negative warmth increases penalty magnitude slightly
119
- },
120
- window.KIMI_WARMTH_CONFIG || {}
121
- );
122
-
123
- // Relationship stage thresholds (can be overridden by window.KIMI_RELATIONSHIP_THRESHOLDS)
124
- // Stages reflect progression: acquaintance -> friend -> close_friend -> romantic -> intimate -> deep_bond
125
- this.RELATIONSHIP_STAGE_THRESHOLDS = Object.assign(
126
- {
127
- acquaintance: { minAffection: 0, minRomance: 0 },
128
- friend: { minAffection: 40, minRomance: 0 },
129
- close_friend: { minAffection: 60, minRomance: 10 },
130
- romantic: { minAffection: 70, minRomance: 35 },
131
- intimate: { minAffection: 82, minRomance: 55 },
132
- deep_bond: { minAffection: 92, minRomance: 75 }
133
- },
134
- window.KIMI_RELATIONSHIP_THRESHOLDS || {}
135
- );
136
- this._currentRelationshipStage = "acquaintance";
137
  }
138
  // (Affection is an independent trait again; previous derived computation removed.)
139
  // ===== UNIFIED EMOTION ANALYSIS =====
@@ -194,15 +156,6 @@ class KimiEmotionSystem {
194
  negative: 1
195
  };
196
 
197
- // Relationship-aware sensitivity adjustments (non-destructive copy)
198
- let stage = this._currentRelationshipStage || "acquaintance";
199
- const relBoost = { acquaintance: 0, friend: 0.05, close_friend: 0.1, romantic: 0.18, intimate: 0.25, deep_bond: 0.3 };
200
- const mult = 1 + (relBoost[stage] || 0);
201
- const stageSensitivity = { ...sensitivity };
202
- stageSensitivity.romantic *= mult;
203
- stageSensitivity.flirtatious *= mult;
204
- stageSensitivity.kiss *= 1 + (relBoost[stage] || 0) * 1.2;
205
-
206
  // Normalize keyword lists to handle accents/contractions
207
  const normalizeList = arr => (Array.isArray(arr) ? arr.map(x => this.normalizeText(String(x))).filter(Boolean) : []);
208
  const normalizedPositiveWords = normalizeList(positiveWords);
@@ -218,7 +171,7 @@ class KimiEmotionSystem {
218
  const hits = check.keywords.reduce((acc, word) => acc + (this.countTokenMatches(lowerText, String(word)) ? 1 : 0), 0);
219
  if (hits > 0) {
220
  const key = check.emotion;
221
- const weight = stageSensitivity[key] != null ? stageSensitivity[key] : 1;
222
  const score = hits * weight;
223
  if (score > bestScore) {
224
  bestScore = score;
@@ -268,32 +221,24 @@ class KimiEmotionSystem {
268
  let playfulness = safe(traits?.playfulness, this.TRAIT_DEFAULTS.playfulness);
269
  let humor = safe(traits?.humor, this.TRAIT_DEFAULTS.humor);
270
  let intelligence = safe(traits?.intelligence, this.TRAIT_DEFAULTS.intelligence);
271
- let trust = safe(traits?.trust, this.TRAIT_DEFAULTS.trust);
272
- let intimacy = safe(traits?.intimacy, this.TRAIT_DEFAULTS.intimacy);
273
-
274
- // Unified adjustment functions (parametric): soft diminishing returns / protection low end.
275
- // Tunable via window.KIMI_ADJUST_TUNING = { upExponent, downExponent, minLossFactor, maxLossFactor, minGainFactor }
276
- const tuning = window.KIMI_ADJUST_TUNING || {};
277
- const upExp = typeof tuning.upExponent === "number" ? tuning.upExponent : 1.2; // >1 speeds early gains, slows late
278
- const downExp = typeof tuning.downExponent === "number" ? tuning.downExponent : 1.1; // >1 accelerates high losses
279
- const minGainFactor = typeof tuning.minGainFactor === "number" ? tuning.minGainFactor : 0.25; // floor near 100
280
- const minLossFactor = typeof tuning.minLossFactor === "number" ? tuning.minLossFactor : 0.35; // floor near 0
281
- const maxLossFactor = typeof tuning.maxLossFactor === "number" ? tuning.maxLossFactor : 1.15; // cap high-end loss accel
282
 
 
283
  const adjustUp = (val, amount) => {
284
- // Factor scales with remaining headroom (distance to 100)
285
- const headroom = Math.max(0, 100 - val) / 100; // 1 at 0, 0 at 100
286
- const factor = Math.max(minGainFactor, Math.pow(headroom, upExp));
287
- return val + amount * factor;
 
 
288
  };
 
289
  const adjustDown = (val, amount) => {
290
- // Loss factor scales with current level (higher -> larger)
291
- const level = Math.max(0, Math.min(100, val)) / 100; // 0..1
292
- // curve amplifies as level increases
293
- let factor = Math.pow(level, downExp) * maxLossFactor;
294
- // Provide protection at very low end
295
- if (level < 0.2) factor = Math.min(factor, minLossFactor);
296
- return val - amount * factor;
297
  };
298
 
299
  // Unified emotion-based adjustments - More balanced and realistic progression
@@ -318,131 +263,24 @@ class KimiEmotionSystem {
318
  return baseDelta * GLOSS * t;
319
  };
320
 
321
- // Lightweight per-call token cache (avoids repeated normalization/tokenization)
322
- const _tokenCache = new Map();
323
- const getTokenCount = phrase => {
324
- if (!phrase) return 0;
325
- const key = String(phrase);
326
- if (_tokenCache.has(key)) return _tokenCache.get(key);
327
- const c = this.tokenizeText(this.normalizeText(key)).length;
328
- _tokenCache.set(key, c);
329
- return c;
330
- };
331
-
332
- // Warmth decay (linear drift toward 0)
333
- const nowTs = Date.now();
334
- if (this._warmth !== 0) {
335
- const mins = (nowTs - this._lastWarmthDecay) / 60000;
336
- if (mins > 0.05) {
337
- const decayAmt = this.WARMTH_CFG.decayPerMinute * mins;
338
- if (this._warmth > 0) this._warmth = Math.max(0, this._warmth - decayAmt);
339
- else this._warmth = Math.min(0, this._warmth + decayAmt);
340
- this._lastWarmthDecay = nowTs;
341
- }
342
- }
343
-
344
- // Derive a simple intensity factor from message length & punctuation emphasis
345
- const wordCount = getTokenCount(text || "");
346
- let intensity = 1;
347
- if (wordCount >= 8 && wordCount < 25) intensity = 1.05;
348
- else if (wordCount >= 25 && wordCount < 60) intensity = 1.12;
349
- else if (wordCount >= 60) intensity = 1.18;
350
- // Emphasis markers (!, ❀️, ???) add a small boost
351
- const emphasisMatches = (text && text.match(/[!?!]{2,}|❀️|πŸ’–|😍/g)) || [];
352
- if (emphasisMatches.length > 0) intensity += Math.min(0.12, 0.04 * emphasisMatches.length);
353
-
354
- // ===== Contextual affectionate profanity & chaotic lexicon handling =====
355
- const lower = (text || "").toLowerCase();
356
- // Compliment anti-spam (exact token based) with exponential damping
357
- this._complimentHistory = this._complimentHistory || [];
358
- const nowMs = Date.now();
359
- // Keep only last 60s entries
360
- this._complimentHistory = this._complimentHistory.filter(t => nowMs - t < 60000);
361
- const complimentTokens = ["merveilleuse", "merveilleux", "magnifique", "adorable", "charmant", "formidable"];
362
- const messageTokens = this.tokenizeText(lower);
363
- const complimentHits = messageTokens.filter(tok => complimentTokens.includes(tok)).length;
364
- if (complimentHits > 0) {
365
- for (let i = 0; i < complimentHits; i++) this._complimentHistory.push(nowMs);
366
- }
367
- const complimentDensity = this._complimentHistory.length; // raw count last 60s
368
- // Exponential damping factor: each additional compliment reduces gains multiplicatively
369
- // baseFactor^ (density-1), clamped
370
- const baseFactor = 0.88; // 12% reduction per extra compliment
371
- let complimentDampFactor = 1;
372
- if (complimentDensity > 1) {
373
- complimentDampFactor = Math.pow(baseFactor, complimentDensity - 1);
374
- }
375
- complimentDampFactor = Math.max(0.3, Math.min(1, complimentDampFactor));
376
- const lovePatterns = [
377
- /je t(?:'|e) ?aime/,
378
- /i love you/,
379
- /ti amo/,
380
- /te quiero/,
381
- /te amo/,
382
- /ich liebe dich/,
383
- /愛してる/,
384
- /ζˆ‘ηˆ±δ½ /,
385
- /ti voglio bene/
386
- ];
387
- const softProfanity = /(putain|fuck|fucking|merde|shit|bordel)/;
388
- const positiveAdj = /(adorable|magnifique|formidable|belle|bello|hermos[ao]|beautiful|amazing|wonderful|gorgeous)/;
389
- const chaoticWords = /(chaos|chaotique|rebelle|rebel|wild|sauvage)/;
390
- const conjugalTerms = /(ma femme|mon mari)/;
391
-
392
- let affectionateProfane = false;
393
- if (lovePatterns.some(r => r.test(lower)) && softProfanity.test(lower) && positiveAdj.test(lower)) {
394
- affectionateProfane = true;
395
- }
396
- const containsChaos = chaoticWords.test(lower);
397
- const containsConjugal = conjugalTerms.test(lower);
398
-
399
- // If affectionate profanity detected while emotion not negative, gently bias toward romantic
400
- if (affectionateProfane && emotion && emotion !== this.EMOTIONS.NEGATIVE) {
401
- // Micro pre-boost before base map (acts like extra intensity)
402
- intensity *= 1.04;
403
- // Optionally upgrade neutral/positive to romantic
404
- if (emotion === this.EMOTIONS.POSITIVE || emotion === this.EMOTIONS.NEUTRAL) {
405
- emotion = this.EMOTIONS.ROMANTIC;
406
- }
407
- }
408
-
409
- // Conjugal gating: if conjugal term appears but relationship stage < romantic, reduce romantic sensitivity
410
- if (containsConjugal && emotion === this.EMOTIONS.ROMANTIC) {
411
- const stageOrder = ["acquaintance", "friend", "close_friend", "romantic", "intimate", "deep_bond"];
412
- const currentIdx = stageOrder.indexOf(this._currentRelationshipStage || "acquaintance");
413
- if (currentIdx >= 0 && currentIdx < stageOrder.indexOf("romantic")) {
414
- intensity *= 0.75; // soften premature strong romantic signal
415
- }
416
- }
417
-
418
  // Apply emotion deltas from centralized map (if defined)
419
  const map = this.EMOTION_TRAIT_EFFECTS?.[emotion];
420
- const cfg = window.KIMI_EMOTION_CONFIG || null;
421
- const traitScalar = cfg?.traitScalar || {};
422
- const emotionScalar = cfg?.emotionScalar || {};
423
- const emoScale = emotionScalar[emotion] || 1;
424
  if (map) {
425
  for (const [traitName, baseDelta] of Object.entries(map)) {
426
- let delta = baseDelta * emoScale * intensity; // apply emotion & intensity
427
- const perTraitScale = traitScalar[traitName];
428
- if (typeof perTraitScale === "number") delta *= perTraitScale;
429
  if (delta === 0) continue;
430
  switch (traitName) {
431
  case "affection":
432
- let adjAffDelta = delta;
433
- if (delta > 0) adjAffDelta *= complimentDampFactor;
434
  affection =
435
- adjAffDelta > 0
436
- ? Math.min(100, adjustUp(affection, scaleGain("affection", adjAffDelta)))
437
- : Math.max(0, adjustDown(affection, scaleLoss("affection", Math.abs(adjAffDelta))));
438
  break;
439
  case "romance":
440
- let adjRomDelta = delta;
441
- if (delta > 0) adjRomDelta *= complimentDampFactor;
442
  romance =
443
- adjRomDelta > 0
444
- ? Math.min(100, adjustUp(romance, scaleGain("romance", adjRomDelta)))
445
- : Math.max(0, adjustDown(romance, scaleLoss("romance", Math.abs(adjRomDelta))));
446
  break;
447
  case "empathy":
448
  empathy =
@@ -468,37 +306,28 @@ class KimiEmotionSystem {
468
  ? Math.min(100, adjustUp(intelligence, scaleGain("intelligence", delta)))
469
  : Math.max(0, adjustDown(intelligence, scaleLoss("intelligence", Math.abs(delta))));
470
  break;
471
- case "trust":
472
- trust =
473
- delta > 0
474
- ? Math.min(100, adjustUp(trust, scaleGain("trust", delta * 0.6)))
475
- : Math.max(0, adjustDown(trust, scaleLoss("trust", Math.abs(delta) * 0.9)));
476
- break;
477
- case "intimacy":
478
- intimacy =
479
- delta > 0
480
- ? Math.min(100, adjustUp(intimacy, scaleGain("intimacy", delta * 0.5)))
481
- : Math.max(0, adjustDown(intimacy, scaleLoss("intimacy", Math.abs(delta) * 0.85)));
482
- break;
483
  }
484
  }
485
  }
486
 
487
- // Micro direct trust/intimacy boost for respectful conjugal reference (stage-aware, only positive tone)
488
- if (containsConjugal && emotion === this.EMOTIONS.ROMANTIC) {
489
- const stageOrder = ["acquaintance", "friend", "close_friend", "romantic", "intimate", "deep_bond"];
490
- const currentIdx = stageOrder.indexOf(this._currentRelationshipStage || "acquaintance");
491
- const romanticIdx = stageOrder.indexOf("romantic");
492
- let scale = 0.18; // base micro boost
493
- if (currentIdx < romanticIdx) scale *= 0.5; // earlier stages smaller
494
- // Compliment spam damping
495
- scale *= complimentDampFactor;
496
- trust = Math.min(100, adjustUp(trust, scaleGain("affection", scale * 0.8)));
497
- intimacy = Math.min(100, adjustUp(intimacy, scaleGain("romance", scale * 0.6)));
498
  }
499
 
500
- // Cross-trait interactions removed here (now centralized exclusively in _applyCrossTraitModifiers)
501
- // to avoid double application of synergy boosts.
 
 
 
 
 
 
 
 
 
 
502
 
503
  // Content-based adjustments (unified)
504
  await this._analyzeTextContent(
@@ -512,71 +341,6 @@ class KimiEmotionSystem {
512
  adjustUp
513
  );
514
 
515
- // Micro contextual boosts (post content analysis, pre synergy)
516
- if (affectionateProfane) {
517
- // Treat as emphatic endearment: small extra romance & affection
518
- affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.25 * intensity)));
519
- romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.22 * intensity)));
520
- // Warmth gain
521
- this._warmth = Math.max(-this.WARMTH_CFG.maxAbs, Math.min(this.WARMTH_CFG.maxAbs, this._warmth + 8 * intensity));
522
- // Trust/intimacy micro boost if not spamming (use last delta heuristic: only if romance<90)
523
- if (romance < 90) {
524
- trust = Math.min(100, adjustUp(trust, scaleGain("trust", 0.12 * intensity)));
525
- intimacy = Math.min(100, adjustUp(intimacy, scaleGain("intimacy", 0.1 * intensity)));
526
- }
527
- }
528
- if (containsChaos) {
529
- playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.18 * intensity)));
530
- // Slight warmth nudge (a playful chaotic vibe)
531
- this._warmth = Math.max(-this.WARMTH_CFG.maxAbs, Math.min(this.WARMTH_CFG.maxAbs, this._warmth + 3 * intensity));
532
- }
533
- // Additional warmth gain for strong romantic emotion without profanity pattern
534
- if (!affectionateProfane && emotion === this.EMOTIONS.ROMANTIC) {
535
- const romanticPulse = 4 * intensity;
536
- this._warmth = Math.max(-this.WARMTH_CFG.maxAbs, Math.min(this.WARMTH_CFG.maxAbs, this._warmth + romanticPulse));
537
- }
538
-
539
- // Relationship affirmation memory (deduplicate recent)
540
- if (
541
- (affectionateProfane || (emotion === this.EMOTIONS.ROMANTIC && lovePatterns.some(r => r.test(lower)))) &&
542
- this.db?.db?.memories
543
- ) {
544
- try {
545
- const cutoff = Date.now() - 1000 * 60 * 60 * 6; // 6h
546
- const recent = await this.db.db.memories
547
- .where("category")
548
- .equals("relationships")
549
- .and(m => (m.tags || []).includes("relationship:affirmation") && new Date(m.timestamp).getTime() > cutoff)
550
- .limit(1)
551
- .toArray();
552
- if (!recent || recent.length === 0) {
553
- const content = affectionateProfane
554
- ? "Intense affectionate profanity declaration"
555
- : "Romantic love affirmation";
556
- this.db.db.memories
557
- .add({
558
- category: "relationships",
559
- type: "affirmation",
560
- content,
561
- importance: 0.9,
562
- timestamp: new Date(),
563
- character: selectedCharacter,
564
- isActive: true,
565
- tags: ["relationship:affirmation", "relationship:love"],
566
- lastModified: new Date(),
567
- createdAt: new Date(),
568
- lastAccess: new Date(),
569
- accessCount: 0
570
- })
571
- .then(id => {
572
- if (window.kimiEventBus) window.kimiEventBus.emit("memory:stored", { memory: { id, content } });
573
- });
574
- }
575
- } catch (e) {
576
- /* silent */
577
- }
578
- }
579
-
580
  // Cross-trait modifiers (applied after primary emotion & content changes)
581
  ({ affection, romance, empathy, playfulness, humor, intelligence } = this._applyCrossTraitModifiers({
582
  affection,
@@ -594,98 +358,24 @@ class KimiEmotionSystem {
594
  // Preserve fractional progress to allow gradual visible changes
595
  const to2 = v => Number(Number(v).toFixed(2));
596
  const clamp = v => Math.max(0, Math.min(100, v));
597
- // Warmth amplification post base deltas (affects core relational traits)
598
- if (this._warmth !== 0) {
599
- const ampRatio = Math.min(1, Math.abs(this._warmth) / this.WARMTH_CFG.maxAbs);
600
- const sign = this._warmth >= 0 ? 1 : -this.WARMTH_CFG.negativeMultiplier;
601
- const amplify = (val, base, scale) => clamp(val + sign * (val - base) * scale * ampRatio);
602
- affection = amplify(affection, this.TRAIT_DEFAULTS.affection, this.WARMTH_CFG.affectionAmplifierAtMax);
603
- romance = amplify(romance, this.TRAIT_DEFAULTS.romance, this.WARMTH_CFG.romanceAmplifierAtMax);
604
- trust = amplify(trust, this.TRAIT_DEFAULTS.trust, this.WARMTH_CFG.trustAmplifierAtMax);
605
- intimacy = amplify(intimacy, this.TRAIT_DEFAULTS.intimacy, this.WARMTH_CFG.romanceAmplifierAtMax * 0.8);
606
- }
607
-
608
- let updatedTraits = {
609
  affection: to2(clamp(affection)),
610
  romance: to2(clamp(romance)),
611
  empathy: to2(clamp(empathy)),
612
  playfulness: to2(clamp(playfulness)),
613
  humor: to2(clamp(humor)),
614
- intelligence: to2(clamp(intelligence)),
615
- trust: to2(clamp(trust)),
616
- intimacy: to2(clamp(intimacy))
617
  };
618
 
619
- // Damping: limit per-message total movement across sensitive relational traits
620
- const DAMP_CFG = Object.assign(
621
- {
622
- maxTotalDelta: 6, // sum of |delta| capped
623
- focus: ["affection", "romance", "trust", "intimacy"],
624
- softThreshold: 3.5 // start proportionally scaling beyond this
625
- },
626
- window.KIMI_DAMPING_CONFIG || {}
627
- );
628
- let total = 0;
629
- const deltas = {};
630
- for (const key of DAMP_CFG.focus) {
631
- const prev = typeof traits?.[key] === "number" ? traits[key] : this.TRAIT_DEFAULTS[key];
632
- const after = updatedTraits[key];
633
- const delta = after - prev;
634
- deltas[key] = delta;
635
- total += Math.abs(delta);
636
- }
637
- if (total > DAMP_CFG.softThreshold) {
638
- const scale = total > DAMP_CFG.maxTotalDelta ? DAMP_CFG.maxTotalDelta / total : DAMP_CFG.softThreshold / total;
639
- if (scale < 1) {
640
- for (const key of DAMP_CFG.focus) {
641
- const prev = typeof traits?.[key] === "number" ? traits[key] : this.TRAIT_DEFAULTS[key];
642
- updatedTraits[key] = to2(clamp(prev + deltas[key] * scale));
643
- }
644
- }
645
- }
646
-
647
- if (cfg && typeof cfg.finalize === "function") {
648
- try {
649
- const fin = cfg.finalize({ ...updatedTraits });
650
- if (fin && typeof fin === "object") updatedTraits = { ...updatedTraits, ...fin };
651
- } catch (e) {
652
- console.warn("Finalize hook error", e);
653
- }
654
- }
655
-
656
- // Emit event before persistence for observers/plugins
657
- if (window.kimiEventBus) {
658
- window.kimiEventBus.emit("traits:computed", { emotion, text, character: selectedCharacter, updatedTraits });
659
- window.kimiEventBus.emit("relationship:trustChanged", { trust: updatedTraits.trust, character: selectedCharacter });
660
- window.kimiEventBus.emit("relationship:intimacyChanged", {
661
- intimacy: updatedTraits.intimacy,
662
- character: selectedCharacter
663
- });
664
- window.kimiEventBus.emit("relationship:warmthChanged", { warmth: this._warmth, character: selectedCharacter });
665
- }
666
-
667
- // Update relationship stage based on new traits (affection & romance)
668
- try {
669
- this._updateRelationshipStage(updatedTraits, selectedCharacter);
670
- } catch (e) {
671
- /* non-blocking */
672
- }
673
-
674
  // Prepare persistence with smoothing / threshold to avoid tiny writes
675
  const toPersist = {};
676
  for (const [trait, candValue] of Object.entries(updatedTraits)) {
677
  const current = typeof traits?.[trait] === "number" ? traits[trait] : this.TRAIT_DEFAULTS[trait];
678
  const prep = this._preparePersistTrait(trait, current, candValue, selectedCharacter);
679
- if (prep.shouldPersist) {
680
- toPersist[trait] = prep.value;
681
- this._pendingDrift[trait] = 0; // reset drift
682
- }
683
  }
684
  if (Object.keys(toPersist).length > 0) {
685
- if (window.kimiEventBus) window.kimiEventBus.emit("traits:willPersist", { character: selectedCharacter, toPersist });
686
  await this.db.setPersonalityBatch(toPersist, selectedCharacter);
687
- if (window.kimiEventBus)
688
- window.kimiEventBus.emit("traits:didPersist", { character: selectedCharacter, persisted: toPersist });
689
  }
690
 
691
  return updatedTraits;
@@ -742,22 +432,7 @@ class KimiEmotionSystem {
742
  };
743
 
744
  const pendingUpdates = {};
745
- // Basic message intensity heuristic (long message -> slightly higher impact)
746
- const tokenCount = this.tokenizeText(lowerUser).length;
747
- const intensityFactor = tokenCount <= 4 ? 0.7 : tokenCount <= 12 ? 1 : tokenCount <= 30 ? 1.1 : 1.2;
748
- const MAX_HITS_PER_WORD = 5; // cap repetition farming
749
-
750
- for (const trait of [
751
- "humor",
752
- "intelligence",
753
- "romance",
754
- "affection",
755
- "playfulness",
756
- "empathy",
757
- "trust",
758
- "intimacy",
759
- "boundary"
760
- ]) {
761
  const posWords = getPersonalityWords(trait, "positive");
762
  const negWords = getPersonalityWords(trait, "negative");
763
  let currentVal =
@@ -771,21 +446,15 @@ class KimiEmotionSystem {
771
  let posScore = 0;
772
  let negScore = 0;
773
  for (const w of posWords) {
774
- const uHits = Math.min(MAX_HITS_PER_WORD, this.countTokenMatches(lowerUser, String(w)));
775
- const kHits = Math.min(MAX_HITS_PER_WORD, this.countTokenMatches(lowerKimi, String(w)));
776
- // sqrt dampening avoids farming same word
777
- posScore += Math.sqrt(uHits) * 1.0 + Math.sqrt(kHits) * 0.5;
778
  }
779
  for (const w of negWords) {
780
- const uHits = Math.min(MAX_HITS_PER_WORD, this.countTokenMatches(lowerUser, String(w)));
781
- const kHits = Math.min(MAX_HITS_PER_WORD, this.countTokenMatches(lowerKimi, String(w)));
782
- negScore += Math.sqrt(uHits) * 1.0 + Math.sqrt(kHits) * 0.5;
783
  }
784
 
785
  let rawDelta = posScore * posFactor - negScore * negFactor;
786
- const isBoundary = trait === "boundary";
787
- // Apply message intensity scaling (kept modest)
788
- rawDelta *= intensityFactor;
789
 
790
  // Track negative streaks per trait (only when net negative & no positives)
791
  if (!this.negativeStreaks[trait]) this.negativeStreaks[trait] = 0;
@@ -805,27 +474,12 @@ class KimiEmotionSystem {
805
 
806
  if (rawDelta !== 0) {
807
  let newVal = currentVal + rawDelta;
808
- if (rawDelta > 0) newVal = Math.min(100, newVal);
809
- else newVal = Math.max(0, newVal);
810
- pendingUpdates[trait] = newVal;
811
-
812
- if (isBoundary) {
813
- // Propagate boundary delta to trust/empathy/intimacy with scaled mapping
814
- const bDelta = rawDelta;
815
- if (bDelta > 0.05) {
816
- const trustBase = pendingUpdates.trust ?? traits.trust ?? this.TRAIT_DEFAULTS.trust;
817
- const empathyBase = pendingUpdates.empathy ?? traits.empathy ?? this.TRAIT_DEFAULTS.empathy;
818
- const intimacyBase = pendingUpdates.intimacy ?? traits.intimacy ?? this.TRAIT_DEFAULTS.intimacy;
819
- pendingUpdates.trust = Math.min(100, trustBase + bDelta * 0.6);
820
- pendingUpdates.empathy = Math.min(100, empathyBase + bDelta * 0.35);
821
- pendingUpdates.intimacy = Math.min(100, intimacyBase + bDelta * 0.25);
822
- } else if (bDelta < -0.05) {
823
- const trustBase = pendingUpdates.trust ?? traits.trust ?? this.TRAIT_DEFAULTS.trust;
824
- const intimacyBase = pendingUpdates.intimacy ?? traits.intimacy ?? this.TRAIT_DEFAULTS.intimacy;
825
- pendingUpdates.trust = Math.max(0, trustBase + bDelta * 0.7); // bDelta negative
826
- pendingUpdates.intimacy = Math.max(0, intimacyBase + bDelta * 0.5);
827
- }
828
  }
 
829
  }
830
  }
831
 
@@ -838,15 +492,10 @@ class KimiEmotionSystem {
838
  for (const [trait, candValue] of Object.entries(pendingUpdates)) {
839
  const current = typeof traits?.[trait] === "number" ? traits[trait] : this.TRAIT_DEFAULTS[trait];
840
  const prep = this._preparePersistTrait(trait, current, candValue, character);
841
- if (prep.shouldPersist) {
842
- toPersist[trait] = prep.value;
843
- this._pendingDrift[trait] = 0;
844
- }
845
  }
846
  if (Object.keys(toPersist).length > 0) {
847
- if (window.kimiEventBus) window.kimiEventBus.emit("traits:willPersist", { character, toPersist });
848
  await this.db.setPersonalityBatch(toPersist, character);
849
- if (window.kimiEventBus) window.kimiEventBus.emit("traits:didPersist", { character, persisted: toPersist });
850
  }
851
  }
852
  }
@@ -1007,22 +656,16 @@ class KimiEmotionSystem {
1007
 
1008
  // Decide whether to persist based on absolute change threshold. Returns {shouldPersist, value}
1009
  _preparePersistTrait(trait, currentValue, candidateValue, character = null) {
1010
- // Adaptive threshold with drift accumulation.
1011
  const alpha = (window.KIMI_SMOOTHING_ALPHA && Number(window.KIMI_SMOOTHING_ALPHA)) || 0.3;
1012
- const baseThreshold = (window.KIMI_PERSIST_THRESHOLD && Number(window.KIMI_PERSIST_THRESHOLD)) || 0.15; // lowered from 0.25
1013
- // Initialize drift bucket
1014
- if (typeof this._pendingDrift[trait] !== "number") this._pendingDrift[trait] = 0;
1015
 
1016
  const smoothed = this._applyEMA(currentValue, candidateValue, alpha);
1017
- const delta = smoothed - currentValue;
1018
- this._pendingDrift[trait] += delta;
1019
- const absAccum = Math.abs(this._pendingDrift[trait]);
1020
-
1021
- if (absAccum < baseThreshold) {
1022
  return { shouldPersist: false, value: currentValue };
1023
  }
1024
- const newValue = Number(Number(currentValue + this._pendingDrift[trait]).toFixed(2));
1025
- return { shouldPersist: true, value: newValue };
1026
  }
1027
 
1028
  // ===== UTILITY METHODS =====
@@ -1112,78 +755,12 @@ class KimiEmotionSystem {
1112
 
1113
  getMoodCategoryFromPersonality(traits) {
1114
  const avg = this.calculatePersonalityAverage(traits);
1115
- const cfg = window.KIMI_EMOTION_CONFIG && window.KIMI_EMOTION_CONFIG.moodThresholds;
1116
- // Default thresholds
1117
- const pos = cfg?.positive ?? 80;
1118
- const neutralHigh = cfg?.neutralHigh ?? 55;
1119
- const neutralLow = cfg?.neutralLow ?? 35;
1120
- const neg = cfg?.negative ?? 15;
1121
- if (avg >= pos) return "speakingPositive";
1122
- if (avg >= neutralHigh) return "neutral";
1123
- if (avg >= neutralLow) return "neutral";
1124
- if (avg >= neg) return "speakingNegative";
1125
- return "speakingNegative";
1126
- }
1127
-
1128
- getRelationshipStage(affection, romance) {
1129
- const stages = ["deep_bond", "intimate", "romantic", "close_friend", "friend", "acquaintance"]; // check highest first
1130
- for (const stage of stages) {
1131
- const t = this.RELATIONSHIP_STAGE_THRESHOLDS[stage];
1132
- if (!t) continue;
1133
- if (affection >= t.minAffection && romance >= t.minRomance) return stage;
1134
- }
1135
- return "acquaintance";
1136
- }
1137
 
1138
- _updateRelationshipStage(traits, character) {
1139
- const affection = traits.affection ?? this.TRAIT_DEFAULTS.affection;
1140
- const romance = traits.romance ?? this.TRAIT_DEFAULTS.romance;
1141
- const prev = this._currentRelationshipStage;
1142
- const next = this.getRelationshipStage(affection, romance);
1143
- if (next !== prev) {
1144
- this._currentRelationshipStage = next;
1145
- if (window.kimiEventBus) {
1146
- try {
1147
- window.kimiEventBus.emit("relationship:stageChanged", {
1148
- previous: prev,
1149
- current: next,
1150
- traits: { affection, romance },
1151
- character
1152
- });
1153
- } catch (e) {}
1154
- }
1155
- // Optionally add a memory note (only upward transitions)
1156
- if (typeof this.db?.db?.memories !== "undefined" && prev !== next) {
1157
- const upwardOrder = ["acquaintance", "friend", "close_friend", "romantic", "intimate", "deep_bond"];
1158
- if (upwardOrder.indexOf(next) > upwardOrder.indexOf(prev)) {
1159
- try {
1160
- this.db.db.memories
1161
- .add({
1162
- category: "relationships",
1163
- type: "system_stage",
1164
- content: `Relationship stage advanced to ${next}`,
1165
- importance: 0.85,
1166
- timestamp: new Date(),
1167
- character: character,
1168
- isActive: true,
1169
- tags: ["relationship:stage", `relationship:stage_${next}`],
1170
- lastModified: new Date(),
1171
- createdAt: new Date(),
1172
- lastAccess: new Date(),
1173
- accessCount: 0
1174
- })
1175
- .then(id => {
1176
- if (window.kimiEventBus)
1177
- try {
1178
- window.kimiEventBus.emit("memory:stored", { memory: { id, stage: next } });
1179
- } catch (e) {}
1180
- });
1181
- } catch (e) {
1182
- /* non-blocking */
1183
- }
1184
- }
1185
- }
1186
- }
1187
  }
1188
  }
1189
 
 
9
  * - Each delta passes through adjustUp / adjustDown with global + per-trait multipliers
10
  * (window.KIMI_TRAIT_ADJUSTMENT) for consistent scaling.
11
  * 2. Content keyword analysis (_analyzeTextContent) may override interim trait values (explicit matches).
12
+ * 3. Cross-trait modifiers (_applyCrossTraitModifiers) apply synergy / balancing rules (e.g. high empathy boosts affection, high romance stabilizes affection, intelligence supports empathy/humor).
13
  * 4. Conversation-based drift (updatePersonalityFromConversation) uses TRAIT_KEYWORD_MODEL:
14
  * - Counts positive/negative keyword hits (user weighted 1.0, model weighted 0.5).
15
  * - Computes rawDelta = posHits*posFactor - negHits*negFactor.
16
  * - Applies sustained negativity amplification after streakPenaltyAfter.
17
  * - Clamps magnitude to maxStep per trait, then applies directly with bounds [0,100].
18
  * 5. Persistence: _preparePersistTrait decides threshold & smoothing before batch write.
19
+ * 6. Global personality average (UI) = mean of six core traits (affection included).
20
  * NOTE: Affection is fully independent (no derived average). All adjustments centralized here to avoid duplication.
21
  */
22
  this.db = database;
23
  this.negativeStreaks = {};
 
 
24
 
25
  // Unified emotion mappings
26
  this.EMOTIONS = {
 
66
  intelligence: 70, // Competent baseline intellect
67
  empathy: 75, // Warm & caring baseline
68
  humor: 60, // Mild sense of humor baseline
69
+ romance: 50 // Neutral romance baseline (earned over time)
 
 
70
  };
71
 
72
  // Central emotion -> trait base deltas (pre global multipliers & gainCfg scaling)
73
  // Positive numbers increase trait, negative decrease.
74
  // Keep values small; final effect passes through adjustUp/adjustDown and global multipliers.
 
 
75
  this.EMOTION_TRAIT_EFFECTS = {
76
+ positive: { affection: 0.45, empathy: 0.2, playfulness: 0.25, humor: 0.25 },
77
+ negative: { affection: -0.7, empathy: 0.3 },
78
+ romantic: { romance: 0.7, affection: 0.55, empathy: 0.15 },
79
+ flirtatious: { romance: 0.55, playfulness: 0.45, affection: 0.25 },
80
+ laughing: { humor: 0.85, playfulness: 0.5, affection: 0.25 },
81
+ dancing: { playfulness: 1.1, affection: 0.45 },
82
+ surprise: { intelligence: 0.12, empathy: 0.12 },
83
+ shy: { romance: -0.3, affection: -0.12 },
84
+ confident: { intelligence: 0.15, affection: 0.55 },
85
+ listening: { empathy: 0.6, intelligence: 0.25 },
86
+ kiss: { romance: 0.85, affection: 0.7 },
87
+ goodbye: { affection: -0.15, empathy: 0.1 }
88
  };
89
 
90
  // Trait keyword scaling model for conversation analysis (per-message delta shaping)
 
94
  empathy: { posFactor: 0.4, negFactor: 0.5, streakPenaltyAfter: 3, maxStep: 1.5 },
95
  playfulness: { posFactor: 0.45, negFactor: 0.4, streakPenaltyAfter: 4, maxStep: 1.4 },
96
  humor: { posFactor: 0.55, negFactor: 0.45, streakPenaltyAfter: 4, maxStep: 1.6 },
97
+ intelligence: { posFactor: 0.35, negFactor: 0.55, streakPenaltyAfter: 2, maxStep: 1.2 }
 
 
98
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
  // (Affection is an independent trait again; previous derived computation removed.)
101
  // ===== UNIFIED EMOTION ANALYSIS =====
 
156
  negative: 1
157
  };
158
 
 
 
 
 
 
 
 
 
 
159
  // Normalize keyword lists to handle accents/contractions
160
  const normalizeList = arr => (Array.isArray(arr) ? arr.map(x => this.normalizeText(String(x))).filter(Boolean) : []);
161
  const normalizedPositiveWords = normalizeList(positiveWords);
 
171
  const hits = check.keywords.reduce((acc, word) => acc + (this.countTokenMatches(lowerText, String(word)) ? 1 : 0), 0);
172
  if (hits > 0) {
173
  const key = check.emotion;
174
+ const weight = sensitivity[key] != null ? sensitivity[key] : 1;
175
  const score = hits * weight;
176
  if (score > bestScore) {
177
  bestScore = score;
 
221
  let playfulness = safe(traits?.playfulness, this.TRAIT_DEFAULTS.playfulness);
222
  let humor = safe(traits?.humor, this.TRAIT_DEFAULTS.humor);
223
  let intelligence = safe(traits?.intelligence, this.TRAIT_DEFAULTS.intelligence);
 
 
 
 
 
 
 
 
 
 
 
224
 
225
+ // Unified adjustment functions - More balanced progression for better user experience
226
  const adjustUp = (val, amount) => {
227
+ // Gradual slowdown only at very high levels to allow natural progression
228
+ if (val >= 95) return val + amount * 0.2; // Slow near max to preserve challenge
229
+ if (val >= 88) return val + amount * 0.5; // Moderate slowdown at very high levels
230
+ if (val >= 80) return val + amount * 0.7; // Slight slowdown at high levels
231
+ if (val >= 60) return val + amount * 0.9; // Nearly normal progression in mid-high range
232
+ return val + amount; // Normal progression below 60%
233
  };
234
+
235
  const adjustDown = (val, amount) => {
236
+ // Faster decline at higher values - easier to lose than to gain
237
+ if (val >= 80) return val - amount * 1.2; // Faster loss at high levels
238
+ if (val >= 60) return val - amount; // Normal loss at medium levels
239
+ if (val >= 40) return val - amount * 0.8; // Slower loss at low-medium levels
240
+ if (val <= 20) return val - amount * 0.4; // Very slow loss at low levels
241
+ return val - amount * 0.6; // Moderate loss between 20-40
 
242
  };
243
 
244
  // Unified emotion-based adjustments - More balanced and realistic progression
 
263
  return baseDelta * GLOSS * t;
264
  };
265
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  // Apply emotion deltas from centralized map (if defined)
267
  const map = this.EMOTION_TRAIT_EFFECTS?.[emotion];
 
 
 
 
268
  if (map) {
269
  for (const [traitName, baseDelta] of Object.entries(map)) {
270
+ const delta = baseDelta; // base delta -> will be scaled below
 
 
271
  if (delta === 0) continue;
272
  switch (traitName) {
273
  case "affection":
 
 
274
  affection =
275
+ delta > 0
276
+ ? Math.min(100, adjustUp(affection, scaleGain("affection", delta)))
277
+ : Math.max(0, adjustDown(affection, scaleLoss("affection", Math.abs(delta))));
278
  break;
279
  case "romance":
 
 
280
  romance =
281
+ delta > 0
282
+ ? Math.min(100, adjustUp(romance, scaleGain("romance", delta)))
283
+ : Math.max(0, adjustDown(romance, scaleLoss("romance", Math.abs(delta))));
284
  break;
285
  case "empathy":
286
  empathy =
 
306
  ? Math.min(100, adjustUp(intelligence, scaleGain("intelligence", delta)))
307
  : Math.max(0, adjustDown(intelligence, scaleLoss("intelligence", Math.abs(delta))));
308
  break;
 
 
 
 
 
 
 
 
 
 
 
 
309
  }
310
  }
311
  }
312
 
313
+ // Cross-trait interactions - traits influence each other for more realistic personality development
314
+ // High empathy should boost affection over time
315
+ if (empathy >= 75 && affection < empathy - 5) {
316
+ affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.1)));
 
 
 
 
 
 
 
317
  }
318
 
319
+ // High intelligence should slightly boost empathy (understanding others)
320
+ if (intelligence >= 80 && empathy < intelligence - 10) {
321
+ empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.05)));
322
+ }
323
+
324
+ // Humor and playfulness should reinforce each other
325
+ if (humor >= 70 && playfulness < humor - 10) {
326
+ playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.05)));
327
+ }
328
+ if (playfulness >= 70 && humor < playfulness - 10) {
329
+ humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.05)));
330
+ }
331
 
332
  // Content-based adjustments (unified)
333
  await this._analyzeTextContent(
 
341
  adjustUp
342
  );
343
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  // Cross-trait modifiers (applied after primary emotion & content changes)
345
  ({ affection, romance, empathy, playfulness, humor, intelligence } = this._applyCrossTraitModifiers({
346
  affection,
 
358
  // Preserve fractional progress to allow gradual visible changes
359
  const to2 = v => Number(Number(v).toFixed(2));
360
  const clamp = v => Math.max(0, Math.min(100, v));
361
+ const updatedTraits = {
 
 
 
 
 
 
 
 
 
 
 
362
  affection: to2(clamp(affection)),
363
  romance: to2(clamp(romance)),
364
  empathy: to2(clamp(empathy)),
365
  playfulness: to2(clamp(playfulness)),
366
  humor: to2(clamp(humor)),
367
+ intelligence: to2(clamp(intelligence))
 
 
368
  };
369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  // Prepare persistence with smoothing / threshold to avoid tiny writes
371
  const toPersist = {};
372
  for (const [trait, candValue] of Object.entries(updatedTraits)) {
373
  const current = typeof traits?.[trait] === "number" ? traits[trait] : this.TRAIT_DEFAULTS[trait];
374
  const prep = this._preparePersistTrait(trait, current, candValue, selectedCharacter);
375
+ if (prep.shouldPersist) toPersist[trait] = prep.value;
 
 
 
376
  }
377
  if (Object.keys(toPersist).length > 0) {
 
378
  await this.db.setPersonalityBatch(toPersist, selectedCharacter);
 
 
379
  }
380
 
381
  return updatedTraits;
 
432
  };
433
 
434
  const pendingUpdates = {};
435
+ for (const trait of ["humor", "intelligence", "romance", "affection", "playfulness", "empathy"]) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  const posWords = getPersonalityWords(trait, "positive");
437
  const negWords = getPersonalityWords(trait, "negative");
438
  let currentVal =
 
446
  let posScore = 0;
447
  let negScore = 0;
448
  for (const w of posWords) {
449
+ posScore += this.countTokenMatches(lowerUser, String(w)) * 1.0;
450
+ posScore += this.countTokenMatches(lowerKimi, String(w)) * 0.5;
 
 
451
  }
452
  for (const w of negWords) {
453
+ negScore += this.countTokenMatches(lowerUser, String(w)) * 1.0;
454
+ negScore += this.countTokenMatches(lowerKimi, String(w)) * 0.5;
 
455
  }
456
 
457
  let rawDelta = posScore * posFactor - negScore * negFactor;
 
 
 
458
 
459
  // Track negative streaks per trait (only when net negative & no positives)
460
  if (!this.negativeStreaks[trait]) this.negativeStreaks[trait] = 0;
 
474
 
475
  if (rawDelta !== 0) {
476
  let newVal = currentVal + rawDelta;
477
+ if (rawDelta > 0) {
478
+ newVal = Math.min(100, newVal);
479
+ } else {
480
+ newVal = Math.max(0, newVal);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  }
482
+ pendingUpdates[trait] = newVal;
483
  }
484
  }
485
 
 
492
  for (const [trait, candValue] of Object.entries(pendingUpdates)) {
493
  const current = typeof traits?.[trait] === "number" ? traits[trait] : this.TRAIT_DEFAULTS[trait];
494
  const prep = this._preparePersistTrait(trait, current, candValue, character);
495
+ if (prep.shouldPersist) toPersist[trait] = prep.value;
 
 
 
496
  }
497
  if (Object.keys(toPersist).length > 0) {
 
498
  await this.db.setPersonalityBatch(toPersist, character);
 
499
  }
500
  }
501
  }
 
656
 
657
  // Decide whether to persist based on absolute change threshold. Returns {shouldPersist, value}
658
  _preparePersistTrait(trait, currentValue, candidateValue, character = null) {
659
+ // Configurable via globals
660
  const alpha = (window.KIMI_SMOOTHING_ALPHA && Number(window.KIMI_SMOOTHING_ALPHA)) || 0.3;
661
+ const threshold = (window.KIMI_PERSIST_THRESHOLD && Number(window.KIMI_PERSIST_THRESHOLD)) || 0.25; // percent absolute
 
 
662
 
663
  const smoothed = this._applyEMA(currentValue, candidateValue, alpha);
664
+ const absDelta = Math.abs(smoothed - currentValue);
665
+ if (absDelta < threshold) {
 
 
 
666
  return { shouldPersist: false, value: currentValue };
667
  }
668
+ return { shouldPersist: true, value: Number(Number(smoothed).toFixed(2)) };
 
669
  }
670
 
671
  // ===== UTILITY METHODS =====
 
755
 
756
  getMoodCategoryFromPersonality(traits) {
757
  const avg = this.calculatePersonalityAverage(traits);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
 
759
+ if (avg >= 80) return "speakingPositive";
760
+ if (avg >= 60) return "neutral";
761
+ if (avg >= 40) return "neutral";
762
+ if (avg >= 20) return "speakingNegative";
763
+ return "speakingNegative";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
  }
765
  }
766
 
kimi-js/kimi-memory-system.js CHANGED
@@ -14,21 +14,6 @@ class KimiMemorySystem {
14
  important: "Important Events"
15
  };
16
 
17
- // Passive decay configuration (tunable via global window.KIMI_MEMORY_DECAY before init())
18
- this.decayConfig = Object.assign(
19
- {
20
- enabled: true,
21
- intervalMs: 60 * 60 * 1000, // hourly
22
- halfLifeDays: 90,
23
- minImportance: 0.05,
24
- protectCategories: ["important", "relationships", "personal"],
25
- recentBoostDays: 7,
26
- accessRefreshBoost: 0.02
27
- },
28
- window.KIMI_MEMORY_DECAY || {}
29
- );
30
- this._lastDecayRun = 0;
31
-
32
  // Patterns for automatic memory extraction (multilingual)
33
  this.extractionPatterns = {
34
  personal: [
@@ -231,49 +216,11 @@ class KimiMemorySystem {
231
  this.selectedCharacter = await this.db.getSelectedCharacter();
232
  await this.createMemoryTables();
233
 
234
- // Load last decay run timestamp (persisted across sessions)
235
- try {
236
- const storedLast = await this.db.getPreference("memoryLastDecayRun", 0);
237
- if (storedLast && typeof storedLast === "number" && storedLast > 0) {
238
- this._lastDecayRun = storedLast;
239
- }
240
- } catch (e) {
241
- if (window.KIMI_DEBUG_MEMORIES) console.warn("Could not load memoryLastDecayRun", e);
242
- }
243
-
244
  // Migrer les IDs incompatibles si nΓ©cessaire
245
  await this.migrateIncompatibleIDs();
246
 
247
  // Start background migration to populate keywords for existing memories (non-blocking)
248
  this.populateKeywordsForAllMemories().catch(e => console.warn("Keyword population failed", e));
249
-
250
- // Schedule passive decay loop if enabled
251
- if (this.decayConfig.enabled) {
252
- // Prevent duplicate timers if init() called multiple times
253
- if (this._decayTimer) {
254
- clearTimeout(this._decayTimer);
255
- }
256
- const scheduleDecay = async () => {
257
- try {
258
- if (typeof this.applyMemoryDecay === "function") {
259
- await this.applyMemoryDecay();
260
- } else {
261
- console.warn("Memory decay skipped: applyMemoryDecay() not implemented");
262
- return; // do not reschedule endlessly if missing
263
- }
264
- } catch (e) {
265
- console.warn("memory decay tick failed", e);
266
- }
267
- this._decayTimer = setTimeout(scheduleDecay, this.decayConfig.intervalMs);
268
- };
269
- this._decayTimer = setTimeout(scheduleDecay, this.decayConfig.intervalMs);
270
- if (window.KIMI_DEBUG_MEMORIES) {
271
- console.log(
272
- "βš™οΈ Passive memory decay scheduled. applyMemoryDecay present:",
273
- typeof this.applyMemoryDecay === "function"
274
- );
275
- }
276
- }
277
  } catch (error) {
278
  console.error("Memory system initialization error:", error);
279
  }
@@ -758,20 +705,12 @@ class KimiMemorySystem {
758
  console.log(`Memory added with ID: ${id}`);
759
  }
760
 
761
- // Intelligent purge if over limits
762
- await this.smartPurgeMemories();
763
 
764
  // Notify LLM system to refresh context
765
  this.notifyLLMContextUpdate();
766
 
767
- // Emit event for observers (plugins, UI debug) after successful add
768
- if (window.kimiEventBus) {
769
- try {
770
- window.kimiEventBus.emit("memory:stored", { memory });
771
- } catch (e) {
772
- console.warn("memory:stored emit failed", e);
773
- }
774
- }
775
  return memory;
776
  } catch (error) {
777
  console.error("Error adding memory:", error);
@@ -939,47 +878,8 @@ class KimiMemorySystem {
939
  if (memoryData.content && memoryData.content.length > 24) importance += 0.05;
940
  if (memoryData.confidence && memoryData.confidence > 0.9) importance += 0.05;
941
 
942
- // Trait influence (pull current personality if available)
943
- try {
944
- if (window.kimiEmotionSystem && this.selectedCharacter) {
945
- const traits =
946
- window.kimiEmotionSystem.db && window.kimiEmotionSystem.db.cachedPersonality
947
- ? window.kimiEmotionSystem.db.cachedPersonality[this.selectedCharacter] || {}
948
- : null;
949
- if (traits) {
950
- const aff = typeof traits.affection === "number" ? traits.affection : 55;
951
- const emp = typeof traits.empathy === "number" ? traits.empathy : 75;
952
- // Scale 0..100 -> 0..1 then small weighted boost
953
- importance += (aff / 100) * 0.05; // affection: emotional salience
954
- if (memoryData.category === "personal" || memoryData.category === "relationships") {
955
- importance += (emp / 100) * 0.05; // empathy: care for personal/relationship
956
- }
957
- }
958
- }
959
- } catch {}
960
-
961
- // Frequency & recency influence (if existing memory object passed with stats)
962
- if (typeof memoryData.accessCount === "number") {
963
- const capped = Math.min(10, Math.max(0, memoryData.accessCount));
964
- importance += (capped / 10) * 0.05; // up to +0.05
965
- }
966
- if (memoryData.lastAccess instanceof Date) {
967
- const ageMs = Date.now() - memoryData.lastAccess.getTime();
968
- const days = ageMs / 86400000;
969
- if (days < 1)
970
- importance += 0.03; // very recent recall
971
- else if (days < 7) importance += 0.01;
972
- }
973
-
974
- // Decay slight if very old (timestamp far in past) without access metadata
975
- if (memoryData.timestamp instanceof Date) {
976
- const ageDays = (Date.now() - memoryData.timestamp.getTime()) / 86400000;
977
- if (ageDays > 45) importance -= 0.04;
978
- else if (ageDays > 90) importance -= 0.06; // stronger decay after 3 months
979
- }
980
-
981
  // Round to two decimals to avoid floating point artifacts
982
- return Math.min(1.0, Math.max(0.0, Math.round(importance * 100) / 100));
983
  }
984
 
985
  // Derive semantic tags from memory content to assist prioritization and merging
@@ -1332,101 +1232,6 @@ class KimiMemorySystem {
1332
  }
1333
  }
1334
 
1335
- // SMART PURGE: multi-factor scoring to deactivate least valuable memories.
1336
- // Factors (low score purged first):
1337
- // - Lower importance
1338
- // - Older (timestamp, lastAccess)
1339
- // - Low accessCount
1340
- // - Category weight (preferences/activities lower, important/personal protected)
1341
- // - Stale (not accessed recently and no boundary / relationship milestone tags)
1342
- async smartPurgeMemories() {
1343
- if (!this.db) return;
1344
- try {
1345
- const maxEntries = window.KIMI_MAX_MEMORIES || this.maxMemoryEntries || 100;
1346
- const memories = (await this.getAllMemories()).filter(m => m.isActive);
1347
- if (memories.length <= maxEntries) return; // nothing to do
1348
-
1349
- const now = Date.now();
1350
- const PROTECT_TAGS = new Set([
1351
- "relationship:first_meet",
1352
- "relationship:first_date",
1353
- "relationship:first_kiss",
1354
- "relationship:anniversary",
1355
- "relationship:moved_in",
1356
- "boundary:dislike",
1357
- "boundary:preference",
1358
- "boundary:limit",
1359
- "boundary:consent"
1360
- ]);
1361
- const categoryBase = {
1362
- important: 1.0,
1363
- personal: 0.9,
1364
- relationships: 0.85,
1365
- goals: 0.75,
1366
- experiences: 0.6,
1367
- preferences: 0.5,
1368
- activities: 0.45
1369
- };
1370
- const recentWindowMs = 14 * 86400000; // 14 days
1371
- const freshWindowMs = 2 * 86400000; // 2 days (very recent boost)
1372
-
1373
- const scored = memories.map(m => {
1374
- const importance = typeof m.importance === "number" ? m.importance : 0.5;
1375
- const created = new Date(m.timestamp).getTime();
1376
- const lastAccess = m.lastAccess ? new Date(m.lastAccess).getTime() : created;
1377
- const ageDays = (now - created) / 86400000;
1378
- const idleDays = (now - lastAccess) / 86400000;
1379
- const catW = categoryBase[m.category] || 0.5;
1380
- const access = m.accessCount || 0;
1381
- const tags = new Set(m.tags || []);
1382
- const hasProtectTag = [...tags].some(t => PROTECT_TAGS.has(t));
1383
- const recent = now - lastAccess < recentWindowMs;
1384
- const veryRecent = now - lastAccess < freshWindowMs;
1385
- // Build score (higher = keep)
1386
- let score = 0;
1387
- score += importance * 2.2; // primary weight
1388
- score += catW * 0.8;
1389
- score += Math.min(access, 20) * 0.05; // up to +1
1390
- if (recent) score += 0.4;
1391
- if (veryRecent) score += 0.3;
1392
- if (hasProtectTag) score += 0.6;
1393
- // Penalties
1394
- score -= Math.min(Math.max(idleDays - 30, 0) * 0.01, 0.5); // idle after 30d
1395
- score -= Math.min(ageDays * 0.002, 0.4); // very old slight penalty
1396
- return { memory: m, score };
1397
- });
1398
-
1399
- // Sort ascending by score (lowest first) to know which to purge
1400
- scored.sort((a, b) => a.score - b.score);
1401
- const excess = scored.length - maxEntries;
1402
- if (excess <= 0) return;
1403
-
1404
- const toPurge = scored
1405
- .slice(0, excess)
1406
- .filter(s => s.score < 2.2) // avoid purging those with already decent score
1407
- .map(s => s.memory);
1408
- if (toPurge.length === 0) return;
1409
-
1410
- for (const mem of toPurge) {
1411
- try {
1412
- await this.updateMemory(mem.id, { isActive: false, lastModified: new Date() });
1413
- } catch (e) {
1414
- console.warn("Failed smart purge memory", mem.id, e);
1415
- }
1416
- }
1417
- if (window.kimiEventBus) {
1418
- try {
1419
- window.kimiEventBus.emit("memory:purged", {
1420
- purged: toPurge.length,
1421
- remaining: memories.length - toPurge.length
1422
- });
1423
- } catch (e) {}
1424
- }
1425
- } catch (e) {
1426
- console.warn("smartPurgeMemories failed", e);
1427
- }
1428
- }
1429
-
1430
  // MEMORY RETRIEVAL FOR LLM
1431
  async getRelevantMemories(context = "", limit = 10) {
1432
  if (!this.memoryEnabled) return [];
@@ -1542,36 +1347,6 @@ class KimiMemorySystem {
1542
  score += (memory.confidence || 0.5) * 0.05;
1543
  score += (memory.importance || 0.5) * 0.05;
1544
 
1545
- // Relationship specific boosts
1546
- try {
1547
- const tags = new Set(memory.tags || []);
1548
- const relTags = [
1549
- "relationship:stage",
1550
- "relationship:first_meet",
1551
- "relationship:first_date",
1552
- "relationship:first_kiss",
1553
- "relationship:anniversary",
1554
- "relationship:moved_in"
1555
- ];
1556
- if (memory.category === "relationships") score += 0.08;
1557
- if ([...tags].some(t => relTags.includes(t))) score += 0.07;
1558
- if ([...tags].some(t => t.startsWith("boundary:"))) score += 0.06; // boundaries important contextually
1559
- if ([...tags].some(t => t.startsWith("relationship:stage_"))) score += 0.05;
1560
- } catch {}
1561
-
1562
- // Warmth influence (pull from emotion system if present). High warmth favors relational memories.
1563
- try {
1564
- if (window.kimiEmotionSystem && typeof window.kimiEmotionSystem._warmth === "number") {
1565
- const w = window.kimiEmotionSystem._warmth; // -50..50
1566
- if (
1567
- w > 5 &&
1568
- (memory.category === "relationships" || (memory.tags || []).some(t => t.startsWith("relationship:")))
1569
- ) {
1570
- score += Math.min(0.06, (w / 50) * 0.06);
1571
- }
1572
- }
1573
- } catch {}
1574
-
1575
  return Math.min(1.0, score);
1576
  }
1577
 
@@ -2183,135 +1958,9 @@ class KimiMemorySystem {
2183
  return false;
2184
  }
2185
  }
2186
-
2187
- // === PASSIVE MEMORY DECAY ===
2188
- // Gradually lowers importance of older / unused memories while protecting key categories.
2189
- async applyMemoryDecay() {
2190
- // Guards
2191
- if (!this.db || !this.db.db || !this.db.db.memories) return false;
2192
- if (!this.memoryEnabled) return false;
2193
- const cfg = this.decayConfig || {};
2194
- if (!cfg.enabled) return false;
2195
- if (!cfg.halfLifeDays || cfg.halfLifeDays <= 0) return false;
2196
-
2197
- const now = Date.now();
2198
- const lastRun = this._lastDecayRun || 0;
2199
- // If never run, just set timestamp and skip decay to avoid immediate drop on startup
2200
- if (!lastRun) {
2201
- this._lastDecayRun = now;
2202
- if (window.KIMI_DEBUG_MEMORIES) console.log("⏳ Memory decay initialized (no decay applied on first run)");
2203
- return true;
2204
- }
2205
-
2206
- const deltaMs = now - lastRun;
2207
- const deltaDays = deltaMs / 86400000; // convert ms -> days
2208
- if (deltaDays <= 0) return true;
2209
-
2210
- this._lastDecayRun = now;
2211
- // Persist last run timestamp (fire and forget)
2212
- try {
2213
- this.db.setPreference && this.db.setPreference("memoryLastDecayRun", this._lastDecayRun);
2214
- } catch {}
2215
-
2216
- // Pre-calc exponential decay factor based on half-life
2217
- // importance' = minImportance + (importance - minImportance) * 0.5^(deltaDays / halfLife)
2218
- const halfLife = cfg.halfLifeDays;
2219
- const minImp = typeof cfg.minImportance === "number" ? cfg.minImportance : 0.05;
2220
- const protectCats = new Set(cfg.protectCategories || []);
2221
- const recentBoostDays = typeof cfg.recentBoostDays === "number" ? cfg.recentBoostDays : 7;
2222
- const accessRefreshBoost = typeof cfg.accessRefreshBoost === "number" ? cfg.accessRefreshBoost : 0.02;
2223
- const decayPow = Math.pow(0.5, deltaDays / halfLife);
2224
-
2225
- let updated = 0;
2226
- let skipped = 0;
2227
- let protectedCount = 0;
2228
-
2229
- try {
2230
- const memories = await this.getAllMemories();
2231
- const ops = [];
2232
- for (const mem of memories) {
2233
- if (!mem.isActive) continue; // ignore inactive
2234
- if (protectCats.has(mem.category)) {
2235
- protectedCount++;
2236
- continue; // fully protected categories
2237
- }
2238
-
2239
- if (typeof mem.importance !== "number") {
2240
- // initialize missing importance
2241
- mem.importance = this.calculateImportance(mem);
2242
- }
2243
-
2244
- const originalImportance = mem.importance;
2245
- // Apply exponential decay toward min importance
2246
- let newImportance = minImp + (originalImportance - minImp) * decayPow;
2247
-
2248
- // Recent access boost (prevents too-fast fading of freshly used memories)
2249
- try {
2250
- if (mem.lastAccess) {
2251
- const lastAccessDays = (now - new Date(mem.lastAccess).getTime()) / 86400000;
2252
- if (lastAccessDays <= recentBoostDays) {
2253
- newImportance += (recentBoostDays - lastAccessDays) * 0.002; // small tapering boost
2254
- }
2255
- }
2256
- } catch {}
2257
-
2258
- // Access refresh micro-boost if accessCount increased recently (heuristic: lastAccess within decay window)
2259
- try {
2260
- if (mem.lastAccess && now - new Date(mem.lastAccess).getTime() <= deltaMs) {
2261
- newImportance += accessRefreshBoost;
2262
- }
2263
- } catch {}
2264
-
2265
- // Clamp
2266
- if (newImportance > 1) newImportance = 1;
2267
- if (newImportance < 0) newImportance = 0;
2268
-
2269
- // Skip tiny changes to reduce writes
2270
- if (Math.abs(newImportance - originalImportance) < 0.005) {
2271
- skipped++;
2272
- continue;
2273
- }
2274
-
2275
- mem.importance = Number(newImportance.toFixed(3));
2276
- mem.lastModified = new Date();
2277
- ops.push(this.db.db.memories.update(mem.id, { importance: mem.importance, lastModified: mem.lastModified }));
2278
- updated++;
2279
-
2280
- // Batch writes to avoid blocking UI thread
2281
- if (ops.length >= 50) {
2282
- await Promise.all(ops);
2283
- ops.length = 0;
2284
- }
2285
- }
2286
- if (ops.length) await Promise.all(ops);
2287
- } catch (e) {
2288
- console.warn("Memory decay pass failed", e);
2289
- return false;
2290
- }
2291
-
2292
- if (window.KIMI_DEBUG_MEMORIES || updated) {
2293
- console.log(
2294
- `πŸ§ͺ Memory decay run: Ξ”days=${deltaDays.toFixed(3)} updated=${updated} skipped=${skipped} protected=${protectedCount}`
2295
- );
2296
- }
2297
- return true;
2298
- }
2299
-
2300
- // Manually trigger a decay run and get a simple report (promise of boolean)
2301
- async runDecayNow() {
2302
- if (window.KIMI_DEBUG_MEMORIES) console.log("▢️ Manual memory decay trigger");
2303
- return this.applyMemoryDecay();
2304
- }
2305
-
2306
- // Stop passive decay scheduling (e.g., when disabling memory system)
2307
- stopMemoryDecay() {
2308
- if (this._decayTimer) {
2309
- clearTimeout(this._decayTimer);
2310
- this._decayTimer = null;
2311
- if (window.KIMI_DEBUG_MEMORIES) console.log("⏹ Passive memory decay stopped");
2312
- }
2313
- }
2314
  }
2315
 
2316
  window.KimiMemorySystem = KimiMemorySystem;
2317
  export default KimiMemorySystem;
 
 
 
14
  important: "Important Events"
15
  };
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  // Patterns for automatic memory extraction (multilingual)
18
  this.extractionPatterns = {
19
  personal: [
 
216
  this.selectedCharacter = await this.db.getSelectedCharacter();
217
  await this.createMemoryTables();
218
 
 
 
 
 
 
 
 
 
 
 
219
  // Migrer les IDs incompatibles si nΓ©cessaire
220
  await this.migrateIncompatibleIDs();
221
 
222
  // Start background migration to populate keywords for existing memories (non-blocking)
223
  this.populateKeywordsForAllMemories().catch(e => console.warn("Keyword population failed", e));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  } catch (error) {
225
  console.error("Memory system initialization error:", error);
226
  }
 
705
  console.log(`Memory added with ID: ${id}`);
706
  }
707
 
708
+ // Cleanup old memories if we exceed limit
709
+ await this.cleanupOldMemories();
710
 
711
  // Notify LLM system to refresh context
712
  this.notifyLLMContextUpdate();
713
 
 
 
 
 
 
 
 
 
714
  return memory;
715
  } catch (error) {
716
  console.error("Error adding memory:", error);
 
878
  if (memoryData.content && memoryData.content.length > 24) importance += 0.05;
879
  if (memoryData.confidence && memoryData.confidence > 0.9) importance += 0.05;
880
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
881
  // Round to two decimals to avoid floating point artifacts
882
+ return Math.min(1.0, Math.round(importance * 100) / 100);
883
  }
884
 
885
  // Derive semantic tags from memory content to assist prioritization and merging
 
1232
  }
1233
  }
1234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1235
  // MEMORY RETRIEVAL FOR LLM
1236
  async getRelevantMemories(context = "", limit = 10) {
1237
  if (!this.memoryEnabled) return [];
 
1347
  score += (memory.confidence || 0.5) * 0.05;
1348
  score += (memory.importance || 0.5) * 0.05;
1349
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1350
  return Math.min(1.0, score);
1351
  }
1352
 
 
1958
  return false;
1959
  }
1960
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1961
  }
1962
 
1963
  window.KimiMemorySystem = KimiMemorySystem;
1964
  export default KimiMemorySystem;
1965
+
1966
+ window.KimiMemorySystem = KimiMemorySystem;
kimi-js/kimi-memory.js CHANGED
@@ -1,8 +1,4 @@
1
  // ===== KIMI MEMORY MANAGER =====
2
- // ===== LEGACY KIMI MEMORY (FAVORABILITY) =====
3
- // LEGACY NOTE: This file is kept for backward compatibility (older favorability logic / UI hooks).
4
- // New memory extraction & storage is in `kimi-memory-system.js`.
5
- // Future work: gradually migrate any remaining calls to the new system and remove this file.
6
  class KimiMemory {
7
  constructor(database) {
8
  this.db = database;
@@ -111,7 +107,10 @@ class KimiMemory {
111
  }
112
  }
113
 
114
- // Legacy wrapper: prefer updateGlobalPersonalityUI() elsewhere.
 
 
 
115
  updateFavorabilityBar() {
116
  if (window.updateGlobalPersonalityUI) {
117
  window.updateGlobalPersonalityUI();
 
1
  // ===== KIMI MEMORY MANAGER =====
 
 
 
 
2
  class KimiMemory {
3
  constructor(database) {
4
  this.db = database;
 
107
  }
108
  }
109
 
110
+ /**
111
+ * @deprecated Use updateGlobalPersonalityUI().
112
+ * Thin wrapper retained for backward compatibility only.
113
+ */
114
  updateFavorabilityBar() {
115
  if (window.updateGlobalPersonalityUI) {
116
  window.updateGlobalPersonalityUI();
kimi-js/kimi-module.js CHANGED
@@ -330,6 +330,25 @@ function updateFavorabilityLabel(characterKey) {
330
  }
331
  }
332
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  // Update UI elements (bar + percentage text + label) based on overall personality average
334
  async function updateGlobalPersonalityUI(characterKey = null) {
335
  try {
@@ -337,10 +356,7 @@ async function updateGlobalPersonalityUI(characterKey = null) {
337
  if (!db) return;
338
  const character = characterKey || (await db.getSelectedCharacter());
339
  const traits = await db.getAllPersonalityTraits(character);
340
- const avg =
341
- window.kimiEmotionSystem && typeof window.kimiEmotionSystem.calculatePersonalityAverage === "function"
342
- ? Number(window.kimiEmotionSystem.calculatePersonalityAverage(traits).toFixed(2))
343
- : 50;
344
  // Reuse existing favorability bar elements for global average
345
  const bar = document.getElementById("favorability-bar");
346
  const text = document.getElementById("favorability-text");
 
330
  }
331
  }
332
 
333
+ // Delegated personality average computation (single source of truth in KimiEmotionSystem)
334
+ function computePersonalityAverage(traits) {
335
+ if (window.kimiEmotionSystem && typeof window.kimiEmotionSystem.calculatePersonalityAverage === "function") {
336
+ return Number(window.kimiEmotionSystem.calculatePersonalityAverage(traits).toFixed(2));
337
+ }
338
+ // Fallback minimal (should rarely occur before emotion system init)
339
+ const keys = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
340
+ let sum = 0,
341
+ count = 0;
342
+ for (const k of keys) {
343
+ const v = traits && traits[k];
344
+ if (typeof v === "number" && isFinite(v)) {
345
+ sum += Math.max(0, Math.min(100, v));
346
+ count++;
347
+ }
348
+ }
349
+ return count ? Number((sum / count).toFixed(2)) : 0;
350
+ }
351
+
352
  // Update UI elements (bar + percentage text + label) based on overall personality average
353
  async function updateGlobalPersonalityUI(characterKey = null) {
354
  try {
 
356
  if (!db) return;
357
  const character = characterKey || (await db.getSelectedCharacter());
358
  const traits = await db.getAllPersonalityTraits(character);
359
+ const avg = computePersonalityAverage(traits);
 
 
 
360
  // Reuse existing favorability bar elements for global average
361
  const bar = document.getElementById("favorability-bar");
362
  const text = document.getElementById("favorability-text");
kimi-js/kimi-personality-utils.js CHANGED
@@ -5,7 +5,25 @@
5
  if (window.kimiEmotionSystem && typeof window.kimiEmotionSystem.calculatePersonalityAverage === "function") {
6
  return window.kimiEmotionSystem.calculatePersonalityAverage(traits || {});
7
  }
8
- return 50;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
10
- window.KimiPersonalityUtils = { calcAverage };
11
  })();
 
5
  if (window.kimiEmotionSystem && typeof window.kimiEmotionSystem.calculatePersonalityAverage === "function") {
6
  return window.kimiEmotionSystem.calculatePersonalityAverage(traits || {});
7
  }
8
+ const keys = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
9
+ let sum = 0,
10
+ c = 0;
11
+ for (const k of keys) {
12
+ const v = traits && traits[k];
13
+ if (typeof v === "number" && isFinite(v)) {
14
+ sum += Math.max(0, Math.min(100, v));
15
+ c++;
16
+ }
17
+ }
18
+ return c ? Number((sum / c).toFixed(2)) : 0;
19
+ }
20
+ /**
21
+ * @deprecated Call updateGlobalPersonalityUI() directly.
22
+ */
23
+ async function refreshUI(characterKey = null) {
24
+ if (window.updateGlobalPersonalityUI) {
25
+ return window.updateGlobalPersonalityUI(characterKey);
26
+ }
27
  }
28
+ window.KimiPersonalityUtils = { calcAverage, refreshUI };
29
  })();
kimi-js/kimi-utils.js CHANGED
@@ -901,51 +901,11 @@ class KimiVideoManager {
901
 
902
  this._prefetchLikely(category);
903
 
904
- const previous = { context: this.currentContext, emotion: this.currentEmotion };
905
- if (window.kimiEventBus) {
906
- try {
907
- window.kimiEventBus.emit("video:willChange", {
908
- previous,
909
- next: { context: category, emotion, videoPath },
910
- ts: now
911
- });
912
- } catch (e) {}
913
- }
914
- // Anti-repetition strengthened: ensure recent history exists
915
- if (!this._recentVideoHistory) this._recentVideoHistory = {};
916
- const MAX_RECENT = 3;
917
- // Replace selected video if it appears in recent list and there are alternatives
918
- const recentList = this._recentVideoHistory[category] || [];
919
- if (recentList.includes(videoPath) && (this.videoCategories[category] || []).length > 1) {
920
- const alts = (this.videoCategories[category] || []).filter(v => !recentList.includes(v));
921
- if (alts.length > 0) {
922
- videoPath =
923
- typeof this._pickScoredVideo === "function"
924
- ? this._pickScoredVideo(category, alts, traits)
925
- : alts[Math.floor(Math.random() * alts.length)];
926
- }
927
- }
928
  this.loadAndSwitchVideo(videoPath, priority);
929
  // Always store normalized category as currentContext so event bindings match speakingPositive/Negative
930
  this.currentContext = category;
931
  this.currentEmotion = emotion;
932
  this.lastSwitchTime = now;
933
- // Update history
934
- const hist = this._recentVideoHistory[category] || [];
935
- hist.push(videoPath);
936
- while (hist.length > MAX_RECENT) hist.shift();
937
- this._recentVideoHistory[category] = hist;
938
- if (window.kimiEventBus) {
939
- try {
940
- window.kimiEventBus.emit("video:didChange", {
941
- context: category,
942
- emotion,
943
- videoPath,
944
- recent: [...hist],
945
- ts: now
946
- });
947
- } catch (e) {}
948
- }
949
  }
950
 
951
  setupEventListenersForContext(context) {
@@ -1056,30 +1016,18 @@ class KimiVideoManager {
1056
  if (traits && typeof affection === "number") {
1057
  let weights = candidateVideos.map(video => {
1058
  if (category === "speakingPositive") {
1059
- let warmth = 0;
1060
- try {
1061
- if (window.kimiEmotionSystem && typeof window.kimiEmotionSystem._warmth === "number")
1062
- warmth = window.kimiEmotionSystem._warmth;
1063
- } catch {}
1064
- const warmthRatio = Math.min(1, Math.max(-1, warmth / (window.kimiEmotionSystem?.WARMTH_CFG?.maxAbs || 50)));
1065
- const base = 1 + (affection / 100) * 0.35;
1066
  const rom = typeof traits.romance === "number" ? traits.romance : 50;
1067
  const hum = typeof traits.humor === "number" ? traits.humor : 50;
1068
- const romanceComponent = (rom / 100) * (emotion === "romantic" ? 0.35 : 0.2);
1069
- const humorComponent = (hum / 100) * (emotion === "laughing" ? 0.35 : 0.15);
1070
- const warmthBoost = warmthRatio >= 0 ? 1 + warmthRatio * 0.55 : 1 + warmthRatio * 0.25; // negative warmth reduces positive weight
1071
- return base * (1 + romanceComponent + humorComponent) * warmthBoost;
1072
  }
1073
  if (category === "speakingNegative") {
1074
- let warmth = 0;
1075
- try {
1076
- if (window.kimiEmotionSystem && typeof window.kimiEmotionSystem._warmth === "number")
1077
- warmth = window.kimiEmotionSystem._warmth;
1078
- } catch {}
1079
- const warmthRatio = Math.min(1, Math.max(-1, warmth / (window.kimiEmotionSystem?.WARMTH_CFG?.maxAbs || 50)));
1080
- const base = 1 + ((100 - affection) / 100) * 0.35;
1081
- const warmthPenalty = warmthRatio > 0 ? 1 - warmthRatio * 0.65 : 1 - warmthRatio * 0.2; // cold slightly raises chance
1082
- return base * warmthPenalty;
1083
  }
1084
  if (category === "neutral") {
1085
  // Neutral videos when affection is moderate, also influenced by intelligence
@@ -1939,12 +1887,28 @@ class KimiVideoManager {
1939
  }
1940
 
1941
  function getMoodCategoryFromPersonality(traits) {
1942
- // Use unified emotion system if available
1943
  if (window.kimiEmotionSystem) {
1944
  return window.kimiEmotionSystem.getMoodCategoryFromPersonality(traits);
1945
  }
1946
- // Fallback simplified: neutral baseline until system ready
1947
- return "neutral";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1948
  }
1949
 
1950
  // Centralized initialization manager
@@ -2331,6 +2295,23 @@ class KimiUIStateManager {
2331
  this.state.activeTab = tabName;
2332
  if (this.tabManager) this.tabManager.activateTab(tabName);
2333
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2334
  async setTranscript(text) {
2335
  this.state.transcript = text;
2336
  // Always use the proper transcript management via VoiceManager
 
901
 
902
  this._prefetchLikely(category);
903
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904
  this.loadAndSwitchVideo(videoPath, priority);
905
  // Always store normalized category as currentContext so event bindings match speakingPositive/Negative
906
  this.currentContext = category;
907
  this.currentEmotion = emotion;
908
  this.lastSwitchTime = now;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
909
  }
910
 
911
  setupEventListenersForContext(context) {
 
1016
  if (traits && typeof affection === "number") {
1017
  let weights = candidateVideos.map(video => {
1018
  if (category === "speakingPositive") {
1019
+ // Positive videos favored by affection, romance, and humor
1020
+ const base = 1 + (affection / 100) * 0.4; // Affection influence factor
1021
+ let bonus = 0;
 
 
 
 
1022
  const rom = typeof traits.romance === "number" ? traits.romance : 50;
1023
  const hum = typeof traits.humor === "number" ? traits.humor : 50;
1024
+ if (emotion === "romantic") bonus += (rom / 100) * 0.3; // Romance context bonus
1025
+ if (emotion === "laughing") bonus += (hum / 100) * 0.3; // Humor context bonus
1026
+ return base + bonus;
 
1027
  }
1028
  if (category === "speakingNegative") {
1029
+ // Negative videos when affection is low (reduced weight to balance)
1030
+ return 1 + ((100 - affection) / 100) * 0.3; // Low-affection influence factor
 
 
 
 
 
 
 
1031
  }
1032
  if (category === "neutral") {
1033
  // Neutral videos when affection is moderate, also influenced by intelligence
 
1887
  }
1888
 
1889
  function getMoodCategoryFromPersonality(traits) {
1890
+ // Use unified emotion system
1891
  if (window.kimiEmotionSystem) {
1892
  return window.kimiEmotionSystem.getMoodCategoryFromPersonality(traits);
1893
  }
1894
+
1895
+ // Fallback (should not be reached) - must match emotion system calculation
1896
+ const keys = ["affection", "romance", "empathy", "playfulness", "humor", "intelligence"];
1897
+ let sum = 0;
1898
+ let count = 0;
1899
+ keys.forEach(key => {
1900
+ if (typeof traits[key] === "number") {
1901
+ sum += traits[key];
1902
+ count++;
1903
+ }
1904
+ });
1905
+ const avg = count > 0 ? sum / count : 50;
1906
+
1907
+ if (avg >= 80) return "speakingPositive";
1908
+ if (avg >= 60) return "neutral";
1909
+ if (avg >= 40) return "neutral";
1910
+ if (avg >= 20) return "speakingNegative";
1911
+ return "speakingNegative";
1912
  }
1913
 
1914
  // Centralized initialization manager
 
2295
  this.state.activeTab = tabName;
2296
  if (this.tabManager) this.tabManager.activateTab(tabName);
2297
  }
2298
+ /**
2299
+ * @deprecated Prefer calling updateGlobalPersonalityUI() after updating traits.
2300
+ * This direct setter will be removed in a future cleanup.
2301
+ */
2302
+ setPersonalityAverage(value) {
2303
+ const v = Number(value) || 0;
2304
+ const clamped = Math.max(0, Math.min(100, v));
2305
+ this.state.favorability = clamped;
2306
+ window.KimiDOMUtils.setText("#favorability-text", `${clamped.toFixed(2)}%`);
2307
+ window.KimiDOMUtils.get("#favorability-bar").style.width = `${clamped}%`;
2308
+ }
2309
+ /**
2310
+ * @deprecated Use setPersonalityAverage() (itself deprecated) or updateGlobalPersonalityUI().
2311
+ */
2312
+ setFavorability(value) {
2313
+ this.setPersonalityAverage(value);
2314
+ }
2315
  async setTranscript(text) {
2316
  this.state.transcript = text;
2317
  // Always use the proper transcript management via VoiceManager
kimi-js/kimi-voices.js CHANGED
@@ -214,7 +214,6 @@ class KimiVoiceManager {
214
  this.selectedLanguage = window.KimiLanguageUtils.normalizeLanguageCode(selectedLanguage || "en") || "en";
215
  }
216
  const effectiveLang = await this.getEffectiveLanguage(this.selectedLanguage);
217
- this.effectiveLang = effectiveLang;
218
 
219
  const savedVoice = await this.db?.getPreference("selectedVoice", "auto");
220
 
@@ -388,8 +387,7 @@ class KimiVoiceManager {
388
  autoOption.textContent = "Automatic (Best voice for selected language)";
389
  voiceSelect.appendChild(autoOption);
390
 
391
- const baseLang = this.effectiveLang || this.selectedLanguage;
392
- const filteredVoices = this.getVoicesForLanguage(baseLang);
393
 
394
  // If browser is not Chrome or Edge, do NOT expose voice options even when voices exist.
395
  // This avoids misleading users on Brave/Firefox/Opera/Safari who might think TTS is supported when it's not.
@@ -873,35 +871,16 @@ class KimiVoiceManager {
873
  this.recognition = new this.SpeechRecognition();
874
  this.recognition.continuous = true;
875
 
876
- // Ensure UI language loaded before computing effectiveLang
877
- if (!this.selectedLanguage) {
878
- try {
879
- const prefLang = await this.db?.getPreference("selectedLanguage", "en");
880
- if (prefLang)
881
- this.selectedLanguage = window.KimiLanguageUtils.normalizeLanguageCode(prefLang) || prefLang || "en";
882
- } catch {}
883
- }
884
-
885
  // Resolve effective language (block invalid 'auto')
886
- const effectiveLang = await this.getEffectiveLanguage(this.selectedLanguage);
887
- this.effectiveLang = effectiveLang;
888
- const langCode = this.getLanguageCode(effectiveLang || "en");
889
  try {
890
  this.recognition.lang = langCode;
891
  } catch (e) {
892
  console.warn("Could not set recognition.lang, fallback en-US", e);
893
  this.recognition.lang = "en-US";
894
  }
895
- if (this.recognition.lang.toLowerCase().slice(0, 2) !== effectiveLang.slice(0, 2)) {
896
- console.warn(
897
- `🎀 Recognition language fallback mismatch: requested='${effectiveLang}' actual='${this.recognition.lang}'`
898
- );
899
- this._setASRBadgeState(true, effectiveLang, this.recognition.lang);
900
- } else {
901
- this._setASRBadgeState(false);
902
- }
903
- const uiLang = this.selectedLanguage || effectiveLang;
904
- console.log(`🎀 SpeechRecognition initialized (ui=${uiLang}, effective=${effectiveLang}, lang=${this.recognition.lang})`);
905
  this.recognition.interimResults = true;
906
 
907
  // Add onstart handler to confirm permission
@@ -1524,22 +1503,6 @@ class KimiVoiceManager {
1524
  autoStopDuration: this.autoStopDuration
1525
  };
1526
  }
1527
-
1528
- _setASRBadgeState(mismatch, requested = "", actual = "") {
1529
- try {
1530
- const badge = document.getElementById("asr-lang-badge");
1531
- if (!badge) return;
1532
- if (!mismatch) {
1533
- badge.style.display = "none";
1534
- badge.textContent = "ASR";
1535
- badge.title = "ASR language matches UI language";
1536
- return;
1537
- }
1538
- badge.style.display = "inline-block";
1539
- badge.textContent = "ASR*";
1540
- badge.title = `Speech recognition fallback. UI=${requested} actual=${actual}`;
1541
- } catch {}
1542
- }
1543
  }
1544
 
1545
  // Export for usage
 
214
  this.selectedLanguage = window.KimiLanguageUtils.normalizeLanguageCode(selectedLanguage || "en") || "en";
215
  }
216
  const effectiveLang = await this.getEffectiveLanguage(this.selectedLanguage);
 
217
 
218
  const savedVoice = await this.db?.getPreference("selectedVoice", "auto");
219
 
 
387
  autoOption.textContent = "Automatic (Best voice for selected language)";
388
  voiceSelect.appendChild(autoOption);
389
 
390
+ const filteredVoices = this.getVoicesForLanguage(this.selectedLanguage);
 
391
 
392
  // If browser is not Chrome or Edge, do NOT expose voice options even when voices exist.
393
  // This avoids misleading users on Brave/Firefox/Opera/Safari who might think TTS is supported when it's not.
 
871
  this.recognition = new this.SpeechRecognition();
872
  this.recognition.continuous = true;
873
 
 
 
 
 
 
 
 
 
 
874
  // Resolve effective language (block invalid 'auto')
875
+ const normalized = await this.getEffectiveLanguage(this.selectedLanguage);
876
+ const langCode = this.getLanguageCode(normalized || "en");
 
877
  try {
878
  this.recognition.lang = langCode;
879
  } catch (e) {
880
  console.warn("Could not set recognition.lang, fallback en-US", e);
881
  this.recognition.lang = "en-US";
882
  }
883
+ console.log(`🎀 SpeechRecognition initialized (lang=${this.recognition.lang})`);
 
 
 
 
 
 
 
 
 
884
  this.recognition.interimResults = true;
885
 
886
  // Add onstart handler to confirm permission
 
1503
  autoStopDuration: this.autoStopDuration
1504
  };
1505
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1506
  }
1507
 
1508
  // Export for usage