Spaces:
Running
Running
Upload 34 files
Browse files- CHANGELOG.md +10 -0
- index.html +3 -4
- kimi-css/kimi-style.css +88 -6
- kimi-js/kimi-config.js +31 -0
- kimi-js/kimi-constants.js +96 -34
- kimi-js/kimi-database.js +78 -16
- kimi-js/kimi-debug-utils.js +133 -0
- kimi-js/kimi-emotion-system.js +204 -38
- kimi-js/kimi-error-manager.js +41 -0
- kimi-js/kimi-llm-manager.js +80 -12
- kimi-js/kimi-main.js +1 -0
- kimi-js/kimi-memory-database-optimization.js +211 -0
- kimi-js/kimi-memory-system.js +416 -90
- kimi-js/kimi-memory.js +34 -10
- kimi-js/kimi-module.js +38 -93
- kimi-js/kimi-script.js +16 -3
- kimi-js/kimi-utils.js +35 -0
- kimi-js/kimi-videos.js +259 -69
- kimi-js/kimi-voices.js +106 -90
CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1 |
# Virtual Kimi App Changelog
|
2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
# [1.1.5.1] - 2025-09-04
|
4 |
|
5 |
### Bug Fixes
|
|
|
1 |
# Virtual Kimi App Changelog
|
2 |
|
3 |
+
# [1.1.6.1] - 2025-09-05
|
4 |
+
|
5 |
+
### Changed
|
6 |
+
|
7 |
+
- Improved text formatting in the chat window.
|
8 |
+
|
9 |
+
### Bug Fixes
|
10 |
+
|
11 |
+
- Fixed some issues.
|
12 |
+
|
13 |
# [1.1.5.1] - 2025-09-04
|
14 |
|
15 |
### Bug Fixes
|
index.html
CHANGED
@@ -1045,8 +1045,8 @@
|
|
1045 |
<h3><i class="fas fa-code"></i> Technical Information</h3>
|
1046 |
<div class="tech-info">
|
1047 |
<p><strong>Created date :</strong> July 16, 2025</p>
|
1048 |
-
<p><strong>Version :</strong> v1.1.
|
1049 |
-
<p><strong>Last update :</strong> September
|
1050 |
<p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
|
1051 |
API</p>
|
1052 |
<p><strong>Status :</strong> ✅ Stable and functional</p>
|
@@ -1066,17 +1066,16 @@
|
|
1066 |
Keep this order when adding new managers. -->
|
1067 |
<script src="dexie.min.js"></script>
|
1068 |
<script src="kimi-locale/i18n.js"></script>
|
1069 |
-
<script type="module" src="kimi-js/kimi-personality-utils.js"></script>
|
1070 |
<script type="module" src="kimi-js/kimi-utils.js"></script>
|
1071 |
<script type="module" src="kimi-js/kimi-main.js"></script>
|
1072 |
<script type="module" src="kimi-js/kimi-config.js"></script>
|
|
|
1073 |
<script type="module" src="kimi-js/kimi-error-manager.js"></script>
|
1074 |
<script type="module" src="kimi-js/kimi-security.js"></script>
|
1075 |
<script type="module" src="kimi-js/kimi-voices.js"></script>
|
1076 |
<script type="module" src="kimi-js/kimi-constants.js"></script>
|
1077 |
<script type="module" src="kimi-js/kimi-memory-ui.js"></script>
|
1078 |
<script type="module" src="kimi-js/kimi-appearance.js"></script>
|
1079 |
-
<script type="module" src="kimi-js/kimi-data-manager.js"></script>
|
1080 |
<script type="module" src="kimi-js/kimi-module.js"></script>
|
1081 |
<script type="module" src="kimi-js/kimi-script.js"></script>
|
1082 |
<script type="module" src="kimi-js/kimi-plugin-manager.js"></script>
|
|
|
1045 |
<h3><i class="fas fa-code"></i> Technical Information</h3>
|
1046 |
<div class="tech-info">
|
1047 |
<p><strong>Created date :</strong> July 16, 2025</p>
|
1048 |
+
<p><strong>Version :</strong> v1.1.6.1</p>
|
1049 |
+
<p><strong>Last update :</strong> September 05, 2025</p>
|
1050 |
<p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
|
1051 |
API</p>
|
1052 |
<p><strong>Status :</strong> ✅ Stable and functional</p>
|
|
|
1066 |
Keep this order when adding new managers. -->
|
1067 |
<script src="dexie.min.js"></script>
|
1068 |
<script src="kimi-locale/i18n.js"></script>
|
|
|
1069 |
<script type="module" src="kimi-js/kimi-utils.js"></script>
|
1070 |
<script type="module" src="kimi-js/kimi-main.js"></script>
|
1071 |
<script type="module" src="kimi-js/kimi-config.js"></script>
|
1072 |
+
<script type="module" src="kimi-js/kimi-debug-utils.js"></script>
|
1073 |
<script type="module" src="kimi-js/kimi-error-manager.js"></script>
|
1074 |
<script type="module" src="kimi-js/kimi-security.js"></script>
|
1075 |
<script type="module" src="kimi-js/kimi-voices.js"></script>
|
1076 |
<script type="module" src="kimi-js/kimi-constants.js"></script>
|
1077 |
<script type="module" src="kimi-js/kimi-memory-ui.js"></script>
|
1078 |
<script type="module" src="kimi-js/kimi-appearance.js"></script>
|
|
|
1079 |
<script type="module" src="kimi-js/kimi-module.js"></script>
|
1080 |
<script type="module" src="kimi-js/kimi-script.js"></script>
|
1081 |
<script type="module" src="kimi-js/kimi-plugin-manager.js"></script>
|
kimi-css/kimi-style.css
CHANGED
@@ -965,8 +965,10 @@ body {
|
|
965 |
left: 50%;
|
966 |
transform: translateX(-50%);
|
967 |
width: 80%;
|
968 |
-
max-width:
|
|
|
969 |
max-height: 400px;
|
|
|
970 |
padding: 15px;
|
971 |
background: var(--transcript-bg);
|
972 |
backdrop-filter: blur(10px);
|
@@ -979,6 +981,7 @@ body {
|
|
979 |
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
980 |
overflow-y: auto;
|
981 |
overflow-x: hidden;
|
|
|
982 |
}
|
983 |
|
984 |
.transcript-container.visible {
|
@@ -1091,11 +1094,20 @@ button:focus,
|
|
1091 |
padding: 12px 16px;
|
1092 |
border-radius: 18px;
|
1093 |
font-size: 0.95rem;
|
1094 |
-
line-height: 1.3;
|
1095 |
-
white-space: pre-line
|
1096 |
animation: messageSlideIn 0.3s ease-out;
|
1097 |
}
|
1098 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1099 |
.message.user {
|
1100 |
align-self: flex-end;
|
1101 |
background: var(--chat-message-user-bg);
|
@@ -1693,10 +1705,13 @@ button:focus,
|
|
1693 |
.transcript-container {
|
1694 |
bottom: 200px;
|
1695 |
width: 90%;
|
|
|
|
|
1696 |
}
|
1697 |
|
1698 |
#transcript {
|
1699 |
font-size: 1rem;
|
|
|
1700 |
}
|
1701 |
|
1702 |
.message {
|
@@ -1769,12 +1784,17 @@ button:focus,
|
|
1769 |
}
|
1770 |
|
1771 |
.transcript-container {
|
1772 |
-
bottom:
|
1773 |
-
width:
|
|
|
|
|
|
|
|
|
1774 |
}
|
1775 |
|
1776 |
#transcript {
|
1777 |
-
font-size:
|
|
|
1778 |
}
|
1779 |
|
1780 |
.message {
|
@@ -1802,6 +1822,68 @@ button:focus,
|
|
1802 |
}
|
1803 |
}
|
1804 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1805 |
/* Animation pour l'indicateur d'attente */
|
1806 |
.waiting-indicator {
|
1807 |
display: block;
|
|
|
965 |
left: 50%;
|
966 |
transform: translateX(-50%);
|
967 |
width: 80%;
|
968 |
+
max-width: 580px;
|
969 |
+
min-width: 280px;
|
970 |
max-height: 400px;
|
971 |
+
min-height: 100px;
|
972 |
padding: 15px;
|
973 |
background: var(--transcript-bg);
|
974 |
backdrop-filter: blur(10px);
|
|
|
981 |
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
982 |
overflow-y: auto;
|
983 |
overflow-x: hidden;
|
984 |
+
z-index: 100;
|
985 |
}
|
986 |
|
987 |
.transcript-container.visible {
|
|
|
1094 |
padding: 12px 16px;
|
1095 |
border-radius: 18px;
|
1096 |
font-size: 0.95rem;
|
1097 |
+
line-height: 1.3; /* Espacement entre lignes dans un même paragraphe */
|
1098 |
+
white-space: normal; /* Plus besoin de pre-line avec les <p> */
|
1099 |
animation: messageSlideIn 0.3s ease-out;
|
1100 |
}
|
1101 |
|
1102 |
+
/* Contrôle de l'espacement entre paragraphes (sauts de ligne) */
|
1103 |
+
.message p {
|
1104 |
+
margin: 0 0 0.8em 0; /* Espacement entre paragraphes */
|
1105 |
+
}
|
1106 |
+
|
1107 |
+
.message p:last-child {
|
1108 |
+
margin-bottom: 0; /* Pas d'espacement après le dernier paragraphe */
|
1109 |
+
}
|
1110 |
+
|
1111 |
.message.user {
|
1112 |
align-self: flex-end;
|
1113 |
background: var(--chat-message-user-bg);
|
|
|
1705 |
.transcript-container {
|
1706 |
bottom: 200px;
|
1707 |
width: 90%;
|
1708 |
+
max-height: 400px;
|
1709 |
+
padding: 12px;
|
1710 |
}
|
1711 |
|
1712 |
#transcript {
|
1713 |
font-size: 1rem;
|
1714 |
+
line-height: 1.4;
|
1715 |
}
|
1716 |
|
1717 |
.message {
|
|
|
1784 |
}
|
1785 |
|
1786 |
.transcript-container {
|
1787 |
+
bottom: 180px;
|
1788 |
+
width: 95%;
|
1789 |
+
max-height: 300px;
|
1790 |
+
padding: 10px;
|
1791 |
+
left: 50%;
|
1792 |
+
transform: translateX(-50%);
|
1793 |
}
|
1794 |
|
1795 |
#transcript {
|
1796 |
+
font-size: 0.9rem;
|
1797 |
+
line-height: 1.3;
|
1798 |
}
|
1799 |
|
1800 |
.message {
|
|
|
1822 |
}
|
1823 |
}
|
1824 |
|
1825 |
+
/* ===== TABLET SPECIFIC STYLES ===== */
|
1826 |
+
@media (min-width: 601px) and (max-width: 1024px) {
|
1827 |
+
.transcript-container {
|
1828 |
+
bottom: 200px;
|
1829 |
+
width: 85%;
|
1830 |
+
max-height: 350px;
|
1831 |
+
padding: 15px;
|
1832 |
+
max-width: 500px;
|
1833 |
+
}
|
1834 |
+
|
1835 |
+
#transcript {
|
1836 |
+
font-size: 1.1rem;
|
1837 |
+
line-height: 1.4;
|
1838 |
+
}
|
1839 |
+
}
|
1840 |
+
|
1841 |
+
/* ===== VERY SMALL SCREENS ===== */
|
1842 |
+
@media (max-width: 400px) {
|
1843 |
+
.transcript-container {
|
1844 |
+
bottom: 160px;
|
1845 |
+
width: 98%;
|
1846 |
+
max-height: 250px;
|
1847 |
+
padding: 8px;
|
1848 |
+
border-radius: 8px;
|
1849 |
+
}
|
1850 |
+
|
1851 |
+
#transcript {
|
1852 |
+
font-size: 0.85rem;
|
1853 |
+
line-height: 1.3;
|
1854 |
+
}
|
1855 |
+
}
|
1856 |
+
|
1857 |
+
/* ===== VERY LARGE SCREENS ===== */
|
1858 |
+
@media (min-width: 1400px) {
|
1859 |
+
.transcript-container {
|
1860 |
+
max-width: 600px;
|
1861 |
+
max-height: 500px;
|
1862 |
+
padding: 20px;
|
1863 |
+
bottom: 200px;
|
1864 |
+
}
|
1865 |
+
|
1866 |
+
#transcript {
|
1867 |
+
font-size: 1.3rem;
|
1868 |
+
line-height: 1.5;
|
1869 |
+
}
|
1870 |
+
}
|
1871 |
+
|
1872 |
+
/* ===== LANDSCAPE MODE ON MOBILE ===== */
|
1873 |
+
@media (max-width: 768px) and (orientation: landscape) {
|
1874 |
+
.transcript-container {
|
1875 |
+
bottom: 120px;
|
1876 |
+
max-height: 200px;
|
1877 |
+
width: 70%;
|
1878 |
+
max-width: 400px;
|
1879 |
+
}
|
1880 |
+
|
1881 |
+
#transcript {
|
1882 |
+
font-size: 0.9rem;
|
1883 |
+
line-height: 1.3;
|
1884 |
+
}
|
1885 |
+
}
|
1886 |
+
|
1887 |
/* Animation pour l'indicateur d'attente */
|
1888 |
.waiting-indicator {
|
1889 |
display: block;
|
kimi-js/kimi-config.js
CHANGED
@@ -67,6 +67,16 @@ window.KIMI_CONFIG = {
|
|
67 |
NETWORK_ERROR: "Network error"
|
68 |
},
|
69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
// Available themes
|
71 |
THEMES: {
|
72 |
dark: "Dark Night",
|
@@ -109,6 +119,27 @@ window.KIMI_CONFIG.get = function (path, fallback = null) {
|
|
109 |
}
|
110 |
};
|
111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
window.KIMI_CONFIG.validate = function (value, type) {
|
113 |
try {
|
114 |
const range = this.RANGES[type];
|
|
|
67 |
NETWORK_ERROR: "Network error"
|
68 |
},
|
69 |
|
70 |
+
// Debug configuration (centralized)
|
71 |
+
DEBUG: {
|
72 |
+
ENABLED: false, // Master debug switch
|
73 |
+
VOICE: false, // Voice system debug
|
74 |
+
VIDEO: false, // Video system debug
|
75 |
+
MEMORY: false, // Memory system debug
|
76 |
+
API: false, // API calls debug
|
77 |
+
SYNC: false // Synchronization debug
|
78 |
+
},
|
79 |
+
|
80 |
// Available themes
|
81 |
THEMES: {
|
82 |
dark: "Dark Night",
|
|
|
119 |
}
|
120 |
};
|
121 |
|
122 |
+
// Centralized debug logging utility
|
123 |
+
window.KIMI_CONFIG.debugLog = function (category, message, ...args) {
|
124 |
+
if (!this.DEBUG.ENABLED) return;
|
125 |
+
|
126 |
+
const categoryEnabled = category === "GENERAL" ? true : this.DEBUG[category];
|
127 |
+
if (!categoryEnabled) return;
|
128 |
+
|
129 |
+
const prefix =
|
130 |
+
category === "GENERAL"
|
131 |
+
? "🔧"
|
132 |
+
: {
|
133 |
+
VOICE: "🎤",
|
134 |
+
VIDEO: "🎬",
|
135 |
+
MEMORY: "💾",
|
136 |
+
API: "📡",
|
137 |
+
SYNC: "🔄"
|
138 |
+
}[category] || "🔧";
|
139 |
+
|
140 |
+
console.log(`${prefix} [${category}]`, message, ...args);
|
141 |
+
};
|
142 |
+
|
143 |
window.KIMI_CONFIG.validate = function (value, type) {
|
144 |
try {
|
145 |
const range = this.RANGES[type];
|
kimi-js/kimi-constants.js
CHANGED
@@ -262,7 +262,10 @@ window.KIMI_CONTEXT_NEGATIVE = {
|
|
262 |
"idiote",
|
263 |
"stupide",
|
264 |
"con",
|
|
|
|
|
265 |
"connard",
|
|
|
266 |
"salope"
|
267 |
],
|
268 |
es: [
|
@@ -1018,26 +1021,56 @@ window.KIMI_TRAIT_ADJUSTMENT = {
|
|
1018 |
}
|
1019 |
};
|
1020 |
|
1021 |
-
//
|
|
|
|
|
|
|
1022 |
window.getEmotionKeywords = function (emotion, language = "en") {
|
|
|
|
|
|
|
|
|
|
|
|
|
1023 |
const keywords = window.KIMI_CONTEXT_KEYWORDS?.[language] || window.KIMI_CONTEXT_KEYWORDS?.en || {};
|
1024 |
-
|
|
|
|
|
|
|
1025 |
};
|
1026 |
|
1027 |
-
// Helper function to get personality keywords with fallback
|
1028 |
window.getPersonalityKeywords = function (trait, type, language = "en") {
|
|
|
|
|
|
|
|
|
|
|
|
|
1029 |
const keywords = window.KIMI_PERSONALITY_KEYWORDS?.[language] || window.KIMI_PERSONALITY_KEYWORDS?.en || {};
|
1030 |
-
|
|
|
|
|
|
|
1031 |
};
|
1032 |
|
1033 |
-
// Helper function to get positive/negative context words
|
1034 |
window.getContextWords = function (type, language = "en") {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1035 |
if (type === "positive") {
|
1036 |
-
|
1037 |
} else if (type === "negative") {
|
1038 |
-
|
1039 |
}
|
1040 |
-
|
|
|
|
|
1041 |
};
|
1042 |
|
1043 |
// Helper function to validate character traits
|
@@ -1045,18 +1078,30 @@ window.validateCharacterTraits = function (traits) {
|
|
1045 |
const validatedTraits = {};
|
1046 |
const requiredTraits = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
|
1047 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1048 |
for (const trait of requiredTraits) {
|
1049 |
const value = traits[trait];
|
1050 |
if (typeof value === "number" && value >= 0 && value <= 100) {
|
1051 |
validatedTraits[trait] = value;
|
1052 |
} else {
|
1053 |
-
|
1054 |
-
if (window.KimiEmotionSystem) {
|
1055 |
-
const emotionSystem = new window.KimiEmotionSystem();
|
1056 |
-
validatedTraits[trait] = emotionSystem.TRAIT_DEFAULTS[trait] || 50;
|
1057 |
-
} else {
|
1058 |
-
validatedTraits[trait] = 50;
|
1059 |
-
}
|
1060 |
}
|
1061 |
}
|
1062 |
|
@@ -1079,13 +1124,14 @@ window.KIMI_CHARACTERS = {
|
|
1079 |
name: "Kimi",
|
1080 |
summary: "Dreamy, intuitive, captivated by cosmic metaphors",
|
1081 |
traits: {
|
1082 |
-
//
|
1083 |
-
|
1084 |
-
|
1085 |
-
|
1086 |
-
|
1087 |
-
|
1088 |
-
|
|
|
1089 |
},
|
1090 |
age: 23,
|
1091 |
birthplace: "Tokyo, Japan",
|
@@ -1196,22 +1242,38 @@ window.KIMI_EMOTIONAL_RESPONSES = {
|
|
1196 |
cold: ["Hello.", "Yes?", "What do you want?", "I am here.", "How can I help you?"]
|
1197 |
};
|
1198 |
|
1199 |
-
// Function to get localized emotional responses from translation files
|
1200 |
window.getLocalizedEmotionalResponse = function (type, index = null) {
|
|
|
|
|
|
|
|
|
|
|
|
|
1201 |
if (!window.kimiI18nManager) {
|
1202 |
// Fallback to default responses if i18n not available
|
1203 |
-
|
1204 |
-
|
1205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1206 |
}
|
1207 |
|
1208 |
-
const count =
|
1209 |
-
const randomIndex = index !== null ? index : Math.floor(Math.random() * count) + 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1210 |
|
1211 |
-
|
1212 |
-
|
1213 |
-
(window.KIMI_EMOTIONAL_RESPONSES[type]
|
1214 |
-
? window.KIMI_EMOTIONAL_RESPONSES[type][Math.floor(Math.random() * window.KIMI_EMOTIONAL_RESPONSES[type].length)]
|
1215 |
-
: "")
|
1216 |
-
);
|
1217 |
};
|
|
|
262 |
"idiote",
|
263 |
"stupide",
|
264 |
"con",
|
265 |
+
"conne",
|
266 |
+
"connasse",
|
267 |
"connard",
|
268 |
+
"pute",
|
269 |
"salope"
|
270 |
],
|
271 |
es: [
|
|
|
1021 |
}
|
1022 |
};
|
1023 |
|
1024 |
+
// Cached keyword lookups for performance
|
1025 |
+
const _keywordCache = new Map();
|
1026 |
+
|
1027 |
+
// Helper function to get emotion keywords with fallback and caching
|
1028 |
window.getEmotionKeywords = function (emotion, language = "en") {
|
1029 |
+
const cacheKey = `${emotion}-${language}`;
|
1030 |
+
|
1031 |
+
if (_keywordCache.has(cacheKey)) {
|
1032 |
+
return _keywordCache.get(cacheKey);
|
1033 |
+
}
|
1034 |
+
|
1035 |
const keywords = window.KIMI_CONTEXT_KEYWORDS?.[language] || window.KIMI_CONTEXT_KEYWORDS?.en || {};
|
1036 |
+
const result = keywords[emotion] || [];
|
1037 |
+
|
1038 |
+
_keywordCache.set(cacheKey, result);
|
1039 |
+
return result;
|
1040 |
};
|
1041 |
|
1042 |
+
// Helper function to get personality keywords with fallback and caching
|
1043 |
window.getPersonalityKeywords = function (trait, type, language = "en") {
|
1044 |
+
const cacheKey = `${trait}-${type}-${language}`;
|
1045 |
+
|
1046 |
+
if (_keywordCache.has(cacheKey)) {
|
1047 |
+
return _keywordCache.get(cacheKey);
|
1048 |
+
}
|
1049 |
+
|
1050 |
const keywords = window.KIMI_PERSONALITY_KEYWORDS?.[language] || window.KIMI_PERSONALITY_KEYWORDS?.en || {};
|
1051 |
+
const result = keywords[trait]?.[type] || [];
|
1052 |
+
|
1053 |
+
_keywordCache.set(cacheKey, result);
|
1054 |
+
return result;
|
1055 |
};
|
1056 |
|
1057 |
+
// Helper function to get positive/negative context words with caching
|
1058 |
window.getContextWords = function (type, language = "en") {
|
1059 |
+
const cacheKey = `context-${type}-${language}`;
|
1060 |
+
|
1061 |
+
if (_keywordCache.has(cacheKey)) {
|
1062 |
+
return _keywordCache.get(cacheKey);
|
1063 |
+
}
|
1064 |
+
|
1065 |
+
let result = [];
|
1066 |
if (type === "positive") {
|
1067 |
+
result = window.KIMI_CONTEXT_POSITIVE?.[language] || window.KIMI_CONTEXT_POSITIVE?.en || [];
|
1068 |
} else if (type === "negative") {
|
1069 |
+
result = window.KIMI_CONTEXT_NEGATIVE?.[language] || window.KIMI_CONTEXT_NEGATIVE?.en || [];
|
1070 |
}
|
1071 |
+
|
1072 |
+
_keywordCache.set(cacheKey, result);
|
1073 |
+
return result;
|
1074 |
};
|
1075 |
|
1076 |
// Helper function to validate character traits
|
|
|
1078 |
const validatedTraits = {};
|
1079 |
const requiredTraits = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
|
1080 |
|
1081 |
+
// Use centralized trait defaults API
|
1082 |
+
const getDefaults = () => {
|
1083 |
+
if (window.getTraitDefaults) {
|
1084 |
+
return window.getTraitDefaults();
|
1085 |
+
}
|
1086 |
+
// Fallback defaults that match KimiEmotionSystem.TRAIT_DEFAULTS
|
1087 |
+
return {
|
1088 |
+
affection: 55,
|
1089 |
+
playfulness: 55,
|
1090 |
+
intelligence: 70,
|
1091 |
+
empathy: 75,
|
1092 |
+
humor: 60,
|
1093 |
+
romance: 50
|
1094 |
+
};
|
1095 |
+
};
|
1096 |
+
|
1097 |
+
const defaults = getDefaults();
|
1098 |
+
|
1099 |
for (const trait of requiredTraits) {
|
1100 |
const value = traits[trait];
|
1101 |
if (typeof value === "number" && value >= 0 && value <= 100) {
|
1102 |
validatedTraits[trait] = value;
|
1103 |
} else {
|
1104 |
+
validatedTraits[trait] = defaults[trait] || 50;
|
|
|
|
|
|
|
|
|
|
|
|
|
1105 |
}
|
1106 |
}
|
1107 |
|
|
|
1124 |
name: "Kimi",
|
1125 |
summary: "Dreamy, intuitive, captivated by cosmic metaphors",
|
1126 |
traits: {
|
1127 |
+
// Default character profile - MUST match KimiEmotionSystem.TRAIT_DEFAULTS exactly
|
1128 |
+
// Kimi is the default character, so her traits serve as the system's fallback values
|
1129 |
+
affection: 55, // Baseline neutral affection
|
1130 |
+
playfulness: 55, // Moderately playful baseline
|
1131 |
+
intelligence: 70, // Competent baseline intellect
|
1132 |
+
empathy: 75, // Warm & caring baseline
|
1133 |
+
humor: 60, // Mild sense of humor baseline
|
1134 |
+
romance: 50 // Neutral romance baseline (earned over time)
|
1135 |
},
|
1136 |
age: 23,
|
1137 |
birthplace: "Tokyo, Japan",
|
|
|
1242 |
cold: ["Hello.", "Yes?", "What do you want?", "I am here.", "How can I help you?"]
|
1243 |
};
|
1244 |
|
1245 |
+
// Function to get localized emotional responses from translation files (with better error handling)
|
1246 |
window.getLocalizedEmotionalResponse = function (type, index = null) {
|
1247 |
+
// Validate input
|
1248 |
+
if (!type || typeof type !== "string") {
|
1249 |
+
console.warn("getLocalizedEmotionalResponse: invalid type provided");
|
1250 |
+
return "";
|
1251 |
+
}
|
1252 |
+
|
1253 |
if (!window.kimiI18nManager) {
|
1254 |
// Fallback to default responses if i18n not available
|
1255 |
+
const responses = window.KIMI_EMOTIONAL_RESPONSES[type];
|
1256 |
+
if (!responses || !Array.isArray(responses) || responses.length === 0) {
|
1257 |
+
return "";
|
1258 |
+
}
|
1259 |
+
return responses[Math.floor(Math.random() * responses.length)];
|
1260 |
+
}
|
1261 |
+
|
1262 |
+
const responses = window.KIMI_EMOTIONAL_RESPONSES[type];
|
1263 |
+
if (!responses || !Array.isArray(responses)) {
|
1264 |
+
return "";
|
1265 |
}
|
1266 |
|
1267 |
+
const count = responses.length;
|
1268 |
+
const randomIndex = index !== null ? Math.max(1, Math.min(count, index)) : Math.floor(Math.random() * count) + 1;
|
1269 |
+
|
1270 |
+
const translatedResponse = window.kimiI18nManager.t(`emotional_response_${type}_${randomIndex}`);
|
1271 |
+
|
1272 |
+
// If translation exists and isn't the key itself, use it
|
1273 |
+
if (translatedResponse && translatedResponse !== `emotional_response_${type}_${randomIndex}`) {
|
1274 |
+
return translatedResponse;
|
1275 |
+
}
|
1276 |
|
1277 |
+
// Fallback to default responses
|
1278 |
+
return responses[Math.floor(Math.random() * count)];
|
|
|
|
|
|
|
|
|
1279 |
};
|
kimi-js/kimi-database.js
CHANGED
@@ -17,7 +17,7 @@ class KimiDatabase {
|
|
17 |
settings: "category",
|
18 |
personality: "[character+trait],character",
|
19 |
llmModels: "id",
|
20 |
-
memories: "++id,[character+category],character,timestamp,isActive"
|
21 |
})
|
22 |
.upgrade(async tx => {
|
23 |
try {
|
@@ -93,6 +93,36 @@ class KimiDatabase {
|
|
93 |
// Non-blocking: continue on error
|
94 |
}
|
95 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
}
|
97 |
|
98 |
async setConversationsBatch(conversationsArray) {
|
@@ -104,6 +134,12 @@ class KimiDatabase {
|
|
104 |
}
|
105 |
} catch (error) {
|
106 |
console.error("Error restoring conversations:", error);
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
}
|
108 |
}
|
109 |
|
@@ -116,6 +152,12 @@ class KimiDatabase {
|
|
116 |
}
|
117 |
} catch (error) {
|
118 |
console.error("Error restoring LLM models:", error);
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
}
|
120 |
}
|
121 |
|
@@ -124,6 +166,16 @@ class KimiDatabase {
|
|
124 |
return await this.db.memories.toArray();
|
125 |
} catch (error) {
|
126 |
console.warn("Error getting all memories:", error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
return [];
|
128 |
}
|
129 |
}
|
@@ -148,10 +200,16 @@ class KimiDatabase {
|
|
148 |
}
|
149 |
|
150 |
getUnifiedTraitDefaults() {
|
|
|
|
|
|
|
|
|
|
|
151 |
if (window.KimiEmotionSystem) {
|
152 |
const emotionSystem = new window.KimiEmotionSystem(this);
|
153 |
return emotionSystem.TRAIT_DEFAULTS;
|
154 |
}
|
|
|
155 |
return {
|
156 |
affection: 55,
|
157 |
playfulness: 55,
|
@@ -751,24 +809,23 @@ class KimiDatabase {
|
|
751 |
|
752 |
// Use unified defaults from emotion system
|
753 |
if (defaultValue === null) {
|
754 |
-
|
|
|
|
|
|
|
755 |
const emotionSystem = new window.KimiEmotionSystem(this);
|
756 |
defaultValue = emotionSystem.TRAIT_DEFAULTS[trait] || 50;
|
757 |
} else {
|
758 |
-
//
|
759 |
-
|
760 |
-
|
761 |
-
|
762 |
-
|
763 |
-
|
764 |
-
|
765 |
-
|
766 |
-
|
767 |
-
|
768 |
-
humor: 60,
|
769 |
-
romance: 50
|
770 |
-
}[trait] || 50;
|
771 |
-
}
|
772 |
}
|
773 |
}
|
774 |
|
@@ -1011,9 +1068,14 @@ class KimiDatabase {
|
|
1011 |
|
1012 |
// Validation stricte : empêcher NaN ou valeurs non numériques
|
1013 |
const getDefault = trait => {
|
|
|
|
|
|
|
|
|
1014 |
if (window.KimiEmotionSystem) {
|
1015 |
return new window.KimiEmotionSystem(this).TRAIT_DEFAULTS[trait] || 50;
|
1016 |
}
|
|
|
1017 |
const fallback = { affection: 55, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
|
1018 |
return fallback[trait] || 50;
|
1019 |
};
|
|
|
17 |
settings: "category",
|
18 |
personality: "[character+trait],character",
|
19 |
llmModels: "id",
|
20 |
+
memories: "++id,[character+category],character,timestamp,isActive,importance"
|
21 |
})
|
22 |
.upgrade(async tx => {
|
23 |
try {
|
|
|
93 |
// Non-blocking: continue on error
|
94 |
}
|
95 |
});
|
96 |
+
|
97 |
+
// Version 5: Clean schema with proper memory field defaults
|
98 |
+
this.db
|
99 |
+
.version(5)
|
100 |
+
.stores({
|
101 |
+
conversations: "++id,timestamp,favorability,character",
|
102 |
+
preferences: "key",
|
103 |
+
settings: "category",
|
104 |
+
personality: "[character+trait],character",
|
105 |
+
llmModels: "id",
|
106 |
+
memories: "++id,[character+category],character,timestamp,isActive,importance,accessCount"
|
107 |
+
})
|
108 |
+
.upgrade(async tx => {
|
109 |
+
try {
|
110 |
+
// Ensure all memories have required fields for compatibility
|
111 |
+
const memories = tx.table("memories");
|
112 |
+
const now = new Date().toISOString();
|
113 |
+
await memories.toCollection().modify(rec => {
|
114 |
+
if (rec.isActive == null) rec.isActive = true;
|
115 |
+
if (rec.importance == null) rec.importance = 0.5;
|
116 |
+
if (rec.accessCount == null) rec.accessCount = 0;
|
117 |
+
if (!rec.character) rec.character = "kimi";
|
118 |
+
if (!rec.createdAt) rec.createdAt = rec.timestamp || now;
|
119 |
+
if (!rec.lastAccess) rec.lastAccess = rec.timestamp || now;
|
120 |
+
});
|
121 |
+
console.log("✅ Database upgraded to v5: memory compatibility ensured");
|
122 |
+
} catch (e) {
|
123 |
+
console.warn("Database upgrade v5 non-critical error:", e);
|
124 |
+
}
|
125 |
+
});
|
126 |
}
|
127 |
|
128 |
async setConversationsBatch(conversationsArray) {
|
|
|
134 |
}
|
135 |
} catch (error) {
|
136 |
console.error("Error restoring conversations:", error);
|
137 |
+
// Log to error manager for tracking
|
138 |
+
if (window.kimiErrorManager) {
|
139 |
+
window.kimiErrorManager.logDatabaseError("restoreConversations", error, {
|
140 |
+
conversationCount: conversationsArray.length
|
141 |
+
});
|
142 |
+
}
|
143 |
}
|
144 |
}
|
145 |
|
|
|
152 |
}
|
153 |
} catch (error) {
|
154 |
console.error("Error restoring LLM models:", error);
|
155 |
+
// Log to error manager for tracking
|
156 |
+
if (window.kimiErrorManager) {
|
157 |
+
window.kimiErrorManager.logDatabaseError("setLLMModelsBatch", error, {
|
158 |
+
modelCount: modelsArray.length
|
159 |
+
});
|
160 |
+
}
|
161 |
}
|
162 |
}
|
163 |
|
|
|
166 |
return await this.db.memories.toArray();
|
167 |
} catch (error) {
|
168 |
console.warn("Error getting all memories:", error);
|
169 |
+
// Log to error manager for tracking
|
170 |
+
if (window.kimiErrorManager) {
|
171 |
+
const errorType = error.name === "SchemaError" ? "SchemaError" : "DatabaseError";
|
172 |
+
window.kimiErrorManager.logError(errorType, error, {
|
173 |
+
operation: "getAllMemories",
|
174 |
+
suggestion: error.message?.includes("not indexed")
|
175 |
+
? "Clear browser data to force schema upgrade"
|
176 |
+
: "Check database integrity"
|
177 |
+
});
|
178 |
+
}
|
179 |
return [];
|
180 |
}
|
181 |
}
|
|
|
200 |
}
|
201 |
|
202 |
getUnifiedTraitDefaults() {
|
203 |
+
// Use centralized API instead of hardcoded values
|
204 |
+
if (window.getTraitDefaults) {
|
205 |
+
return window.getTraitDefaults();
|
206 |
+
}
|
207 |
+
// Fallback: create new instance only if no global API available
|
208 |
if (window.KimiEmotionSystem) {
|
209 |
const emotionSystem = new window.KimiEmotionSystem(this);
|
210 |
return emotionSystem.TRAIT_DEFAULTS;
|
211 |
}
|
212 |
+
// Ultimate fallback (should never be reached in normal operation)
|
213 |
return {
|
214 |
affection: 55,
|
215 |
playfulness: 55,
|
|
|
809 |
|
810 |
// Use unified defaults from emotion system
|
811 |
if (defaultValue === null) {
|
812 |
+
// Use centralized API for trait defaults
|
813 |
+
if (window.getTraitDefaults) {
|
814 |
+
defaultValue = window.getTraitDefaults()[trait] || 50;
|
815 |
+
} else if (window.KimiEmotionSystem) {
|
816 |
const emotionSystem = new window.KimiEmotionSystem(this);
|
817 |
defaultValue = emotionSystem.TRAIT_DEFAULTS[trait] || 50;
|
818 |
} else {
|
819 |
+
// Ultimate fallback (hardcoded values - should be avoided)
|
820 |
+
defaultValue =
|
821 |
+
{
|
822 |
+
affection: 55,
|
823 |
+
playfulness: 55,
|
824 |
+
intelligence: 70,
|
825 |
+
empathy: 75,
|
826 |
+
humor: 60,
|
827 |
+
romance: 50
|
828 |
+
}[trait] || 50;
|
|
|
|
|
|
|
|
|
829 |
}
|
830 |
}
|
831 |
|
|
|
1068 |
|
1069 |
// Validation stricte : empêcher NaN ou valeurs non numériques
|
1070 |
const getDefault = trait => {
|
1071 |
+
// Use centralized API for consistency
|
1072 |
+
if (window.getTraitDefaults) {
|
1073 |
+
return window.getTraitDefaults()[trait] || 50;
|
1074 |
+
}
|
1075 |
if (window.KimiEmotionSystem) {
|
1076 |
return new window.KimiEmotionSystem(this).TRAIT_DEFAULTS[trait] || 50;
|
1077 |
}
|
1078 |
+
// Ultimate fallback (should be avoided)
|
1079 |
const fallback = { affection: 55, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
|
1080 |
return fallback[trait] || 50;
|
1081 |
};
|
kimi-js/kimi-debug-utils.js
ADDED
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// KIMI DEBUG UTILITIES
|
2 |
+
// Centralized debug management for production optimization
|
3 |
+
//
|
4 |
+
// USAGE:
|
5 |
+
// debugOn() - Enable all debug logs
|
6 |
+
// debugOff() - Disable all debug logs (production mode)
|
7 |
+
// debugStatus() - Show current debug configuration
|
8 |
+
// kimiDebugAll() - Complete debug dashboard (includes errors)
|
9 |
+
// kimiDiagnosDB() - Database schema diagnostics
|
10 |
+
//
|
11 |
+
// CATEGORIES:
|
12 |
+
// KimiDebugController.setDebugCategory("VIDEO", true)
|
13 |
+
// KimiDebugController.setDebugCategory("MEMORY", false)
|
14 |
+
// Available: VIDEO, VOICE, MEMORY, API, SYNC
|
15 |
+
|
16 |
+
// Global debug controller
|
17 |
+
window.KimiDebugController = {
|
18 |
+
// Enable/disable all debug features
|
19 |
+
setGlobalDebug(enabled) {
|
20 |
+
if (window.KIMI_CONFIG && window.KIMI_CONFIG.DEBUG) {
|
21 |
+
window.KIMI_CONFIG.DEBUG.ENABLED = enabled;
|
22 |
+
window.KIMI_CONFIG.DEBUG.VOICE = enabled;
|
23 |
+
window.KIMI_CONFIG.DEBUG.VIDEO = enabled;
|
24 |
+
window.KIMI_CONFIG.DEBUG.MEMORY = enabled;
|
25 |
+
window.KIMI_CONFIG.DEBUG.API = enabled;
|
26 |
+
window.KIMI_CONFIG.DEBUG.SYNC = enabled;
|
27 |
+
}
|
28 |
+
|
29 |
+
// Legacy flags (to be removed)
|
30 |
+
window.KIMI_DEBUG_SYNC = enabled;
|
31 |
+
window.KIMI_DEBUG_MEMORIES = enabled;
|
32 |
+
window.KIMI_DEBUG_API_AUDIT = enabled;
|
33 |
+
window.DEBUG_SAFE_LOGS = enabled;
|
34 |
+
|
35 |
+
console.log(`🔧 Global debug ${enabled ? "ENABLED" : "DISABLED"}`);
|
36 |
+
},
|
37 |
+
|
38 |
+
// Enable specific debug category
|
39 |
+
setDebugCategory(category, enabled) {
|
40 |
+
if (window.KIMI_CONFIG && window.KIMI_CONFIG.DEBUG) {
|
41 |
+
if (category in window.KIMI_CONFIG.DEBUG) {
|
42 |
+
window.KIMI_CONFIG.DEBUG[category] = enabled;
|
43 |
+
console.log(`🔧 Debug category ${category} ${enabled ? "ENABLED" : "DISABLED"}`);
|
44 |
+
}
|
45 |
+
}
|
46 |
+
|
47 |
+
// Video manager specific
|
48 |
+
if (category === "VIDEO" && window.kimiVideo) {
|
49 |
+
window.kimiVideo.setDebug(enabled);
|
50 |
+
}
|
51 |
+
},
|
52 |
+
|
53 |
+
// Production mode (all debug off)
|
54 |
+
setProductionMode() {
|
55 |
+
this.setGlobalDebug(false);
|
56 |
+
console.log("🚀 Production mode activated - all debug logs disabled");
|
57 |
+
},
|
58 |
+
|
59 |
+
// Development mode (selective debug on)
|
60 |
+
setDevelopmentMode() {
|
61 |
+
this.setGlobalDebug(true);
|
62 |
+
console.log("🛠️ Development mode activated - debug logs enabled");
|
63 |
+
},
|
64 |
+
|
65 |
+
// Get current debug status
|
66 |
+
getDebugStatus() {
|
67 |
+
const status = {
|
68 |
+
global: window.KIMI_CONFIG?.DEBUG?.ENABLED || false,
|
69 |
+
voice: window.KIMI_CONFIG?.DEBUG?.VOICE || false,
|
70 |
+
video: window.KIMI_CONFIG?.DEBUG?.VIDEO || false,
|
71 |
+
memory: window.KIMI_CONFIG?.DEBUG?.MEMORY || false,
|
72 |
+
api: window.KIMI_CONFIG?.DEBUG?.API || false,
|
73 |
+
sync: window.KIMI_CONFIG?.DEBUG?.SYNC || false
|
74 |
+
};
|
75 |
+
|
76 |
+
console.table(status);
|
77 |
+
return status;
|
78 |
+
}
|
79 |
+
};
|
80 |
+
|
81 |
+
// Quick shortcuts for console
|
82 |
+
window.debugOn = () => window.KimiDebugController.setDevelopmentMode();
|
83 |
+
window.debugOff = () => window.KimiDebugController.setProductionMode();
|
84 |
+
window.debugStatus = () => window.KimiDebugController.getDebugStatus();
|
85 |
+
|
86 |
+
// Integration with error manager for unified debugging
|
87 |
+
window.kimiDebugAll = () => {
|
88 |
+
console.group("🔧 Kimi Debug Dashboard");
|
89 |
+
window.KimiDebugController.getDebugStatus();
|
90 |
+
if (window.kimiErrorManager) {
|
91 |
+
window.kimiErrorManager.printErrorSummary();
|
92 |
+
}
|
93 |
+
console.groupEnd();
|
94 |
+
};
|
95 |
+
|
96 |
+
// Database diagnostics helper
|
97 |
+
window.kimiDiagnosDB = async () => {
|
98 |
+
console.group("🔍 Database Diagnostics");
|
99 |
+
try {
|
100 |
+
if (window.kimiDB) {
|
101 |
+
console.log("📊 Database version:", window.kimiDB.db.verno);
|
102 |
+
console.log("📋 Available tables:", Object.keys(window.kimiDB.db._dbSchema));
|
103 |
+
|
104 |
+
// Check memories table schema
|
105 |
+
const memoriesSchema = window.kimiDB.db._dbSchema.memories;
|
106 |
+
if (memoriesSchema) {
|
107 |
+
console.log("🧠 Memories schema:", memoriesSchema);
|
108 |
+
const hasCharacterIsActiveIndex = memoriesSchema.indexes?.some(
|
109 |
+
idx =>
|
110 |
+
idx.name === "[character+isActive]" ||
|
111 |
+
(idx.keyPath?.includes("character") && idx.keyPath?.includes("isActive"))
|
112 |
+
);
|
113 |
+
console.log("✅ [character+isActive] index:", hasCharacterIsActiveIndex ? "PRESENT" : "❌ MISSING");
|
114 |
+
|
115 |
+
if (!hasCharacterIsActiveIndex) {
|
116 |
+
console.warn(
|
117 |
+
"🚨 SOLUTION: Clear browser data (Application > Storage > Clear Site Data) to force schema upgrade"
|
118 |
+
);
|
119 |
+
}
|
120 |
+
}
|
121 |
+
} else {
|
122 |
+
console.warn("❌ Database not initialized yet");
|
123 |
+
}
|
124 |
+
} catch (error) {
|
125 |
+
console.error("Error during database diagnostics:", error);
|
126 |
+
}
|
127 |
+
console.groupEnd();
|
128 |
+
};
|
129 |
+
|
130 |
+
// Auto-initialize to production mode for performance
|
131 |
+
if (typeof window.KIMI_CONFIG !== "undefined") {
|
132 |
+
window.KimiDebugController.setProductionMode();
|
133 |
+
}
|
kimi-js/kimi-emotion-system.js
CHANGED
@@ -6,6 +6,11 @@ class KimiEmotionSystem {
|
|
6 |
this.db = database;
|
7 |
this.negativeStreaks = {};
|
8 |
|
|
|
|
|
|
|
|
|
|
|
9 |
// Unified emotion mappings
|
10 |
this.EMOTIONS = {
|
11 |
// Base emotions
|
@@ -26,23 +31,59 @@ class KimiEmotionSystem {
|
|
26 |
GOODBYE: "goodbye"
|
27 |
};
|
28 |
|
29 |
-
// Unified video context mapping
|
30 |
this.emotionToVideoCategory = {
|
|
|
31 |
positive: "speakingPositive",
|
32 |
negative: "speakingNegative",
|
33 |
neutral: "neutral",
|
|
|
|
|
34 |
dancing: "dancing",
|
35 |
listening: "listening",
|
|
|
|
|
36 |
romantic: "speakingPositive",
|
37 |
laughing: "speakingPositive",
|
38 |
surprise: "speakingPositive",
|
39 |
confident: "speakingPositive",
|
40 |
-
shy: "neutral",
|
41 |
flirtatious: "speakingPositive",
|
42 |
kiss: "speakingPositive",
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
};
|
45 |
|
|
|
|
|
|
|
|
|
46 |
// Unified trait defaults - Balanced for progressive experience
|
47 |
this.TRAIT_DEFAULTS = {
|
48 |
affection: 55, // Baseline neutral affection
|
@@ -81,7 +122,152 @@ class KimiEmotionSystem {
|
|
81 |
intelligence: { posFactor: 0.35, negFactor: 0.55, streakPenaltyAfter: 2, maxStep: 1.2 }
|
82 |
};
|
83 |
}
|
84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
// ===== UNIFIED EMOTION ANALYSIS =====
|
86 |
analyzeEmotion(text, lang = "auto") {
|
87 |
if (!text || typeof text !== "string") return this.EMOTIONS.NEUTRAL;
|
@@ -358,8 +544,10 @@ class KimiEmotionSystem {
|
|
358 |
const prep = this._preparePersistTrait(trait, current, candValue, selectedCharacter);
|
359 |
if (prep.shouldPersist) toPersist[trait] = prep.value;
|
360 |
}
|
|
|
|
|
361 |
if (Object.keys(toPersist).length > 0) {
|
362 |
-
|
363 |
}
|
364 |
|
365 |
return updatedTraits;
|
@@ -467,8 +655,6 @@ class KimiEmotionSystem {
|
|
467 |
}
|
468 |
}
|
469 |
|
470 |
-
// Affection stays as independently adjusted by keywords & emotion (no derived override)
|
471 |
-
|
472 |
// Flush pending updates in a single batch write to avoid overwrites
|
473 |
if (Object.keys(pendingUpdates).length > 0) {
|
474 |
// Apply smoothing/threshold per trait (read current values)
|
@@ -484,21 +670,6 @@ class KimiEmotionSystem {
|
|
484 |
}
|
485 |
}
|
486 |
|
487 |
-
// ===== UNIFIED VIDEO CONTEXT MAPPING =====
|
488 |
-
mapEmotionToVideoCategory(emotion) {
|
489 |
-
return this.emotionToVideoCategory[emotion] || "neutral";
|
490 |
-
}
|
491 |
-
|
492 |
-
// ===== VALIDATION SYSTEM =====
|
493 |
-
validateEmotion(emotion) {
|
494 |
-
const validEmotions = Object.values(this.EMOTIONS);
|
495 |
-
if (!validEmotions.includes(emotion)) {
|
496 |
-
console.warn(`Invalid emotion detected: ${emotion}, falling back to neutral`);
|
497 |
-
return this.EMOTIONS.NEUTRAL;
|
498 |
-
}
|
499 |
-
return emotion;
|
500 |
-
}
|
501 |
-
|
502 |
validatePersonalityTrait(trait, value) {
|
503 |
if (typeof value !== "number" || value < 0 || value > 100) {
|
504 |
console.warn(`Invalid trait value for ${trait}: ${value}, using default`);
|
@@ -803,36 +974,31 @@ window.refreshPersonalityAverageUI = async function (characterKey = null) {
|
|
803 |
export default KimiEmotionSystem;
|
804 |
|
805 |
// ===== BACKWARD COMPATIBILITY LAYER =====
|
806 |
-
//
|
807 |
-
|
808 |
if (!window.kimiEmotionSystem) {
|
809 |
window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB);
|
810 |
}
|
811 |
-
return window.kimiEmotionSystem
|
|
|
|
|
|
|
|
|
|
|
812 |
};
|
813 |
|
814 |
// Replace the old updatePersonalityTraitsFromEmotion function
|
815 |
window.updatePersonalityTraitsFromEmotion = async function (emotion, text) {
|
816 |
-
|
817 |
-
window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB);
|
818 |
-
}
|
819 |
-
|
820 |
-
const updatedTraits = await window.kimiEmotionSystem.updatePersonalityFromEmotion(emotion, text);
|
821 |
-
|
822 |
return updatedTraits;
|
823 |
};
|
824 |
|
825 |
// Replace getPersonalityAverage function
|
826 |
window.getPersonalityAverage = function (traits) {
|
827 |
-
|
828 |
-
window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB);
|
829 |
-
}
|
830 |
-
return window.kimiEmotionSystem.calculatePersonalityAverage(traits);
|
831 |
};
|
832 |
|
833 |
// Unified trait defaults accessor
|
834 |
window.getTraitDefaults = function () {
|
835 |
-
|
836 |
-
const temp = new KimiEmotionSystem(window.kimiDB);
|
837 |
-
return temp.TRAIT_DEFAULTS;
|
838 |
};
|
|
|
6 |
this.db = database;
|
7 |
this.negativeStreaks = {};
|
8 |
|
9 |
+
// Debouncing system for personality updates
|
10 |
+
this._personalityUpdateQueue = {};
|
11 |
+
this._personalityUpdateTimer = null;
|
12 |
+
this._personalityUpdateDelay = 300; // ms
|
13 |
+
|
14 |
// Unified emotion mappings
|
15 |
this.EMOTIONS = {
|
16 |
// Base emotions
|
|
|
31 |
GOODBYE: "goodbye"
|
32 |
};
|
33 |
|
34 |
+
// Unified video context mapping - CENTRALIZED SOURCE OF TRUTH
|
35 |
this.emotionToVideoCategory = {
|
36 |
+
// Base emotional states
|
37 |
positive: "speakingPositive",
|
38 |
negative: "speakingNegative",
|
39 |
neutral: "neutral",
|
40 |
+
|
41 |
+
// Special contexts (always take priority)
|
42 |
dancing: "dancing",
|
43 |
listening: "listening",
|
44 |
+
|
45 |
+
// Specific emotions mapped to appropriate categories
|
46 |
romantic: "speakingPositive",
|
47 |
laughing: "speakingPositive",
|
48 |
surprise: "speakingPositive",
|
49 |
confident: "speakingPositive",
|
|
|
50 |
flirtatious: "speakingPositive",
|
51 |
kiss: "speakingPositive",
|
52 |
+
|
53 |
+
// Neutral/subdued emotions
|
54 |
+
shy: "neutral",
|
55 |
+
goodbye: "neutral",
|
56 |
+
|
57 |
+
// Explicit context mappings (for compatibility)
|
58 |
+
speaking: "speakingPositive", // Generic speaking defaults to positive
|
59 |
+
speakingPositive: "speakingPositive",
|
60 |
+
speakingNegative: "speakingNegative"
|
61 |
+
};
|
62 |
+
|
63 |
+
// Emotion priority weights for conflict resolution
|
64 |
+
this.emotionPriorities = {
|
65 |
+
dancing: 10, // Maximum priority - immersive experience
|
66 |
+
kiss: 9, // Very high - intimate moment
|
67 |
+
romantic: 8, // High - emotional connection
|
68 |
+
listening: 7, // High - active interaction
|
69 |
+
flirtatious: 6, // Medium-high - playful interaction
|
70 |
+
laughing: 6, // Medium-high - positive expression
|
71 |
+
surprise: 5, // Medium - reaction
|
72 |
+
confident: 5, // Medium - personality expression
|
73 |
+
speaking: 4, // Medium-low - generic speaking context
|
74 |
+
positive: 4, // Medium-low - general positive
|
75 |
+
negative: 4, // Medium-low - general negative
|
76 |
+
neutral: 3, // Low - default state
|
77 |
+
shy: 3, // Low - subdued state
|
78 |
+
goodbye: 2, // Very low - transitional
|
79 |
+
speakingPositive: 4, // Medium-low - for consistency
|
80 |
+
speakingNegative: 4 // Medium-low - for consistency
|
81 |
};
|
82 |
|
83 |
+
// Context/emotion validation system for system integrity
|
84 |
+
this.validContexts = ["dancing", "listening", "speaking", "speakingPositive", "speakingNegative", "neutral"];
|
85 |
+
this.validEmotions = Object.values(this.EMOTIONS);
|
86 |
+
|
87 |
// Unified trait defaults - Balanced for progressive experience
|
88 |
this.TRAIT_DEFAULTS = {
|
89 |
affection: 55, // Baseline neutral affection
|
|
|
122 |
intelligence: { posFactor: 0.35, negFactor: 0.55, streakPenaltyAfter: 2, maxStep: 1.2 }
|
123 |
};
|
124 |
}
|
125 |
+
|
126 |
+
// ===== DEBOUNCED PERSONALITY UPDATE SYSTEM =====
|
127 |
+
_debouncedPersonalityUpdate(updates, character) {
|
128 |
+
// Merge with existing queued updates for this character
|
129 |
+
if (!this._personalityUpdateQueue[character]) {
|
130 |
+
this._personalityUpdateQueue[character] = {};
|
131 |
+
}
|
132 |
+
Object.assign(this._personalityUpdateQueue[character], updates);
|
133 |
+
|
134 |
+
// Clear existing timer and set new one
|
135 |
+
if (this._personalityUpdateTimer) {
|
136 |
+
clearTimeout(this._personalityUpdateTimer);
|
137 |
+
}
|
138 |
+
|
139 |
+
this._personalityUpdateTimer = setTimeout(async () => {
|
140 |
+
try {
|
141 |
+
const allUpdates = { ...this._personalityUpdateQueue };
|
142 |
+
this._personalityUpdateQueue = {};
|
143 |
+
this._personalityUpdateTimer = null;
|
144 |
+
|
145 |
+
// Process all queued updates
|
146 |
+
for (const [char, traits] of Object.entries(allUpdates)) {
|
147 |
+
if (Object.keys(traits).length > 0) {
|
148 |
+
await this.db.setPersonalityBatch(traits, char);
|
149 |
+
|
150 |
+
// Emit unified personality update event
|
151 |
+
if (typeof window !== "undefined" && window.dispatchEvent) {
|
152 |
+
window.dispatchEvent(
|
153 |
+
new CustomEvent("personality:updated", {
|
154 |
+
detail: { character: char, traits: traits }
|
155 |
+
})
|
156 |
+
);
|
157 |
+
}
|
158 |
+
}
|
159 |
+
}
|
160 |
+
} catch (error) {
|
161 |
+
console.error("Error in debounced personality update:", error);
|
162 |
+
}
|
163 |
+
}, this._personalityUpdateDelay);
|
164 |
+
}
|
165 |
+
|
166 |
+
// ===== CENTRALIZED VALIDATION SYSTEM =====
|
167 |
+
validateContext(context) {
|
168 |
+
if (!context || typeof context !== "string") return "neutral";
|
169 |
+
const normalized = context.toLowerCase().trim();
|
170 |
+
|
171 |
+
// Check if it's a valid context
|
172 |
+
if (this.validContexts.includes(normalized)) return normalized;
|
173 |
+
|
174 |
+
// Check if it's a valid emotion that can be mapped to context
|
175 |
+
if (this.emotionToVideoCategory[normalized]) return normalized;
|
176 |
+
|
177 |
+
return "neutral"; // Safe fallback
|
178 |
+
}
|
179 |
+
|
180 |
+
validateEmotion(emotion) {
|
181 |
+
if (!emotion || typeof emotion !== "string") return "neutral";
|
182 |
+
const normalized = emotion.toLowerCase().trim();
|
183 |
+
|
184 |
+
// Check if it's a valid emotion
|
185 |
+
if (this.validEmotions.includes(normalized)) return normalized;
|
186 |
+
|
187 |
+
// Check common aliases
|
188 |
+
const aliases = {
|
189 |
+
happy: "positive",
|
190 |
+
sad: "negative",
|
191 |
+
mad: "negative",
|
192 |
+
angry: "negative",
|
193 |
+
excited: "positive",
|
194 |
+
calm: "neutral",
|
195 |
+
romance: "romantic",
|
196 |
+
laugh: "laughing",
|
197 |
+
dance: "dancing",
|
198 |
+
// Speaking contexts as emotion aliases
|
199 |
+
speaking: "positive", // Generic speaking defaults to positive
|
200 |
+
speakingpositive: "positive",
|
201 |
+
speakingnegative: "negative"
|
202 |
+
};
|
203 |
+
|
204 |
+
if (aliases[normalized]) return aliases[normalized];
|
205 |
+
|
206 |
+
return "neutral"; // Safe fallback
|
207 |
+
}
|
208 |
+
|
209 |
+
validateVideoCategory(category) {
|
210 |
+
const validCategories = ["dancing", "listening", "speakingPositive", "speakingNegative", "neutral"];
|
211 |
+
if (!category || typeof category !== "string") return "neutral";
|
212 |
+
|
213 |
+
const normalized = category.toLowerCase().trim();
|
214 |
+
return validCategories.includes(normalized) ? normalized : "neutral";
|
215 |
+
}
|
216 |
+
|
217 |
+
// Enhanced emotion analysis with validation
|
218 |
+
analyzeEmotionValidated(text, lang = "auto") {
|
219 |
+
const rawEmotion = this.analyzeEmotion(text, lang);
|
220 |
+
return this.validateEmotion(rawEmotion);
|
221 |
+
}
|
222 |
+
|
223 |
+
// ===== UTILITY METHODS FOR SYSTEM INTEGRATION =====
|
224 |
+
// Centralized method to get video category for any emotion/context combination
|
225 |
+
getVideoCategory(emotionOrContext, traits = null) {
|
226 |
+
// Handle the case where we get both context and emotion (e.g., from determineCategory calls)
|
227 |
+
// Priority: Specific contexts > Specific emotions > Generic fallbacks
|
228 |
+
|
229 |
+
// Try context validation first for immediate context matches
|
230 |
+
let validated = this.validateContext(emotionOrContext);
|
231 |
+
if (validated !== "neutral" || emotionOrContext === "neutral") {
|
232 |
+
// Valid context found or explicitly neutral
|
233 |
+
const category = this.emotionToVideoCategory[validated] || "neutral";
|
234 |
+
return this.validateVideoCategory(category);
|
235 |
+
}
|
236 |
+
|
237 |
+
// If no valid context, try as emotion
|
238 |
+
validated = this.validateEmotion(emotionOrContext);
|
239 |
+
const category = this.emotionToVideoCategory[validated] || "neutral";
|
240 |
+
return this.validateVideoCategory(category);
|
241 |
+
} // Get priority weight for any emotion/context
|
242 |
+
getPriorityWeight(emotionOrContext) {
|
243 |
+
// Try context validation first, then emotion validation
|
244 |
+
let validated = this.validateContext(emotionOrContext);
|
245 |
+
if (validated === "neutral" && emotionOrContext !== "neutral") {
|
246 |
+
// If context validation gave neutral but input wasn't neutral, try as emotion
|
247 |
+
validated = this.validateEmotion(emotionOrContext);
|
248 |
+
}
|
249 |
+
|
250 |
+
return this.emotionPriorities[validated] || 3; // Default medium-low priority
|
251 |
+
}
|
252 |
+
|
253 |
+
// Check if an emotion/context should override current state
|
254 |
+
shouldOverride(newEmotion, currentEmotion, currentContext = null) {
|
255 |
+
const newPriority = this.getPriorityWeight(newEmotion);
|
256 |
+
const currentPriority = Math.max(this.getPriorityWeight(currentEmotion), this.getPriorityWeight(currentContext));
|
257 |
+
|
258 |
+
return newPriority > currentPriority;
|
259 |
+
}
|
260 |
+
|
261 |
+
// Utility to normalize and validate a complete emotion/context request
|
262 |
+
normalizeEmotionRequest(context, emotion, traits = null) {
|
263 |
+
return {
|
264 |
+
context: this.validateContext(context),
|
265 |
+
emotion: this.validateEmotion(emotion),
|
266 |
+
category: this.getVideoCategory(emotion || context, traits),
|
267 |
+
priority: this.getPriorityWeight(emotion || context)
|
268 |
+
};
|
269 |
+
}
|
270 |
+
|
271 |
// ===== UNIFIED EMOTION ANALYSIS =====
|
272 |
analyzeEmotion(text, lang = "auto") {
|
273 |
if (!text || typeof text !== "string") return this.EMOTIONS.NEUTRAL;
|
|
|
544 |
const prep = this._preparePersistTrait(trait, current, candValue, selectedCharacter);
|
545 |
if (prep.shouldPersist) toPersist[trait] = prep.value;
|
546 |
}
|
547 |
+
|
548 |
+
// Use debounced update instead of immediate DB write
|
549 |
if (Object.keys(toPersist).length > 0) {
|
550 |
+
this._debouncedPersonalityUpdate(toPersist, selectedCharacter);
|
551 |
}
|
552 |
|
553 |
return updatedTraits;
|
|
|
655 |
}
|
656 |
}
|
657 |
|
|
|
|
|
658 |
// Flush pending updates in a single batch write to avoid overwrites
|
659 |
if (Object.keys(pendingUpdates).length > 0) {
|
660 |
// Apply smoothing/threshold per trait (read current values)
|
|
|
670 |
}
|
671 |
}
|
672 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
673 |
validatePersonalityTrait(trait, value) {
|
674 |
if (typeof value !== "number" || value < 0 || value > 100) {
|
675 |
console.warn(`Invalid trait value for ${trait}: ${value}, using default`);
|
|
|
974 |
export default KimiEmotionSystem;
|
975 |
|
976 |
// ===== BACKWARD COMPATIBILITY LAYER =====
|
977 |
+
// Ensure single instance of KimiEmotionSystem (Singleton pattern)
|
978 |
+
function getKimiEmotionSystemInstance() {
|
979 |
if (!window.kimiEmotionSystem) {
|
980 |
window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB);
|
981 |
}
|
982 |
+
return window.kimiEmotionSystem;
|
983 |
+
}
|
984 |
+
|
985 |
+
// Replace the old kimiAnalyzeEmotion function
|
986 |
+
window.kimiAnalyzeEmotion = function (text, lang = "auto") {
|
987 |
+
return getKimiEmotionSystemInstance().analyzeEmotion(text, lang);
|
988 |
};
|
989 |
|
990 |
// Replace the old updatePersonalityTraitsFromEmotion function
|
991 |
window.updatePersonalityTraitsFromEmotion = async function (emotion, text) {
|
992 |
+
const updatedTraits = await getKimiEmotionSystemInstance().updatePersonalityFromEmotion(emotion, text);
|
|
|
|
|
|
|
|
|
|
|
993 |
return updatedTraits;
|
994 |
};
|
995 |
|
996 |
// Replace getPersonalityAverage function
|
997 |
window.getPersonalityAverage = function (traits) {
|
998 |
+
return getKimiEmotionSystemInstance().calculatePersonalityAverage(traits);
|
|
|
|
|
|
|
999 |
};
|
1000 |
|
1001 |
// Unified trait defaults accessor
|
1002 |
window.getTraitDefaults = function () {
|
1003 |
+
return getKimiEmotionSystemInstance().TRAIT_DEFAULTS;
|
|
|
|
|
1004 |
};
|
kimi-js/kimi-error-manager.js
CHANGED
@@ -169,6 +169,44 @@ class KimiErrorManager {
|
|
169 |
throw error;
|
170 |
}
|
171 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
}
|
173 |
|
174 |
// Create global instance
|
@@ -176,3 +214,6 @@ window.kimiErrorManager = new KimiErrorManager();
|
|
176 |
|
177 |
// Export class for manual instantiation if needed
|
178 |
window.KimiErrorManager = KimiErrorManager;
|
|
|
|
|
|
|
|
169 |
throw error;
|
170 |
}
|
171 |
}
|
172 |
+
|
173 |
+
// Debug helpers for development
|
174 |
+
getErrorSummary() {
|
175 |
+
const summary = {
|
176 |
+
totalErrors: this.errorLog.length,
|
177 |
+
critical: this.errorLog.filter(e => e.severity === "critical").length,
|
178 |
+
warning: this.errorLog.filter(e => e.severity === "warning").length,
|
179 |
+
recent: this.errorLog.filter(e => {
|
180 |
+
const errorTime = new Date(e.timestamp);
|
181 |
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
182 |
+
return errorTime > fiveMinutesAgo;
|
183 |
+
}).length,
|
184 |
+
types: [...new Set(this.errorLog.map(e => e.type))]
|
185 |
+
};
|
186 |
+
return summary;
|
187 |
+
}
|
188 |
+
|
189 |
+
printErrorSummary() {
|
190 |
+
const summary = this.getErrorSummary();
|
191 |
+
console.group("🔍 Kimi Error Manager Summary");
|
192 |
+
console.log(`📊 Total Errors: ${summary.totalErrors}`);
|
193 |
+
console.log(`🚨 Critical: ${summary.critical}`);
|
194 |
+
console.log(`⚠️ Warnings: ${summary.warning}`);
|
195 |
+
console.log(`⏰ Recent (5min): ${summary.recent}`);
|
196 |
+
console.log(`📋 Error Types:`, summary.types);
|
197 |
+
if (summary.totalErrors > 0) {
|
198 |
+
console.log(`💡 Use kimiErrorManager.getErrorLog() to see details`);
|
199 |
+
}
|
200 |
+
console.groupEnd();
|
201 |
+
return summary;
|
202 |
+
}
|
203 |
+
|
204 |
+
clearAndSummarize() {
|
205 |
+
const summary = this.getErrorSummary();
|
206 |
+
this.clearErrorLog();
|
207 |
+
console.log("🧹 Error log cleared. Previous summary:", summary);
|
208 |
+
return summary;
|
209 |
+
}
|
210 |
}
|
211 |
|
212 |
// Create global instance
|
|
|
214 |
|
215 |
// Export class for manual instantiation if needed
|
216 |
window.KimiErrorManager = KimiErrorManager;
|
217 |
+
|
218 |
+
// Global debugging helper
|
219 |
+
window.kimiDebugErrors = () => window.kimiErrorManager.printErrorSummary();
|
kimi-js/kimi-llm-manager.js
CHANGED
@@ -95,7 +95,9 @@ class KimiLLMManager {
|
|
95 |
try {
|
96 |
await this.refreshRemoteModels();
|
97 |
} catch (e) {
|
98 |
-
|
|
|
|
|
99 |
}
|
100 |
|
101 |
// Migration: prefer llmModelId; if legacy defaultLLMModel exists and llmModelId missing, migrate
|
@@ -282,19 +284,25 @@ class KimiLLMManager {
|
|
282 |
"\nUse these memories naturally in conversation to show you remember the user. Don't just repeat them verbatim.\n";
|
283 |
}
|
284 |
} catch (error) {
|
285 |
-
|
|
|
|
|
286 |
}
|
287 |
}
|
288 |
// Read per-character preference metrics so displayed counters reflect actual stored values
|
289 |
-
// Prefer the personality trait 'affection' where available (authoritative source)
|
290 |
const totalInteractions = Number(await this.db.getPreference(`totalInteractions_${character}`, 0)) || 0;
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
|
|
|
|
|
|
|
|
|
|
298 |
const lastInteraction = await this.db.getPreference(`lastInteraction_${character}`, "First time");
|
299 |
// Days together is computed and displayed in the UI (see `updateStats()` in `kimi-module.js`).
|
300 |
let daysTogether = 0;
|
@@ -407,7 +415,7 @@ class KimiLLMManager {
|
|
407 |
"",
|
408 |
"LEARNED PREFERENCES:",
|
409 |
`- Total interactions: ${totalInteractions}`,
|
410 |
-
`- Current
|
411 |
`- Last interaction: ${lastInteraction}`,
|
412 |
`- Days together: ${daysTogether}`,
|
413 |
"",
|
@@ -442,6 +450,12 @@ class KimiLLMManager {
|
|
442 |
this.personalityPrompt = await this.assemblePrompt("");
|
443 |
} catch (error) {
|
444 |
console.warn("Error refreshing memory context:", error);
|
|
|
|
|
|
|
|
|
|
|
|
|
445 |
}
|
446 |
}
|
447 |
|
@@ -459,7 +473,53 @@ class KimiLLMManager {
|
|
459 |
}
|
460 |
|
461 |
async chat(userMessage, options = {}) {
|
462 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
463 |
const llmSettings = {
|
464 |
temperature: await this.db.getPreference("llmTemperature", 0.9),
|
465 |
maxTokens: await this.db.getPreference("llmMaxTokens", 400),
|
@@ -518,6 +578,14 @@ class KimiLLMManager {
|
|
518 |
return await this.chatWithOpenAICompatibleStreaming(userMessage, onToken, opts);
|
519 |
} catch (error) {
|
520 |
console.error("Error during streaming chat:", error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
521 |
// Fallback to non-streaming if streaming fails
|
522 |
return await this.chat(userMessage, options);
|
523 |
}
|
|
|
95 |
try {
|
96 |
await this.refreshRemoteModels();
|
97 |
} catch (e) {
|
98 |
+
if (window.KIMI_CONFIG?.DEBUG?.API) {
|
99 |
+
console.warn("Unable to refresh remote models list:", e?.message || e);
|
100 |
+
}
|
101 |
}
|
102 |
|
103 |
// Migration: prefer llmModelId; if legacy defaultLLMModel exists and llmModelId missing, migrate
|
|
|
284 |
"\nUse these memories naturally in conversation to show you remember the user. Don't just repeat them verbatim.\n";
|
285 |
}
|
286 |
} catch (error) {
|
287 |
+
if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
|
288 |
+
console.warn("Error loading memories for personality:", error);
|
289 |
+
}
|
290 |
}
|
291 |
}
|
292 |
// Read per-character preference metrics so displayed counters reflect actual stored values
|
|
|
293 |
const totalInteractions = Number(await this.db.getPreference(`totalInteractions_${character}`, 0)) || 0;
|
294 |
+
|
295 |
+
// Get current personality average for relationship context (replacing old favorabilityLevel)
|
296 |
+
const currentPersonality = await this.db.getAllPersonalityTraits(character);
|
297 |
+
const relationshipLevel = window.getPersonalityAverage
|
298 |
+
? window.getPersonalityAverage(currentPersonality)
|
299 |
+
: (currentPersonality.affection +
|
300 |
+
currentPersonality.romance +
|
301 |
+
currentPersonality.empathy +
|
302 |
+
currentPersonality.playfulness +
|
303 |
+
currentPersonality.humor +
|
304 |
+
currentPersonality.intelligence) /
|
305 |
+
6;
|
306 |
const lastInteraction = await this.db.getPreference(`lastInteraction_${character}`, "First time");
|
307 |
// Days together is computed and displayed in the UI (see `updateStats()` in `kimi-module.js`).
|
308 |
let daysTogether = 0;
|
|
|
415 |
"",
|
416 |
"LEARNED PREFERENCES:",
|
417 |
`- Total interactions: ${totalInteractions}`,
|
418 |
+
`- Current relationship level: ${relationshipLevel.toFixed(1)}%`,
|
419 |
`- Last interaction: ${lastInteraction}`,
|
420 |
`- Days together: ${daysTogether}`,
|
421 |
"",
|
|
|
450 |
this.personalityPrompt = await this.assemblePrompt("");
|
451 |
} catch (error) {
|
452 |
console.warn("Error refreshing memory context:", error);
|
453 |
+
// Log to error manager for tracking memory context issues
|
454 |
+
if (window.kimiErrorManager) {
|
455 |
+
window.kimiErrorManager.logError("MemoryContextError", error, {
|
456 |
+
operation: "refreshMemoryContext"
|
457 |
+
});
|
458 |
+
}
|
459 |
}
|
460 |
}
|
461 |
|
|
|
473 |
}
|
474 |
|
475 |
async chat(userMessage, options = {}) {
|
476 |
+
// Use error manager wrapper for robust error handling
|
477 |
+
return (
|
478 |
+
window.kimiErrorManager?.wrapAsync(
|
479 |
+
async () => {
|
480 |
+
// Get LLM settings from individual preferences (FIXED: was using grouped settings)
|
481 |
+
const llmSettings = {
|
482 |
+
temperature: await this.db.getPreference("llmTemperature", 0.9),
|
483 |
+
maxTokens: await this.db.getPreference("llmMaxTokens", 400),
|
484 |
+
top_p: await this.db.getPreference("llmTopP", 0.9),
|
485 |
+
frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", 0.9),
|
486 |
+
presence_penalty: await this.db.getPreference("llmPresencePenalty", 0.8)
|
487 |
+
};
|
488 |
+
const temperature = typeof options.temperature === "number" ? options.temperature : llmSettings.temperature;
|
489 |
+
const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens;
|
490 |
+
const opts = { ...options, temperature, maxTokens };
|
491 |
+
try {
|
492 |
+
const provider = await this.db.getPreference("llmProvider", "openrouter");
|
493 |
+
if (provider === "openrouter") {
|
494 |
+
return await this.chatWithOpenRouter(userMessage, opts);
|
495 |
+
}
|
496 |
+
if (provider === "ollama") {
|
497 |
+
return await this.chatWithLocal(userMessage, opts);
|
498 |
+
}
|
499 |
+
return await this.chatWithOpenAICompatible(userMessage, opts);
|
500 |
+
} catch (error) {
|
501 |
+
console.error("Error during chat:", error);
|
502 |
+
if (error.message && error.message.includes("API")) {
|
503 |
+
return this.getFallbackResponse(userMessage, "api");
|
504 |
+
}
|
505 |
+
if ((error.message && error.message.includes("model")) || error.message.includes("model")) {
|
506 |
+
return this.getFallbackResponse(userMessage, "model");
|
507 |
+
}
|
508 |
+
if ((error.message && error.message.includes("connection")) || error.message.includes("network")) {
|
509 |
+
return this.getFallbackResponse(userMessage, "network");
|
510 |
+
}
|
511 |
+
return this.getFallbackResponse(userMessage);
|
512 |
+
}
|
513 |
+
},
|
514 |
+
{ operation: "chat", userMessageLength: userMessage?.length || 0 }
|
515 |
+
) ||
|
516 |
+
// Fallback if error manager not available
|
517 |
+
this.chatDirectly(userMessage, options)
|
518 |
+
);
|
519 |
+
}
|
520 |
+
|
521 |
+
// Fallback method without error manager wrapper
|
522 |
+
async chatDirectly(userMessage, options = {}) {
|
523 |
const llmSettings = {
|
524 |
temperature: await this.db.getPreference("llmTemperature", 0.9),
|
525 |
maxTokens: await this.db.getPreference("llmMaxTokens", 400),
|
|
|
578 |
return await this.chatWithOpenAICompatibleStreaming(userMessage, onToken, opts);
|
579 |
} catch (error) {
|
580 |
console.error("Error during streaming chat:", error);
|
581 |
+
// Log API error for tracking
|
582 |
+
if (window.kimiErrorManager) {
|
583 |
+
window.kimiErrorManager.logAPIError("streamingChat", error, {
|
584 |
+
provider: await this.db.getPreference("llmProvider", "openrouter").catch(() => "unknown"),
|
585 |
+
messageLength: userMessage?.length || 0,
|
586 |
+
options: opts
|
587 |
+
});
|
588 |
+
}
|
589 |
// Fallback to non-streaming if streaming fails
|
590 |
return await this.chat(userMessage, options);
|
591 |
}
|
kimi-js/kimi-main.js
CHANGED
@@ -5,6 +5,7 @@ import KimiLLMManager from "./kimi-llm-manager.js";
|
|
5 |
import KimiEmotionSystem from "./kimi-emotion-system.js";
|
6 |
|
7 |
// Expose module imports to legacy code paths that still rely on window
|
|
|
8 |
window.KimiProviderUtils = window.KimiProviderUtils || KimiProviderUtils;
|
9 |
window.KimiLLMManager = window.KimiLLMManager || KimiLLMManager;
|
10 |
window.KimiEmotionSystem = window.KimiEmotionSystem || KimiEmotionSystem;
|
|
|
5 |
import KimiEmotionSystem from "./kimi-emotion-system.js";
|
6 |
|
7 |
// Expose module imports to legacy code paths that still rely on window
|
8 |
+
// Ensure KimiProviderUtils is available (imported from kimi-utils.js)
|
9 |
window.KimiProviderUtils = window.KimiProviderUtils || KimiProviderUtils;
|
10 |
window.KimiLLMManager = window.KimiLLMManager || KimiLLMManager;
|
11 |
window.KimiEmotionSystem = window.KimiEmotionSystem || KimiEmotionSystem;
|
kimi-js/kimi-memory-database-optimization.js
ADDED
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// ===== KIMI MEMORY DATABASE OPTIMIZATION SUGGESTIONS =====
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Performance Optimization Guide for Kimi Memory System
|
5 |
+
*
|
6 |
+
* This file contains recommendations for database schema optimizations
|
7 |
+
* to improve query performance in the memory system.
|
8 |
+
*/
|
9 |
+
|
10 |
+
// RECOMMENDED DEXIE INDEX CONFIGURATION
|
11 |
+
// Add these indexes to your database schema for optimal performance
|
12 |
+
|
13 |
+
const RECOMMENDED_MEMORY_INDEXES = {
|
14 |
+
// Current schema (for reference)
|
15 |
+
current: "++id, character, category, type, timestamp, isActive, confidence, importance, keywords",
|
16 |
+
|
17 |
+
// OPTIMIZED schema with composite indexes
|
18 |
+
optimized: [
|
19 |
+
"++id", // Primary key (auto-increment)
|
20 |
+
|
21 |
+
// Single field indexes (existing)
|
22 |
+
"character",
|
23 |
+
"category",
|
24 |
+
"type",
|
25 |
+
"timestamp",
|
26 |
+
"isActive",
|
27 |
+
"confidence",
|
28 |
+
"importance",
|
29 |
+
"*keywords", // Multi-entry index for keyword array
|
30 |
+
|
31 |
+
// COMPOSITE INDEXES for frequent query patterns
|
32 |
+
"[character+isActive]", // Filter by character and active status
|
33 |
+
"[character+category]", // Get memories by character and category
|
34 |
+
"[character+category+isActive]", // Most common query pattern
|
35 |
+
"[character+timestamp]", // Chronological queries by character
|
36 |
+
"[isActive+importance]", // Get active memories by importance
|
37 |
+
"[isActive+timestamp]", // Recent active memories
|
38 |
+
"[character+type+isActive]", // Filter by character, type, and status
|
39 |
+
|
40 |
+
// Advanced composite indexes for complex queries
|
41 |
+
"[character+isActive+importance]", // Prioritized active memories by character
|
42 |
+
"[character+category+timestamp]" // Category-specific chronological queries
|
43 |
+
]
|
44 |
+
};
|
45 |
+
|
46 |
+
// QUERY OPTIMIZATION EXAMPLES
|
47 |
+
const OPTIMIZED_QUERY_PATTERNS = {
|
48 |
+
// BEFORE: Multiple filter operations
|
49 |
+
getAllMemoriesOld: `
|
50 |
+
db.memories
|
51 |
+
.where("character").equals(character)
|
52 |
+
.filter(m => m.isActive !== false)
|
53 |
+
.reverse()
|
54 |
+
.sortBy("timestamp")
|
55 |
+
`,
|
56 |
+
|
57 |
+
// AFTER: Use composite index
|
58 |
+
getAllMemoriesOptimized: `
|
59 |
+
db.memories
|
60 |
+
.where("[character+isActive]").equals([character, true])
|
61 |
+
.reverse()
|
62 |
+
.sortBy("timestamp")
|
63 |
+
`,
|
64 |
+
|
65 |
+
// BEFORE: Filter after retrieval
|
66 |
+
getMemoriesByCategoryOld: `
|
67 |
+
db.memories
|
68 |
+
.where("[character+category]").equals([character, category])
|
69 |
+
.and(m => m.isActive)
|
70 |
+
`,
|
71 |
+
|
72 |
+
// AFTER: Direct composite index
|
73 |
+
getMemoriesByCategoryOptimized: `
|
74 |
+
db.memories
|
75 |
+
.where("[character+category+isActive]").equals([character, category, true])
|
76 |
+
`,
|
77 |
+
|
78 |
+
// NEW: Efficient importance-based queries
|
79 |
+
getTopMemoriesByImportance: `
|
80 |
+
db.memories
|
81 |
+
.where("[character+isActive+importance]")
|
82 |
+
.between([character, true, 0.8], [character, true, 1.0])
|
83 |
+
.reverse()
|
84 |
+
.limit(10)
|
85 |
+
`
|
86 |
+
};
|
87 |
+
|
88 |
+
// PERFORMANCE MONITORING UTILITIES
|
89 |
+
class MemoryDatabaseProfiler {
|
90 |
+
constructor() {
|
91 |
+
this.queryTimes = new Map();
|
92 |
+
this.queryCount = new Map();
|
93 |
+
}
|
94 |
+
|
95 |
+
// Wrap database operations to measure performance
|
96 |
+
async profileQuery(queryName, queryFn) {
|
97 |
+
const start = performance.now();
|
98 |
+
const result = await queryFn();
|
99 |
+
const duration = performance.now() - start;
|
100 |
+
|
101 |
+
// Update statistics
|
102 |
+
if (!this.queryTimes.has(queryName)) {
|
103 |
+
this.queryTimes.set(queryName, []);
|
104 |
+
this.queryCount.set(queryName, 0);
|
105 |
+
}
|
106 |
+
|
107 |
+
this.queryTimes.get(queryName).push(duration);
|
108 |
+
this.queryCount.set(queryName, this.queryCount.get(queryName) + 1);
|
109 |
+
|
110 |
+
// Log slow queries
|
111 |
+
if (duration > 50) {
|
112 |
+
// 50ms threshold
|
113 |
+
console.warn(`🐌 Slow query detected: ${queryName} took ${duration.toFixed(2)}ms`);
|
114 |
+
}
|
115 |
+
|
116 |
+
return result;
|
117 |
+
}
|
118 |
+
|
119 |
+
// Get performance statistics
|
120 |
+
getStats() {
|
121 |
+
const stats = {};
|
122 |
+
|
123 |
+
for (const [queryName, times] of this.queryTimes.entries()) {
|
124 |
+
const count = this.queryCount.get(queryName);
|
125 |
+
const avgTime = times.reduce((sum, time) => sum + time, 0) / times.length;
|
126 |
+
const maxTime = Math.max(...times);
|
127 |
+
const minTime = Math.min(...times);
|
128 |
+
|
129 |
+
stats[queryName] = {
|
130 |
+
count,
|
131 |
+
avgTime: Math.round(avgTime * 100) / 100,
|
132 |
+
maxTime: Math.round(maxTime * 100) / 100,
|
133 |
+
minTime: Math.round(minTime * 100) / 100,
|
134 |
+
totalTime: Math.round(times.reduce((sum, time) => sum + time, 0) * 100) / 100
|
135 |
+
};
|
136 |
+
}
|
137 |
+
|
138 |
+
return stats;
|
139 |
+
}
|
140 |
+
|
141 |
+
// Reset statistics
|
142 |
+
reset() {
|
143 |
+
this.queryTimes.clear();
|
144 |
+
this.queryCount.clear();
|
145 |
+
}
|
146 |
+
}
|
147 |
+
|
148 |
+
// MEMORY USAGE OPTIMIZATION
|
149 |
+
const MEMORY_CLEANUP_STRATEGIES = {
|
150 |
+
// Strategy 1: Batch cleanup operations
|
151 |
+
batchCleanup: async (db, maxBatchSize = 100) => {
|
152 |
+
const oldMemories = await db.memories.where("isActive").equals(false).limit(maxBatchSize).toArray();
|
153 |
+
|
154 |
+
if (oldMemories.length > 0) {
|
155 |
+
await db.memories.bulkDelete(oldMemories.map(m => m.id));
|
156 |
+
console.log(`🧹 Batch deleted ${oldMemories.length} inactive memories`);
|
157 |
+
}
|
158 |
+
},
|
159 |
+
|
160 |
+
// Strategy 2: Incremental keyword cleanup
|
161 |
+
cleanupKeywords: async db => {
|
162 |
+
const memoriesWithEmptyKeywords = await db.memories
|
163 |
+
.filter(m => !m.keywords || m.keywords.length === 0)
|
164 |
+
.limit(50)
|
165 |
+
.toArray();
|
166 |
+
|
167 |
+
for (const memory of memoriesWithEmptyKeywords) {
|
168 |
+
const keywords = deriveKeywords(memory.content || "");
|
169 |
+
await db.memories.update(memory.id, { keywords });
|
170 |
+
}
|
171 |
+
},
|
172 |
+
|
173 |
+
// Strategy 3: Compress old memories
|
174 |
+
compressOldMemories: async (db, daysThreshold = 90) => {
|
175 |
+
const cutoff = new Date();
|
176 |
+
cutoff.setDate(cutoff.getDate() - daysThreshold);
|
177 |
+
|
178 |
+
const oldMemories = await db.memories
|
179 |
+
.where("timestamp")
|
180 |
+
.below(cutoff)
|
181 |
+
.and(m => m.isActive && !m.compressed)
|
182 |
+
.limit(20)
|
183 |
+
.toArray();
|
184 |
+
|
185 |
+
for (const memory of oldMemories) {
|
186 |
+
// Compress by removing redundant fields and shortening content
|
187 |
+
const compressed = {
|
188 |
+
compressed: true,
|
189 |
+
originalLength: memory.content?.length || 0,
|
190 |
+
content: memory.content?.substring(0, 100) + "...",
|
191 |
+
// Remove non-essential fields
|
192 |
+
sourceText: undefined,
|
193 |
+
tags: memory.tags?.slice(0, 3) // Keep only top 3 tags
|
194 |
+
};
|
195 |
+
|
196 |
+
await db.memories.update(memory.id, compressed);
|
197 |
+
}
|
198 |
+
}
|
199 |
+
};
|
200 |
+
|
201 |
+
// EXPORT UTILITIES
|
202 |
+
if (typeof window !== "undefined") {
|
203 |
+
window.KIMI_MEMORY_DB_OPTIMIZATION = {
|
204 |
+
RECOMMENDED_MEMORY_INDEXES,
|
205 |
+
OPTIMIZED_QUERY_PATTERNS,
|
206 |
+
MemoryDatabaseProfiler,
|
207 |
+
MEMORY_CLEANUP_STRATEGIES
|
208 |
+
};
|
209 |
+
}
|
210 |
+
|
211 |
+
export { RECOMMENDED_MEMORY_INDEXES, OPTIMIZED_QUERY_PATTERNS, MemoryDatabaseProfiler, MEMORY_CLEANUP_STRATEGIES };
|
kimi-js/kimi-memory-system.js
CHANGED
@@ -4,6 +4,99 @@ class KimiMemorySystem {
|
|
4 |
this.db = database;
|
5 |
this.memoryEnabled = true;
|
6 |
this.maxMemoryEntries = 100;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
this.memoryCategories = {
|
8 |
personal: "Personal Information",
|
9 |
preferences: "Likes & Dislikes",
|
@@ -200,6 +293,45 @@ class KimiMemorySystem {
|
|
200 |
/请记住(.+)/i
|
201 |
]
|
202 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
203 |
}
|
204 |
|
205 |
async init() {
|
@@ -216,11 +348,9 @@ class KimiMemorySystem {
|
|
216 |
this.selectedCharacter = await this.db.getSelectedCharacter();
|
217 |
await this.createMemoryTables();
|
218 |
|
219 |
-
//
|
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 |
}
|
@@ -238,10 +368,15 @@ class KimiMemorySystem {
|
|
238 |
async extractMemoryFromText(userText, kimiResponse = null) {
|
239 |
if (!this.memoryEnabled || !userText) return [];
|
240 |
|
|
|
|
|
|
|
|
|
|
|
241 |
const extractedMemories = [];
|
242 |
const text = userText.toLowerCase();
|
243 |
|
244 |
-
|
245 |
|
246 |
// Enhanced extraction with context awareness
|
247 |
const existingMemories = await this.getAllMemories();
|
@@ -249,19 +384,20 @@ class KimiMemorySystem {
|
|
249 |
// First, check for explicit memory requests
|
250 |
const explicitRequests = this.detectExplicitMemoryRequests(userText);
|
251 |
if (explicitRequests.length > 0) {
|
252 |
-
|
253 |
extractedMemories.push(...explicitRequests);
|
254 |
}
|
255 |
|
256 |
-
// Extract using patterns
|
257 |
-
|
|
|
258 |
for (const pattern of patterns) {
|
259 |
const match = text.match(pattern);
|
260 |
if (match && match[1]) {
|
261 |
const content = match[1].trim();
|
262 |
|
263 |
// Skip very short or generic content
|
264 |
-
if (content.length <
|
265 |
continue;
|
266 |
}
|
267 |
|
@@ -274,12 +410,12 @@ class KimiMemorySystem {
|
|
274 |
content: content,
|
275 |
sourceText: userText,
|
276 |
confidence: this.calculateExtractionConfidence(match, userText),
|
277 |
-
|
278 |
-
character: this.selectedCharacter,
|
279 |
isUpdate: isUpdate
|
280 |
};
|
281 |
|
282 |
-
|
283 |
extractedMemories.push(memory);
|
284 |
}
|
285 |
}
|
@@ -292,14 +428,29 @@ class KimiMemorySystem {
|
|
292 |
// Save extracted memories with intelligent deduplication
|
293 |
const savedMemories = [];
|
294 |
for (const memory of extractedMemories) {
|
295 |
-
|
296 |
-
|
297 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
298 |
}
|
299 |
|
300 |
if (savedMemories.length > 0) {
|
301 |
-
|
302 |
-
|
|
|
|
|
303 |
console.log("📝 No memories extracted from this text");
|
304 |
}
|
305 |
|
@@ -469,12 +620,12 @@ class KimiMemorySystem {
|
|
469 |
// Check if content is too generic to be useful
|
470 |
isGenericContent(content) {
|
471 |
const genericWords = ["yes", "no", "ok", "okay", "sure", "thanks", "hello", "hi", "bye"];
|
472 |
-
return genericWords.includes(content.toLowerCase()) || content.length <
|
473 |
}
|
474 |
|
475 |
// Calculate confidence based on context and pattern strength
|
476 |
calculateExtractionConfidence(match, fullText) {
|
477 |
-
let confidence =
|
478 |
|
479 |
// Boost confidence for explicit statements
|
480 |
const lower = fullText.toLowerCase();
|
@@ -496,20 +647,20 @@ class KimiMemorySystem {
|
|
496 |
lower.includes("我叫") ||
|
497 |
lower.includes("我的名字是")
|
498 |
) {
|
499 |
-
confidence +=
|
500 |
}
|
501 |
|
502 |
// Boost for longer, more specific content
|
503 |
-
if (match[1] && match[1].trim().length >
|
504 |
-
confidence +=
|
505 |
}
|
506 |
|
507 |
// Reduce confidence for uncertain language
|
508 |
if (fullText.includes("maybe") || fullText.includes("perhaps") || fullText.includes("might")) {
|
509 |
-
confidence -=
|
510 |
}
|
511 |
|
512 |
-
return Math.min(
|
513 |
}
|
514 |
|
515 |
// Generate a short title (2-5 words max) from content for auto-extracted memories
|
@@ -525,9 +676,9 @@ class KimiMemorySystem {
|
|
525 |
if (words.length === 0) return "";
|
526 |
// Prefer 3 words when available, minimum 2 when possible, maximum 5
|
527 |
let take;
|
528 |
-
if (words.length >=
|
529 |
else take = words.length; // 1 or 2
|
530 |
-
take = Math.min(
|
531 |
|
532 |
const slice = words.slice(0, take);
|
533 |
// Capitalize first word for nicer title
|
@@ -541,7 +692,7 @@ class KimiMemorySystem {
|
|
541 |
|
542 |
for (const memory of categoryMemories) {
|
543 |
const similarity = this.calculateSimilarity(memory.content, content);
|
544 |
-
if (similarity >
|
545 |
// Lower threshold for updates
|
546 |
return true;
|
547 |
}
|
@@ -598,8 +749,8 @@ class KimiMemorySystem {
|
|
598 |
content: name,
|
599 |
sourceText: text,
|
600 |
confidence: 0.7,
|
601 |
-
|
602 |
-
character: this.selectedCharacter
|
603 |
});
|
604 |
}
|
605 |
}
|
@@ -688,12 +839,11 @@ class KimiMemorySystem {
|
|
688 |
: "",
|
689 |
sourceText: memoryData.sourceText || "",
|
690 |
confidence: memoryData.confidence || 1.0,
|
691 |
-
|
692 |
-
character: memoryData.character || this.selectedCharacter,
|
693 |
isActive: true,
|
694 |
tags: [...new Set([...(memoryData.tags || []), ...this.deriveMemoryTags(memoryData)])],
|
695 |
lastModified: now,
|
696 |
-
createdAt: now,
|
697 |
lastAccess: now,
|
698 |
accessCount: 0,
|
699 |
importance: this.calculateImportance(memoryData)
|
@@ -702,7 +852,9 @@ class KimiMemorySystem {
|
|
702 |
if (this.db.db.memories) {
|
703 |
const id = await this.db.db.memories.add(memory);
|
704 |
memory.id = id; // Store the auto-generated ID
|
705 |
-
|
|
|
|
|
706 |
}
|
707 |
|
708 |
// Cleanup old memories if we exceed limit
|
@@ -714,6 +866,7 @@ class KimiMemorySystem {
|
|
714 |
return memory;
|
715 |
} catch (error) {
|
716 |
console.error("Error adding memory:", error);
|
|
|
717 |
}
|
718 |
}
|
719 |
|
@@ -782,31 +935,33 @@ class KimiMemorySystem {
|
|
782 |
}
|
783 |
}
|
784 |
|
785 |
-
//
|
786 |
determineMergeStrategy(existing, newData) {
|
787 |
const similarity = this.calculateSimilarity(existing.content, newData.content);
|
788 |
-
const newConfidence = newData.confidence ||
|
|
|
789 |
|
790 |
-
//
|
791 |
-
if (similarity >
|
792 |
-
return "boost_confidence";
|
793 |
}
|
794 |
|
795 |
-
//
|
796 |
-
if (similarity >
|
|
|
797 |
if (newData.content.length > existing.content.length * 1.5) {
|
798 |
-
return "update_content";
|
799 |
-
} else {
|
800 |
-
return "merge_content";
|
801 |
}
|
|
|
|
|
802 |
}
|
803 |
|
804 |
-
// For names, handle as variants
|
805 |
if (existing.category === "personal" && this.areRelatedNames(existing.content, newData.content)) {
|
806 |
return "add_variant";
|
807 |
}
|
808 |
|
809 |
-
// Default
|
810 |
return "merge_content";
|
811 |
}
|
812 |
|
@@ -875,8 +1030,9 @@ class KimiMemorySystem {
|
|
875 |
}
|
876 |
|
877 |
// Longer details and high confidence
|
878 |
-
if (memoryData.content && memoryData.content.length >
|
879 |
-
|
|
|
880 |
|
881 |
// Round to two decimals to avoid floating point artifacts
|
882 |
return Math.min(1.0, Math.round(importance * 100) / 100);
|
@@ -1010,7 +1166,7 @@ class KimiMemorySystem {
|
|
1010 |
if (!this.db) return [];
|
1011 |
|
1012 |
try {
|
1013 |
-
character = character || this.selectedCharacter;
|
1014 |
|
1015 |
if (this.db.db.memories) {
|
1016 |
const memories = await this.db.db.memories
|
@@ -1034,13 +1190,14 @@ class KimiMemorySystem {
|
|
1034 |
if (!this.db) return [];
|
1035 |
|
1036 |
try {
|
1037 |
-
character = character || this.selectedCharacter;
|
1038 |
|
1039 |
if (this.db.db.memories) {
|
|
|
1040 |
const memories = await this.db.db.memories
|
1041 |
.where("character")
|
1042 |
.equals(character)
|
1043 |
-
.
|
1044 |
.reverse()
|
1045 |
.sortBy("timestamp");
|
1046 |
|
@@ -1077,12 +1234,7 @@ class KimiMemorySystem {
|
|
1077 |
const contentSimilarity = this.calculateSimilarity(memory.content, memoryData.content);
|
1078 |
|
1079 |
// Different thresholds based on category
|
1080 |
-
|
1081 |
-
if (memoryData.category === "personal") {
|
1082 |
-
threshold = 0.6; // Names and personal info can vary more
|
1083 |
-
} else if (memoryData.category === "preferences") {
|
1084 |
-
threshold = 0.7; // Preferences can be expressed differently
|
1085 |
-
}
|
1086 |
|
1087 |
if (contentSimilarity > threshold) {
|
1088 |
return memory;
|
@@ -1171,11 +1323,130 @@ class KimiMemorySystem {
|
|
1171 |
.toLowerCase()
|
1172 |
.replace(/[\p{P}\p{S}]/gu, " ")
|
1173 |
.split(/\s+/)
|
1174 |
-
.filter(w => w.length > 2 && !
|
1175 |
)
|
1176 |
];
|
1177 |
}
|
1178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1179 |
async cleanupOldMemories() {
|
1180 |
if (!this.db) return;
|
1181 |
|
@@ -1189,17 +1460,30 @@ class KimiMemorySystem {
|
|
1189 |
// Soft-expire memories older than TTL by marking isActive=false
|
1190 |
const now = Date.now();
|
1191 |
const ttlMs = ttlDays * 24 * 60 * 60 * 1000;
|
|
|
|
|
1192 |
for (const mem of memories) {
|
1193 |
-
const created = new Date(mem
|
1194 |
if (now - created > ttlMs) {
|
1195 |
try {
|
1196 |
await this.updateMemory(mem.id, { isActive: false });
|
|
|
1197 |
} catch (e) {
|
1198 |
-
console.
|
|
|
|
|
|
|
|
|
|
|
|
|
1199 |
}
|
1200 |
}
|
1201 |
}
|
1202 |
|
|
|
|
|
|
|
|
|
1203 |
// Refresh active memories after TTL purge
|
1204 |
const activeMemories = (await this.getAllMemories()).filter(m => m.isActive);
|
1205 |
|
@@ -1210,22 +1494,38 @@ class KimiMemorySystem {
|
|
1210 |
const scoreA =
|
1211 |
(a.importance || 0.5) * -1 +
|
1212 |
(a.accessCount || 0) * 0.01 +
|
1213 |
-
new Date(a
|
1214 |
const scoreB =
|
1215 |
(b.importance || 0.5) * -1 +
|
1216 |
(b.accessCount || 0) * 0.01 +
|
1217 |
-
new Date(b
|
1218 |
return scoreB - scoreA;
|
1219 |
});
|
1220 |
|
1221 |
const toDeactivate = activeMemories.slice(maxEntries);
|
|
|
|
|
|
|
1222 |
for (const mem of toDeactivate) {
|
1223 |
try {
|
1224 |
await this.updateMemory(mem.id, { isActive: false });
|
|
|
1225 |
} catch (e) {
|
1226 |
-
console.
|
|
|
|
|
|
|
|
|
|
|
|
|
1227 |
}
|
1228 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
1229 |
}
|
1230 |
} catch (error) {
|
1231 |
console.error("Error cleaning up old memories:", error);
|
@@ -1281,7 +1581,7 @@ class KimiMemorySystem {
|
|
1281 |
let score = memory.importance || 0.5;
|
1282 |
|
1283 |
// Boost recent memories
|
1284 |
-
const daysSinceCreation =
|
1285 |
score += Math.max(0, (7 - daysSinceCreation) / 7) * 0.2; // Recent boost
|
1286 |
|
1287 |
// Boost frequently accessed memories
|
@@ -1311,7 +1611,7 @@ class KimiMemorySystem {
|
|
1311 |
let score = 0;
|
1312 |
|
1313 |
// Enhanced content similarity with keyword matching
|
1314 |
-
score += this.calculateSimilarity(memory.content, context) *
|
1315 |
|
1316 |
// Keyword overlap boost (derived keywords)
|
1317 |
try {
|
@@ -1319,7 +1619,7 @@ class KimiMemorySystem {
|
|
1319 |
const ctxKeys = this.deriveKeywords(context || "");
|
1320 |
const keyOverlap = ctxKeys.filter(k => memKeys.includes(k)).length;
|
1321 |
if (ctxKeys.length > 0) {
|
1322 |
-
score += (keyOverlap / ctxKeys.length) *
|
1323 |
}
|
1324 |
} catch (e) {
|
1325 |
// fallback to original keyword matching
|
@@ -1330,22 +1630,24 @@ class KimiMemorySystem {
|
|
1330 |
}
|
1331 |
}
|
1332 |
if (contextWords.length > 0) {
|
1333 |
-
score += (keywordMatches / contextWords.length) *
|
1334 |
}
|
1335 |
}
|
1336 |
|
1337 |
-
// (legacy keyword matching handled above)
|
1338 |
-
|
1339 |
// Category relevance bonus based on context
|
1340 |
-
score += this.getCategoryRelevance(memory.category, context) *
|
1341 |
|
1342 |
// Recent memories get bonus for current conversation
|
1343 |
-
const daysSinceCreation =
|
1344 |
-
score +=
|
|
|
|
|
|
|
|
|
1345 |
|
1346 |
// Confidence and importance boost
|
1347 |
-
score += (memory.confidence || 0.5) *
|
1348 |
-
score += (memory.importance || 0.5) *
|
1349 |
|
1350 |
return Math.min(1.0, score);
|
1351 |
}
|
@@ -1538,30 +1840,56 @@ class KimiMemorySystem {
|
|
1538 |
// Touch multiple memories to update lastAccess and accessCount
|
1539 |
async _touchMemories(memories, limit = 5) {
|
1540 |
if (!this.db || !Array.isArray(memories) || memories.length === 0) return;
|
|
|
1541 |
try {
|
1542 |
const top = memories.slice(0, limit);
|
1543 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
1544 |
for (const m of top) {
|
1545 |
try {
|
1546 |
const id = m.id;
|
1547 |
const existing = await this.db.db.memories.get(id);
|
1548 |
if (existing) {
|
1549 |
const lastAccess = existing.lastAccess ? new Date(existing.lastAccess).getTime() : 0;
|
1550 |
-
|
1551 |
-
|
1552 |
-
if (now - lastAccess >
|
1553 |
-
|
1554 |
-
|
1555 |
-
|
|
|
|
|
|
|
|
|
1556 |
}
|
1557 |
}
|
1558 |
} catch (e) {
|
1559 |
-
console.warn("Error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1560 |
}
|
1561 |
}
|
1562 |
-
await Promise.all(ops);
|
1563 |
} catch (e) {
|
1564 |
-
console.warn("Error in _touchMemories", e);
|
1565 |
}
|
1566 |
}
|
1567 |
|
@@ -1654,7 +1982,7 @@ class KimiMemorySystem {
|
|
1654 |
// Exclude existing summaries to avoid summarizing summaries repeatedly
|
1655 |
const recent = all.filter(
|
1656 |
m =>
|
1657 |
-
new Date(m
|
1658 |
m.isActive &&
|
1659 |
m.type !== "summary" &&
|
1660 |
!(m.tags && m.tags.includes("summary"))
|
@@ -1688,7 +2016,7 @@ class KimiMemorySystem {
|
|
1688 |
sourceText: summaryContent,
|
1689 |
summaryJson: JSON.stringify(summaryJson),
|
1690 |
confidence: 0.9,
|
1691 |
-
|
1692 |
character: this.selectedCharacter,
|
1693 |
isActive: true,
|
1694 |
tags: ["summary"]
|
@@ -1723,7 +2051,7 @@ class KimiMemorySystem {
|
|
1723 |
// Exclude existing summaries to avoid recursive summarization
|
1724 |
const recent = all.filter(
|
1725 |
m =>
|
1726 |
-
new Date(m
|
1727 |
m.isActive &&
|
1728 |
m.type !== "summary" &&
|
1729 |
!(m.tags && m.tags.includes("summary"))
|
@@ -1731,7 +2059,7 @@ class KimiMemorySystem {
|
|
1731 |
if (!recent.length) return null;
|
1732 |
|
1733 |
// Build aggregate content from readable fields in chronological order
|
1734 |
-
recent.sort((a, b) => new Date(a
|
1735 |
const texts = recent
|
1736 |
.map(r => {
|
1737 |
const raw =
|
@@ -1962,5 +2290,3 @@ class KimiMemorySystem {
|
|
1962 |
|
1963 |
window.KimiMemorySystem = KimiMemorySystem;
|
1964 |
export default KimiMemorySystem;
|
1965 |
-
|
1966 |
-
window.KimiMemorySystem = KimiMemorySystem;
|
|
|
4 |
this.db = database;
|
5 |
this.memoryEnabled = true;
|
6 |
this.maxMemoryEntries = 100;
|
7 |
+
|
8 |
+
// Performance optimization: keyword cache with LRU eviction
|
9 |
+
this.keywordCache = new Map(); // keyword_language -> boolean (is common)
|
10 |
+
this.keywordCacheSize = 1000; // Limit memory usage
|
11 |
+
this.keywordCacheHits = 0;
|
12 |
+
this.keywordCacheMisses = 0;
|
13 |
+
|
14 |
+
// Performance monitoring
|
15 |
+
this.queryStats = {
|
16 |
+
extractionTime: [],
|
17 |
+
addMemoryTime: [],
|
18 |
+
retrievalTime: []
|
19 |
+
};
|
20 |
+
|
21 |
+
// Centralized configuration for all thresholds and magic numbers
|
22 |
+
this.config = {
|
23 |
+
// Content validation thresholds
|
24 |
+
minContentLength: 2,
|
25 |
+
longContentThreshold: 24,
|
26 |
+
titleWordCount: {
|
27 |
+
preferred: 3,
|
28 |
+
min: 1,
|
29 |
+
max: 5
|
30 |
+
},
|
31 |
+
|
32 |
+
// Similarity and confidence thresholds
|
33 |
+
similarity: {
|
34 |
+
personal: 0.6, // Names can vary more (Jean vs Jean-Pierre)
|
35 |
+
preferences: 0.7, // Preferences can be expressed differently
|
36 |
+
default: 0.8, // General similarity threshold
|
37 |
+
veryHigh: 0.9, // For boost_confidence strategy
|
38 |
+
update: 0.3 // Lower threshold for memory updates
|
39 |
+
},
|
40 |
+
|
41 |
+
// Confidence scoring
|
42 |
+
confidence: {
|
43 |
+
base: 0.6,
|
44 |
+
explicitRequest: 1.0,
|
45 |
+
naturalExpression: 0.7,
|
46 |
+
bonusForLongContent: 0.1,
|
47 |
+
bonusForExplicitStatement: 0.3,
|
48 |
+
penaltyForUncertainty: 0.2,
|
49 |
+
min: 0.1,
|
50 |
+
max: 1.0
|
51 |
+
},
|
52 |
+
|
53 |
+
// Memory management
|
54 |
+
cleanup: {
|
55 |
+
maxEntries: 100,
|
56 |
+
ttlDays: 365,
|
57 |
+
batchSize: 100,
|
58 |
+
touchMinutes: 60
|
59 |
+
},
|
60 |
+
|
61 |
+
// Performance settings
|
62 |
+
cache: {
|
63 |
+
keywordCacheSize: 1000,
|
64 |
+
statHistorySize: 100
|
65 |
+
},
|
66 |
+
|
67 |
+
// Scoring weights for importance calculation
|
68 |
+
importance: {
|
69 |
+
categoryWeights: {
|
70 |
+
important: 1.0,
|
71 |
+
personal: 0.9,
|
72 |
+
relationships: 0.85,
|
73 |
+
goals: 0.75,
|
74 |
+
experiences: 0.65,
|
75 |
+
preferences: 0.6,
|
76 |
+
activities: 0.5
|
77 |
+
},
|
78 |
+
bonuses: {
|
79 |
+
relationshipMilestone: 0.15,
|
80 |
+
boundaries: 0.15,
|
81 |
+
strongEmotion: 0.05,
|
82 |
+
futureReference: 0.05,
|
83 |
+
longContent: 0.05,
|
84 |
+
highConfidence: 0.05
|
85 |
+
}
|
86 |
+
},
|
87 |
+
|
88 |
+
// Relevance calculation weights
|
89 |
+
relevance: {
|
90 |
+
contentSimilarity: 0.35,
|
91 |
+
keywordOverlap: 0.25,
|
92 |
+
categoryRelevance: 0.1,
|
93 |
+
recencyBonus: 0.1,
|
94 |
+
confidenceBonus: 0.05,
|
95 |
+
importanceBonus: 0.05,
|
96 |
+
recentDaysThreshold: 30
|
97 |
+
}
|
98 |
+
};
|
99 |
+
|
100 |
this.memoryCategories = {
|
101 |
personal: "Personal Information",
|
102 |
preferences: "Likes & Dislikes",
|
|
|
293 |
/请记住(.+)/i
|
294 |
]
|
295 |
};
|
296 |
+
|
297 |
+
// Performance optimization: pre-compile regex patterns
|
298 |
+
this.compiledPatterns = {};
|
299 |
+
this.initializeCompiledPatterns();
|
300 |
+
}
|
301 |
+
|
302 |
+
// Pre-compile all regex patterns for better performance
|
303 |
+
initializeCompiledPatterns() {
|
304 |
+
try {
|
305 |
+
for (const [category, patterns] of Object.entries(this.extractionPatterns)) {
|
306 |
+
this.compiledPatterns[category] = patterns.map(pattern => {
|
307 |
+
if (pattern instanceof RegExp) {
|
308 |
+
return pattern; // Already compiled
|
309 |
+
}
|
310 |
+
return new RegExp(pattern.source, pattern.flags);
|
311 |
+
});
|
312 |
+
}
|
313 |
+
|
314 |
+
if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
|
315 |
+
const totalPatterns = Object.values(this.compiledPatterns).reduce((sum, arr) => sum + arr.length, 0);
|
316 |
+
console.log(`🚀 Pre-compiled ${totalPatterns} regex patterns for memory extraction`);
|
317 |
+
}
|
318 |
+
} catch (error) {
|
319 |
+
console.error("Error pre-compiling regex patterns:", error);
|
320 |
+
// Fallback: use original patterns
|
321 |
+
this.compiledPatterns = this.extractionPatterns;
|
322 |
+
}
|
323 |
+
}
|
324 |
+
|
325 |
+
// Utility method to get consistent creation timestamp
|
326 |
+
getCreationTimestamp(memory) {
|
327 |
+
// Prefer createdAt, fallback to timestamp for backward compatibility
|
328 |
+
return memory.createdAt || memory.timestamp || new Date();
|
329 |
+
}
|
330 |
+
|
331 |
+
// Utility method to calculate days since creation
|
332 |
+
getDaysSinceCreation(memory) {
|
333 |
+
const created = new Date(this.getCreationTimestamp(memory)).getTime();
|
334 |
+
return (Date.now() - created) / (1000 * 60 * 60 * 24);
|
335 |
}
|
336 |
|
337 |
async init() {
|
|
|
348 |
this.selectedCharacter = await this.db.getSelectedCharacter();
|
349 |
await this.createMemoryTables();
|
350 |
|
351 |
+
// Legacy migrations disabled - uncomment if needed for old databases
|
352 |
+
// await this.migrateIncompatibleIDs();
|
353 |
+
// this.populateKeywordsForAllMemories().catch(e => console.warn("Keyword population failed", e));
|
|
|
|
|
354 |
} catch (error) {
|
355 |
console.error("Memory system initialization error:", error);
|
356 |
}
|
|
|
368 |
async extractMemoryFromText(userText, kimiResponse = null) {
|
369 |
if (!this.memoryEnabled || !userText) return [];
|
370 |
|
371 |
+
// Ensure selectedCharacter is initialized
|
372 |
+
if (!this.selectedCharacter) {
|
373 |
+
this.selectedCharacter = this.db ? await this.db.getSelectedCharacter() : "kimi";
|
374 |
+
}
|
375 |
+
|
376 |
const extractedMemories = [];
|
377 |
const text = userText.toLowerCase();
|
378 |
|
379 |
+
// Memory extraction processing (debug info reduced for performance)
|
380 |
|
381 |
// Enhanced extraction with context awareness
|
382 |
const existingMemories = await this.getAllMemories();
|
|
|
384 |
// First, check for explicit memory requests
|
385 |
const explicitRequests = this.detectExplicitMemoryRequests(userText);
|
386 |
if (explicitRequests.length > 0) {
|
387 |
+
// Explicit memory requests detected
|
388 |
extractedMemories.push(...explicitRequests);
|
389 |
}
|
390 |
|
391 |
+
// Extract using pre-compiled patterns for better performance
|
392 |
+
const patternsToUse = this.compiledPatterns || this.extractionPatterns;
|
393 |
+
for (const [category, patterns] of Object.entries(patternsToUse)) {
|
394 |
for (const pattern of patterns) {
|
395 |
const match = text.match(pattern);
|
396 |
if (match && match[1]) {
|
397 |
const content = match[1].trim();
|
398 |
|
399 |
// Skip very short or generic content
|
400 |
+
if (content.length < this.config.minContentLength || this.isGenericContent(content)) {
|
401 |
continue;
|
402 |
}
|
403 |
|
|
|
410 |
content: content,
|
411 |
sourceText: userText,
|
412 |
confidence: this.calculateExtractionConfidence(match, userText),
|
413 |
+
createdAt: new Date(), // Use createdAt consistently
|
414 |
+
character: this.selectedCharacter || "kimi", // Fallback protection
|
415 |
isUpdate: isUpdate
|
416 |
};
|
417 |
|
418 |
+
// Pattern match detected
|
419 |
extractedMemories.push(memory);
|
420 |
}
|
421 |
}
|
|
|
428 |
// Save extracted memories with intelligent deduplication
|
429 |
const savedMemories = [];
|
430 |
for (const memory of extractedMemories) {
|
431 |
+
try {
|
432 |
+
console.log("💾 Saving memory:", memory.content);
|
433 |
+
const saved = await this.addMemory(memory);
|
434 |
+
if (saved) {
|
435 |
+
savedMemories.push(saved);
|
436 |
+
} else {
|
437 |
+
console.warn("⚠️ Memory was not saved (possibly filtered or merged):", memory.content);
|
438 |
+
}
|
439 |
+
} catch (error) {
|
440 |
+
console.error("❌ Failed to save memory:", {
|
441 |
+
content: memory.content,
|
442 |
+
category: memory.category,
|
443 |
+
error: error.message
|
444 |
+
});
|
445 |
+
// Continue processing other memories even if one fails
|
446 |
+
}
|
447 |
}
|
448 |
|
449 |
if (savedMemories.length > 0) {
|
450 |
+
if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
|
451 |
+
console.log(`✅ Successfully extracted and saved ${savedMemories.length} memories`);
|
452 |
+
}
|
453 |
+
} else if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
|
454 |
console.log("📝 No memories extracted from this text");
|
455 |
}
|
456 |
|
|
|
620 |
// Check if content is too generic to be useful
|
621 |
isGenericContent(content) {
|
622 |
const genericWords = ["yes", "no", "ok", "okay", "sure", "thanks", "hello", "hi", "bye"];
|
623 |
+
return genericWords.includes(content.toLowerCase()) || content.length < this.config.minContentLength;
|
624 |
}
|
625 |
|
626 |
// Calculate confidence based on context and pattern strength
|
627 |
calculateExtractionConfidence(match, fullText) {
|
628 |
+
let confidence = this.config.confidence.base; // Base confidence from config
|
629 |
|
630 |
// Boost confidence for explicit statements
|
631 |
const lower = fullText.toLowerCase();
|
|
|
647 |
lower.includes("我叫") ||
|
648 |
lower.includes("我的名字是")
|
649 |
) {
|
650 |
+
confidence += this.config.confidence.bonusForExplicitStatement;
|
651 |
}
|
652 |
|
653 |
// Boost for longer, more specific content
|
654 |
+
if (match[1] && match[1].trim().length > this.config.longContentThreshold) {
|
655 |
+
confidence += this.config.confidence.bonusForLongContent;
|
656 |
}
|
657 |
|
658 |
// Reduce confidence for uncertain language
|
659 |
if (fullText.includes("maybe") || fullText.includes("perhaps") || fullText.includes("might")) {
|
660 |
+
confidence -= this.config.confidence.penaltyForUncertainty;
|
661 |
}
|
662 |
|
663 |
+
return Math.min(this.config.confidence.max, Math.max(this.config.confidence.min, confidence));
|
664 |
}
|
665 |
|
666 |
// Generate a short title (2-5 words max) from content for auto-extracted memories
|
|
|
676 |
if (words.length === 0) return "";
|
677 |
// Prefer 3 words when available, minimum 2 when possible, maximum 5
|
678 |
let take;
|
679 |
+
if (words.length >= this.config.titleWordCount.preferred) take = this.config.titleWordCount.preferred;
|
680 |
else take = words.length; // 1 or 2
|
681 |
+
take = Math.min(this.config.titleWordCount.max, Math.max(this.config.titleWordCount.min, take));
|
682 |
|
683 |
const slice = words.slice(0, take);
|
684 |
// Capitalize first word for nicer title
|
|
|
692 |
|
693 |
for (const memory of categoryMemories) {
|
694 |
const similarity = this.calculateSimilarity(memory.content, content);
|
695 |
+
if (similarity > this.config.similarity.update) {
|
696 |
// Lower threshold for updates
|
697 |
return true;
|
698 |
}
|
|
|
749 |
content: name,
|
750 |
sourceText: text,
|
751 |
confidence: 0.7,
|
752 |
+
createdAt: new Date(), // Use createdAt consistently
|
753 |
+
character: this.selectedCharacter || "kimi" // Fallback protection
|
754 |
});
|
755 |
}
|
756 |
}
|
|
|
839 |
: "",
|
840 |
sourceText: memoryData.sourceText || "",
|
841 |
confidence: memoryData.confidence || 1.0,
|
842 |
+
createdAt: memoryData.createdAt || memoryData.timestamp || now, // Unified timestamp handling
|
843 |
+
character: memoryData.character || this.selectedCharacter || "kimi", // Fallback protection
|
844 |
isActive: true,
|
845 |
tags: [...new Set([...(memoryData.tags || []), ...this.deriveMemoryTags(memoryData)])],
|
846 |
lastModified: now,
|
|
|
847 |
lastAccess: now,
|
848 |
accessCount: 0,
|
849 |
importance: this.calculateImportance(memoryData)
|
|
|
852 |
if (this.db.db.memories) {
|
853 |
const id = await this.db.db.memories.add(memory);
|
854 |
memory.id = id; // Store the auto-generated ID
|
855 |
+
if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
|
856 |
+
console.log(`Memory added with ID: ${id}`);
|
857 |
+
}
|
858 |
}
|
859 |
|
860 |
// Cleanup old memories if we exceed limit
|
|
|
866 |
return memory;
|
867 |
} catch (error) {
|
868 |
console.error("Error adding memory:", error);
|
869 |
+
return null; // Return null instead of undefined for clearer error handling
|
870 |
}
|
871 |
}
|
872 |
|
|
|
935 |
}
|
936 |
}
|
937 |
|
938 |
+
// Simplified memory merge strategy determination
|
939 |
determineMergeStrategy(existing, newData) {
|
940 |
const similarity = this.calculateSimilarity(existing.content, newData.content);
|
941 |
+
const newConfidence = newData.confidence || this.config.confidence.base;
|
942 |
+
const existingConfidence = existing.confidence || this.config.confidence.base;
|
943 |
|
944 |
+
// Very high similarity (>90%) - boost confidence if new is more confident
|
945 |
+
if (similarity > this.config.similarity.veryHigh) {
|
946 |
+
return newConfidence > existingConfidence ? "boost_confidence" : "merge_content";
|
947 |
}
|
948 |
|
949 |
+
// High similarity (>70%) - decide based on content length and specificity
|
950 |
+
if (similarity > this.config.similarity.preferences) {
|
951 |
+
// If new content is significantly longer (50% more), it's likely more detailed
|
952 |
if (newData.content.length > existing.content.length * 1.5) {
|
953 |
+
return "update_content";
|
|
|
|
|
954 |
}
|
955 |
+
// If existing is longer, merge to preserve information
|
956 |
+
return "merge_content";
|
957 |
}
|
958 |
|
959 |
+
// For personal names, handle as variants if they're related
|
960 |
if (existing.category === "personal" && this.areRelatedNames(existing.content, newData.content)) {
|
961 |
return "add_variant";
|
962 |
}
|
963 |
|
964 |
+
// Default strategy for moderate similarity
|
965 |
return "merge_content";
|
966 |
}
|
967 |
|
|
|
1030 |
}
|
1031 |
|
1032 |
// Longer details and high confidence
|
1033 |
+
if (memoryData.content && memoryData.content.length > this.config.longContentThreshold)
|
1034 |
+
importance += this.config.importance.bonuses.longContent;
|
1035 |
+
if (memoryData.confidence && memoryData.confidence > 0.9) importance += this.config.importance.bonuses.highConfidence;
|
1036 |
|
1037 |
// Round to two decimals to avoid floating point artifacts
|
1038 |
return Math.min(1.0, Math.round(importance * 100) / 100);
|
|
|
1166 |
if (!this.db) return [];
|
1167 |
|
1168 |
try {
|
1169 |
+
character = character || this.selectedCharacter || "kimi"; // Unified fallback
|
1170 |
|
1171 |
if (this.db.db.memories) {
|
1172 |
const memories = await this.db.db.memories
|
|
|
1190 |
if (!this.db) return [];
|
1191 |
|
1192 |
try {
|
1193 |
+
character = character || this.selectedCharacter || "kimi";
|
1194 |
|
1195 |
if (this.db.db.memories) {
|
1196 |
+
// Use simple character filter - compatible with all data
|
1197 |
const memories = await this.db.db.memories
|
1198 |
.where("character")
|
1199 |
.equals(character)
|
1200 |
+
.filter(memory => memory.isActive !== false) // Include records without isActive field
|
1201 |
.reverse()
|
1202 |
.sortBy("timestamp");
|
1203 |
|
|
|
1234 |
const contentSimilarity = this.calculateSimilarity(memory.content, memoryData.content);
|
1235 |
|
1236 |
// Different thresholds based on category
|
1237 |
+
const threshold = this.config.similarity[memoryData.category] || this.config.similarity.default;
|
|
|
|
|
|
|
|
|
|
|
1238 |
|
1239 |
if (contentSimilarity > threshold) {
|
1240 |
return memory;
|
|
|
1323 |
.toLowerCase()
|
1324 |
.replace(/[\p{P}\p{S}]/gu, " ")
|
1325 |
.split(/\s+/)
|
1326 |
+
.filter(w => w.length > 2 && !this.isCommonWordSafe(w))
|
1327 |
)
|
1328 |
];
|
1329 |
}
|
1330 |
|
1331 |
+
// Safe wrapper for isCommonWord to avoid undefined function errors
|
1332 |
+
isCommonWordSafe(word, language = "en") {
|
1333 |
+
const cacheKey = `${word.toLowerCase()}_${language}`;
|
1334 |
+
|
1335 |
+
// Check cache first
|
1336 |
+
if (this.keywordCache.has(cacheKey)) {
|
1337 |
+
this.keywordCacheHits++;
|
1338 |
+
return this.keywordCache.get(cacheKey);
|
1339 |
+
}
|
1340 |
+
|
1341 |
+
// Cache miss - compute the result
|
1342 |
+
this.keywordCacheMisses++;
|
1343 |
+
let isCommon = false;
|
1344 |
+
|
1345 |
+
try {
|
1346 |
+
isCommon = typeof this.isCommonWord === "function" ? this.isCommonWord(word, language) : false;
|
1347 |
+
} catch (error) {
|
1348 |
+
console.warn("Error checking common word:", error);
|
1349 |
+
isCommon = false;
|
1350 |
+
}
|
1351 |
+
|
1352 |
+
// Add to cache with LRU eviction
|
1353 |
+
if (this.keywordCache.size >= this.keywordCacheSize) {
|
1354 |
+
// Simple LRU: remove oldest entry (first in Map)
|
1355 |
+
const firstKey = this.keywordCache.keys().next().value;
|
1356 |
+
this.keywordCache.delete(firstKey);
|
1357 |
+
}
|
1358 |
+
|
1359 |
+
this.keywordCache.set(cacheKey, isCommon);
|
1360 |
+
return isCommon;
|
1361 |
+
}
|
1362 |
+
|
1363 |
+
// Get cache statistics for debugging
|
1364 |
+
getKeywordCacheStats() {
|
1365 |
+
const total = this.keywordCacheHits + this.keywordCacheMisses;
|
1366 |
+
return {
|
1367 |
+
size: this.keywordCache.size,
|
1368 |
+
hits: this.keywordCacheHits,
|
1369 |
+
misses: this.keywordCacheMisses,
|
1370 |
+
hitRate: total > 0 ? ((this.keywordCacheHits / total) * 100).toFixed(2) + "%" : "0%"
|
1371 |
+
};
|
1372 |
+
}
|
1373 |
+
|
1374 |
+
// Get performance statistics for debugging and optimization
|
1375 |
+
getPerformanceStats() {
|
1376 |
+
const calculateStats = times => {
|
1377 |
+
if (times.length === 0) return { avg: 0, max: 0, min: 0, count: 0 };
|
1378 |
+
return {
|
1379 |
+
avg: Math.round((times.reduce((sum, t) => sum + t, 0) / times.length) * 100) / 100,
|
1380 |
+
max: Math.round(Math.max(...times) * 100) / 100,
|
1381 |
+
min: Math.round(Math.min(...times) * 100) / 100,
|
1382 |
+
count: times.length
|
1383 |
+
};
|
1384 |
+
};
|
1385 |
+
|
1386 |
+
return {
|
1387 |
+
keywordCache: this.getKeywordCacheStats(),
|
1388 |
+
extraction: calculateStats(this.queryStats.extractionTime),
|
1389 |
+
addMemory: calculateStats(this.queryStats.addMemoryTime),
|
1390 |
+
retrieval: calculateStats(this.queryStats.retrievalTime)
|
1391 |
+
};
|
1392 |
+
}
|
1393 |
+
|
1394 |
+
// Performance wrapper for memory extraction
|
1395 |
+
async extractMemoryFromTextTimed(userText, kimiResponse = null) {
|
1396 |
+
const start = performance.now();
|
1397 |
+
const result = await this.extractMemoryFromText(userText, kimiResponse);
|
1398 |
+
const duration = performance.now() - start;
|
1399 |
+
|
1400 |
+
this.queryStats.extractionTime.push(duration);
|
1401 |
+
if (this.queryStats.extractionTime.length > 100) {
|
1402 |
+
this.queryStats.extractionTime.shift(); // Keep only last 100 measurements
|
1403 |
+
}
|
1404 |
+
|
1405 |
+
if (duration > 100 && window.KIMI_CONFIG?.DEBUG?.MEMORY) {
|
1406 |
+
console.warn(`🐌 Slow memory extraction: ${duration.toFixed(2)}ms for text length ${userText?.length || 0}`);
|
1407 |
+
}
|
1408 |
+
|
1409 |
+
return result;
|
1410 |
+
}
|
1411 |
+
|
1412 |
+
// Get current configuration for debugging and monitoring
|
1413 |
+
getConfiguration() {
|
1414 |
+
return {
|
1415 |
+
...this.config,
|
1416 |
+
memoryCategories: this.memoryCategories,
|
1417 |
+
runtime: {
|
1418 |
+
memoryEnabled: this.memoryEnabled,
|
1419 |
+
maxMemoryEntries: this.maxMemoryEntries,
|
1420 |
+
selectedCharacter: this.selectedCharacter,
|
1421 |
+
keywordCacheSize: this.keywordCache.size,
|
1422 |
+
compiledPatternsCount: Object.values(this.compiledPatterns || {}).reduce((sum, arr) => sum + arr.length, 0)
|
1423 |
+
}
|
1424 |
+
};
|
1425 |
+
}
|
1426 |
+
|
1427 |
+
// Update configuration at runtime (for advanced users)
|
1428 |
+
updateConfiguration(configPath, value) {
|
1429 |
+
const keys = configPath.split(".");
|
1430 |
+
let current = this.config;
|
1431 |
+
|
1432 |
+
// Navigate to the parent object
|
1433 |
+
for (let i = 0; i < keys.length - 1; i++) {
|
1434 |
+
if (!current[keys[i]]) current[keys[i]] = {};
|
1435 |
+
current = current[keys[i]];
|
1436 |
+
}
|
1437 |
+
|
1438 |
+
// Set the value
|
1439 |
+
const lastKey = keys[keys.length - 1];
|
1440 |
+
const oldValue = current[lastKey];
|
1441 |
+
current[lastKey] = value;
|
1442 |
+
|
1443 |
+
if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
|
1444 |
+
console.log(`🔧 Configuration updated: ${configPath} = ${value} (was: ${oldValue})`);
|
1445 |
+
}
|
1446 |
+
|
1447 |
+
return { oldValue, newValue: value };
|
1448 |
+
}
|
1449 |
+
|
1450 |
async cleanupOldMemories() {
|
1451 |
if (!this.db) return;
|
1452 |
|
|
|
1460 |
// Soft-expire memories older than TTL by marking isActive=false
|
1461 |
const now = Date.now();
|
1462 |
const ttlMs = ttlDays * 24 * 60 * 60 * 1000;
|
1463 |
+
const expiredMemories = [];
|
1464 |
+
|
1465 |
for (const mem of memories) {
|
1466 |
+
const created = new Date(this.getCreationTimestamp(mem)).getTime();
|
1467 |
if (now - created > ttlMs) {
|
1468 |
try {
|
1469 |
await this.updateMemory(mem.id, { isActive: false });
|
1470 |
+
expiredMemories.push(mem.id);
|
1471 |
} catch (e) {
|
1472 |
+
console.error(`Memory expiration failed for ID ${mem.id}:`, {
|
1473 |
+
error: e.message,
|
1474 |
+
memoryId: mem.id,
|
1475 |
+
createdAt: this.getCreationTimestamp(mem),
|
1476 |
+
character: mem.character
|
1477 |
+
});
|
1478 |
+
// Continue with other memories even if one fails
|
1479 |
}
|
1480 |
}
|
1481 |
}
|
1482 |
|
1483 |
+
if (window.KIMI_CONFIG?.DEBUG?.MEMORY && expiredMemories.length > 0) {
|
1484 |
+
console.log(`Successfully expired ${expiredMemories.length} memories:`, expiredMemories);
|
1485 |
+
}
|
1486 |
+
|
1487 |
// Refresh active memories after TTL purge
|
1488 |
const activeMemories = (await this.getAllMemories()).filter(m => m.isActive);
|
1489 |
|
|
|
1494 |
const scoreA =
|
1495 |
(a.importance || 0.5) * -1 +
|
1496 |
(a.accessCount || 0) * 0.01 +
|
1497 |
+
new Date(this.getCreationTimestamp(a)).getTime() / (1000 * 60 * 60 * 24);
|
1498 |
const scoreB =
|
1499 |
(b.importance || 0.5) * -1 +
|
1500 |
(b.accessCount || 0) * 0.01 +
|
1501 |
+
new Date(this.getCreationTimestamp(b)).getTime() / (1000 * 60 * 60 * 24);
|
1502 |
return scoreB - scoreA;
|
1503 |
});
|
1504 |
|
1505 |
const toDeactivate = activeMemories.slice(maxEntries);
|
1506 |
+
const deactivatedMemories = [];
|
1507 |
+
const failedDeactivations = [];
|
1508 |
+
|
1509 |
for (const mem of toDeactivate) {
|
1510 |
try {
|
1511 |
await this.updateMemory(mem.id, { isActive: false });
|
1512 |
+
deactivatedMemories.push(mem.id);
|
1513 |
} catch (e) {
|
1514 |
+
console.error(`Memory deactivation failed for ID ${mem.id}:`, {
|
1515 |
+
error: e.message,
|
1516 |
+
memoryId: mem.id,
|
1517 |
+
importance: mem.importance,
|
1518 |
+
character: mem.character
|
1519 |
+
});
|
1520 |
+
failedDeactivations.push(mem.id);
|
1521 |
}
|
1522 |
}
|
1523 |
+
|
1524 |
+
if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
|
1525 |
+
console.log(
|
1526 |
+
`Memory cleanup: ${deactivatedMemories.length} deactivated, ${failedDeactivations.length} failed`
|
1527 |
+
);
|
1528 |
+
}
|
1529 |
}
|
1530 |
} catch (error) {
|
1531 |
console.error("Error cleaning up old memories:", error);
|
|
|
1581 |
let score = memory.importance || 0.5;
|
1582 |
|
1583 |
// Boost recent memories
|
1584 |
+
const daysSinceCreation = this.getDaysSinceCreation(memory);
|
1585 |
score += Math.max(0, (7 - daysSinceCreation) / 7) * 0.2; // Recent boost
|
1586 |
|
1587 |
// Boost frequently accessed memories
|
|
|
1611 |
let score = 0;
|
1612 |
|
1613 |
// Enhanced content similarity with keyword matching
|
1614 |
+
score += this.calculateSimilarity(memory.content, context) * this.config.relevance.contentSimilarity;
|
1615 |
|
1616 |
// Keyword overlap boost (derived keywords)
|
1617 |
try {
|
|
|
1619 |
const ctxKeys = this.deriveKeywords(context || "");
|
1620 |
const keyOverlap = ctxKeys.filter(k => memKeys.includes(k)).length;
|
1621 |
if (ctxKeys.length > 0) {
|
1622 |
+
score += (keyOverlap / ctxKeys.length) * this.config.relevance.keywordOverlap;
|
1623 |
}
|
1624 |
} catch (e) {
|
1625 |
// fallback to original keyword matching
|
|
|
1630 |
}
|
1631 |
}
|
1632 |
if (contextWords.length > 0) {
|
1633 |
+
score += (keywordMatches / contextWords.length) * this.config.relevance.keywordOverlap;
|
1634 |
}
|
1635 |
}
|
1636 |
|
|
|
|
|
1637 |
// Category relevance bonus based on context
|
1638 |
+
score += this.getCategoryRelevance(memory.category, context) * this.config.relevance.categoryRelevance;
|
1639 |
|
1640 |
// Recent memories get bonus for current conversation
|
1641 |
+
const daysSinceCreation = this.getDaysSinceCreation(memory);
|
1642 |
+
score +=
|
1643 |
+
Math.max(
|
1644 |
+
0,
|
1645 |
+
(this.config.relevance.recentDaysThreshold - daysSinceCreation) / this.config.relevance.recentDaysThreshold
|
1646 |
+
) * this.config.relevance.recencyBonus;
|
1647 |
|
1648 |
// Confidence and importance boost
|
1649 |
+
score += (memory.confidence || 0.5) * this.config.relevance.confidenceBonus;
|
1650 |
+
score += (memory.importance || 0.5) * this.config.relevance.importanceBonus;
|
1651 |
|
1652 |
return Math.min(1.0, score);
|
1653 |
}
|
|
|
1840 |
// Touch multiple memories to update lastAccess and accessCount
|
1841 |
async _touchMemories(memories, limit = 5) {
|
1842 |
if (!this.db || !Array.isArray(memories) || memories.length === 0) return;
|
1843 |
+
|
1844 |
try {
|
1845 |
const top = memories.slice(0, limit);
|
1846 |
+
const now = new Date();
|
1847 |
+
const minMinutes = window.KIMI_MEMORY_TOUCH_MINUTES || 60;
|
1848 |
+
const minTouchInterval = minMinutes * 60 * 1000;
|
1849 |
+
|
1850 |
+
// Batch collection: gather all updates before executing
|
1851 |
+
const batchUpdates = [];
|
1852 |
+
|
1853 |
for (const m of top) {
|
1854 |
try {
|
1855 |
const id = m.id;
|
1856 |
const existing = await this.db.db.memories.get(id);
|
1857 |
if (existing) {
|
1858 |
const lastAccess = existing.lastAccess ? new Date(existing.lastAccess).getTime() : 0;
|
1859 |
+
|
1860 |
+
// Only touch if enough time has passed
|
1861 |
+
if (now.getTime() - lastAccess > minTouchInterval) {
|
1862 |
+
batchUpdates.push({
|
1863 |
+
key: id,
|
1864 |
+
changes: {
|
1865 |
+
accessCount: (existing.accessCount || 0) + 1,
|
1866 |
+
lastAccess: now
|
1867 |
+
}
|
1868 |
+
});
|
1869 |
}
|
1870 |
}
|
1871 |
} catch (e) {
|
1872 |
+
console.warn("Error preparing memory touch batch for", m && m.id, e);
|
1873 |
+
}
|
1874 |
+
}
|
1875 |
+
|
1876 |
+
// Execute all updates in a single batch operation
|
1877 |
+
if (batchUpdates.length > 0) {
|
1878 |
+
if (this.db.db.memories.bulkUpdate) {
|
1879 |
+
// Use bulkUpdate if available (Dexie 3.x+)
|
1880 |
+
await this.db.db.memories.bulkUpdate(batchUpdates);
|
1881 |
+
} else {
|
1882 |
+
// Fallback: parallel individual updates (still better than sequential)
|
1883 |
+
const updatePromises = batchUpdates.map(update => this.db.db.memories.update(update.key, update.changes));
|
1884 |
+
await Promise.all(updatePromises);
|
1885 |
+
}
|
1886 |
+
|
1887 |
+
if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
|
1888 |
+
console.log(`📊 Batch touched ${batchUpdates.length} memories`);
|
1889 |
}
|
1890 |
}
|
|
|
1891 |
} catch (e) {
|
1892 |
+
console.warn("Error in _touchMemories batch processing", e);
|
1893 |
}
|
1894 |
}
|
1895 |
|
|
|
1982 |
// Exclude existing summaries to avoid summarizing summaries repeatedly
|
1983 |
const recent = all.filter(
|
1984 |
m =>
|
1985 |
+
new Date(this.getCreationTimestamp(m)).getTime() >= cutoff &&
|
1986 |
m.isActive &&
|
1987 |
m.type !== "summary" &&
|
1988 |
!(m.tags && m.tags.includes("summary"))
|
|
|
2016 |
sourceText: summaryContent,
|
2017 |
summaryJson: JSON.stringify(summaryJson),
|
2018 |
confidence: 0.9,
|
2019 |
+
createdAt: new Date(), // Use createdAt consistently
|
2020 |
character: this.selectedCharacter,
|
2021 |
isActive: true,
|
2022 |
tags: ["summary"]
|
|
|
2051 |
// Exclude existing summaries to avoid recursive summarization
|
2052 |
const recent = all.filter(
|
2053 |
m =>
|
2054 |
+
new Date(this.getCreationTimestamp(m)).getTime() >= cutoff &&
|
2055 |
m.isActive &&
|
2056 |
m.type !== "summary" &&
|
2057 |
!(m.tags && m.tags.includes("summary"))
|
|
|
2059 |
if (!recent.length) return null;
|
2060 |
|
2061 |
// Build aggregate content from readable fields in chronological order
|
2062 |
+
recent.sort((a, b) => new Date(this.getCreationTimestamp(a)) - new Date(this.getCreationTimestamp(b)));
|
2063 |
const texts = recent
|
2064 |
.map(r => {
|
2065 |
const raw =
|
|
|
2290 |
|
2291 |
window.KimiMemorySystem = KimiMemorySystem;
|
2292 |
export default KimiMemorySystem;
|
|
|
|
kimi-js/kimi-memory.js
CHANGED
@@ -22,14 +22,12 @@ class KimiMemory {
|
|
22 |
}
|
23 |
try {
|
24 |
this.selectedCharacter = await this.db.getSelectedCharacter();
|
25 |
-
// Start with lower favorability level - relationships must be built over time
|
26 |
-
this.favorabilityLevel = await this.db.getPreference(`favorabilityLevel_${this.selectedCharacter}`, 50);
|
27 |
|
28 |
-
// Load affection trait from personality database with
|
29 |
const charDefAff =
|
30 |
(window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[this.selectedCharacter]?.traits?.affection) || null;
|
31 |
-
const
|
32 |
-
const defaultAff = typeof charDefAff === "number" ? charDefAff :
|
33 |
this.affectionTrait = await this.db.getPersonalityTrait("affection", defaultAff, this.selectedCharacter);
|
34 |
|
35 |
this.preferences = {
|
@@ -53,7 +51,17 @@ class KimiMemory {
|
|
53 |
|
54 |
try {
|
55 |
const character = await this.db.getSelectedCharacter();
|
56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
|
58 |
// Legacy interactions counter kept for backward compatibility (not shown in UI now)
|
59 |
let total = await this.db.getPreference(`totalInteractions_${character}`, 0);
|
@@ -100,7 +108,12 @@ class KimiMemory {
|
|
100 |
try {
|
101 |
this.selectedCharacter = await this.db.getSelectedCharacter();
|
102 |
// Use unified default that matches KimiEmotionSystem
|
103 |
-
|
|
|
|
|
|
|
|
|
|
|
104 |
this.updateFavorabilityBar();
|
105 |
} catch (error) {
|
106 |
console.error("Error updating affection trait:", error);
|
@@ -117,13 +130,24 @@ class KimiMemory {
|
|
117 |
}
|
118 |
}
|
119 |
|
120 |
-
getGreeting() {
|
121 |
const i18n = window.kimiI18nManager;
|
122 |
|
123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
return i18n?.t("greeting_low") || "Hello.";
|
125 |
}
|
126 |
-
if (
|
127 |
return i18n?.t("greeting_mid") || "Hi. How can I help you?";
|
128 |
}
|
129 |
return i18n?.t("greeting_high") || "Hello my love! 💕";
|
|
|
22 |
}
|
23 |
try {
|
24 |
this.selectedCharacter = await this.db.getSelectedCharacter();
|
|
|
|
|
25 |
|
26 |
+
// Load affection trait from personality database with unified defaults
|
27 |
const charDefAff =
|
28 |
(window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[this.selectedCharacter]?.traits?.affection) || null;
|
29 |
+
const unifiedDefaults = window.kimiEmotionSystem?.TRAIT_DEFAULTS || { affection: 55 };
|
30 |
+
const defaultAff = typeof charDefAff === "number" ? charDefAff : unifiedDefaults.affection;
|
31 |
this.affectionTrait = await this.db.getPersonalityTrait("affection", defaultAff, this.selectedCharacter);
|
32 |
|
33 |
this.preferences = {
|
|
|
51 |
|
52 |
try {
|
53 |
const character = await this.db.getSelectedCharacter();
|
54 |
+
|
55 |
+
// Use global personality average for conversation favorability score
|
56 |
+
let relationshipLevel = 50; // fallback
|
57 |
+
try {
|
58 |
+
const traits = await this.db.getAllPersonalityTraits(character);
|
59 |
+
relationshipLevel = window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50;
|
60 |
+
} catch (error) {
|
61 |
+
console.warn("Error calculating relationship level for conversation:", error);
|
62 |
+
}
|
63 |
+
|
64 |
+
await this.db.saveConversation(userText, kimiResponse, relationshipLevel, new Date(), character);
|
65 |
|
66 |
// Legacy interactions counter kept for backward compatibility (not shown in UI now)
|
67 |
let total = await this.db.getPreference(`totalInteractions_${character}`, 0);
|
|
|
108 |
try {
|
109 |
this.selectedCharacter = await this.db.getSelectedCharacter();
|
110 |
// Use unified default that matches KimiEmotionSystem
|
111 |
+
const unifiedDefaults = window.kimiEmotionSystem?.TRAIT_DEFAULTS || { affection: 55 };
|
112 |
+
this.affectionTrait = await this.db.getPersonalityTrait(
|
113 |
+
"affection",
|
114 |
+
unifiedDefaults.affection,
|
115 |
+
this.selectedCharacter
|
116 |
+
);
|
117 |
this.updateFavorabilityBar();
|
118 |
} catch (error) {
|
119 |
console.error("Error updating affection trait:", error);
|
|
|
130 |
}
|
131 |
}
|
132 |
|
133 |
+
async getGreeting() {
|
134 |
const i18n = window.kimiI18nManager;
|
135 |
|
136 |
+
// Use global personality average instead of just affection trait
|
137 |
+
let relationshipLevel = 50; // fallback
|
138 |
+
try {
|
139 |
+
if (this.db) {
|
140 |
+
const traits = await this.db.getAllPersonalityTraits(this.selectedCharacter);
|
141 |
+
relationshipLevel = window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50;
|
142 |
+
}
|
143 |
+
} catch (error) {
|
144 |
+
console.warn("Error calculating greeting level:", error);
|
145 |
+
}
|
146 |
+
|
147 |
+
if (relationshipLevel <= 10) {
|
148 |
return i18n?.t("greeting_low") || "Hello.";
|
149 |
}
|
150 |
+
if (relationshipLevel < 40) {
|
151 |
return i18n?.t("greeting_mid") || "Hi. How can I help you?";
|
152 |
}
|
153 |
return i18n?.t("greeting_high") || "Hello my love! 💕";
|
kimi-js/kimi-module.js
CHANGED
@@ -22,23 +22,9 @@ function updateFavorabilityLabel(characterKey) {
|
|
22 |
}
|
23 |
}
|
24 |
|
25 |
-
//
|
26 |
function computePersonalityAverage(traits) {
|
27 |
-
|
28 |
-
return Number(window.kimiEmotionSystem.calculatePersonalityAverage(traits).toFixed(2));
|
29 |
-
}
|
30 |
-
// Fallback minimal (should rarely occur before emotion system init)
|
31 |
-
const keys = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
|
32 |
-
let sum = 0,
|
33 |
-
count = 0;
|
34 |
-
for (const k of keys) {
|
35 |
-
const v = traits && traits[k];
|
36 |
-
if (typeof v === "number" && isFinite(v)) {
|
37 |
-
sum += Math.max(0, Math.min(100, v));
|
38 |
-
count++;
|
39 |
-
}
|
40 |
-
}
|
41 |
-
return count ? Number((sum / count).toFixed(2)) : 0;
|
42 |
}
|
43 |
|
44 |
// Update UI elements (bar + percentage text + label) based on overall personality average
|
@@ -293,10 +279,8 @@ async function analyzeAndReact(text, useAdvancedLLM = true, onStreamToken = null
|
|
293 |
const affection = typeof traits.affection === "number" ? traits.affection : 55;
|
294 |
const characterTraits = window.KIMI_CHARACTERS[selectedCharacter]?.traits || "";
|
295 |
|
296 |
-
//
|
297 |
-
|
298 |
-
kimiVideo.startListening();
|
299 |
-
}
|
300 |
|
301 |
if (typeof window.updatePersonalityTraitsFromEmotion === "function") {
|
302 |
await window.updatePersonalityTraitsFromEmotion(reaction, sanitizedText);
|
@@ -341,13 +325,9 @@ async function analyzeAndReact(text, useAdvancedLLM = true, onStreamToken = null
|
|
341 |
if (userAskedDance) {
|
342 |
kimiVideo.switchToContext("dancing", "dancing", null, updatedTraits, updatedTraits.affection);
|
343 |
} else {
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
{ reaction: reaction, intensity: emotionIntensity },
|
348 |
-
updatedTraits,
|
349 |
-
updatedTraits.affection
|
350 |
-
);
|
351 |
}
|
352 |
|
353 |
if (kimiLLM.updatePersonalityFromResponse) {
|
@@ -528,7 +508,12 @@ function addMessageToChat(sender, text, conversationId = null) {
|
|
528 |
messageTimeDiv.appendChild(deleteBtn);
|
529 |
|
530 |
const textDiv = document.createElement("div");
|
531 |
-
|
|
|
|
|
|
|
|
|
|
|
532 |
|
533 |
messageDiv.appendChild(textDiv);
|
534 |
messageDiv.appendChild(messageTimeDiv);
|
@@ -539,7 +524,12 @@ function addMessageToChat(sender, text, conversationId = null) {
|
|
539 |
// Return an object that allows updating the message content for streaming
|
540 |
return {
|
541 |
updateText: newText => {
|
542 |
-
|
|
|
|
|
|
|
|
|
|
|
543 |
// Throttle scrolling to prevent visual stuttering during streaming
|
544 |
if (!textDiv._scrollTimeout) {
|
545 |
textDiv._scrollTimeout = setTimeout(() => {
|
@@ -973,7 +963,9 @@ async function loadAvailableModels() {
|
|
973 |
|
974 |
// Only log once when models are loaded, not repeated calls
|
975 |
if (!loadAvailableModels._lastLoadTime || Date.now() - loadAvailableModels._lastLoadTime > 5000) {
|
976 |
-
|
|
|
|
|
977 |
loadAvailableModels._lastLoadTime = Date.now();
|
978 |
}
|
979 |
const createCard = (id, model) => {
|
@@ -1243,25 +1235,7 @@ async function loadAvailableModels() {
|
|
1243 |
}
|
1244 |
}
|
1245 |
|
1246 |
-
// Debug
|
1247 |
-
window.debugLoadModels = async function () {
|
1248 |
-
console.log("🔧 Manual debug of loadAvailableModels");
|
1249 |
-
console.log("🔧 window.kimiLLM:", window.kimiLLM);
|
1250 |
-
console.log("🔧 Models container:", document.getElementById("models-container"));
|
1251 |
-
|
1252 |
-
if (window.kimiLLM) {
|
1253 |
-
try {
|
1254 |
-
const stats = await window.kimiLLM.getModelStats();
|
1255 |
-
console.log("🔧 Model stats:", stats);
|
1256 |
-
} catch (error) {
|
1257 |
-
console.error("🔧 Error getting model stats:", error);
|
1258 |
-
}
|
1259 |
-
}
|
1260 |
-
|
1261 |
-
if (window.loadAvailableModels) {
|
1262 |
-
await window.loadAvailableModels();
|
1263 |
-
}
|
1264 |
-
};
|
1265 |
|
1266 |
async function sendMessage() {
|
1267 |
const chatInput = document.getElementById("chat-input");
|
@@ -1320,7 +1294,10 @@ async function sendMessage() {
|
|
1320 |
}
|
1321 |
|
1322 |
try {
|
1323 |
-
|
|
|
|
|
|
|
1324 |
let emotionDetected = false;
|
1325 |
|
1326 |
const response = await analyzeAndReact(message, true, token => {
|
@@ -1331,7 +1308,10 @@ async function sendMessage() {
|
|
1331 |
// Progressive analysis disabled to prevent UI flickering during streaming
|
1332 |
// All analysis will be done after streaming completes
|
1333 |
});
|
1334 |
-
|
|
|
|
|
|
|
1335 |
|
1336 |
// Final processing after streaming completes
|
1337 |
let finalResponse = streamingResponse || response;
|
@@ -1968,54 +1948,19 @@ async function syncPersonalityTraits(characterName = null) {
|
|
1968 |
return updatedTraits;
|
1969 |
}
|
1970 |
|
1971 |
-
//
|
1972 |
function validateEmotionContext(emotion) {
|
1973 |
-
|
1974 |
-
const normalized = emotion === "speakingPositive" ? "positive" : emotion === "speakingNegative" ? "negative" : emotion;
|
1975 |
-
// Use unified emotion system for validation
|
1976 |
-
if (window.kimiEmotionSystem) {
|
1977 |
-
return window.kimiEmotionSystem.validateEmotion(normalized);
|
1978 |
-
}
|
1979 |
-
|
1980 |
-
// Fallback validation
|
1981 |
-
const validEmotions = [
|
1982 |
-
"positive",
|
1983 |
-
"negative",
|
1984 |
-
"neutral",
|
1985 |
-
"dancing",
|
1986 |
-
"listening",
|
1987 |
-
"romantic",
|
1988 |
-
"laughing",
|
1989 |
-
"surprise",
|
1990 |
-
"confident",
|
1991 |
-
"shy",
|
1992 |
-
"flirtatious",
|
1993 |
-
"kiss",
|
1994 |
-
"goodbye",
|
1995 |
-
"speakingPositive",
|
1996 |
-
"speakingNegative",
|
1997 |
-
"speaking"
|
1998 |
-
];
|
1999 |
-
|
2000 |
-
if (!validEmotions.includes(normalized)) {
|
2001 |
-
console.warn(`Invalid emotion detected: ${normalized}, falling back to neutral`);
|
2002 |
-
return "neutral";
|
2003 |
-
}
|
2004 |
-
|
2005 |
-
return normalized;
|
2006 |
}
|
2007 |
|
2008 |
-
//
|
2009 |
async function ensureVideoContextConsistency() {
|
2010 |
-
if (!window.kimiVideo) return;
|
2011 |
|
2012 |
-
const
|
2013 |
-
|
2014 |
-
|
2015 |
-
const selectedCharacter = await kimiDB.getSelectedCharacter();
|
2016 |
-
const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
|
2017 |
|
2018 |
-
// Validate current video context
|
2019 |
const currentInfo = window.kimiVideo.getCurrentVideoInfo();
|
2020 |
const validatedEmotion = validateEmotionContext(currentInfo.emotion);
|
2021 |
|
|
|
22 |
}
|
23 |
}
|
24 |
|
25 |
+
// Simplified personality average computation using centralized system
|
26 |
function computePersonalityAverage(traits) {
|
27 |
+
return window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
}
|
29 |
|
30 |
// Update UI elements (bar + percentage text + label) based on overall personality average
|
|
|
279 |
const affection = typeof traits.affection === "number" ? traits.affection : 55;
|
280 |
const characterTraits = window.KIMI_CHARACTERS[selectedCharacter]?.traits || "";
|
281 |
|
282 |
+
// Only trigger listening videos for voice input, NOT for text chat
|
283 |
+
// Text chat should keep neutral videos until LLM response processing begins
|
|
|
|
|
284 |
|
285 |
if (typeof window.updatePersonalityTraitsFromEmotion === "function") {
|
286 |
await window.updatePersonalityTraitsFromEmotion(reaction, sanitizedText);
|
|
|
325 |
if (userAskedDance) {
|
326 |
kimiVideo.switchToContext("dancing", "dancing", null, updatedTraits, updatedTraits.affection);
|
327 |
} else {
|
328 |
+
// Use emotion analysis from the LLM RESPONSE, not user input
|
329 |
+
const responseEmotion = window.kimiEmotionSystem?.analyzeEmotionValidated(response) || "positive";
|
330 |
+
kimiVideo.respondWithEmotion(responseEmotion, updatedTraits, updatedTraits.affection);
|
|
|
|
|
|
|
|
|
331 |
}
|
332 |
|
333 |
if (kimiLLM.updatePersonalityFromResponse) {
|
|
|
508 |
messageTimeDiv.appendChild(deleteBtn);
|
509 |
|
510 |
const textDiv = document.createElement("div");
|
511 |
+
// Use formatted text with HTML support (secure formatting)
|
512 |
+
if (text && window.KimiValidationUtils && window.KimiValidationUtils.formatChatText) {
|
513 |
+
textDiv.innerHTML = window.KimiValidationUtils.formatChatText(text);
|
514 |
+
} else {
|
515 |
+
textDiv.textContent = text || ""; // Fallback to plain text
|
516 |
+
}
|
517 |
|
518 |
messageDiv.appendChild(textDiv);
|
519 |
messageDiv.appendChild(messageTimeDiv);
|
|
|
524 |
// Return an object that allows updating the message content for streaming
|
525 |
return {
|
526 |
updateText: newText => {
|
527 |
+
// Use formatted text for streaming updates too
|
528 |
+
if (newText && window.KimiValidationUtils && window.KimiValidationUtils.formatChatText) {
|
529 |
+
textDiv.innerHTML = window.KimiValidationUtils.formatChatText(newText);
|
530 |
+
} else {
|
531 |
+
textDiv.textContent = newText;
|
532 |
+
}
|
533 |
// Throttle scrolling to prevent visual stuttering during streaming
|
534 |
if (!textDiv._scrollTimeout) {
|
535 |
textDiv._scrollTimeout = setTimeout(() => {
|
|
|
963 |
|
964 |
// Only log once when models are loaded, not repeated calls
|
965 |
if (!loadAvailableModels._lastLoadTime || Date.now() - loadAvailableModels._lastLoadTime > 5000) {
|
966 |
+
if (window.KIMI_CONFIG?.DEBUG?.ENABLED) {
|
967 |
+
console.log(`✅ Loaded ${Object.keys(stats.available).length} LLM models`);
|
968 |
+
}
|
969 |
loadAvailableModels._lastLoadTime = Date.now();
|
970 |
}
|
971 |
const createCard = (id, model) => {
|
|
|
1235 |
}
|
1236 |
}
|
1237 |
|
1238 |
+
// Debug utilities removed for production optimization
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1239 |
|
1240 |
async function sendMessage() {
|
1241 |
const chatInput = document.getElementById("chat-input");
|
|
|
1294 |
}
|
1295 |
|
1296 |
try {
|
1297 |
+
// Start streaming response processing
|
1298 |
+
if (window.KIMI_CONFIG?.DEBUG?.ENABLED) {
|
1299 |
+
console.log("🔄 Starting streaming response...");
|
1300 |
+
}
|
1301 |
let emotionDetected = false;
|
1302 |
|
1303 |
const response = await analyzeAndReact(message, true, token => {
|
|
|
1308 |
// Progressive analysis disabled to prevent UI flickering during streaming
|
1309 |
// All analysis will be done after streaming completes
|
1310 |
});
|
1311 |
+
// Streaming completed
|
1312 |
+
if (window.KIMI_CONFIG?.DEBUG?.ENABLED) {
|
1313 |
+
console.log("✅ Streaming completed, final response length:", streamingResponse.length);
|
1314 |
+
}
|
1315 |
|
1316 |
// Final processing after streaming completes
|
1317 |
let finalResponse = streamingResponse || response;
|
|
|
1948 |
return updatedTraits;
|
1949 |
}
|
1950 |
|
1951 |
+
// Simplified validation using centralized emotion system
|
1952 |
function validateEmotionContext(emotion) {
|
1953 |
+
return window.kimiEmotionSystem?.validateEmotion(emotion) || "neutral";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1954 |
}
|
1955 |
|
1956 |
+
// Simplified video context consistency check using centralized system
|
1957 |
async function ensureVideoContextConsistency() {
|
1958 |
+
if (!window.kimiVideo || !window.kimiDB) return;
|
1959 |
|
1960 |
+
const selectedCharacter = await window.kimiDB.getSelectedCharacter();
|
1961 |
+
const traits = await window.kimiDB.getAllPersonalityTraits(selectedCharacter);
|
|
|
|
|
|
|
1962 |
|
1963 |
+
// Validate current video context using centralized validation
|
1964 |
const currentInfo = window.kimiVideo.getCurrentVideoInfo();
|
1965 |
const validatedEmotion = validateEmotionContext(currentInfo.emotion);
|
1966 |
|
kimi-js/kimi-script.js
CHANGED
@@ -83,6 +83,13 @@ document.addEventListener("DOMContentLoaded", async function () {
|
|
83 |
}
|
84 |
} catch (error) {
|
85 |
console.error("Initialization error:", error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
}
|
87 |
// Centralized helpers for API config UI
|
88 |
const ApiUi = {
|
@@ -196,6 +203,12 @@ document.addEventListener("DOMContentLoaded", async function () {
|
|
196 |
}
|
197 |
} catch (e) {
|
198 |
console.warn("Failed to initialize API config UI:", e);
|
|
|
|
|
|
|
|
|
|
|
|
|
199 |
}
|
200 |
}
|
201 |
// Hydrate API config UI from DB after ApiUi is defined and function declared
|
@@ -520,7 +533,7 @@ document.addEventListener("DOMContentLoaded", async function () {
|
|
520 |
console.error("sendMessage function not available");
|
521 |
}
|
522 |
});
|
523 |
-
console.log("Send button event listener attached");
|
524 |
} else {
|
525 |
console.error("Send button not found");
|
526 |
}
|
@@ -551,7 +564,7 @@ document.addEventListener("DOMContentLoaded", async function () {
|
|
551 |
el.addEventListener("focus", a);
|
552 |
setTimeout(a, 0);
|
553 |
})(chatInput);
|
554 |
-
console.log("Chat input event listener attached");
|
555 |
} else {
|
556 |
console.error("Chat input not found");
|
557 |
}
|
@@ -998,7 +1011,7 @@ document.addEventListener("DOMContentLoaded", async function () {
|
|
998 |
safeTraits[key] = v;
|
999 |
}
|
1000 |
if (window.KIMI_DEBUG_SYNC) {
|
1001 |
-
|
1002 |
}
|
1003 |
// Centralize side-effects elsewhere; aggregator remains a coalesced logger only.
|
1004 |
}
|
|
|
83 |
}
|
84 |
} catch (error) {
|
85 |
console.error("Initialization error:", error);
|
86 |
+
// Log initialization error to error manager
|
87 |
+
if (window.kimiErrorManager) {
|
88 |
+
window.kimiErrorManager.logInitError("KimiApp", error, {
|
89 |
+
selectedCharacter: selectedCharacter,
|
90 |
+
stage: "main_initialization"
|
91 |
+
});
|
92 |
+
}
|
93 |
}
|
94 |
// Centralized helpers for API config UI
|
95 |
const ApiUi = {
|
|
|
203 |
}
|
204 |
} catch (e) {
|
205 |
console.warn("Failed to initialize API config UI:", e);
|
206 |
+
// Log UI initialization error
|
207 |
+
if (window.kimiErrorManager) {
|
208 |
+
window.kimiErrorManager.logUIError("ApiConfigUI", e, {
|
209 |
+
stage: "api_config_initialization"
|
210 |
+
});
|
211 |
+
}
|
212 |
}
|
213 |
}
|
214 |
// Hydrate API config UI from DB after ApiUi is defined and function declared
|
|
|
533 |
console.error("sendMessage function not available");
|
534 |
}
|
535 |
});
|
536 |
+
console.log("✅ Send button event listener attached");
|
537 |
} else {
|
538 |
console.error("Send button not found");
|
539 |
}
|
|
|
564 |
el.addEventListener("focus", a);
|
565 |
setTimeout(a, 0);
|
566 |
})(chatInput);
|
567 |
+
console.log("✅ Chat input event listener attached");
|
568 |
} else {
|
569 |
console.error("Chat input not found");
|
570 |
}
|
|
|
1011 |
safeTraits[key] = v;
|
1012 |
}
|
1013 |
if (window.KIMI_DEBUG_SYNC) {
|
1014 |
+
window.KIMI_CONFIG?.debugLog("SYNC", `Personality updated for ${character}:`, safeTraits);
|
1015 |
}
|
1016 |
// Centralize side-effects elsewhere; aggregator remains a coalesced logger only.
|
1017 |
}
|
kimi-js/kimi-utils.js
CHANGED
@@ -19,6 +19,41 @@ window.KimiValidationUtils = {
|
|
19 |
div.textContent = text;
|
20 |
return div.innerHTML;
|
21 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
validateRange(value, key) {
|
23 |
const bounds = {
|
24 |
voiceRate: { min: 0.5, max: 2, def: 1.1 },
|
|
|
19 |
div.textContent = text;
|
20 |
return div.innerHTML;
|
21 |
},
|
22 |
+
/**
|
23 |
+
* Format chat text with simple markdown-like syntax (secure)
|
24 |
+
* Supports: **bold**, *italic*, and preserves line breaks
|
25 |
+
* Security: All text is escaped first, then selective formatting is applied
|
26 |
+
*/
|
27 |
+
formatChatText(text) {
|
28 |
+
if (!text || typeof text !== "string") return "";
|
29 |
+
|
30 |
+
// First: Escape all HTML to prevent XSS
|
31 |
+
let escaped = this.escapeHtml(text);
|
32 |
+
|
33 |
+
// Optional: Replace em-dash with regular dash if preferred
|
34 |
+
escaped = escaped.replace(/—/g, "-");
|
35 |
+
|
36 |
+
// Second: Apply simple formatting (only on escaped text)
|
37 |
+
// **bold** -> <strong>bold</strong>
|
38 |
+
escaped = escaped.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
39 |
+
|
40 |
+
// *italic* -> <em>italic</em> (but not if already inside **)
|
41 |
+
escaped = escaped.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, "<em>$1</em>");
|
42 |
+
|
43 |
+
// Smart paragraph handling: only create <p> for double line breaks or real paragraphs
|
44 |
+
// Split by double line breaks (\n\n) to identify real paragraphs
|
45 |
+
const realParagraphs = escaped.split(/\n\s*\n/).filter(para => para.trim().length > 0);
|
46 |
+
|
47 |
+
if (realParagraphs.length > 1) {
|
48 |
+
// Multiple paragraphs found - wrap each in <p>
|
49 |
+
escaped = realParagraphs.map(p => `<p>${p.trim().replace(/\n/g, " ")}</p>`).join("");
|
50 |
+
} else {
|
51 |
+
// Single paragraph - just convert single \n to spaces (natural text flow)
|
52 |
+
escaped = escaped.replace(/\n/g, " ");
|
53 |
+
}
|
54 |
+
|
55 |
+
return escaped;
|
56 |
+
},
|
57 |
validateRange(value, key) {
|
58 |
const bounds = {
|
59 |
voiceRate: { min: 0.5, max: 2, def: 1.1 },
|
kimi-js/kimi-videos.js
CHANGED
@@ -60,6 +60,115 @@ class KimiVideoManager {
|
|
60 |
this._consecutiveErrorCount = 0;
|
61 |
// Track per-video load attempts to adapt timeouts & avoid faux échecs
|
62 |
this._videoAttempts = new Map();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
}
|
64 |
|
65 |
//Centralized crossfade transition between two videos.
|
@@ -177,18 +286,55 @@ class KimiVideoManager {
|
|
177 |
console.log("🎬 VideoManager: history summary", summary);
|
178 |
}
|
179 |
|
180 |
-
_priorityWeight(context) {
|
181 |
-
|
182 |
-
if (
|
183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
184 |
}
|
185 |
|
186 |
_enqueuePendingSwitch(req) {
|
187 |
-
//
|
188 |
-
const maxSize =
|
189 |
-
|
190 |
-
if (
|
191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
192 |
}
|
193 |
}
|
194 |
|
@@ -380,9 +526,7 @@ class KimiVideoManager {
|
|
380 |
// Respect sticky context (avoid overrides while dancing is requested/playing)
|
381 |
if (this._stickyContext === "dancing" && context !== "dancing") {
|
382 |
const categoryForPriority = this.determineCategory(context, emotion, traits);
|
383 |
-
const priorityWeight = this._priorityWeight(
|
384 |
-
categoryForPriority === "speakingPositive" || categoryForPriority === "speakingNegative" ? "speaking" : context
|
385 |
-
);
|
386 |
if (Date.now() < (this._stickyUntil || 0)) {
|
387 |
this._enqueuePendingSwitch({
|
388 |
context,
|
@@ -410,9 +554,7 @@ class KimiVideoManager {
|
|
410 |
) {
|
411 |
// Queue the request with appropriate priority to be processed after current clip
|
412 |
const categoryForPriority = this.determineCategory(context, emotion, traits);
|
413 |
-
const priorityWeight = this._priorityWeight(
|
414 |
-
categoryForPriority === "speakingPositive" || categoryForPriority === "speakingNegative" ? "speaking" : context
|
415 |
-
);
|
416 |
this._enqueuePendingSwitch({
|
417 |
context,
|
418 |
emotion,
|
@@ -436,7 +578,7 @@ class KimiVideoManager {
|
|
436 |
this.currentEmotionContext &&
|
437 |
this.currentEmotionContext !== emotion
|
438 |
) {
|
439 |
-
const priorityWeight = this._priorityWeight(
|
440 |
this._enqueuePendingSwitch({
|
441 |
context,
|
442 |
emotion,
|
@@ -512,6 +654,17 @@ class KimiVideoManager {
|
|
512 |
this.lastSwitchTime = Date.now();
|
513 |
return;
|
514 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
515 |
if (window.voiceManager && window.voiceManager.isListening && context === "listening") {
|
516 |
const listeningPath = this.selectOptimalVideo(category, specificVideo, traits, affection, emotion);
|
517 |
const listeningCurrent = this.activeVideo.querySelector("source").getAttribute("src");
|
@@ -652,11 +805,11 @@ class KimiVideoManager {
|
|
652 |
}
|
653 |
}
|
654 |
|
655 |
-
//
|
656 |
selectOptimalVideo(category, specificVideo = null, traits = null, affection = null, emotion = null) {
|
657 |
const availableVideos = this.videoCategories[category] || this.videoCategories.neutral;
|
658 |
|
659 |
-
if (specificVideo && availableVideos.includes(specificVideo)) {
|
660 |
if (typeof this.updatePlayHistory === "function") this.updatePlayHistory(category, specificVideo);
|
661 |
this._logSelection(category, specificVideo, availableVideos);
|
662 |
return specificVideo;
|
@@ -664,23 +817,34 @@ class KimiVideoManager {
|
|
664 |
|
665 |
const currentVideoSrc = this.activeVideo.querySelector("source").getAttribute("src");
|
666 |
|
667 |
-
// Filter out recently played videos using adaptive history
|
668 |
const recentlyPlayed = this.playHistory[category] || [];
|
669 |
-
let candidateVideos = availableVideos.filter(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
670 |
|
671 |
-
// If no
|
672 |
if (candidateVideos.length === 0) {
|
673 |
candidateVideos = availableVideos.filter(video => video !== currentVideoSrc);
|
674 |
}
|
675 |
|
676 |
-
// Ultimate fallback
|
677 |
if (candidateVideos.length === 0) {
|
678 |
candidateVideos = availableVideos;
|
679 |
}
|
680 |
|
681 |
-
//
|
682 |
if (candidateVideos.length === 0) {
|
683 |
-
|
|
|
|
|
|
|
|
|
684 |
}
|
685 |
|
686 |
// If traits and affection are provided, weight the selection more subtly
|
@@ -775,46 +939,17 @@ class KimiVideoManager {
|
|
775 |
}
|
776 |
}
|
777 |
|
778 |
-
//
|
779 |
determineCategory(context, emotion = "neutral", traits = null) {
|
780 |
-
//
|
781 |
-
|
782 |
-
|
783 |
-
positive: "speakingPositive",
|
784 |
-
negative: "speakingNegative",
|
785 |
-
neutral: "neutral",
|
786 |
-
surprise: "speakingPositive",
|
787 |
-
laughing: "speakingPositive",
|
788 |
-
shy: "neutral",
|
789 |
-
confident: "speakingPositive",
|
790 |
-
romantic: "speakingPositive",
|
791 |
-
flirtatious: "speakingPositive",
|
792 |
-
goodbye: "neutral",
|
793 |
-
kiss: "speakingPositive",
|
794 |
-
dancing: "dancing",
|
795 |
-
speaking: "speakingPositive",
|
796 |
-
speakingPositive: "speakingPositive",
|
797 |
-
speakingNegative: "speakingNegative"
|
798 |
-
};
|
799 |
-
|
800 |
-
// Prefer explicit context mapping if provided (e.g., 'listening','dancing')
|
801 |
-
if (emotionToCategory[context]) {
|
802 |
-
return emotionToCategory[context];
|
803 |
-
}
|
804 |
-
// Normalize generic 'speaking' by emotion polarity
|
805 |
-
if (context === "speaking") {
|
806 |
-
if (emotion === "positive") return "speakingPositive";
|
807 |
-
if (emotion === "negative") return "speakingNegative";
|
808 |
-
return "neutral";
|
809 |
-
}
|
810 |
-
// Map by emotion label when possible
|
811 |
-
if (emotionToCategory[emotion]) {
|
812 |
-
return emotionToCategory[emotion];
|
813 |
}
|
814 |
-
return "neutral";
|
815 |
-
}
|
816 |
|
817 |
-
|
|
|
|
|
|
|
818 |
async startListening(traits = null, affection = null) {
|
819 |
// If already listening and playing, avoid redundant switch
|
820 |
if (this.currentContext === "listening" && !this.activeVideo.paused && !this.activeVideo.ended) {
|
@@ -1073,7 +1208,10 @@ class KimiVideoManager {
|
|
1073 |
}
|
1074 |
}
|
1075 |
|
1076 |
-
|
|
|
|
|
|
|
1077 |
this.autoTransitionTimer = setTimeout(() => {
|
1078 |
if (this.currentContext !== "neutral" && this.currentContext !== "listening") {
|
1079 |
if (!this._processPendingSwitches()) {
|
@@ -1139,7 +1277,10 @@ class KimiVideoManager {
|
|
1139 |
}
|
1140 |
// Only log high priority or error cases to reduce noise
|
1141 |
if (priority === "speaking" || priority === "high") {
|
1142 |
-
|
|
|
|
|
|
|
1143 |
}
|
1144 |
|
1145 |
// Si une vidéo haute priorité arrive, on peut interrompre le chargement en cours
|
@@ -1396,7 +1537,9 @@ class KimiVideoManager {
|
|
1396 |
try {
|
1397 |
const src = this.activeVideo?.querySelector("source")?.getAttribute("src");
|
1398 |
const info = { context: this.currentContext, emotion: this.currentEmotion };
|
1399 |
-
|
|
|
|
|
1400 |
// Recompute autoTransitionDuration from actual duration if available (C)
|
1401 |
try {
|
1402 |
const d = this.activeVideo.duration;
|
@@ -1549,25 +1692,72 @@ class KimiVideoManager {
|
|
1549 |
}
|
1550 |
|
1551 |
// METHODS TO ANALYZE EMOTIONS FROM TEXT
|
1552 |
-
// CLEANUP
|
1553 |
destroy() {
|
|
|
1554 |
clearTimeout(this.autoTransitionTimer);
|
|
|
|
|
|
|
|
|
1555 |
this.autoTransitionTimer = null;
|
|
|
|
|
|
|
|
|
|
|
1556 |
if (this._visibilityHandler) {
|
1557 |
document.removeEventListener("visibilitychange", this._visibilityHandler);
|
1558 |
this._visibilityHandler = null;
|
1559 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1560 |
}
|
1561 |
|
1562 |
-
//
|
1563 |
setMoodByPersonality(traits) {
|
1564 |
if (this._stickyContext === "dancing" || this.currentContext === "dancing") return;
|
1565 |
-
|
1566 |
-
//
|
|
|
|
|
|
|
1567 |
let emotion = category;
|
1568 |
if (category === "speakingPositive") emotion = "positive";
|
1569 |
else if (category === "speakingNegative") emotion = "negative";
|
1570 |
-
|
1571 |
this.switchToContext(category, emotion, null, traits, traits.affection);
|
1572 |
}
|
1573 |
|
|
|
60 |
this._consecutiveErrorCount = 0;
|
61 |
// Track per-video load attempts to adapt timeouts & avoid faux échecs
|
62 |
this._videoAttempts = new Map();
|
63 |
+
|
64 |
+
// Error handling and recovery system
|
65 |
+
this._errorRecoveryAttempts = 0;
|
66 |
+
this._maxRecoveryAttempts = 3;
|
67 |
+
this._lastErrorTime = 0;
|
68 |
+
this._errorThreshold = 5000; // 5 seconds between error recovery attempts
|
69 |
+
}
|
70 |
+
|
71 |
+
// ===== ERROR HANDLING AND RECOVERY SYSTEM =====
|
72 |
+
_handleVideoError(error, videoSrc = null, context = "unknown") {
|
73 |
+
this._consecutiveErrorCount++;
|
74 |
+
this._lastErrorTime = Date.now();
|
75 |
+
|
76 |
+
const errorInfo = {
|
77 |
+
error: error.message || "Unknown video error",
|
78 |
+
videoSrc: videoSrc || "unknown",
|
79 |
+
context,
|
80 |
+
timestamp: Date.now(),
|
81 |
+
consecutiveCount: this._consecutiveErrorCount
|
82 |
+
};
|
83 |
+
|
84 |
+
if (window.KIMI_CONFIG?.DEBUG?.VIDEO) {
|
85 |
+
console.warn("🎬 Video error occurred:", errorInfo);
|
86 |
+
}
|
87 |
+
|
88 |
+
// Track failures for this specific video
|
89 |
+
if (videoSrc) {
|
90 |
+
this._recentFailures.set(videoSrc, Date.now());
|
91 |
+
}
|
92 |
+
|
93 |
+
// Attempt recovery if not too many consecutive errors
|
94 |
+
if (this._consecutiveErrorCount <= this._maxRecoveryAttempts) {
|
95 |
+
this._attemptErrorRecovery(context);
|
96 |
+
} else {
|
97 |
+
console.error("🎬 Too many consecutive video errors, disabling auto-recovery");
|
98 |
+
this._fallbackToSafeState();
|
99 |
+
}
|
100 |
+
}
|
101 |
+
|
102 |
+
_attemptErrorRecovery(context) {
|
103 |
+
console.log("🎬 Attempting video error recovery...");
|
104 |
+
|
105 |
+
// Try to switch to a safe neutral video
|
106 |
+
setTimeout(() => {
|
107 |
+
try {
|
108 |
+
// Choose a different neutral video that hasn't failed recently
|
109 |
+
const neutralVideos = this.videoCategories.neutral || [];
|
110 |
+
const safeVideos = neutralVideos.filter(
|
111 |
+
video =>
|
112 |
+
!this._recentFailures.has(video) || Date.now() - this._recentFailures.get(video) > this._failureCooldown
|
113 |
+
);
|
114 |
+
|
115 |
+
if (safeVideos.length > 0) {
|
116 |
+
const safeVideo = safeVideos[0];
|
117 |
+
this._resetErrorState();
|
118 |
+
this.loadAndSwitchVideo(safeVideo, "recovery");
|
119 |
+
console.log("🎬 Video error recovery successful");
|
120 |
+
} else {
|
121 |
+
this._fallbackToSafeState();
|
122 |
+
}
|
123 |
+
} catch (recoveryError) {
|
124 |
+
console.error("🎬 Video recovery failed:", recoveryError);
|
125 |
+
this._fallbackToSafeState();
|
126 |
+
}
|
127 |
+
}, 1000); // Small delay before recovery attempt
|
128 |
+
}
|
129 |
+
|
130 |
+
_fallbackToSafeState() {
|
131 |
+
console.log("🎬 Falling back to safe state - pausing video system");
|
132 |
+
|
133 |
+
// Pause both videos to avoid further errors
|
134 |
+
try {
|
135 |
+
this.activeVideo?.pause();
|
136 |
+
this.inactiveVideo?.pause();
|
137 |
+
} catch (e) {
|
138 |
+
// Silent fallback
|
139 |
+
}
|
140 |
+
|
141 |
+
// Clear all pending operations
|
142 |
+
this._pendingSwitches.length = 0;
|
143 |
+
this._stickyContext = null;
|
144 |
+
this._stickyUntil = 0;
|
145 |
+
this.isEmotionVideoPlaying = false;
|
146 |
+
|
147 |
+
// Reset state with long cooldown
|
148 |
+
setTimeout(() => {
|
149 |
+
this._resetErrorState();
|
150 |
+
console.log("🎬 Video system ready for retry");
|
151 |
+
}, 10000);
|
152 |
+
}
|
153 |
+
|
154 |
+
_resetErrorState() {
|
155 |
+
this._consecutiveErrorCount = 0;
|
156 |
+
this._errorRecoveryAttempts = 0;
|
157 |
+
|
158 |
+
// Clean old failure records
|
159 |
+
const now = Date.now();
|
160 |
+
for (const [video, timestamp] of this._recentFailures.entries()) {
|
161 |
+
if (now - timestamp > this._failureCooldown) {
|
162 |
+
this._recentFailures.delete(video);
|
163 |
+
}
|
164 |
+
}
|
165 |
+
}
|
166 |
+
|
167 |
+
_isVideoSafe(videoSrc) {
|
168 |
+
if (!this._recentFailures.has(videoSrc)) return true;
|
169 |
+
|
170 |
+
const lastFailure = this._recentFailures.get(videoSrc);
|
171 |
+
return Date.now() - lastFailure > this._failureCooldown;
|
172 |
}
|
173 |
|
174 |
//Centralized crossfade transition between two videos.
|
|
|
286 |
console.log("🎬 VideoManager: history summary", summary);
|
287 |
}
|
288 |
|
289 |
+
_priorityWeight(context, emotion = "neutral") {
|
290 |
+
// Use centralized priority system from emotion system if available
|
291 |
+
if (window.kimiEmotionSystem?.getPriorityWeight) {
|
292 |
+
// Try emotion first (more specific), then context
|
293 |
+
const emotionPriority = window.kimiEmotionSystem.getPriorityWeight(emotion);
|
294 |
+
const contextPriority = window.kimiEmotionSystem.getPriorityWeight(context);
|
295 |
+
return Math.max(emotionPriority, contextPriority);
|
296 |
+
}
|
297 |
+
|
298 |
+
// Legacy fallback priorities if emotion system not available
|
299 |
+
if (context === "dancing") return 10;
|
300 |
+
if (context === "listening") return 7;
|
301 |
+
if (context === "speaking" || context === "speakingPositive" || context === "speakingNegative") return 4;
|
302 |
+
return 3; // Default priority for neutral and other contexts
|
303 |
}
|
304 |
|
305 |
_enqueuePendingSwitch(req) {
|
306 |
+
// Intelligent queue management - limit to 3 for better responsiveness
|
307 |
+
const maxSize = 3;
|
308 |
+
|
309 |
+
// Check if we already have a similar request (same context + emotion)
|
310 |
+
const existingIndex = this._pendingSwitches.findIndex(
|
311 |
+
pending => pending.context === req.context && pending.emotion === req.emotion
|
312 |
+
);
|
313 |
+
|
314 |
+
if (existingIndex !== -1) {
|
315 |
+
// Replace existing similar request with newer one
|
316 |
+
this._pendingSwitches[existingIndex] = req;
|
317 |
+
this._logDebug("Replaced similar pending switch", { context: req.context, emotion: req.emotion });
|
318 |
+
} else {
|
319 |
+
// Add new request
|
320 |
+
this._pendingSwitches.push(req);
|
321 |
+
|
322 |
+
// If exceeded max size, remove oldest lower-priority request
|
323 |
+
if (this._pendingSwitches.length > maxSize) {
|
324 |
+
// Sort by priority weight (lower = remove first) then by age (older = remove first)
|
325 |
+
this._pendingSwitches.sort((a, b) => {
|
326 |
+
const priorityDiff = (b.priorityWeight || 1) - (a.priorityWeight || 1);
|
327 |
+
if (priorityDiff !== 0) return priorityDiff;
|
328 |
+
return a.requestedAt - b.requestedAt; // Older first
|
329 |
+
});
|
330 |
+
|
331 |
+
// Remove the lowest priority, oldest request
|
332 |
+
const removed = this._pendingSwitches.shift();
|
333 |
+
this._logDebug("Removed low-priority pending switch", {
|
334 |
+
removed: removed.context,
|
335 |
+
queueSize: this._pendingSwitches.length
|
336 |
+
});
|
337 |
+
}
|
338 |
}
|
339 |
}
|
340 |
|
|
|
526 |
// Respect sticky context (avoid overrides while dancing is requested/playing)
|
527 |
if (this._stickyContext === "dancing" && context !== "dancing") {
|
528 |
const categoryForPriority = this.determineCategory(context, emotion, traits);
|
529 |
+
const priorityWeight = this._priorityWeight(context, emotion);
|
|
|
|
|
530 |
if (Date.now() < (this._stickyUntil || 0)) {
|
531 |
this._enqueuePendingSwitch({
|
532 |
context,
|
|
|
554 |
) {
|
555 |
// Queue the request with appropriate priority to be processed after current clip
|
556 |
const categoryForPriority = this.determineCategory(context, emotion, traits);
|
557 |
+
const priorityWeight = this._priorityWeight(context, emotion);
|
|
|
|
|
558 |
this._enqueuePendingSwitch({
|
559 |
context,
|
560 |
emotion,
|
|
|
578 |
this.currentEmotionContext &&
|
579 |
this.currentEmotionContext !== emotion
|
580 |
) {
|
581 |
+
const priorityWeight = this._priorityWeight(context, emotion);
|
582 |
this._enqueuePendingSwitch({
|
583 |
context,
|
584 |
emotion,
|
|
|
654 |
this.lastSwitchTime = Date.now();
|
655 |
return;
|
656 |
}
|
657 |
+
|
658 |
+
// ALSO handle speaking contexts even when TTS is not yet flagged as speaking
|
659 |
+
// This ensures immediate response to speaking context requests
|
660 |
+
if (context === "speaking" || context === "speakingPositive" || context === "speakingNegative") {
|
661 |
+
const speakingPath = this.selectOptimalVideo(category, specificVideo, traits, affection, emotion);
|
662 |
+
this.loadAndSwitchVideo(speakingPath, priority);
|
663 |
+
this.currentContext = category;
|
664 |
+
this.currentEmotion = emotion;
|
665 |
+
this.lastSwitchTime = Date.now();
|
666 |
+
return;
|
667 |
+
}
|
668 |
if (window.voiceManager && window.voiceManager.isListening && context === "listening") {
|
669 |
const listeningPath = this.selectOptimalVideo(category, specificVideo, traits, affection, emotion);
|
670 |
const listeningCurrent = this.activeVideo.querySelector("source").getAttribute("src");
|
|
|
805 |
}
|
806 |
}
|
807 |
|
808 |
+
// Enhanced selectOptimalVideo with safety checks
|
809 |
selectOptimalVideo(category, specificVideo = null, traits = null, affection = null, emotion = null) {
|
810 |
const availableVideos = this.videoCategories[category] || this.videoCategories.neutral;
|
811 |
|
812 |
+
if (specificVideo && availableVideos.includes(specificVideo) && this._isVideoSafe(specificVideo)) {
|
813 |
if (typeof this.updatePlayHistory === "function") this.updatePlayHistory(category, specificVideo);
|
814 |
this._logSelection(category, specificVideo, availableVideos);
|
815 |
return specificVideo;
|
|
|
817 |
|
818 |
const currentVideoSrc = this.activeVideo.querySelector("source").getAttribute("src");
|
819 |
|
820 |
+
// Filter out recently played videos using adaptive history AND safety checks
|
821 |
const recentlyPlayed = this.playHistory[category] || [];
|
822 |
+
let candidateVideos = availableVideos.filter(
|
823 |
+
video => video !== currentVideoSrc && !recentlyPlayed.includes(video) && this._isVideoSafe(video)
|
824 |
+
);
|
825 |
+
|
826 |
+
// If no safe fresh videos, allow recently played but safe videos (not current)
|
827 |
+
if (candidateVideos.length === 0) {
|
828 |
+
candidateVideos = availableVideos.filter(video => video !== currentVideoSrc && this._isVideoSafe(video));
|
829 |
+
}
|
830 |
|
831 |
+
// If still no safe videos, use any available (excluding current)
|
832 |
if (candidateVideos.length === 0) {
|
833 |
candidateVideos = availableVideos.filter(video => video !== currentVideoSrc);
|
834 |
}
|
835 |
|
836 |
+
// Ultimate fallback - use all available
|
837 |
if (candidateVideos.length === 0) {
|
838 |
candidateVideos = availableVideos;
|
839 |
}
|
840 |
|
841 |
+
// Final fallback to neutral category if current category is empty
|
842 |
if (candidateVideos.length === 0) {
|
843 |
+
const neutralVideos = this.videoCategories.neutral || [];
|
844 |
+
candidateVideos = neutralVideos.filter(video => this._isVideoSafe(video));
|
845 |
+
if (candidateVideos.length === 0) {
|
846 |
+
candidateVideos = neutralVideos; // Last resort
|
847 |
+
}
|
848 |
}
|
849 |
|
850 |
// If traits and affection are provided, weight the selection more subtly
|
|
|
939 |
}
|
940 |
}
|
941 |
|
942 |
+
// Simplified determineCategory - pure delegation to centralized system
|
943 |
determineCategory(context, emotion = "neutral", traits = null) {
|
944 |
+
// Use centralized emotion system exclusively for consistency
|
945 |
+
if (window.kimiEmotionSystem?.getVideoCategory) {
|
946 |
+
return window.kimiEmotionSystem.getVideoCategory(context || emotion, traits);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
947 |
}
|
|
|
|
|
948 |
|
949 |
+
// Minimal fallback only if emotion system completely unavailable
|
950 |
+
console.warn("KimiEmotionSystem not available - using minimal fallback");
|
951 |
+
return "neutral";
|
952 |
+
} // SPECIALIZED METHODS FOR EACH CONTEXT
|
953 |
async startListening(traits = null, affection = null) {
|
954 |
// If already listening and playing, avoid redundant switch
|
955 |
if (this.currentContext === "listening" && !this.activeVideo.paused && !this.activeVideo.ended) {
|
|
|
1208 |
}
|
1209 |
}
|
1210 |
|
1211 |
+
// Auto-transition timing
|
1212 |
+
if (window.KIMI_CONFIG?.DEBUG?.VIDEO) {
|
1213 |
+
console.log(`Auto-transition scheduled in ${duration / 1000}s (${this.currentContext} → neutral)`);
|
1214 |
+
}
|
1215 |
this.autoTransitionTimer = setTimeout(() => {
|
1216 |
if (this.currentContext !== "neutral" && this.currentContext !== "listening") {
|
1217 |
if (!this._processPendingSwitches()) {
|
|
|
1277 |
}
|
1278 |
// Only log high priority or error cases to reduce noise
|
1279 |
if (priority === "speaking" || priority === "high") {
|
1280 |
+
// Video loading with priority
|
1281 |
+
if (window.KIMI_CONFIG?.DEBUG?.VIDEO) {
|
1282 |
+
console.log(`🎬 Loading video: ${videoSrc} (priority: ${priority})`);
|
1283 |
+
}
|
1284 |
}
|
1285 |
|
1286 |
// Si une vidéo haute priorité arrive, on peut interrompre le chargement en cours
|
|
|
1537 |
try {
|
1538 |
const src = this.activeVideo?.querySelector("source")?.getAttribute("src");
|
1539 |
const info = { context: this.currentContext, emotion: this.currentEmotion };
|
1540 |
+
if (this._debug) {
|
1541 |
+
console.log("🎬 VideoManager: Now playing:", src, info);
|
1542 |
+
}
|
1543 |
// Recompute autoTransitionDuration from actual duration if available (C)
|
1544 |
try {
|
1545 |
const d = this.activeVideo.duration;
|
|
|
1692 |
}
|
1693 |
|
1694 |
// METHODS TO ANALYZE EMOTIONS FROM TEXT
|
1695 |
+
// CLEANUP - Enhanced memory management
|
1696 |
destroy() {
|
1697 |
+
// Clear all timers
|
1698 |
clearTimeout(this.autoTransitionTimer);
|
1699 |
+
clearTimeout(this._warmupTimer);
|
1700 |
+
clearTimeout(this._listeningGraceTimer);
|
1701 |
+
clearTimeout(this._pendingSpeakSwitch);
|
1702 |
+
|
1703 |
this.autoTransitionTimer = null;
|
1704 |
+
this._warmupTimer = null;
|
1705 |
+
this._listeningGraceTimer = null;
|
1706 |
+
this._pendingSpeakSwitch = null;
|
1707 |
+
|
1708 |
+
// Remove all event listeners
|
1709 |
if (this._visibilityHandler) {
|
1710 |
document.removeEventListener("visibilitychange", this._visibilityHandler);
|
1711 |
this._visibilityHandler = null;
|
1712 |
}
|
1713 |
+
|
1714 |
+
if (this._firstInteractionHandler) {
|
1715 |
+
window.removeEventListener("click", this._firstInteractionHandler);
|
1716 |
+
window.removeEventListener("keydown", this._firstInteractionHandler);
|
1717 |
+
this._firstInteractionHandler = null;
|
1718 |
+
}
|
1719 |
+
|
1720 |
+
// Clean up video loading handlers
|
1721 |
+
this._cleanupLoadingHandlers();
|
1722 |
+
|
1723 |
+
// Clear global ended handler
|
1724 |
+
if (this._globalEndedHandler) {
|
1725 |
+
this.activeVideo?.removeEventListener("ended", this._globalEndedHandler);
|
1726 |
+
this.inactiveVideo?.removeEventListener("ended", this._globalEndedHandler);
|
1727 |
+
this._globalEndedHandler = null;
|
1728 |
+
}
|
1729 |
+
|
1730 |
+
// Clear caches and queues
|
1731 |
+
this._prefetchCache.clear();
|
1732 |
+
this._prefetchInFlight.clear();
|
1733 |
+
this._pendingSwitches.length = 0;
|
1734 |
+
this._videoAttempts.clear();
|
1735 |
+
this._recentFailures.clear();
|
1736 |
+
|
1737 |
+
// Reset history to prevent memory accumulation
|
1738 |
+
this.playHistory = {};
|
1739 |
+
this.emotionHistory.length = 0;
|
1740 |
+
|
1741 |
+
// Reset state flags
|
1742 |
+
this._stickyContext = null;
|
1743 |
+
this._stickyUntil = 0;
|
1744 |
+
this.isEmotionVideoPlaying = false;
|
1745 |
+
this.currentEmotionContext = null;
|
1746 |
+
this._neutralLock = false;
|
1747 |
}
|
1748 |
|
1749 |
+
// Simplified mood setting using centralized emotion system
|
1750 |
setMoodByPersonality(traits) {
|
1751 |
if (this._stickyContext === "dancing" || this.currentContext === "dancing") return;
|
1752 |
+
|
1753 |
+
// Use centralized mood calculation from emotion system
|
1754 |
+
const category = window.kimiEmotionSystem?.getMoodCategoryFromPersonality(traits) || "neutral";
|
1755 |
+
|
1756 |
+
// Normalize emotion for consistent validation
|
1757 |
let emotion = category;
|
1758 |
if (category === "speakingPositive") emotion = "positive";
|
1759 |
else if (category === "speakingNegative") emotion = "negative";
|
1760 |
+
|
1761 |
this.switchToContext(category, emotion, null, traits, traits.affection);
|
1762 |
}
|
1763 |
|
kimi-js/kimi-voices.js
CHANGED
@@ -70,16 +70,22 @@ class KimiVoiceManager {
|
|
70 |
this.transcriptText = document.getElementById("transcript");
|
71 |
|
72 |
if (!this.micButton) {
|
73 |
-
|
|
|
|
|
74 |
return false;
|
75 |
}
|
76 |
|
77 |
// Check transcript elements (non-critical, just warn)
|
78 |
if (!this.transcriptContainer) {
|
79 |
-
|
|
|
|
|
80 |
}
|
81 |
if (!this.transcriptText) {
|
82 |
-
|
|
|
|
|
83 |
}
|
84 |
|
85 |
// Initialize voice synthesis
|
@@ -167,13 +173,17 @@ class KimiVoiceManager {
|
|
167 |
try {
|
168 |
// Check if running on file:// protocol
|
169 |
if (window.location.protocol === "file:") {
|
170 |
-
|
|
|
|
|
171 |
this.micPermissionGranted = false;
|
172 |
return;
|
173 |
}
|
174 |
|
175 |
if (!navigator.permissions) {
|
176 |
-
|
|
|
|
|
177 |
this.micPermissionGranted = false; // Set default state
|
178 |
return;
|
179 |
}
|
@@ -316,10 +326,10 @@ class KimiVoiceManager {
|
|
316 |
);
|
317 |
});
|
318 |
|
319 |
-
// Debug:
|
320 |
-
if (
|
321 |
console.log(`🎤 Female voice found: "${femaleVoice.name}" (${femaleVoice.lang})`);
|
322 |
-
} else {
|
323 |
console.log(
|
324 |
`🎤 No female voice found, using first available: "${filteredVoices[0]?.name}" (${filteredVoices[0]?.lang})`
|
325 |
);
|
@@ -542,23 +552,23 @@ class KimiVoiceManager {
|
|
542 |
|
543 |
// ===== CHAT MESSAGE UTILITIES =====
|
544 |
handleChatMessage(userMessage, kimiResponse) {
|
545 |
-
|
546 |
-
const chatMessages = document.getElementById("chat-messages");
|
547 |
-
|
548 |
-
if (!chatContainer || !chatContainer.classList.contains("visible") || !chatMessages) {
|
549 |
-
return;
|
550 |
-
}
|
551 |
-
|
552 |
const addMessageToChat = window.addMessageToChat || (typeof addMessageToChat !== "undefined" ? addMessageToChat : null);
|
553 |
|
554 |
if (addMessageToChat) {
|
|
|
555 |
addMessageToChat("user", userMessage);
|
556 |
addMessageToChat(this.selectedCharacter.toLowerCase(), kimiResponse);
|
557 |
} else {
|
558 |
-
// Fallback
|
559 |
-
|
560 |
-
|
561 |
-
|
|
|
|
|
|
|
|
|
|
|
562 |
}
|
563 |
}
|
564 |
|
@@ -644,12 +654,38 @@ class KimiVoiceManager {
|
|
644 |
|
645 |
// Get volume using centralized utility
|
646 |
utterance.volume = this.getVoicePreference("volume", options);
|
647 |
-
|
648 |
-
|
649 |
-
|
650 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
651 |
});
|
652 |
}
|
|
|
653 |
if (typeof window.updatePersonalityTraitsFromEmotion === "function") {
|
654 |
window.updatePersonalityTraitsFromEmotion(emotionFromText, text);
|
655 |
}
|
@@ -657,24 +693,30 @@ class KimiVoiceManager {
|
|
657 |
|
658 |
utterance.onstart = async () => {
|
659 |
this.isSpeaking = true;
|
660 |
-
// Note: transcript visibility is already handled by showResponseWithPerfectTiming
|
661 |
-
// This ensures the transcript stays visible while AI is speaking
|
662 |
|
663 |
-
//
|
664 |
try {
|
665 |
-
if (window.kimiVideo
|
666 |
-
const
|
667 |
-
|
668 |
-
|
669 |
-
|
670 |
-
|
671 |
-
|
672 |
-
|
673 |
-
window.kimiVideo.switchToContext("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
674 |
}
|
675 |
}
|
676 |
} catch (e) {
|
677 |
-
|
678 |
}
|
679 |
};
|
680 |
|
@@ -684,24 +726,35 @@ class KimiVoiceManager {
|
|
684 |
this.updateTranscriptVisibility(false);
|
685 |
// Clear any pending hide timeout
|
686 |
this.clearTranscriptTimeout();
|
|
|
|
|
687 |
if (window.kimiVideo) {
|
688 |
-
// Do not force neutral if an emotion clip is still playing (speaking/dancing)
|
689 |
try {
|
690 |
const info = window.kimiVideo.getCurrentVideoInfo ? window.kimiVideo.getCurrentVideoInfo() : null;
|
691 |
-
|
692 |
-
|
693 |
-
|
694 |
-
|
695 |
-
|
696 |
-
|
697 |
-
|
698 |
-
|
699 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
700 |
}
|
701 |
-
} catch (
|
702 |
-
|
703 |
-
window.kimiVideo.returnToNeutral();
|
704 |
-
});
|
705 |
}
|
706 |
}
|
707 |
};
|
@@ -909,6 +962,9 @@ class KimiVoiceManager {
|
|
909 |
}
|
910 |
if (final_transcript && this.onSpeechAnalysis) {
|
911 |
try {
|
|
|
|
|
|
|
912 |
// Auto-stop after silence timeout following final transcript
|
913 |
setTimeout(() => {
|
914 |
this.stopListening();
|
@@ -1216,46 +1272,6 @@ class KimiVoiceManager {
|
|
1216 |
this.onSpeechAnalysis = callback;
|
1217 |
}
|
1218 |
|
1219 |
-
analyzeTextEmotion(text) {
|
1220 |
-
// Use unified emotion system
|
1221 |
-
if (window.kimiAnalyzeEmotion) {
|
1222 |
-
const emotion = window.kimiAnalyzeEmotion(text, "auto");
|
1223 |
-
return this._modulateEmotionByPersonality(emotion);
|
1224 |
-
}
|
1225 |
-
return "neutral";
|
1226 |
-
} // Helper to modulate emotion based on personality traits
|
1227 |
-
_modulateEmotionByPersonality(emotion) {
|
1228 |
-
try {
|
1229 |
-
// Obtain full traits if possible for a more robust modulation
|
1230 |
-
let avg = 50;
|
1231 |
-
if (window.kimiEmotionSystem && window.kimiEmotionSystem.db) {
|
1232 |
-
// Attempt synchronous-like cache via memory first
|
1233 |
-
const traits = {
|
1234 |
-
affection: this.memory?.affectionTrait,
|
1235 |
-
playfulness: this.memory?.playfulnessTrait,
|
1236 |
-
intelligence: this.memory?.intelligenceTrait,
|
1237 |
-
empathy: this.memory?.empathyTrait,
|
1238 |
-
humor: this.memory?.humorTrait,
|
1239 |
-
romance: this.memory?.romanceTrait
|
1240 |
-
};
|
1241 |
-
// If at least affection present, compute average using emotion system helper
|
1242 |
-
if (typeof traits.affection === "number") {
|
1243 |
-
avg = window.kimiEmotionSystem.calculatePersonalityAverage(traits);
|
1244 |
-
}
|
1245 |
-
} else if (this.memory && typeof this.memory.affectionTrait === "number") {
|
1246 |
-
avg = this.memory.affectionTrait; // fallback
|
1247 |
-
}
|
1248 |
-
|
1249 |
-
// Weighted interpretation: very low affection still softens positive expression
|
1250 |
-
// If overall avg low, dampen by shifting to 'shy'.
|
1251 |
-
if (avg <= 20 && emotion !== "neutral") return "shy";
|
1252 |
-
if (avg <= 40 && emotion === "positive") return "shy";
|
1253 |
-
return emotion;
|
1254 |
-
} catch (e) {
|
1255 |
-
return emotion;
|
1256 |
-
}
|
1257 |
-
}
|
1258 |
-
|
1259 |
async testVoice() {
|
1260 |
const testMessages = [
|
1261 |
window.kimiI18nManager?.t("test_voice_message_1") || "Hello my beloved! 💕",
|
|
|
70 |
this.transcriptText = document.getElementById("transcript");
|
71 |
|
72 |
if (!this.micButton) {
|
73 |
+
if (window.KIMI_CONFIG?.DEBUG?.VOICE) {
|
74 |
+
console.warn("Microphone button not found in DOM!");
|
75 |
+
}
|
76 |
return false;
|
77 |
}
|
78 |
|
79 |
// Check transcript elements (non-critical, just warn)
|
80 |
if (!this.transcriptContainer) {
|
81 |
+
if (window.KIMI_CONFIG?.DEBUG?.VOICE) {
|
82 |
+
console.warn("Transcript container not found in DOM - transcript feature will be disabled");
|
83 |
+
}
|
84 |
}
|
85 |
if (!this.transcriptText) {
|
86 |
+
if (window.KIMI_CONFIG?.DEBUG?.VOICE) {
|
87 |
+
console.warn("Transcript text element not found in DOM - transcript feature will be disabled");
|
88 |
+
}
|
89 |
}
|
90 |
|
91 |
// Initialize voice synthesis
|
|
|
173 |
try {
|
174 |
// Check if running on file:// protocol
|
175 |
if (window.location.protocol === "file:") {
|
176 |
+
if (window.KIMI_CONFIG?.DEBUG?.VOICE) {
|
177 |
+
console.log("🎤 Running on file:// protocol - microphone permissions will be requested each time");
|
178 |
+
}
|
179 |
this.micPermissionGranted = false;
|
180 |
return;
|
181 |
}
|
182 |
|
183 |
if (!navigator.permissions) {
|
184 |
+
if (window.KIMI_CONFIG?.DEBUG?.VOICE) {
|
185 |
+
console.log("🎤 Permissions API not available");
|
186 |
+
}
|
187 |
this.micPermissionGranted = false; // Set default state
|
188 |
return;
|
189 |
}
|
|
|
326 |
);
|
327 |
});
|
328 |
|
329 |
+
// Debug: Voice analysis (debug mode only)
|
330 |
+
if (window.KIMI_DEBUG_VOICE) {
|
331 |
console.log(`🎤 Female voice found: "${femaleVoice.name}" (${femaleVoice.lang})`);
|
332 |
+
} else if (window.KIMI_DEBUG_VOICE) {
|
333 |
console.log(
|
334 |
`🎤 No female voice found, using first available: "${filteredVoices[0]?.name}" (${filteredVoices[0]?.lang})`
|
335 |
);
|
|
|
552 |
|
553 |
// ===== CHAT MESSAGE UTILITIES =====
|
554 |
handleChatMessage(userMessage, kimiResponse) {
|
555 |
+
// Always save to chat history, regardless of chat visibility
|
|
|
|
|
|
|
|
|
|
|
|
|
556 |
const addMessageToChat = window.addMessageToChat || (typeof addMessageToChat !== "undefined" ? addMessageToChat : null);
|
557 |
|
558 |
if (addMessageToChat) {
|
559 |
+
// Save messages to history
|
560 |
addMessageToChat("user", userMessage);
|
561 |
addMessageToChat(this.selectedCharacter.toLowerCase(), kimiResponse);
|
562 |
} else {
|
563 |
+
// Fallback: only add to visible chat if available
|
564 |
+
const chatContainer = document.getElementById("chat-container");
|
565 |
+
const chatMessages = document.getElementById("chat-messages");
|
566 |
+
|
567 |
+
if (chatContainer && chatContainer.classList.contains("visible") && chatMessages) {
|
568 |
+
this.createChatMessage(chatMessages, "user", userMessage);
|
569 |
+
this.createChatMessage(chatMessages, this.selectedCharacter.toLowerCase(), kimiResponse);
|
570 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
571 |
+
}
|
572 |
}
|
573 |
}
|
574 |
|
|
|
654 |
|
655 |
// Get volume using centralized utility
|
656 |
utterance.volume = this.getVoicePreference("volume", options);
|
657 |
+
|
658 |
+
// Use centralized emotion system for consistency
|
659 |
+
const emotionFromText = window.kimiEmotionSystem?.analyzeEmotionValidated(text) || "neutral";
|
660 |
+
|
661 |
+
// PRE-PREPARE speaking animation before TTS starts
|
662 |
+
if (window.kimiVideo) {
|
663 |
+
// Always prepare a speaking context based on detected emotion
|
664 |
+
requestAnimationFrame(async () => {
|
665 |
+
try {
|
666 |
+
const traits = await this.db?.getAllPersonalityTraits(
|
667 |
+
window.kimiMemory?.selectedCharacter || (await this.db.getSelectedCharacter())
|
668 |
+
);
|
669 |
+
const affection = traits ? traits.affection : 50;
|
670 |
+
|
671 |
+
// Choose the appropriate speaking context
|
672 |
+
if (emotionFromText === "negative") {
|
673 |
+
window.kimiVideo.switchToContext("speakingNegative", "negative", null, traits || {}, affection);
|
674 |
+
} else if (emotionFromText === "neutral") {
|
675 |
+
// Even neutral text should use speaking context during TTS
|
676 |
+
window.kimiVideo.switchToContext("speakingPositive", "neutral", null, traits || {}, affection);
|
677 |
+
} else {
|
678 |
+
// For positive and specific emotions
|
679 |
+
const videoCategory =
|
680 |
+
window.kimiEmotionSystem?.getVideoCategory(emotionFromText, traits) || "speakingPositive";
|
681 |
+
window.kimiVideo.switchToContext(videoCategory, emotionFromText, null, traits || {}, affection);
|
682 |
+
}
|
683 |
+
} catch (e) {
|
684 |
+
console.warn("Failed to prepare speaking animation:", e);
|
685 |
+
}
|
686 |
});
|
687 |
}
|
688 |
+
|
689 |
if (typeof window.updatePersonalityTraitsFromEmotion === "function") {
|
690 |
window.updatePersonalityTraitsFromEmotion(emotionFromText, text);
|
691 |
}
|
|
|
693 |
|
694 |
utterance.onstart = async () => {
|
695 |
this.isSpeaking = true;
|
|
|
|
|
696 |
|
697 |
+
// IMMEDIATELY switch to appropriate speaking animation when TTS starts
|
698 |
try {
|
699 |
+
if (window.kimiVideo) {
|
700 |
+
const traits = await this.db?.getAllPersonalityTraits(
|
701 |
+
window.kimiMemory?.selectedCharacter || (await this.db.getSelectedCharacter())
|
702 |
+
);
|
703 |
+
const affection = traits ? traits.affection : 50;
|
704 |
+
|
705 |
+
// Choose speaking context based on the detected emotion using centralized logic
|
706 |
+
if (emotionFromText === "negative") {
|
707 |
+
window.kimiVideo.switchToContext("speakingNegative", "negative", null, traits || {}, affection);
|
708 |
+
} else if (emotionFromText === "neutral") {
|
709 |
+
// Even for neutral speech, use speaking context during TTS
|
710 |
+
window.kimiVideo.switchToContext("speakingPositive", "neutral", null, traits || {}, affection);
|
711 |
+
} else {
|
712 |
+
// For positive and specific emotions, use appropriate speaking context
|
713 |
+
const videoCategory =
|
714 |
+
window.kimiEmotionSystem?.getVideoCategory(emotionFromText, traits) || "speakingPositive";
|
715 |
+
window.kimiVideo.switchToContext(videoCategory, emotionFromText, null, traits || {}, affection);
|
716 |
}
|
717 |
}
|
718 |
} catch (e) {
|
719 |
+
console.warn("Failed to switch to speaking context:", e);
|
720 |
}
|
721 |
};
|
722 |
|
|
|
726 |
this.updateTranscriptVisibility(false);
|
727 |
// Clear any pending hide timeout
|
728 |
this.clearTranscriptTimeout();
|
729 |
+
|
730 |
+
// IMMEDIATELY return to neutral when TTS ends
|
731 |
if (window.kimiVideo) {
|
|
|
732 |
try {
|
733 |
const info = window.kimiVideo.getCurrentVideoInfo ? window.kimiVideo.getCurrentVideoInfo() : null;
|
734 |
+
|
735 |
+
// Only return to neutral if currently in a speaking context
|
736 |
+
if (info && (info.context === "speakingPositive" || info.context === "speakingNegative")) {
|
737 |
+
// Use async pattern to get traits for neutral transition
|
738 |
+
(async () => {
|
739 |
+
try {
|
740 |
+
const traits = await this.db?.getAllPersonalityTraits(
|
741 |
+
window.kimiMemory?.selectedCharacter || (await this.db.getSelectedCharacter())
|
742 |
+
);
|
743 |
+
window.kimiVideo.switchToContext(
|
744 |
+
"neutral",
|
745 |
+
"neutral",
|
746 |
+
null,
|
747 |
+
traits || {},
|
748 |
+
traits?.affection || 50
|
749 |
+
);
|
750 |
+
} catch (e) {
|
751 |
+
// Fallback without traits
|
752 |
+
window.kimiVideo.switchToContext("neutral", "neutral", null, {}, 50);
|
753 |
+
}
|
754 |
+
})();
|
755 |
}
|
756 |
+
} catch (e) {
|
757 |
+
console.warn("Failed to return to neutral after TTS:", e);
|
|
|
|
|
758 |
}
|
759 |
}
|
760 |
};
|
|
|
962 |
}
|
963 |
if (final_transcript && this.onSpeechAnalysis) {
|
964 |
try {
|
965 |
+
// Show final user message in transcript before processing
|
966 |
+
await this.showUserMessage(`You: ${final_transcript}`, 2000);
|
967 |
+
|
968 |
// Auto-stop after silence timeout following final transcript
|
969 |
setTimeout(() => {
|
970 |
this.stopListening();
|
|
|
1272 |
this.onSpeechAnalysis = callback;
|
1273 |
}
|
1274 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1275 |
async testVoice() {
|
1276 |
const testMessages = [
|
1277 |
window.kimiI18nManager?.t("test_voice_message_1") || "Hello my beloved! 💕",
|