VirtualKimi commited on
Commit
cfd4e57
·
verified ·
1 Parent(s): 4644bfd

Upload 29 files

Browse files
index.html CHANGED
@@ -397,28 +397,26 @@
397
  </div>
398
 
399
  <div class="config-row">
400
- <div class="config-label-group">
401
- <label class="config-label" id="api-key-label" data-i18n="api_key_label">API Key</label>
402
- <span id="api-key-info" class="help-icon" data-i18n-title="api_key_help_title"
403
- title="Saved = your API key is stored for this provider. Use Test API Key to verify the connection.">
404
- <i class="fas fa-info-circle"></i>
405
- </span>
406
- <span id="api-key-presence" class="presence-dot" aria-label="API key presence"
407
- data-i18n-title="api_key_presence_hint"
408
- title="Green = API key saved for current provider. Grey = no key saved."></span>
409
- </div>
410
  <div class="config-control">
411
- <input type="text" class="kimi-input" id="provider-api-key" name="config-token"
412
- placeholder="API Key..." autocomplete="one-time-code" autocapitalize="none"
413
- autocorrect="off" spellcheck="false" inputmode="text" aria-autocomplete="none"
414
- data-lpignore="true" data-1p-ignore="true" data-bwignore="true"
415
- data-form-type="other" />
416
- <button class="kimi-button" id="toggle-api-key" type="button" aria-pressed="false"
417
- aria-label="Show API key">
418
- <i class="fas fa-eye"></i>
419
- </button>
420
- <span id="api-key-saved" data-i18n="saved"
421
- style="display:none;margin-left:8px;color:#4caf50;font-weight:600;">Saved</span>
 
 
 
 
 
 
 
422
  </div>
423
  </div>
424
 
 
397
  </div>
398
 
399
  <div class="config-row">
400
+ <label class="config-label" id="api-key-label" data-i18n="api_key_label">API Key</label>
 
 
 
 
 
 
 
 
 
401
  <div class="config-control">
402
+ <div class="api-key-input-group">
403
+ <input type="text" class="kimi-input" id="provider-api-key" name="config-token"
404
+ placeholder="API Key..." autocomplete="one-time-code" autocapitalize="none"
405
+ autocorrect="off" spellcheck="false" inputmode="text" aria-autocomplete="none"
406
+ data-lpignore="true" data-1p-ignore="true" data-bwignore="true"
407
+ data-form-type="other" />
408
+ <button class="kimi-button api-key-toggle" id="toggle-api-key" type="button"
409
+ aria-pressed="false" aria-label="Show API key">
410
+ <i class="fas fa-eye"></i>
411
+ </button>
412
+ <span id="api-key-presence" class="presence-dot" aria-label="API key presence"
413
+ data-i18n-title="api_key_presence_hint"
414
+ title="Green = API key saved for current provider. Grey = no key saved."></span>
415
+ </div>
416
+ <div class="api-key-status">
417
+ <span id="api-key-saved" data-i18n="saved"
418
+ style="display:none;color:#4caf50;font-weight:600;">Saved</span>
419
+ </div>
420
  </div>
421
  </div>
422
 
kimi-css/kimi-settings.css CHANGED
@@ -114,15 +114,6 @@
114
  backdrop-filter: blur(10px);
115
  }
116
 
117
- .help-icon {
118
- display: inline-flex;
119
- align-items: center;
120
- justify-content: center;
121
- width: 18px;
122
- height: 18px;
123
- color: var(--modal-text);
124
- }
125
-
126
  .help-button:hover,
127
  .settings-close:hover {
128
  background-color: var(--modal-close-hover-bg);
@@ -310,13 +301,6 @@
310
  opacity: 0.9;
311
  }
312
 
313
- .config-label-group {
314
- display: inline-flex;
315
- align-items: center;
316
- gap: 8px;
317
- flex: 1;
318
- }
319
-
320
  .presence-dot {
321
  display: inline-block;
322
  width: 10px;
@@ -1553,3 +1537,92 @@
1553
  outline: 2px solid var(--primary-pink);
1554
  outline-offset: 2px;
1555
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  backdrop-filter: blur(10px);
115
  }
116
 
 
 
 
 
 
 
 
 
 
117
  .help-button:hover,
118
  .settings-close:hover {
119
  background-color: var(--modal-close-hover-bg);
 
301
  opacity: 0.9;
302
  }
303
 
 
 
 
 
 
 
 
304
  .presence-dot {
305
  display: inline-block;
306
  width: 10px;
 
1537
  outline: 2px solid var(--primary-pink);
1538
  outline-offset: 2px;
1539
  }
