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! 💕",
|