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