1540
+
1541
+ /* ===== API KEY INPUT STYLING ===== */
1542
+
1543
+ .api-key-input-group {
1544
+ display: flex;
1545
+ align-items: center;
1546
+ gap: 8px;
1547
+ position: relative;
1548
+ }
1549
+
1550
+ .api-key-input-group .kimi-input {
1551
+ flex: 1;
1552
+ margin: 0;
1553
+ }
1554
+
1555
+ .api-key-toggle {
1556
+ min-width: 40px;
1557
+ height: 40px;
1558
+ padding: 8px;
1559
+ display: flex;
1560
+ align-items: center;
1561
+ justify-content: center;
1562
+ border-radius: 6px;
1563
+ background: var(--settings-bg-secondary, rgba(255, 255, 255, 0.08));
1564
+ border: 1px solid var(--settings-border-color, rgba(255, 255, 255, 0.15));
1565
+ color: var(--settings-text, #ffffff);
1566
+ transition: all 0.2s ease;
1567
+ cursor: pointer;
1568
+ }
1569
+
1570
+ .api-key-toggle:hover {
1571
+ background: var(--settings-bg-hover, rgba(255, 255, 255, 0.12));
1572
+ border-color: var(--accent-color, #8a2be2);
1573
+ }
1574
+
1575
+ .api-key-toggle:active {
1576
+ transform: scale(0.95);
1577
+ }
1578
+
1579
+ .api-key-input-group .presence-dot {
1580
+ width: 12px;
1581
+ height: 12px;
1582
+ margin-left: 4px;
1583
+ border: 2px solid var(--settings-bg-primary, rgba(0, 0, 0, 0.3));
1584
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
1585
+ transition: all 0.2s ease;
1586
+ }
1587
+
1588
+ .api-key-status {
1589
+ margin-top: 6px;
1590
+ min-height: 18px;
1591
+ display: flex;
1592
+ align-items: center;
1593
+ }
1594
+
1595
+ .api-key-status span {
1596
+ font-size: 0.85rem;
1597
+ display: flex;
1598
+ align-items: center;
1599
+ gap: 6px;
1600
+ }
1601
+
1602
+ .api-key-status span::before {
1603
+ content: "✓";
1604
+ font-weight: bold;
1605
+ font-size: 0.9rem;
1606
+ }
1607
+
1608
+ /* Responsive adjustments */
1609
+ @media (max-width: 600px) {
1610
+ .api-key-input-group {
1611
+ flex-direction: column;
1612
+ align-items: stretch;
1613
+ gap: 12px;
1614
+ }
1615
+
1616
+ .api-key-toggle {
1617
+ align-self: flex-end;
1618
+ min-width: 100px;
1619
+ }
1620
+
1621
+ .api-key-input-group .presence-dot {
1622
+ position: absolute;
1623
+ top: 50%;
1624
+ right: 8px;
1625
+ transform: translateY(-50%);
1626
+ z-index: 10;
1627
+ }
1628
+ }
kimi-js/kimi-llm-manager.js CHANGED
@@ -501,8 +501,8 @@ class KimiLLMManager {
501
  async chatWithOpenAICompatible(userMessage, options = {}) {
502
  const baseUrl = await this.db.getPreference("llmBaseUrl", "https://api.openai.com/v1/chat/completions");
503
  const provider = await this.db.getPreference("llmProvider", "openai");
504
- const apiKey = KimiProviderUtils
505
- ? await KimiProviderUtils.getApiKey(this.db, provider)
506
  : await this.db.getPreference("providerApiKey", "");
507
  const modelId = await this.db.getPreference("llmModelId", this.currentModel || "gpt-4o-mini");
508
  if (!apiKey) {
@@ -606,7 +606,10 @@ class KimiLLMManager {
606
  }
607
 
608
  async chatWithOpenRouter(userMessage, options = {}) {
609
- const apiKey = await this.db.getPreference("providerApiKey");
 
 
 
610
  if (!apiKey) {
611
  throw new Error("OpenRouter API key not configured");
612
  }
@@ -916,7 +919,10 @@ class KimiLLMManager {
916
  // ===== STREAMING METHODS =====
917
 
918
  async chatWithOpenRouterStreaming(userMessage, onToken, options = {}) {
919
- const apiKey = await this.db.getPreference("providerApiKey");
 
 
 
920
  if (!apiKey) {
921
  throw new Error("OpenRouter API key not configured");
922
  }
@@ -1047,9 +1053,12 @@ class KimiLLMManager {
1047
 
1048
  async chatWithOpenAICompatibleStreaming(userMessage, onToken, options = {}) {
1049
  const baseUrl = await this.db.getPreference("llmBaseUrl", "https://api.openai.com/v1/chat/completions");
1050
- const apiKey = await this.db.getPreference("openaiApiKey");
 
 
 
1051
  if (!apiKey) {
1052
- throw new Error("OpenAI API key not configured");
1053
  }
1054
 
1055
  const systemPromptContent = await this.assemblePrompt(userMessage);
@@ -1410,7 +1419,14 @@ class KimiLLMManager {
1410
  testWord = "Hello";
1411
  }
1412
  const systemPrompt = "You are a helpful assistant.";
1413
- let apiKey = await this.db.getPreference("providerApiKey");
 
 
 
 
 
 
 
1414
  let baseUrl = "";
1415
  let payload = {
1416
  model: modelId,
@@ -1442,6 +1458,7 @@ class KimiLLMManager {
1442
  } else {
1443
  throw new Error("Unknown provider: " + provider);
1444
  }
 
1445
  const response = await fetch(baseUrl, {
1446
  method: "POST",
1447
  headers,
@@ -1497,7 +1514,10 @@ class KimiLLMManager {
1497
  if (this._isRefreshingModels) return;
1498
  this._isRefreshingModels = true;
1499
  try {
1500
- const apiKey = await this.db.getPreference("providerApiKey", "");
 
 
 
1501
  const res = await fetch("https://openrouter.ai/api/v1/models", {
1502
  method: "GET",
1503
  headers: {
 
501
  async chatWithOpenAICompatible(userMessage, options = {}) {
502
  const baseUrl = await this.db.getPreference("llmBaseUrl", "https://api.openai.com/v1/chat/completions");
503
  const provider = await this.db.getPreference("llmProvider", "openai");
504
+ const apiKey = window.KimiProviderUtils
505
+ ? await window.KimiProviderUtils.getApiKey(this.db, provider)
506
  : await this.db.getPreference("providerApiKey", "");
507
  const modelId = await this.db.getPreference("llmModelId", this.currentModel || "gpt-4o-mini");
508
  if (!apiKey) {
 
606
  }
607
 
608
  async chatWithOpenRouter(userMessage, options = {}) {
609
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
610
+ const apiKey = await (window.KimiProviderUtils
611
+ ? window.KimiProviderUtils.getApiKey(this.db, provider)
612
+ : this.db.getPreference("providerApiKey"));
613
  if (!apiKey) {
614
  throw new Error("OpenRouter API key not configured");
615
  }
 
919
  // ===== STREAMING METHODS =====
920
 
921
  async chatWithOpenRouterStreaming(userMessage, onToken, options = {}) {
922
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
923
+ const apiKey = await (window.KimiProviderUtils
924
+ ? window.KimiProviderUtils.getApiKey(this.db, provider)
925
+ : this.db.getPreference("providerApiKey"));
926
  if (!apiKey) {
927
  throw new Error("OpenRouter API key not configured");
928
  }
 
1053
 
1054
  async chatWithOpenAICompatibleStreaming(userMessage, onToken, options = {}) {
1055
  const baseUrl = await this.db.getPreference("llmBaseUrl", "https://api.openai.com/v1/chat/completions");
1056
+ const provider = await this.db.getPreference("llmProvider", "openai");
1057
+ const apiKey = window.KimiProviderUtils
1058
+ ? await window.KimiProviderUtils.getApiKey(this.db, provider)
1059
+ : await this.db.getPreference("providerApiKey", "");
1060
  if (!apiKey) {
1061
+ throw new Error("API key not configured for selected provider");
1062
  }
1063
 
1064
  const systemPromptContent = await this.assemblePrompt(userMessage);
 
1419
  testWord = "Hello";
1420
  }
1421
  const systemPrompt = "You are a helpful assistant.";
1422
+ let apiKey = await (window.KimiProviderUtils
1423
+ ? window.KimiProviderUtils.getApiKey(this.db, provider)
1424
+ : this.db.getPreference("providerApiKey"));
1425
+
1426
+ if (!apiKey) {
1427
+ return { success: false, error: "No API key found for provider: " + provider };
1428
+ }
1429
+
1430
  let baseUrl = "";
1431
  let payload = {
1432
  model: modelId,
 
1458
  } else {
1459
  throw new Error("Unknown provider: " + provider);
1460
  }
1461
+
1462
  const response = await fetch(baseUrl, {
1463
  method: "POST",
1464
  headers,
 
1514
  if (this._isRefreshingModels) return;
1515
  this._isRefreshingModels = true;
1516
  try {
1517
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
1518
+ const apiKey = await (window.KimiProviderUtils
1519
+ ? window.KimiProviderUtils.getApiKey(this.db, provider)
1520
+ : this.db.getPreference("providerApiKey", ""));
1521
  const res = await fetch("https://openrouter.ai/api/v1/models", {
1522
  method: "GET",
1523
  headers: {
kimi-js/kimi-module.js CHANGED
@@ -847,8 +847,13 @@ async function loadSettingsData() {
847
  // Update API key input
848
  const apiKeyInput = document.getElementById("provider-api-key");
849
  if (apiKeyInput) {
850
- const keyPref = window.KimiProviderUtils ? window.KimiProviderUtils.getKeyPrefForProvider(provider) : null;
851
- const providerKey = keyPref && preferences[keyPref] ? preferences[keyPref] : apiKey;
 
 
 
 
 
852
  apiKeyInput.value = providerKey || "";
853
  }
854
  const providerSelect = document.getElementById("llm-provider");
@@ -1557,15 +1562,10 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1557
  const interfaceOpacitySlider = document.getElementById("interface-opacity");
1558
  const animationsToggle = document.getElementById("animations-toggle");
1559
 
1560
- // Cleanup existing listeners to prevent memory leaks
1561
- const cleanupListeners = () => {
1562
- if (window._kimiListenerCleanup) {
1563
- window._kimiListenerCleanup.forEach(cleanup => cleanup());
1564
- }
1565
  window._kimiListenerCleanup = [];
1566
- };
1567
-
1568
- cleanupListeners();
1569
 
1570
  // Create debounced functions for better performance
1571
  const debouncedVoiceRateUpdate = window.KimiPerformanceUtils?.debounce(async value => {
@@ -1656,22 +1656,12 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1656
  voiceVolumeSlider.addEventListener("input", listener);
1657
  window._kimiListenerCleanup.push(() => voiceVolumeSlider.removeEventListener("input", listener));
1658
  }
1659
- if (languageSelect) {
1660
- languageSelect.removeEventListener("change", window._kimiLanguageListener);
1661
- window._kimiLanguageListener = async e => {
1662
- if (kimiDB) await kimiDB.setPreference("selectedLanguage", e.target.value);
1663
- if (window.voiceManager && window.voiceManager.initVoices) await window.voiceManager.initVoices();
1664
- };
1665
- languageSelect.addEventListener("change", window._kimiLanguageListener);
1666
- }
1667
- if (voiceSelect) {
1668
- voiceSelect.removeEventListener("change", window._kimiVoiceSelectListener);
1669
- window._kimiVoiceSelectListener = async e => {
1670
- if (kimiDB) await kimiDB.setPreference("selectedVoice", e.target.value);
1671
- if (window.voiceManager && window.voiceManager.initVoices) await window.voiceManager.initVoices();
1672
- };
1673
- voiceSelect.addEventListener("change", window._kimiVoiceSelectListener);
1674
- }
1675
  // Batch personality traits optimization
1676
  let personalityBatchTimeout = null;
1677
  const pendingTraitChanges = {};
 
847
  // Update API key input
848
  const apiKeyInput = document.getElementById("provider-api-key");
849
  if (apiKeyInput) {
850
+ // Get the API key for the current provider
851
+ let providerKey = "";
852
+ if (window.KimiProviderUtils) {
853
+ providerKey = await window.KimiProviderUtils.getApiKey(kimiDB, provider);
854
+ } else {
855
+ providerKey = apiKey; // fallback to old method
856
+ }
857
  apiKeyInput.value = providerKey || "";
858
  }
859
  const providerSelect = document.getElementById("llm-provider");
 
1562
  const interfaceOpacitySlider = document.getElementById("interface-opacity");
1563
  const animationsToggle = document.getElementById("animations-toggle");
1564
 
1565
+ // SIMPLE FIX: Initialize _kimiListenerCleanup to prevent undefined error
1566
+ if (!window._kimiListenerCleanup) {
 
 
 
1567
  window._kimiListenerCleanup = [];
1568
+ }
 
 
1569
 
1570
  // Create debounced functions for better performance
1571
  const debouncedVoiceRateUpdate = window.KimiPerformanceUtils?.debounce(async value => {
 
1656
  voiceVolumeSlider.addEventListener("input", listener);
1657
  window._kimiListenerCleanup.push(() => voiceVolumeSlider.removeEventListener("input", listener));
1658
  }
1659
+ // Note: Language selector event listener is now handled by VoiceManager.setupLanguageSelector()
1660
+ // This prevents duplicate event listeners and ensures proper voice/language coordination
1661
+
1662
+ // Note: Voice selector event listener is now handled by VoiceManager.updateVoiceSelector()
1663
+ // This prevents duplicate event listeners and ensures proper voice preference coordination
1664
+
 
 
 
 
 
 
 
 
 
 
1665
  // Batch personality traits optimization
1666
  let personalityBatchTimeout = null;
1667
  const pendingTraitChanges = {};
kimi-js/kimi-script.js CHANGED
@@ -693,7 +693,6 @@ document.addEventListener("DOMContentLoaded", async function () {
693
  if (baseUrl) await window.kimiDB.setPreference("llmBaseUrl", baseUrl);
694
  if (modelId) await window.kimiDB.setPreference("llmModelId", modelId);
695
  }
696
-
697
  statusSpan.textContent = "Testing in progress...";
698
  statusSpan.style.color = "#ffa726";
699
 
@@ -877,23 +876,25 @@ document.addEventListener("DOMContentLoaded", async function () {
877
 
878
  // Function definitions
879
  function setupUnifiedEventHandlers() {
880
- // Cleanup existing event handlers
881
- if (window._kimiEventCleanup && Array.isArray(window._kimiEventCleanup)) {
882
- window._kimiEventCleanup.forEach(cleanup => {
883
- if (typeof cleanup === "function") cleanup();
884
- });
885
  }
886
- window._kimiEventCleanup = [];
887
 
888
  // Helper function to safely add event listeners
889
  function safeAddEventListener(element, event, handler, identifier) {
890
  if (element && !element[identifier]) {
891
  element.addEventListener(event, handler);
892
  element[identifier] = true;
893
- window._kimiEventCleanup.push(() => {
 
 
894
  element.removeEventListener(event, handler);
895
  element[identifier] = false;
896
- });
 
 
 
897
  }
898
  }
899
 
@@ -924,24 +925,8 @@ document.addEventListener("DOMContentLoaded", async function () {
924
  window.kimiI18nManager = new window.KimiI18nManager();
925
  const lang = await kimiDB.getPreference("selectedLanguage", "en");
926
  await window.kimiI18nManager.setLanguage(lang);
927
- const langSelect = document.getElementById("language-selection");
928
- if (langSelect) {
929
- langSelect.value = lang;
930
- langSelect.addEventListener("change", async function (e) {
931
- const selectedLang = e.target.value;
932
- await kimiDB.setPreference("selectedLanguage", selectedLang);
933
- await window.kimiI18nManager.setLanguage(selectedLang);
934
-
935
- if (window.voiceManager && window.voiceManager.handleLanguageChange) {
936
- await window.voiceManager.handleLanguageChange({ target: { value: selectedLang } });
937
- }
938
-
939
- // Refresh the personality prompt to include new language instruction
940
- if (window.kimiLLM && window.kimiLLM.refreshMemoryContext) {
941
- await window.kimiLLM.refreshMemoryContext();
942
- }
943
- });
944
- }
945
 
946
  window.kimiUIStateManager = new window.KimiUIStateManager();
947
  }
 
693
  if (baseUrl) await window.kimiDB.setPreference("llmBaseUrl", baseUrl);
694
  if (modelId) await window.kimiDB.setPreference("llmModelId", modelId);
695
  }
 
696
  statusSpan.textContent = "Testing in progress...";
697
  statusSpan.style.color = "#ffa726";
698
 
 
876
 
877
  // Function definitions
878
  function setupUnifiedEventHandlers() {
879
+ // SIMPLE FIX: Initialize _kimiEventCleanup to prevent undefined error
880
+ if (!window._kimiEventCleanup) {
881
+ window._kimiEventCleanup = [];
 
 
882
  }
 
883
 
884
  // Helper function to safely add event listeners
885
  function safeAddEventListener(element, event, handler, identifier) {
886
  if (element && !element[identifier]) {
887
  element.addEventListener(event, handler);
888
  element[identifier] = true;
889
+
890
+ // Simple cleanup system
891
+ const cleanupFn = () => {
892
  element.removeEventListener(event, handler);
893
  element[identifier] = false;
894
+ };
895
+
896
+ // Store cleanup function in the simple array
897
+ window._kimiEventCleanup.push(cleanupFn);
898
  }
899
  }
900
 
 
925
  window.kimiI18nManager = new window.KimiI18nManager();
926
  const lang = await kimiDB.getPreference("selectedLanguage", "en");
927
  await window.kimiI18nManager.setLanguage(lang);
928
+ // Note: Language selector event listener is now handled by VoiceManager.setupLanguageSelector()
929
+ // This prevents duplicate event listeners and ensures proper coordination between voice and i18n systems
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
 
931
  window.kimiUIStateManager = new window.KimiUIStateManager();
932
  }
kimi-js/kimi-utils.js CHANGED
@@ -22,7 +22,7 @@ window.KimiValidationUtils = {
22
  validateRange(value, key) {
23
  const bounds = {
24
  voiceRate: { min: 0.5, max: 2, def: 1.1 },
25
- voicePitch: { min: 0, max: 2, def: 1.0 },
26
  voiceVolume: { min: 0, max: 1, def: 0.8 },
27
  llmTemperature: { min: 0, max: 1, def: 0.9 },
28
  llmMaxTokens: { min: 1, max: 8192, def: 400 },
@@ -1818,10 +1818,8 @@ class KimiVideoManager {
1818
  }
1819
 
1820
  // METHODS TO ANALYZE EMOTIONS FROM TEXT
1821
- analyzeTextEmotion(text) {
1822
- // Use unified emotion system
1823
- return window.kimiAnalyzeEmotion ? window.kimiAnalyzeEmotion(text, "auto") : "neutral";
1824
- } // CLEANUP
1825
  destroy() {
1826
  clearTimeout(this.autoTransitionTimer);
1827
  this.autoTransitionTimer = null;
 
22
  validateRange(value, key) {
23
  const bounds = {
24
  voiceRate: { min: 0.5, max: 2, def: 1.1 },
25
+ voicePitch: { min: 0.5, max: 2, def: 1.1 },
26
  voiceVolume: { min: 0, max: 1, def: 0.8 },
27
  llmTemperature: { min: 0, max: 1, def: 0.9 },
28
  llmMaxTokens: { min: 1, max: 8192, def: 400 },
 
1818
  }
1819
 
1820
  // METHODS TO ANALYZE EMOTIONS FROM TEXT
1821
+ // Note: analyzeTextEmotion() moved to KimiVoiceManager for centralized emotion analysis
1822
+ // CLEANUP
 
 
1823
  destroy() {
1824
  clearTimeout(this.autoTransitionTimer);
1825
  this.autoTransitionTimer = null;
kimi-js/kimi-voices.js CHANGED
@@ -7,7 +7,7 @@ class KimiVoiceManager {
7
 
8
  // Voice properties
9
  this.speechSynthesis = window.speechSynthesis;
10
- this.kimiEnglishVoice = null;
11
  this.availableVoices = [];
12
 
13
  // Speech recognition
@@ -31,8 +31,8 @@ class KimiVoiceManager {
31
  this.transcriptHideTimeout = null;
32
  this.listeningTimeout = null;
33
 
34
- // Selected character for responses
35
- this.selectedCharacter = "Kimi";
36
 
37
  // Speaking flag
38
  this.isSpeaking = false;
@@ -54,6 +54,7 @@ class KimiVoiceManager {
54
  // Browser detection
55
  this.browser = this._detectBrowser();
56
  }
 
57
  // ===== INITIALIZATION =====
58
  async init() {
59
  // Avoid double initialization
@@ -83,7 +84,13 @@ class KimiVoiceManager {
83
 
84
  // Initialize voice synthesis
85
  await this.initVoices();
86
- this.setupVoicesChangedListener();
 
 
 
 
 
 
87
  this.setupLanguageSelector();
88
 
89
  // Initialize speech recognition
@@ -93,14 +100,19 @@ class KimiVoiceManager {
93
  // Check current microphone permission status
94
  await this.checkMicrophonePermission();
95
 
96
- // Initialize selected character
97
  if (this.db && typeof this.db.getSelectedCharacter === "function") {
98
- const char = await this.db.getSelectedCharacter();
99
- if (char) this.selectedCharacter = char;
 
 
 
 
 
 
100
  }
101
 
102
  this.isInitialized = true;
103
- console.log("🎤 VoiceManager initialized successfully");
104
  return true;
105
  } catch (error) {
106
  console.error("Error during VoiceManager initialization:", error);
@@ -158,18 +170,16 @@ class KimiVoiceManager {
158
 
159
  if (!navigator.permissions) {
160
  console.log("🎤 Permissions API not available");
 
161
  return;
162
  }
163
 
164
  const permissionStatus = await navigator.permissions.query({ name: "microphone" });
165
  this.micPermissionGranted = permissionStatus.state === "granted";
166
 
167
- console.log("🎤 Initial microphone permission status:", permissionStatus.state);
168
-
169
  // Listen for permission changes
170
  permissionStatus.addEventListener("change", () => {
171
  this.micPermissionGranted = permissionStatus.state === "granted";
172
- console.log("🎤 Microphone permission changed to:", permissionStatus.state);
173
  });
174
  } catch (error) {
175
  console.log("🎤 Could not check microphone permission:", error);
@@ -187,6 +197,13 @@ class KimiVoiceManager {
187
 
188
  this.availableVoices = this.speechSynthesis.getVoices();
189
 
 
 
 
 
 
 
 
190
  // Only get language from DB if not already set
191
  if (!this.selectedLanguage) {
192
  const selectedLanguage = await this.db?.getPreference("selectedLanguage", "en");
@@ -195,43 +212,146 @@ class KimiVoiceManager {
195
 
196
  const savedVoice = await this.db?.getPreference("selectedVoice", "auto");
197
 
198
- let filteredVoices = this.availableVoices.filter(voice => voice.lang.toLowerCase().startsWith(this.selectedLanguage));
199
- if (filteredVoices.length === 0) {
200
- filteredVoices = this.availableVoices.filter(voice => voice.lang.toLowerCase().includes(this.selectedLanguage));
201
- }
202
- if (filteredVoices.length === 0) {
203
- // As a last resort, use any available voice
204
- filteredVoices = this.availableVoices;
205
- }
206
 
207
  if (savedVoice && savedVoice !== "auto") {
208
- let foundVoice = filteredVoices.find(voice => voice.name === savedVoice);
209
- if (!foundVoice) {
210
- foundVoice = this.availableVoices.find(voice => voice.name === savedVoice);
211
- }
212
  if (foundVoice) {
213
- this.kimiEnglishVoice = foundVoice;
214
- this.updateVoiceSelector();
215
- this._initializingVoices = false;
216
- return;
217
- } else if (filteredVoices.length > 0) {
218
- this.kimiEnglishVoice = filteredVoices[0];
219
- await this.db?.setPreference("selectedVoice", this.kimiEnglishVoice.name);
220
  this.updateVoiceSelector();
221
  this._initializingVoices = false;
222
  return;
 
 
 
 
 
 
223
  }
224
  }
225
 
226
- // Prefer female voices if available, otherwise fallback
227
- const femaleVoice = filteredVoices.find(
228
- voice =>
229
- voice.name.toLowerCase().includes("female") ||
230
- (voice.gender && voice.gender.toLowerCase() === "female") ||
231
- voice.name.toLowerCase().includes("woman") ||
232
- voice.name.toLowerCase().includes("girl")
233
- );
234
- this.kimiEnglishVoice = femaleVoice || filteredVoices[0] || this.availableVoices[0];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
  // Do not overwrite "auto" preference here; only update if user selects a specific voice
237
 
@@ -254,47 +374,184 @@ class KimiVoiceManager {
254
  autoOption.textContent = "Automatic (Best voice for selected language)";
255
  voiceSelect.appendChild(autoOption);
256
 
257
- let filteredVoices = this.availableVoices.filter(voice => voice.lang.toLowerCase().startsWith(this.selectedLanguage));
258
- if (filteredVoices.length === 0) {
259
- filteredVoices = this.availableVoices.filter(voice => voice.lang.toLowerCase().includes(this.selectedLanguage));
260
- }
261
  if (filteredVoices.length === 0) {
262
- // Show all voices if none match the selected language
263
- filteredVoices = this.availableVoices;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  }
265
 
266
- filteredVoices.forEach(voice => {
267
- const option = document.createElement("option");
268
- option.value = voice.name;
269
- option.textContent = `${voice.name} (${voice.lang})`;
270
- if (this.kimiEnglishVoice && voice.name === this.kimiEnglishVoice.name) {
271
- option.selected = true;
272
- }
273
- voiceSelect.appendChild(option);
274
- });
275
 
276
- voiceSelect.removeEventListener("change", this.handleVoiceChange);
277
- voiceSelect.addEventListener("change", this.handleVoiceChange.bind(this));
 
278
  }
279
 
280
  async handleVoiceChange(e) {
281
  if (e.target.value === "auto") {
 
282
  await this.db?.setPreference("selectedVoice", "auto");
283
- this.kimiEnglishVoice = null; // Trigger auto-selection next time
284
  } else {
285
- this.kimiEnglishVoice = this.availableVoices.find(voice => voice.name === e.target.value);
 
286
  await this.db?.setPreference("selectedVoice", e.target.value);
287
  }
288
  }
289
 
290
  setupVoicesChangedListener() {
291
  if (this.speechSynthesis.onvoiceschanged !== undefined) {
292
- this.speechSynthesis.onvoiceschanged = async () => await this.initVoices();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  }
295
 
296
  async speak(text, options = {}) {
297
- if (!text || !this.kimiEnglishVoice) {
298
  console.warn("Unable to speak: empty text or voice not initialized");
299
  return;
300
  }
@@ -306,30 +563,9 @@ class KimiVoiceManager {
306
  // Clean text for better speech synthesis
307
  let processedText = this._normalizeForSpeech(text);
308
 
309
- // Detect emotional content for voice adjustments
310
- let customRate = options.rate;
311
- if (customRate === undefined) {
312
- customRate = this.memory?.preferences?.voiceRate;
313
- }
314
- if (customRate === undefined) {
315
- customRate = window.kimiMemory?.preferences?.voiceRate;
316
- }
317
- if (customRate === undefined) {
318
- const rateSlider = document.getElementById("voice-rate");
319
- customRate = rateSlider ? parseFloat(rateSlider.value) : 1.1;
320
- }
321
-
322
- let customPitch = options.pitch;
323
- if (customPitch === undefined) {
324
- customPitch = this.memory?.preferences?.voicePitch;
325
- }
326
- if (customPitch === undefined) {
327
- customPitch = window.kimiMemory?.preferences?.voicePitch;
328
- }
329
- if (customPitch === undefined) {
330
- const pitchSlider = document.getElementById("voice-pitch");
331
- customPitch = pitchSlider ? parseFloat(pitchSlider.value) : 1.1;
332
- }
333
 
334
  // Check for emotional indicators in original text (before processing)
335
  const lowerText = text.toLowerCase();
@@ -347,26 +583,12 @@ class KimiVoiceManager {
347
  }
348
 
349
  const utterance = new SpeechSynthesisUtterance(processedText);
350
- utterance.voice = this.kimiEnglishVoice;
351
  utterance.rate = customRate;
352
  utterance.pitch = customPitch;
353
 
354
- // Get volume from multiple sources with fallback hierarchy
355
- let volume = options.volume;
356
- if (volume === undefined) {
357
- // Try to get from memory preferences
358
- volume = this.memory?.preferences?.voiceVolume;
359
- }
360
- if (volume === undefined) {
361
- // Try to get from kimiMemory global
362
- volume = window.kimiMemory?.preferences?.voiceVolume;
363
- }
364
- if (volume === undefined) {
365
- // Try to get directly from slider
366
- const volumeSlider = document.getElementById("voice-volume");
367
- volume = volumeSlider ? parseFloat(volumeSlider.value) : 0.8;
368
- }
369
- utterance.volume = volume;
370
  const emotionFromText = this.analyzeTextEmotion(text);
371
  if (window.kimiVideo && emotionFromText !== "neutral") {
372
  requestAnimationFrame(() => {
@@ -585,9 +807,7 @@ class KimiVoiceManager {
585
  }
586
  this.recognition = new this.SpeechRecognition();
587
  this.recognition.continuous = true;
588
- let langCode = this.selectedLanguage || "en";
589
- if (langCode === "fr") langCode = "fr-FR";
590
- if (langCode === "en") langCode = "en-US";
591
  this.recognition.lang = langCode;
592
  this.recognition.interimResults = true;
593
 
@@ -629,112 +849,18 @@ class KimiVoiceManager {
629
  this.stopListening();
630
  }, this.silenceTimeout);
631
  (async () => {
 
632
  if (typeof window.analyzeAndReact === "function") {
633
- const response = await window.analyzeAndReact(final_transcript);
634
- if (response) {
635
- const chatContainer = document.getElementById("chat-container");
636
- const chatMessages = document.getElementById("chat-messages");
637
- if (chatContainer && chatContainer.classList.contains("visible") && chatMessages) {
638
- const addMessageToChat =
639
- window.addMessageToChat ||
640
- (typeof addMessageToChat !== "undefined" ? addMessageToChat : null);
641
- if (addMessageToChat) {
642
- addMessageToChat("user", final_transcript);
643
- addMessageToChat("kimi", response);
644
- } else {
645
- const userDiv = document.createElement("div");
646
- userDiv.className = "message user";
647
-
648
- const userMessageDiv = document.createElement("div");
649
- userMessageDiv.textContent = final_transcript;
650
-
651
- const userTimeDiv = document.createElement("div");
652
- userTimeDiv.className = "message-time";
653
- userTimeDiv.textContent = new Date().toLocaleTimeString("en-US", {
654
- hour: "2-digit",
655
- minute: "2-digit"
656
- });
657
-
658
- userDiv.appendChild(userMessageDiv);
659
- userDiv.appendChild(userTimeDiv);
660
- chatMessages.appendChild(userDiv);
661
-
662
- const kimiDiv = document.createElement("div");
663
- kimiDiv.className = "message kimi";
664
-
665
- const kimiMessageDiv = document.createElement("div");
666
- kimiMessageDiv.textContent = response;
667
-
668
- const kimiTimeDiv = document.createElement("div");
669
- kimiTimeDiv.className = "message-time";
670
- kimiTimeDiv.textContent = new Date().toLocaleTimeString("en-US", {
671
- hour: "2-digit",
672
- minute: "2-digit"
673
- });
674
-
675
- kimiDiv.appendChild(kimiMessageDiv);
676
- kimiDiv.appendChild(kimiTimeDiv);
677
- chatMessages.appendChild(kimiDiv);
678
- chatMessages.scrollTop = chatMessages.scrollHeight;
679
- }
680
- }
681
- setTimeout(() => {
682
- this.speak(response);
683
- }, 500);
684
- }
685
- } else {
686
- const response = await this.onSpeechAnalysis(final_transcript);
687
- if (response) {
688
- const chatContainer = document.getElementById("chat-container");
689
- const chatMessages = document.getElementById("chat-messages");
690
- if (chatContainer && chatContainer.classList.contains("visible") && chatMessages) {
691
- const addMessageToChat =
692
- window.addMessageToChat ||
693
- (typeof addMessageToChat !== "undefined" ? addMessageToChat : null);
694
- if (addMessageToChat) {
695
- addMessageToChat("user", final_transcript);
696
- addMessageToChat("kimi", response);
697
- } else {
698
- const userDiv = document.createElement("div");
699
- userDiv.className = "message user";
700
-
701
- const userMessageDiv = document.createElement("div");
702
- userMessageDiv.textContent = final_transcript;
703
-
704
- const userTimeDiv = document.createElement("div");
705
- userTimeDiv.className = "message-time";
706
- userTimeDiv.textContent = new Date().toLocaleTimeString("en-US", {
707
- hour: "2-digit",
708
- minute: "2-digit"
709
- });
710
-
711
- userDiv.appendChild(userMessageDiv);
712
- userDiv.appendChild(userTimeDiv);
713
- chatMessages.appendChild(userDiv);
714
-
715
- const kimiDiv = document.createElement("div");
716
- kimiDiv.className = "message kimi";
717
-
718
- const kimiMessageDiv = document.createElement("div");
719
- kimiMessageDiv.textContent = response;
720
-
721
- const kimiTimeDiv = document.createElement("div");
722
- kimiTimeDiv.className = "message-time";
723
- kimiTimeDiv.textContent = new Date().toLocaleTimeString("en-US", {
724
- hour: "2-digit",
725
- minute: "2-digit"
726
- });
727
-
728
- kimiDiv.appendChild(kimiMessageDiv);
729
- kimiDiv.appendChild(kimiTimeDiv);
730
- chatMessages.appendChild(kimiDiv);
731
- chatMessages.scrollTop = chatMessages.scrollHeight;
732
- }
733
- }
734
- setTimeout(() => {
735
- this.speak(response);
736
- }, 500);
737
- }
738
  }
739
  })();
740
  } catch (error) {
@@ -829,7 +955,6 @@ class KimiVoiceManager {
829
 
830
  // Add the event listener
831
  this.micButton.addEventListener("click", this.handleMicClick);
832
- console.log("🎤 Microphone button event listener setup complete");
833
  }
834
 
835
  async startListening() {
@@ -1011,11 +1136,11 @@ class KimiVoiceManager {
1011
 
1012
  // ===== UTILITY METHODS =====
1013
  isVoiceAvailable() {
1014
- return this.kimiFrenchVoice !== null;
1015
  }
1016
 
1017
  getCurrentVoice() {
1018
- return this.kimiFrenchVoice;
1019
  }
1020
 
1021
  getAvailableVoices() {
@@ -1081,10 +1206,31 @@ class KimiVoiceManager {
1081
  this.speechSynthesis.cancel();
1082
  }
1083
 
 
1084
  if (this.micButton && this.handleMicClick) {
1085
  this.micButton.removeEventListener("click", this.handleMicClick);
1086
  }
1087
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1088
  this.isInitialized = false;
1089
  this.isListening = false;
1090
  this.isStoppingVolontaire = false;
@@ -1096,47 +1242,65 @@ class KimiVoiceManager {
1096
  setupLanguageSelector() {
1097
  const languageSelect = document.getElementById("language-selection");
1098
  if (!languageSelect) return;
 
1099
  languageSelect.value = this.selectedLanguage || "en";
 
 
 
 
 
 
 
 
 
1100
  }
1101
 
1102
  async handleLanguageChange(e) {
1103
  const newLang = e.target.value;
1104
- console.log(`🎤 Language changing to: ${newLang}`);
 
 
1105
  this.selectedLanguage = newLang;
1106
  await this.db?.setPreference("selectedLanguage", newLang);
1107
 
1108
- // Force voice reset when changing language
1109
- const currentVoicePref = await this.db?.getPreference("selectedVoice", "auto");
1110
- if (currentVoicePref === "auto") {
1111
- // Reset voice selection to force auto-selection for new language
1112
- this.kimiEnglishVoice = null;
1113
- console.log(`🎤 Voice reset for auto-selection in ${newLang}`);
1114
  }
1115
 
 
 
 
 
 
 
 
 
 
1116
  await this.initVoices();
1117
- console.log(
1118
- `🎤 Voice initialized for ${newLang}, selected voice:`,
1119
- this.kimiEnglishVoice?.name,
1120
- this.kimiEnglishVoice?.lang
1121
- );
 
1122
 
1123
  if (this.recognition) {
1124
- let langCode = newLang;
1125
- if (langCode === "fr") langCode = "fr-FR";
1126
- else if (langCode === "en") langCode = "en-US";
1127
- else if (langCode === "es") langCode = "es-ES";
1128
- else if (langCode === "de") langCode = "de-DE";
1129
- else if (langCode === "it") langCode = "it-IT";
1130
- else if (langCode === "ja") langCode = "ja-JP";
1131
- else if (langCode === "zh") langCode = "zh-CN";
1132
  this.recognition.lang = langCode;
1133
  }
1134
  }
1135
 
1136
  async updateSelectedCharacter() {
1137
  if (this.db && typeof this.db.getSelectedCharacter === "function") {
1138
- const char = await this.db.getSelectedCharacter();
1139
- if (char) this.selectedCharacter = char;
 
 
 
 
 
 
1140
  }
1141
  }
1142
 
 
7
 
8
  // Voice properties
9
  this.speechSynthesis = window.speechSynthesis;
10
+ this.currentVoice = null;
11
  this.availableVoices = [];
12
 
13
  // Speech recognition
 
31
  this.transcriptHideTimeout = null;
32
  this.listeningTimeout = null;
33
 
34
+ // Selected character for responses (will be updated from database)
35
+ this.selectedCharacter = window.KIMI_CONFIG?.DEFAULTS?.SELECTED_CHARACTER || "Kimi";
36
 
37
  // Speaking flag
38
  this.isSpeaking = false;
 
54
  // Browser detection
55
  this.browser = this._detectBrowser();
56
  }
57
+
58
  // ===== INITIALIZATION =====
59
  async init() {
60
  // Avoid double initialization
 
84
 
85
  // Initialize voice synthesis
86
  await this.initVoices();
87
+
88
+ // Only setup listener once during initialization
89
+ if (!this._voicesListenerSetup) {
90
+ this.setupVoicesChangedListener();
91
+ this._voicesListenerSetup = true;
92
+ }
93
+
94
  this.setupLanguageSelector();
95
 
96
  // Initialize speech recognition
 
100
  // Check current microphone permission status
101
  await this.checkMicrophonePermission();
102
 
103
+ // Initialize selected character with proper display name
104
  if (this.db && typeof this.db.getSelectedCharacter === "function") {
105
+ const charKey = await this.db.getSelectedCharacter();
106
+ if (charKey && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[charKey]) {
107
+ // Use the display name, not the key
108
+ this.selectedCharacter = window.KIMI_CHARACTERS[charKey].name;
109
+ } else if (charKey) {
110
+ // Fallback to key if KIMI_CHARACTERS not available
111
+ this.selectedCharacter = charKey;
112
+ }
113
  }
114
 
115
  this.isInitialized = true;
 
116
  return true;
117
  } catch (error) {
118
  console.error("Error during VoiceManager initialization:", error);
 
170
 
171
  if (!navigator.permissions) {
172
  console.log("🎤 Permissions API not available");
173
+ this.micPermissionGranted = false; // Set default state
174
  return;
175
  }
176
 
177
  const permissionStatus = await navigator.permissions.query({ name: "microphone" });
178
  this.micPermissionGranted = permissionStatus.state === "granted";
179
 
 
 
180
  // Listen for permission changes
181
  permissionStatus.addEventListener("change", () => {
182
  this.micPermissionGranted = permissionStatus.state === "granted";
 
183
  });
184
  } catch (error) {
185
  console.log("🎤 Could not check microphone permission:", error);
 
197
 
198
  this.availableVoices = this.speechSynthesis.getVoices();
199
 
200
+ // Handle case where voices are not loaded yet (common timing issue)
201
+ if (this.availableVoices.length === 0) {
202
+ this._initializingVoices = false;
203
+ // The onvoiceschanged listener will retry initialization
204
+ return;
205
+ }
206
+
207
  // Only get language from DB if not already set
208
  if (!this.selectedLanguage) {
209
  const selectedLanguage = await this.db?.getPreference("selectedLanguage", "en");
 
212
 
213
  const savedVoice = await this.db?.getPreference("selectedVoice", "auto");
214
 
215
+ const filteredVoices = this.getVoicesForLanguage(this.selectedLanguage);
 
 
 
 
 
 
 
216
 
217
  if (savedVoice && savedVoice !== "auto") {
218
+ // Only search within language-compatible voices
219
+ const foundVoice = filteredVoices.find(voice => voice.name === savedVoice);
 
 
220
  if (foundVoice) {
221
+ this.currentVoice = foundVoice;
222
+ console.log(
223
+ `🎤 Voice restored from cache: "${foundVoice.name}" (${foundVoice.lang}) for language "${this.selectedLanguage}"`
224
+ );
 
 
 
225
  this.updateVoiceSelector();
226
  this._initializingVoices = false;
227
  return;
228
+ } else {
229
+ // Saved voice not compatible with current language, fall back to auto-selection
230
+ console.log(
231
+ `🎤 Saved voice "${savedVoice}" not compatible with language "${this.selectedLanguage}", using auto-selection`
232
+ );
233
+ await this.db?.setPreference("selectedVoice", "auto");
234
  }
235
  }
236
 
237
+ // Prefer female voices if available in the language-compatible voices
238
+ // Use real voice names since voice.gender is rarely provided by browsers
239
+ const femaleVoice = filteredVoices.find(voice => {
240
+ const name = voice.name.toLowerCase();
241
+
242
+ // Common female voice names across different platforms
243
+ const femaleNames = [
244
+ // Microsoft voices
245
+ "aria",
246
+ "emma",
247
+ "jenny",
248
+ "michelle",
249
+ "karen",
250
+ "heather",
251
+ "susan",
252
+ "joanna",
253
+ "salli",
254
+ "kimberly",
255
+ "kendra",
256
+ "ivy",
257
+ "rebecca",
258
+ "zira",
259
+ "eva",
260
+ "linda",
261
+ "denise",
262
+ "elsa",
263
+ "nathalie",
264
+ "julie",
265
+ "hortense",
266
+ "marie",
267
+ "pauline",
268
+ "claudia",
269
+ "lucia",
270
+ "paola",
271
+ "bianca",
272
+ "cosima",
273
+ "katja",
274
+ "hedda",
275
+ "helena",
276
+ "naayf",
277
+ "sabina",
278
+ "naja",
279
+ "sara",
280
+ "amelie",
281
+ "lea",
282
+ "manon",
283
+
284
+ // Google voices
285
+ "wavenet-a",
286
+ "wavenet-c",
287
+ "wavenet-e",
288
+ "wavenet-f",
289
+ "wavenet-g",
290
+ "standard-a",
291
+ "standard-c",
292
+ "standard-e",
293
+
294
+ // Apple voices
295
+ "allison",
296
+ "ava",
297
+ "samantha",
298
+ "susan",
299
+ "vicki",
300
+ "victoria",
301
+ "audrey",
302
+ "aurelie",
303
+ "marie",
304
+ "thomas",
305
+ "amelie",
306
+
307
+ // General keywords
308
+ "female",
309
+ "woman",
310
+ "girl",
311
+ "lady"
312
+ ];
313
+
314
+ // Check if voice name contains any female name
315
+ return (
316
+ femaleNames.some(femaleName => name.includes(femaleName)) ||
317
+ (voice.gender && voice.gender.toLowerCase() === "female")
318
+ );
319
+ });
320
+
321
+ // Debug: Check what we actually found
322
+ if (femaleVoice) {
323
+ console.log(`🎤 Female voice found: "${femaleVoice.name}" (${femaleVoice.lang})`);
324
+ } else {
325
+ console.log(
326
+ `🎤 No female voice found, using first available: "${filteredVoices[0]?.name}" (${filteredVoices[0]?.lang})`
327
+ );
328
+ // Debug: Show what voices are available and why they don't match
329
+ if (filteredVoices.length > 0 && filteredVoices.length <= 5) {
330
+ console.log(
331
+ `🎤 Available voices for ${this.selectedLanguage}:`,
332
+ filteredVoices.map(v => ({
333
+ name: v.name,
334
+ lang: v.lang,
335
+ gender: v.gender || "undefined"
336
+ }))
337
+ );
338
+ }
339
+ }
340
+
341
+ // Use female voice if found, otherwise first compatible voice, with proper fallback
342
+ this.currentVoice = femaleVoice || filteredVoices[0] || null;
343
+
344
+ if (!this.currentVoice) {
345
+ console.warn("🎤 No voices available for speech synthesis - this may resolve automatically when voices load");
346
+ this._initializingVoices = false;
347
+ // Don't return here - let the system continue, voices may load later via onvoiceschanged
348
+ // The updateVoiceSelector will handle the empty state gracefully
349
+ } else {
350
+ // Log successful voice selection with language info
351
+ console.log(
352
+ `🎤 Voice loaded: "${this.currentVoice.name}" (${this.currentVoice.lang}) for language "${this.selectedLanguage}"`
353
+ );
354
+ }
355
 
356
  // Do not overwrite "auto" preference here; only update if user selects a specific voice
357
 
 
374
  autoOption.textContent = "Automatic (Best voice for selected language)";
375
  voiceSelect.appendChild(autoOption);
376
 
377
+ const filteredVoices = this.getVoicesForLanguage(this.selectedLanguage);
378
+
 
 
379
  if (filteredVoices.length === 0) {
380
+ // Add a placeholder option when no voices are available
381
+ const noVoicesOption = document.createElement("option");
382
+ noVoicesOption.value = "none";
383
+ noVoicesOption.textContent = "No voices available (loading...)";
384
+ noVoicesOption.disabled = true;
385
+ voiceSelect.appendChild(noVoicesOption);
386
+ } else {
387
+ filteredVoices.forEach(voice => {
388
+ const option = document.createElement("option");
389
+ option.value = voice.name;
390
+ option.textContent = `${voice.name} (${voice.lang})`;
391
+ if (this.currentVoice && voice.name === this.currentVoice.name) {
392
+ option.selected = true;
393
+ }
394
+ voiceSelect.appendChild(option);
395
+ });
396
  }
397
 
398
+ // Remove existing handler before adding new one
399
+ if (this.voiceChangeHandler) {
400
+ voiceSelect.removeEventListener("change", this.voiceChangeHandler);
401
+ }
 
 
 
 
 
402
 
403
+ // Create and store the handler
404
+ this.voiceChangeHandler = this.handleVoiceChange.bind(this);
405
+ voiceSelect.addEventListener("change", this.voiceChangeHandler);
406
  }
407
 
408
  async handleVoiceChange(e) {
409
  if (e.target.value === "auto") {
410
+ console.log(`🎤 Voice set to automatic selection for language "${this.selectedLanguage}"`);
411
  await this.db?.setPreference("selectedVoice", "auto");
412
+ this.currentVoice = null; // Trigger auto-selection next time
413
  } else {
414
+ this.currentVoice = this.availableVoices.find(voice => voice.name === e.target.value);
415
+ console.log(`🎤 Voice manually selected: "${this.currentVoice?.name}" (${this.currentVoice?.lang})`);
416
  await this.db?.setPreference("selectedVoice", e.target.value);
417
  }
418
  }
419
 
420
  setupVoicesChangedListener() {
421
  if (this.speechSynthesis.onvoiceschanged !== undefined) {
422
+ // Prevent multiple event listeners
423
+ this.speechSynthesis.onvoiceschanged = null;
424
+ this.speechSynthesis.onvoiceschanged = async () => {
425
+ // Only reinitialize if voices are actually available now
426
+ if (this.speechSynthesis.getVoices().length > 0) {
427
+ await this.initVoices();
428
+ }
429
+ };
430
+ }
431
+
432
+ // Fallback: Only use timeout if onvoiceschanged is not supported
433
+ if (this.availableVoices.length === 0 && this.speechSynthesis.onvoiceschanged === undefined) {
434
+ setTimeout(async () => {
435
+ await this.initVoices();
436
+ }, 1000);
437
+ }
438
+ }
439
+
440
+ // ===== LANGUAGE UTILITIES =====
441
+ getLanguageCode(langShort) {
442
+ const languageMap = {
443
+ en: "en-US",
444
+ fr: "fr-FR",
445
+ es: "es-ES",
446
+ de: "de-DE",
447
+ it: "it-IT",
448
+ ja: "ja-JP",
449
+ zh: "zh-CN"
450
+ };
451
+ return languageMap[langShort] || langShort;
452
+ }
453
+
454
+ getVoicesForLanguage(language) {
455
+ let filteredVoices = this.availableVoices.filter(voice => voice.lang.toLowerCase().startsWith(language));
456
+ if (filteredVoices.length === 0) {
457
+ filteredVoices = this.availableVoices.filter(voice => voice.lang.toLowerCase().includes(language));
458
+ }
459
+ if (filteredVoices.length === 0) {
460
+ // As last resort, return all voices
461
+ filteredVoices = this.availableVoices;
462
+ }
463
+ return filteredVoices;
464
+ }
465
+
466
+ // ===== VOICE PREFERENCE UTILITIES =====
467
+ getVoicePreference(paramType, options = {}) {
468
+ // Hierarchy: options > memory.preferences > kimiMemory.preferences > DOM element > default
469
+ const defaults = {
470
+ rate: window.KIMI_CONFIG?.DEFAULTS?.VOICE_RATE || 1.1,
471
+ pitch: window.KIMI_CONFIG?.DEFAULTS?.VOICE_PITCH || 1.1,
472
+ volume: window.KIMI_CONFIG?.DEFAULTS?.VOICE_VOLUME || 0.8
473
+ };
474
+
475
+ const elementIds = {
476
+ rate: "voice-rate",
477
+ pitch: "voice-pitch",
478
+ volume: "voice-volume"
479
+ };
480
+
481
+ const memoryKeys = {
482
+ rate: "voiceRate",
483
+ pitch: "voicePitch",
484
+ volume: "voiceVolume"
485
+ };
486
+
487
+ // 1. Check options parameter
488
+ if (options[paramType] !== undefined) {
489
+ return parseFloat(options[paramType]);
490
+ }
491
+
492
+ // 2. Check local memory preferences
493
+ if (this.memory?.preferences?.[memoryKeys[paramType]] !== undefined) {
494
+ return parseFloat(this.memory.preferences[memoryKeys[paramType]]);
495
+ }
496
+
497
+ // 3. Check global memory preferences
498
+ if (window.kimiMemory?.preferences?.[memoryKeys[paramType]] !== undefined) {
499
+ return parseFloat(window.kimiMemory.preferences[memoryKeys[paramType]]);
500
  }
501
+
502
+ // 4. Check DOM element
503
+ const element = document.getElementById(elementIds[paramType]);
504
+ if (element) {
505
+ return parseFloat(element.value);
506
+ }
507
+
508
+ // 5. Return default value
509
+ return defaults[paramType];
510
+ }
511
+
512
+ // ===== CHAT MESSAGE UTILITIES =====
513
+ handleChatMessage(userMessage, kimiResponse) {
514
+ const chatContainer = document.getElementById("chat-container");
515
+ const chatMessages = document.getElementById("chat-messages");
516
+
517
+ if (!chatContainer || !chatContainer.classList.contains("visible") || !chatMessages) {
518
+ return;
519
+ }
520
+
521
+ const addMessageToChat = window.addMessageToChat || (typeof addMessageToChat !== "undefined" ? addMessageToChat : null);
522
+
523
+ if (addMessageToChat) {
524
+ addMessageToChat("user", userMessage);
525
+ addMessageToChat(this.selectedCharacter.toLowerCase(), kimiResponse);
526
+ } else {
527
+ // Fallback manual message creation
528
+ this.createChatMessage(chatMessages, "user", userMessage);
529
+ this.createChatMessage(chatMessages, this.selectedCharacter.toLowerCase(), kimiResponse);
530
+ chatMessages.scrollTop = chatMessages.scrollHeight;
531
+ }
532
+ }
533
+
534
+ createChatMessage(container, sender, text) {
535
+ const messageDiv = document.createElement("div");
536
+ messageDiv.className = `message ${sender}`;
537
+
538
+ const textDiv = document.createElement("div");
539
+ textDiv.textContent = text;
540
+
541
+ const timeDiv = document.createElement("div");
542
+ timeDiv.className = "message-time";
543
+ timeDiv.textContent = new Date().toLocaleTimeString("en-US", {
544
+ hour: "2-digit",
545
+ minute: "2-digit"
546
+ });
547
+
548
+ messageDiv.appendChild(textDiv);
549
+ messageDiv.appendChild(timeDiv);
550
+ container.appendChild(messageDiv);
551
  }
552
 
553
  async speak(text, options = {}) {
554
+ if (!text || !this.currentVoice) {
555
  console.warn("Unable to speak: empty text or voice not initialized");
556
  return;
557
  }
 
563
  // Clean text for better speech synthesis
564
  let processedText = this._normalizeForSpeech(text);
565
 
566
+ // Get voice settings using centralized utility
567
+ let customRate = this.getVoicePreference("rate", options);
568
+ let customPitch = this.getVoicePreference("pitch", options);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
  // Check for emotional indicators in original text (before processing)
571
  const lowerText = text.toLowerCase();
 
583
  }
584
 
585
  const utterance = new SpeechSynthesisUtterance(processedText);
586
+ utterance.voice = this.currentVoice;
587
  utterance.rate = customRate;
588
  utterance.pitch = customPitch;
589
 
590
+ // Get volume using centralized utility
591
+ utterance.volume = this.getVoicePreference("volume", options);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
  const emotionFromText = this.analyzeTextEmotion(text);
593
  if (window.kimiVideo && emotionFromText !== "neutral") {
594
  requestAnimationFrame(() => {
 
807
  }
808
  this.recognition = new this.SpeechRecognition();
809
  this.recognition.continuous = true;
810
+ const langCode = this.getLanguageCode(this.selectedLanguage || "en");
 
 
811
  this.recognition.lang = langCode;
812
  this.recognition.interimResults = true;
813
 
 
849
  this.stopListening();
850
  }, this.silenceTimeout);
851
  (async () => {
852
+ let response;
853
  if (typeof window.analyzeAndReact === "function") {
854
+ response = await window.analyzeAndReact(final_transcript);
855
+ } else if (this.onSpeechAnalysis) {
856
+ response = await this.onSpeechAnalysis(final_transcript);
857
+ }
858
+
859
+ if (response) {
860
+ this.handleChatMessage(final_transcript, response);
861
+ setTimeout(() => {
862
+ this.speak(response);
863
+ }, 500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
864
  }
865
  })();
866
  } catch (error) {
 
955
 
956
  // Add the event listener
957
  this.micButton.addEventListener("click", this.handleMicClick);
 
958
  }
959
 
960
  async startListening() {
 
1136
 
1137
  // ===== UTILITY METHODS =====
1138
  isVoiceAvailable() {
1139
+ return this.currentVoice !== null;
1140
  }
1141
 
1142
  getCurrentVoice() {
1143
+ return this.currentVoice;
1144
  }
1145
 
1146
  getAvailableVoices() {
 
1206
  this.speechSynthesis.cancel();
1207
  }
1208
 
1209
+ // Clean up mic button event listener
1210
  if (this.micButton && this.handleMicClick) {
1211
  this.micButton.removeEventListener("click", this.handleMicClick);
1212
  }
1213
 
1214
+ // Clean up voice selector event listener
1215
+ if (this.voiceChangeHandler) {
1216
+ const voiceSelect = document.getElementById("voice-selection");
1217
+ if (voiceSelect) {
1218
+ voiceSelect.removeEventListener("change", this.voiceChangeHandler);
1219
+ }
1220
+ this.voiceChangeHandler = null;
1221
+ }
1222
+
1223
+ // Clean up language selector event listener
1224
+ if (this.languageChangeHandler) {
1225
+ const languageSelect = document.getElementById("language-selection");
1226
+ if (languageSelect) {
1227
+ languageSelect.removeEventListener("change", this.languageChangeHandler);
1228
+ }
1229
+ this.languageChangeHandler = null;
1230
+ }
1231
+
1232
+ // Reset state
1233
+ this.currentVoice = null;
1234
  this.isInitialized = false;
1235
  this.isListening = false;
1236
  this.isStoppingVolontaire = false;
 
1242
  setupLanguageSelector() {
1243
  const languageSelect = document.getElementById("language-selection");
1244
  if (!languageSelect) return;
1245
+
1246
  languageSelect.value = this.selectedLanguage || "en";
1247
+
1248
+ // Remove existing handler before adding new one
1249
+ if (this.languageChangeHandler) {
1250
+ languageSelect.removeEventListener("change", this.languageChangeHandler);
1251
+ }
1252
+
1253
+ // Create and store the handler
1254
+ this.languageChangeHandler = this.handleLanguageChange.bind(this);
1255
+ languageSelect.addEventListener("change", this.languageChangeHandler);
1256
  }
1257
 
1258
  async handleLanguageChange(e) {
1259
  const newLang = e.target.value;
1260
+ const oldLang = this.selectedLanguage;
1261
+ console.log(`� Language changing: "${oldLang}" → "${newLang}"`);
1262
+
1263
  this.selectedLanguage = newLang;
1264
  await this.db?.setPreference("selectedLanguage", newLang);
1265
 
1266
+ // Update i18n system for interface translations
1267
+ if (window.kimiI18nManager?.setLanguage) {
1268
+ await window.kimiI18nManager.setLanguage(newLang);
 
 
 
1269
  }
1270
 
1271
+ // ALWAYS reset voice when changing language to ensure compatibility
1272
+ const currentVoicePref = await this.db?.getPreference("selectedVoice", "auto");
1273
+
1274
+ // Reset voice selection to force re-selection for new language
1275
+ this.currentVoice = null;
1276
+
1277
+ // CRITICAL: Reset voice preference to "auto" to prevent old voice from being reused
1278
+ await this.db?.setPreference("selectedVoice", "auto");
1279
+
1280
  await this.initVoices();
1281
+
1282
+ if (this.currentVoice) {
1283
+ console.log(`🎤 Voice selected for "${newLang}": "${this.currentVoice.name}" (${this.currentVoice.lang})`);
1284
+ } else {
1285
+ console.warn(`🎤 No voice found for language "${newLang}"`);
1286
+ }
1287
 
1288
  if (this.recognition) {
1289
+ const langCode = this.getLanguageCode(newLang);
 
 
 
 
 
 
 
1290
  this.recognition.lang = langCode;
1291
  }
1292
  }
1293
 
1294
  async updateSelectedCharacter() {
1295
  if (this.db && typeof this.db.getSelectedCharacter === "function") {
1296
+ const charKey = await this.db.getSelectedCharacter();
1297
+ if (charKey && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[charKey]) {
1298
+ // Use the display name, not the key
1299
+ this.selectedCharacter = window.KIMI_CHARACTERS[charKey].name;
1300
+ } else if (charKey) {
1301
+ // Fallback to key if KIMI_CHARACTERS not available
1302
+ this.selectedCharacter = charKey;
1303
+ }
1304
  }
1305
  }
1306
 
kimi-locale/de.json CHANGED
@@ -139,7 +139,6 @@
139
  "storage_used": "Speicher verwendet",
140
  "saved": "Gespeichert!",
141
  "saved_short": "Gespeichert",
142
- "api_key_help_title": "Gespeichert = Ihr API-Schlüssel ist für diesen Anbieter gespeichert. Verwenden Sie ‘Test API Key’, um die Verbindung zu prüfen.",
143
  "reset_done": "Zurückgesetzt!",
144
  "category_listening": "Zuhören",
145
  "category_dancing": "Tanzen",
 
139
  "storage_used": "Speicher verwendet",
140
  "saved": "Gespeichert!",
141
  "saved_short": "Gespeichert",
 
142
  "reset_done": "Zurückgesetzt!",
143
  "category_listening": "Zuhören",
144
  "category_dancing": "Tanzen",
kimi-locale/en.json CHANGED
@@ -139,7 +139,6 @@
139
  "storage_used": "Storage used",
140
  "saved": "Saved!",
141
  "saved_short": "Saved",
142
- "api_key_help_title": "Saved = your API key is stored for this provider. Use Test API Key to verify the connection.",
143
  "reset_done": "Reset!",
144
  "category_listening": "Listening",
145
  "category_dancing": "Dancing",
 
139
  "storage_used": "Storage used",
140
  "saved": "Saved!",
141
  "saved_short": "Saved",
 
142
  "reset_done": "Reset!",
143
  "category_listening": "Listening",
144
  "category_dancing": "Dancing",
kimi-locale/es.json CHANGED
@@ -139,7 +139,6 @@
139
  "storage_used": "Almacenamiento usado",
140
  "saved": "¡Guardado!",
141
  "saved_short": "Guardado",
142
- "api_key_help_title": "Guardado = tu clave API está almacenada para este proveedor. Usa 'Test API Key' para verificar la conexión.",
143
  "reset_done": "¡Restaurado!",
144
  "category_listening": "Escuchando",
145
  "category_dancing": "Bailando",
 
139
  "storage_used": "Almacenamiento usado",
140
  "saved": "¡Guardado!",
141
  "saved_short": "Guardado",
 
142
  "reset_done": "¡Restaurado!",
143
  "category_listening": "Escuchando",
144
  "category_dancing": "Bailando",
kimi-locale/fr.json CHANGED
@@ -139,7 +139,6 @@
139
  "storage_used": "Stockage utilisé",
140
  "saved": "Sauvegardé !",
141
  "saved_short": "Sauvegardé",
142
- "api_key_help_title": "Sauvegardé = votre clé API est stockée pour ce provider. Utilisez ‘Test API Key’ pour vérifier la connexion.",
143
  "reset_done": "Réinitialisé !",
144
  "category_listening": "À l'écoute",
145
  "category_dancing": "En train de danser",
 
139
  "storage_used": "Stockage utilisé",
140
  "saved": "Sauvegardé !",
141
  "saved_short": "Sauvegardé",
 
142
  "reset_done": "Réinitialisé !",
143
  "category_listening": "À l'écoute",
144
  "category_dancing": "En train de danser",
kimi-locale/it.json CHANGED
@@ -139,7 +139,6 @@
139
  "storage_used": "Spazio utilizzato",
140
  "saved": "Salvato!",
141
  "saved_short": "Salvato",
142
- "api_key_help_title": "Saved = your API key is stored for this provider. Use ‘Test API Key’ to verify the connection.",
143
  "reset_done": "Ripristinato!",
144
  "category_listening": "Ascoltando",
145
  "category_dancing": "Ballando",
 
139
  "storage_used": "Spazio utilizzato",
140
  "saved": "Salvato!",
141
  "saved_short": "Salvato",
 
142
  "reset_done": "Ripristinato!",
143
  "category_listening": "Ascoltando",
144
  "category_dancing": "Ballando",
kimi-locale/ja.json CHANGED
@@ -139,7 +139,6 @@
139
  "storage_used": "使用ストレージ",
140
  "saved": "保存されました!",
141
  "saved_short": "保存",
142
- "api_key_help_title": "保存 = このプロバイダー用のAPIキーが保存されました。接続確認には『Test API Key』を使用してください。",
143
  "reset_done": "リセット完了!",
144
  "category_listening": "聞いている",
145
  "category_dancing": "踊っている",
 
139
  "storage_used": "使用ストレージ",
140
  "saved": "保存されました!",
141
  "saved_short": "保存",
 
142
  "reset_done": "リセット完了!",
143
  "category_listening": "聞いている",
144
  "category_dancing": "踊っている",
kimi-locale/zh.json CHANGED
@@ -139,7 +139,6 @@
139
  "storage_used": "已使用存储",
140
  "saved": "已保存!",
141
  "saved_short": "已保存",
142
- "api_key_help_title": "已保存 = 您的API密钥已为该提供商保存。使用“Test API Key”验证连接。",
143
  "reset_done": "重置完成!",
144
  "category_listening": "聆听",
145
  "category_dancing": "跳舞",
 
139
  "storage_used": "已使用存储",
140
  "saved": "已保存!",
141
  "saved_short": "已保存",
 
142
  "reset_done": "重置完成!",
143
  "category_listening": "聆听",
144
  "category_dancing": "跳舞",