VirtualKimi commited on
Commit
75ecd18
Β·
verified Β·
1 Parent(s): c7438a1

Upload 3 files

Browse files
kimi-js/kimi-memory-system.js CHANGED
@@ -1162,6 +1162,26 @@ class KimiMemorySystem {
1162
  }, 500);
1163
  }
1164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1165
  async getMemoriesByCategory(category, character = null) {
1166
  if (!this.db) return [];
1167
 
 
1162
  }, 500);
1163
  }
1164
 
1165
+ // Add cleanup method for memory system
1166
+ cleanup() {
1167
+ if (this.contextUpdateTimeout) {
1168
+ clearTimeout(this.contextUpdateTimeout);
1169
+ this.contextUpdateTimeout = null;
1170
+ }
1171
+
1172
+ // Clear caches to prevent memory leaks
1173
+ if (this.keywordCache) {
1174
+ this.keywordCache.clear();
1175
+ }
1176
+
1177
+ // Reset stats arrays to prevent accumulation
1178
+ if (this.queryStats) {
1179
+ this.queryStats.extractionTime.length = 0;
1180
+ this.queryStats.addMemoryTime.length = 0;
1181
+ this.queryStats.retrievalTime.length = 0;
1182
+ }
1183
+ }
1184
+
1185
  async getMemoriesByCategory(category, character = null) {
1186
  if (!this.db) return [];
1187
 
kimi-js/kimi-videos.js CHANGED
@@ -250,13 +250,13 @@ class KimiVideoManager {
250
  }
251
 
252
  _logDebug(message, payload = null) {
253
- if (!this._debug) return;
254
  if (payload) console.log("🎬 VideoManager:", message, payload);
255
  else console.log("🎬 VideoManager:", message);
256
  }
257
 
258
  _logSelection(category, selectedSrc, candidates = []) {
259
- if (!this._debug) return;
260
  const recent = (this.playHistory && this.playHistory[category]) || [];
261
  const adaptive = typeof this.getAdaptiveHistorySize === "function" ? this.getAdaptiveHistorySize(category) : null;
262
  console.log("🎬 VideoManager: selection", {
@@ -269,7 +269,7 @@ class KimiVideoManager {
269
  }
270
 
271
  debugPrintHistory(category = null) {
272
- if (!this._debug) return;
273
  if (!this.playHistory) {
274
  console.log("🎬 VideoManager: no play history yet");
275
  return;
@@ -800,7 +800,17 @@ class KimiVideoManager {
800
 
801
  // Neutral: on end, pick another neutral to avoid static last frame
802
  if (context === "neutral") {
803
- this._globalEndedHandler = () => this.returnToNeutral();
 
 
 
 
 
 
 
 
 
 
804
  this.activeVideo.addEventListener("ended", this._globalEndedHandler, { once: true });
805
  }
806
  }
@@ -828,27 +838,46 @@ class KimiVideoManager {
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
851
- if (traits && typeof affection === "number") {
852
  let weights = candidateVideos.map(video => {
853
  if (category === "speakingPositive") {
854
  // Positive videos favored by affection, romance, and humor
@@ -1019,7 +1048,7 @@ class KimiVideoManager {
1019
  }
1020
 
1021
  returnToNeutral() {
1022
- // Always ensure we resume playback with a fresh neutral video to avoid freeze
1023
  if (this._neutralLock) return;
1024
  this._neutralLock = true;
1025
  setTimeout(() => {
@@ -1030,39 +1059,79 @@ class KimiVideoManager {
1030
  this.isEmotionVideoPlaying = false;
1031
  this.currentEmotionContext = null;
1032
 
 
 
 
 
 
 
 
1033
  // Si la voix est encore en cours, relancer une vidΓ©o neutre en boucle
1034
  const category = "neutral";
1035
  const currentVideoSrc = this.activeVideo.querySelector("source").getAttribute("src");
1036
  const available = this.videoCategories[category] || [];
1037
  let nextSrc = null;
 
1038
  if (available.length > 0) {
1039
- const candidates = available.filter(v => v !== currentVideoSrc);
1040
- nextSrc =
1041
- candidates.length > 0
1042
- ? candidates[Math.floor(Math.random() * candidates.length)]
1043
- : available[Math.floor(Math.random() * available.length)];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1044
  }
 
1045
  if (nextSrc) {
1046
  this.loadAndSwitchVideo(nextSrc, "normal");
1047
  if (typeof this.updatePlayHistory === "function") this.updatePlayHistory(category, nextSrc);
1048
  this.currentContext = "neutral";
1049
  this.currentEmotion = "neutral";
1050
  this.lastSwitchTime = Date.now();
1051
- // Si la voix est encore en cours, s'assurer qu'on relance une vidΓ©o neutre Γ  la fin
1052
- if (window.voiceManager && window.voiceManager.isSpeaking) {
1053
- this.activeVideo.addEventListener(
1054
- "ended",
1055
- () => {
1056
- if (window.voiceManager && window.voiceManager.isSpeaking) {
1057
- this.returnToNeutral();
1058
- }
1059
- },
1060
- { once: true }
1061
- );
1062
- }
 
 
 
 
 
1063
  } else {
1064
- // Fallback to existing path if list empty
1065
- this.switchToContext("neutral");
 
 
 
 
 
 
 
 
 
 
 
1066
  }
1067
  }
1068
 
@@ -1778,10 +1847,87 @@ class KimiVideoManager {
1778
  this._loadingInProgress = false;
1779
  this._switchInProgress = false;
1780
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1781
  }
1782
 
1783
  // Expose globally for code that expects a window-level KimiVideoManager
1784
  window.KimiVideoManager = KimiVideoManager;
1785
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1786
  // Also provide ES module exports for modern imports
1787
  export { KimiVideoManager };
 
250
  }
251
 
252
  _logDebug(message, payload = null) {
253
+ if (!this._debug && !window.KIMI_CONFIG?.DEBUG?.VIDEO) return;
254
  if (payload) console.log("🎬 VideoManager:", message, payload);
255
  else console.log("🎬 VideoManager:", message);
256
  }
257
 
258
  _logSelection(category, selectedSrc, candidates = []) {
259
+ if (!this._debug && !window.KIMI_CONFIG?.DEBUG?.VIDEO) return;
260
  const recent = (this.playHistory && this.playHistory[category]) || [];
261
  const adaptive = typeof this.getAdaptiveHistorySize === "function" ? this.getAdaptiveHistorySize(category) : null;
262
  console.log("🎬 VideoManager: selection", {
 
269
  }
270
 
271
  debugPrintHistory(category = null) {
272
+ if (!this._debug && !window.KIMI_CONFIG?.DEBUG?.VIDEO) return;
273
  if (!this.playHistory) {
274
  console.log("🎬 VideoManager: no play history yet");
275
  return;
 
800
 
801
  // Neutral: on end, pick another neutral to avoid static last frame
802
  if (context === "neutral") {
803
+ this._globalEndedHandler = () => {
804
+ // Additional safety: ensure we're still in neutral context before looping
805
+ if (this.currentContext === "neutral") {
806
+ this.returnToNeutral();
807
+ } else {
808
+ // Context changed, don't loop back to neutral
809
+ if (window.KIMI_CONFIG?.DEBUG?.VIDEO) {
810
+ console.log("🎬 Context changed from neutral, skipping auto-loop");
811
+ }
812
+ }
813
+ };
814
  this.activeVideo.addEventListener("ended", this._globalEndedHandler, { once: true });
815
  }
816
  }
 
838
  candidateVideos = availableVideos.filter(video => video !== currentVideoSrc && this._isVideoSafe(video));
839
  }
840
 
841
+ // If still no safe videos, use any available (excluding current) - ignore safety temporarily
842
  if (candidateVideos.length === 0) {
843
  candidateVideos = availableVideos.filter(video => video !== currentVideoSrc);
844
+ // Reset safety flags for this category since we're desperate
845
+ if (candidateVideos.length > 0) {
846
+ console.warn(`🎬 All videos in category '${category}' marked unsafe, resetting safety flags`);
847
+ candidateVideos.forEach(video => this._recentFailures.delete(video));
848
+ }
849
  }
850
 
851
  // Ultimate fallback - use all available
852
  if (candidateVideos.length === 0) {
853
  candidateVideos = availableVideos;
854
+ // Reset all safety flags for this category
855
+ availableVideos.forEach(video => this._recentFailures.delete(video));
856
  }
857
 
858
+ // Final fallback to neutral category if current category is empty or exhausted
859
+ if (candidateVideos.length === 0 && category !== "neutral") {
860
+ console.warn(`🎬 Category '${category}' exhausted, falling back to neutral`);
861
  const neutralVideos = this.videoCategories.neutral || [];
862
  candidateVideos = neutralVideos.filter(video => this._isVideoSafe(video));
863
  if (candidateVideos.length === 0) {
864
  candidateVideos = neutralVideos; // Last resort
865
+ // Reset safety flags for neutral videos too
866
+ neutralVideos.forEach(video => this._recentFailures.delete(video));
867
  }
868
  }
869
 
870
+ // Critical error protection: if we still have no candidates, force emergency state
871
+ if (candidateVideos.length === 0) {
872
+ console.error(`🎬 CRITICAL: No videos available for category '${category}' or neutral fallback!`);
873
+ // Clear ALL safety flags as emergency measure
874
+ this._recentFailures.clear();
875
+ // Try to use any video from the original category
876
+ candidateVideos = availableVideos.length > 0 ? availableVideos : this.videoCategories.neutral || [];
877
+ }
878
+
879
  // If traits and affection are provided, weight the selection more subtly
880
+ if (traits && typeof affection === "number" && candidateVideos.length > 1) {
881
  let weights = candidateVideos.map(video => {
882
  if (category === "speakingPositive") {
883
  // Positive videos favored by affection, romance, and humor
 
1048
  }
1049
 
1050
  returnToNeutral() {
1051
+ // Always ensure we resume playbook with a fresh neutral video to avoid freeze
1052
  if (this._neutralLock) return;
1053
  this._neutralLock = true;
1054
  setTimeout(() => {
 
1059
  this.isEmotionVideoPlaying = false;
1060
  this.currentEmotionContext = null;
1061
 
1062
+ // Clean up any existing ended handlers before proceeding
1063
+ if (this._globalEndedHandler) {
1064
+ this.activeVideo.removeEventListener("ended", this._globalEndedHandler);
1065
+ this.inactiveVideo.removeEventListener("ended", this._globalEndedHandler);
1066
+ this._globalEndedHandler = null;
1067
+ }
1068
+
1069
  // Si la voix est encore en cours, relancer une vidΓ©o neutre en boucle
1070
  const category = "neutral";
1071
  const currentVideoSrc = this.activeVideo.querySelector("source").getAttribute("src");
1072
  const available = this.videoCategories[category] || [];
1073
  let nextSrc = null;
1074
+
1075
  if (available.length > 0) {
1076
+ // First try: non-current videos that are safe
1077
+ let candidates = available.filter(v => v !== currentVideoSrc && this._isVideoSafe(v));
1078
+
1079
+ // Second try: any non-current video (ignore safety temporarily)
1080
+ if (candidates.length === 0) {
1081
+ candidates = available.filter(v => v !== currentVideoSrc);
1082
+ // Reset safety flags for neutral videos if all marked as unsafe
1083
+ if (candidates.length > 0) {
1084
+ candidates.forEach(video => this._recentFailures.delete(video));
1085
+ }
1086
+ }
1087
+
1088
+ // Third try: any video including current (ultimate fallback)
1089
+ if (candidates.length === 0) {
1090
+ candidates = [...available];
1091
+ // Reset all safety flags as emergency measure
1092
+ this._recentFailures.clear();
1093
+ }
1094
+
1095
+ nextSrc = candidates[Math.floor(Math.random() * candidates.length)];
1096
  }
1097
+
1098
  if (nextSrc) {
1099
  this.loadAndSwitchVideo(nextSrc, "normal");
1100
  if (typeof this.updatePlayHistory === "function") this.updatePlayHistory(category, nextSrc);
1101
  this.currentContext = "neutral";
1102
  this.currentEmotion = "neutral";
1103
  this.lastSwitchTime = Date.now();
1104
+
1105
+ // Set up proper ended handler for continuous neutral playback
1106
+ this._globalEndedHandler = () => {
1107
+ // Double check we're still in neutral before looping
1108
+ if (this.currentContext === "neutral") {
1109
+ this.returnToNeutral();
1110
+ }
1111
+ };
1112
+ this.activeVideo.addEventListener("ended", this._globalEndedHandler, { once: true });
1113
+
1114
+ // Additional fallback: if video doesn't start playing in 5 seconds, retry
1115
+ setTimeout(() => {
1116
+ if (this.activeVideo.paused && this.currentContext === "neutral") {
1117
+ console.warn("🎬 Neutral video seems stuck, attempting recovery");
1118
+ this.returnToNeutral();
1119
+ }
1120
+ }, 5000);
1121
  } else {
1122
+ // Emergency fallback: force reload the first available neutral video
1123
+ console.error("🎬 No neutral videos available, forcing emergency reload");
1124
+ if (available.length > 0) {
1125
+ this._recentFailures.clear(); // Clear all safety blocks
1126
+ this.loadAndSwitchVideo(available[0], "high");
1127
+ this.currentContext = "neutral";
1128
+ this.currentEmotion = "neutral";
1129
+ this.lastSwitchTime = Date.now();
1130
+ } else {
1131
+ // Critical error: try existing path as absolute last resort
1132
+ console.error("🎬 CRITICAL: No neutral videos defined!");
1133
+ this.switchToContext("neutral");
1134
+ }
1135
  }
1136
  }
1137
 
 
1847
  this._loadingInProgress = false;
1848
  this._switchInProgress = false;
1849
  }
1850
+
1851
+ // Diagnostic method to check video system health
1852
+ _diagnoseVideoState() {
1853
+ const activeVideo = this.activeVideo;
1854
+ const inactiveVideo = this.inactiveVideo;
1855
+
1856
+ const diagnostics = {
1857
+ timestamp: new Date().toISOString(),
1858
+ currentContext: this.currentContext,
1859
+ currentEmotion: this.currentEmotion,
1860
+ activeVideo: {
1861
+ src: activeVideo?.querySelector("source")?.getAttribute("src") || "none",
1862
+ readyState: activeVideo?.readyState || "undefined",
1863
+ networkState: activeVideo?.networkState || "undefined",
1864
+ paused: activeVideo?.paused,
1865
+ ended: activeVideo?.ended,
1866
+ duration: activeVideo?.duration || "unknown"
1867
+ },
1868
+ inactiveVideo: {
1869
+ src: inactiveVideo?.querySelector("source")?.getAttribute("src") || "none",
1870
+ readyState: inactiveVideo?.readyState || "undefined",
1871
+ networkState: inactiveVideo?.networkState || "undefined"
1872
+ },
1873
+ systemState: {
1874
+ switchInProgress: this._switchInProgress,
1875
+ loadingInProgress: this._loadingInProgress,
1876
+ neutralLock: this._neutralLock,
1877
+ stickyContext: this._stickyContext,
1878
+ recentFailuresCount: this._recentFailures.size,
1879
+ consecutiveErrors: this._consecutiveErrorCount
1880
+ }
1881
+ };
1882
+
1883
+ return diagnostics;
1884
+ }
1885
+
1886
+ // Method to force video recovery when stuck
1887
+ _forceVideoRecovery() {
1888
+ if (window.KIMI_CONFIG?.DEBUG?.VIDEO) {
1889
+ console.warn("🎬 Forcing video recovery due to detected stall");
1890
+ }
1891
+
1892
+ // Log current state for debugging only if debug enabled
1893
+ const diagnostics = this._diagnoseVideoState();
1894
+ if (window.KIMI_CONFIG?.DEBUG?.VIDEO) {
1895
+ console.log("🎬 Video diagnostics:", diagnostics);
1896
+ }
1897
+
1898
+ // Clear any problematic state
1899
+ this._switchInProgress = false;
1900
+ this._loadingInProgress = false;
1901
+ this._neutralLock = false;
1902
+
1903
+ // Clear safety flags to allow all videos
1904
+ this._recentFailures.clear();
1905
+
1906
+ // Force return to neutral with high priority
1907
+ this.currentContext = "neutral";
1908
+ this.returnToNeutral();
1909
+
1910
+ return diagnostics;
1911
+ }
1912
  }
1913
 
1914
  // Expose globally for code that expects a window-level KimiVideoManager
1915
  window.KimiVideoManager = KimiVideoManager;
1916
 
1917
+ // Expose diagnostic utilities globally for debugging
1918
+ window.kimiVideoDiagnostics = function () {
1919
+ if (window.videoManager && typeof window.videoManager._diagnoseVideoState === "function") {
1920
+ return window.videoManager._diagnoseVideoState();
1921
+ }
1922
+ return { error: "Video manager not available" };
1923
+ };
1924
+
1925
+ window.kimiVideoRecovery = function () {
1926
+ if (window.videoManager && typeof window.videoManager._forceVideoRecovery === "function") {
1927
+ return window.videoManager._forceVideoRecovery();
1928
+ }
1929
+ return { error: "Video manager not available" };
1930
+ };
1931
+
1932
  // Also provide ES module exports for modern imports
1933
  export { KimiVideoManager };
kimi-js/kimi-voices.js CHANGED
@@ -1518,6 +1518,49 @@ class KimiVoiceManager {
1518
  autoStopDuration: this.autoStopDuration
1519
  };
1520
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1521
  }
1522
 
1523
  // Export for usage
 
1518
  autoStopDuration: this.autoStopDuration
1519
  };
1520
  }
1521
+
1522
+ // Cleanup method to prevent memory leaks
1523
+ cleanup() {
1524
+ // Clean up permission listener if it exists
1525
+ if (this.permissionStatus && this.permissionChangeHandler) {
1526
+ try {
1527
+ this.permissionStatus.removeEventListener("change", this.permissionChangeHandler);
1528
+ } catch (e) {
1529
+ // Silent cleanup - permission API may not support removal
1530
+ }
1531
+ }
1532
+
1533
+ // Clean up timeouts
1534
+ if (this.listeningTimeout) {
1535
+ clearTimeout(this.listeningTimeout);
1536
+ this.listeningTimeout = null;
1537
+ }
1538
+
1539
+ if (this.transcriptHideTimeout) {
1540
+ clearTimeout(this.transcriptHideTimeout);
1541
+ this.transcriptHideTimeout = null;
1542
+ }
1543
+
1544
+ // Clean up recognition
1545
+ if (this.recognition) {
1546
+ try {
1547
+ this.recognition.abort();
1548
+ } catch (e) {
1549
+ // Silent cleanup
1550
+ }
1551
+ }
1552
+
1553
+ // Clean up mic button listener
1554
+ if (this.micButton && this.handleMicClick) {
1555
+ this.micButton.removeEventListener("click", this.handleMicClick);
1556
+ }
1557
+
1558
+ // Clean up voice select listener
1559
+ const voiceSelect = document.getElementById("voice-select");
1560
+ if (voiceSelect && this.voiceChangeHandler) {
1561
+ voiceSelect.removeEventListener("change", this.voiceChangeHandler);
1562
+ }
1563
+ }
1564
  }
1565
 
1566
  // Export for usage