VirtualKimi commited on
Commit
3c9923e
Β·
verified Β·
1 Parent(s): 68ab33c

Upload 34 files

Browse files
CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
  # Virtual Kimi App Changelog
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  # [1.1.3] - 2025-09-01
4
 
5
  ### Bug Fixes
 
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
index.html CHANGED
@@ -140,6 +140,9 @@
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,7 +1089,10 @@
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>
 
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
  </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>
kimi-js/kimi-constants.js CHANGED
@@ -111,7 +111,21 @@ 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: ["amour", "romantique", "tendre", "cΓ’lin", "bisou", "mon cΕ“ur", "chΓ©ri", "ma belle", "passionnΓ©", "adorΓ©"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  flirtatious: [
116
  "flirt",
117
  "taquin",
@@ -213,7 +227,19 @@ window.KIMI_CONTEXT_KEYWORDS = {
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,6 +398,18 @@ window.KIMI_PERSONALITY_KEYWORDS = {
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,7 +448,20 @@ window.KIMI_PERSONALITY_KEYWORDS = {
410
  ]
411
  },
412
  romance: {
413
- positive: ["cΓ’lin", "amour", "romantique", "bisou", "tendresse", "passion", "sΓ©duisant", "charmant", "adorable"],
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  negative: [
415
  "froid",
416
  "froide",
@@ -435,7 +486,10 @@ window.KIMI_PERSONALITY_KEYWORDS = {
435
  "cΓ’lin",
436
  "aimer",
437
  "adorer",
438
- "adorable"
 
 
 
439
  ],
440
  negative: [
441
  "mΓ©chant",
@@ -453,8 +507,14 @@ window.KIMI_PERSONALITY_KEYWORDS = {
453
  "idiote",
454
  "stupide",
455
  "con",
 
456
  "connard",
457
- "salope"
 
 
 
 
 
458
  ]
459
  },
460
  playfulness: {
@@ -485,6 +545,18 @@ window.KIMI_PERSONALITY_KEYWORDS = {
485
  "bienveillance"
486
  ],
487
  negative: ["indiffΓ©rent", "indiffΓ©rente", "froid", "froide", "Γ©goΓ―ste", "ignorer", "mΓ©priser", "dΓ©nigrer", "hostile"]
 
 
 
 
 
 
 
 
 
 
 
 
488
  }
489
  },
490
  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: [
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
 
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
  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
  ]
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
  "cΓ’lin",
487
  "aimer",
488
  "adorer",
489
+ "adorable",
490
+ "merveilleuse",
491
+ "merveilleux",
492
+ "ma femme"
493
  ],
494
  negative: [
495
  "mΓ©chant",
 
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
  "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: {
kimi-js/kimi-emotion-config.js ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // External emotion & personality tuning overrides
2
+ // Can be edited or replaced without touching core system.
3
+ (function () {
4
+ // Load order note:
5
+ // Include this file BEFORE modules that use KimiEmotionSystem so overrides apply on first use.
6
+ // Example (in index.html): event-bus -> emotion-config -> other kimi-*.js.
7
+ window.KIMI_EMOTION_CONFIG = {
8
+ moodThresholds: { positive: 80, neutralHigh: 55, neutralLow: 35, negative: 15 },
9
+ // Optional scaling multipliers per trait for emotion deltas (post base map, pre adjustUp/Down)
10
+ traitScalar: { affection: 1, romance: 1, empathy: 1, playfulness: 1, humor: 1, intelligence: 1 },
11
+ // Optional emotion specific multipliers
12
+ emotionScalar: {
13
+ positive: 1,
14
+ negative: 1,
15
+ romantic: 1,
16
+ flirting: 1,
17
+ laughing: 1,
18
+ dancing: 1,
19
+ surprise: 1,
20
+ shy: 1,
21
+ confident: 1,
22
+ listening: 1,
23
+ kiss: 1,
24
+ goodbye: 1
25
+ },
26
+ // Hook to modify final computed updatedTraits before persistence
27
+ finalize: function (traits) {
28
+ return traits;
29
+ }
30
+ };
31
+ })();
kimi-js/kimi-emotion-system.js CHANGED
@@ -9,18 +9,20 @@ 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 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,25 +68,29 @@ class KimiEmotionSystem {
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,8 +100,40 @@ class KimiEmotionSystem {
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,6 +194,15 @@ class KimiEmotionSystem {
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,7 +218,7 @@ class KimiEmotionSystem {
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,24 +268,32 @@ class KimiEmotionSystem {
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,24 +318,131 @@ class KimiEmotionSystem {
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,28 +468,37 @@ class KimiEmotionSystem {
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,6 +512,71 @@ class KimiEmotionSystem {
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,24 +594,98 @@ class KimiEmotionSystem {
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,7 +742,22 @@ class KimiEmotionSystem {
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,15 +771,21 @@ class KimiEmotionSystem {
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,12 +805,27 @@ class KimiEmotionSystem {
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,10 +838,15 @@ class KimiEmotionSystem {
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,16 +1007,22 @@ class KimiEmotionSystem {
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,13 +1112,79 @@ class KimiEmotionSystem {
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
 
767
  window.KimiEmotionSystem = 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
  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
  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
  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
  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
  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
  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
  ? 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
  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
  // 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
  };
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
  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
 
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
  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
 
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
 
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
 
1190
  window.KimiEmotionSystem = KimiEmotionSystem;
kimi-js/kimi-event-bus.js ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Simple lightweight event bus with optional debug buffer
2
+ (function () {
3
+ const listeners = new Map(); // event -> Set<fn>
4
+ const debugBuffer = [];
5
+ const MAX_DEBUG = 300;
6
+ let debugEnabled = false;
7
+
8
+ function on(event, handler) {
9
+ if (!listeners.has(event)) listeners.set(event, new Set());
10
+ listeners.get(event).add(handler);
11
+ return () => off(event, handler);
12
+ }
13
+ function once(event, handler) {
14
+ const wrap = payload => {
15
+ off(event, wrap);
16
+ try {
17
+ handler(payload);
18
+ } catch (e) {
19
+ console.error(e);
20
+ }
21
+ };
22
+ return on(event, wrap);
23
+ }
24
+ function off(event, handler) {
25
+ const set = listeners.get(event);
26
+ if (set) {
27
+ set.delete(handler);
28
+ if (set.size === 0) listeners.delete(event);
29
+ }
30
+ }
31
+ function emit(event, payload) {
32
+ if (debugEnabled) {
33
+ debugBuffer.push({ ts: Date.now(), event, payload });
34
+ if (debugBuffer.length > MAX_DEBUG) debugBuffer.shift();
35
+ }
36
+ const set = listeners.get(event);
37
+ if (set) {
38
+ for (const h of [...set]) {
39
+ try {
40
+ h(payload);
41
+ } catch (e) {
42
+ console.error("Event handler error", event, e);
43
+ }
44
+ }
45
+ }
46
+ }
47
+ function enableDebug(v = true) {
48
+ debugEnabled = !!v;
49
+ }
50
+ function getDebug() {
51
+ return debugBuffer.slice();
52
+ }
53
+
54
+ window.kimiEventBus = { on, once, off, emit, enableDebug, getDebug };
55
+ })();
kimi-js/kimi-memory-system.js CHANGED
@@ -14,6 +14,21 @@ class KimiMemorySystem {
14
  important: "Important Events"
15
  };
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  // Patterns for automatic memory extraction (multilingual)
18
  this.extractionPatterns = {
19
  personal: [
@@ -216,11 +231,49 @@ class KimiMemorySystem {
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,12 +758,20 @@ class KimiMemorySystem {
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,8 +939,47 @@ class KimiMemorySystem {
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,6 +1332,101 @@ class KimiMemorySystem {
1232
  }
1233
  }
1234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1235
  // MEMORY RETRIEVAL FOR LLM
1236
  async getRelevantMemories(context = "", limit = 10) {
1237
  if (!this.memoryEnabled) return [];
@@ -1347,6 +1542,36 @@ class KimiMemorySystem {
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,9 +2183,135 @@ class KimiMemorySystem {
1958
  return false;
1959
  }
1960
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1961
  }
1962
 
1963
  window.KimiMemorySystem = KimiMemorySystem;
1964
  export default KimiMemorySystem;
1965
-
1966
- window.KimiMemorySystem = 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
  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
  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
  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
  }
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
  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
  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;
 
 
kimi-js/kimi-memory.js CHANGED
@@ -1,4 +1,8 @@
1
  // ===== KIMI MEMORY MANAGER =====
 
 
 
 
2
  class KimiMemory {
3
  constructor(database) {
4
  this.db = database;
@@ -107,10 +111,7 @@ class KimiMemory {
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();
 
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
  }
112
  }
113
 
114
+ // Legacy wrapper: prefer updateGlobalPersonalityUI() elsewhere.
 
 
 
115
  updateFavorabilityBar() {
116
  if (window.updateGlobalPersonalityUI) {
117
  window.updateGlobalPersonalityUI();
kimi-js/kimi-module.js CHANGED
@@ -330,25 +330,6 @@ function updateFavorabilityLabel(characterKey) {
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,7 +337,10 @@ async function updateGlobalPersonalityUI(characterKey = null) {
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");
 
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
  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");
kimi-js/kimi-personality-utils.js CHANGED
@@ -5,25 +5,7 @@
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
  })();
 
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
  })();
kimi-js/kimi-trait-sim.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Simple trait simulation harness for development tuning.
2
+ // Usage: window.kimiTraitSim.run([ { emotion: 'positive', text: 'love joke' }, ... ])
3
+ (function () {
4
+ async function run(sequence, character = null) {
5
+ if (!window.kimiEmotionSystem || !window.kimiEmotionSystem.db) {
6
+ console.warn("Emotion system not ready");
7
+ return;
8
+ }
9
+ const results = [];
10
+ for (const step of sequence) {
11
+ const emotion = step.emotion || window.kimiEmotionSystem.analyzeEmotion(step.text || "", "auto");
12
+ const traits = await window.kimiEmotionSystem.updatePersonalityFromEmotion(emotion, step.text || "", character);
13
+ results.push({ emotion, text: step.text || "", traits: { ...traits } });
14
+ }
15
+ console.table(results.map(r => ({ emotion: r.emotion, ...r.traits })));
16
+ return results;
17
+ }
18
+ window.kimiTraitSim = { run };
19
+ })();
kimi-js/kimi-utils.js CHANGED
@@ -901,11 +901,51 @@ class KimiVideoManager {
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,18 +1056,30 @@ class KimiVideoManager {
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,28 +1939,12 @@ class KimiVideoManager {
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,23 +2331,6 @@ class KimiUIStateManager {
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
 
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
  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
  }
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
  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
kimi-js/kimi-voices.js CHANGED
@@ -214,6 +214,7 @@ class KimiVoiceManager {
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,7 +388,8 @@ class KimiVoiceManager {
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,16 +873,35 @@ class KimiVoiceManager {
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,6 +1524,22 @@ class KimiVoiceManager {
1503
  autoStopDuration: this.autoStopDuration
1504
  };
1505
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1506
  }
1507
 
1508
  // Export for usage
 
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
  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
  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
  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