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