Spaces:
Running
Running
Upload 59 files
Browse files- CHANGELOG.md +7 -0
- index.html +14 -33
- kimi-css/kimi-style.css +1 -11
- kimi-icons/preview1.jpg +0 -0
- kimi-icons/preview2.jpg +0 -0
- kimi-icons/virtualkimi-preview1.jpg +0 -0
- kimi-icons/virtualkimi-preview2.jpg +0 -0
- kimi-js/kimi-config.js +0 -2
- kimi-js/kimi-module.js +26 -9
- kimi-js/kimi-plugin-manager.js +13 -8
- kimi-js/kimi-utils.js +76 -30
- kimi-js/kimi-videos.js +86 -55
- kimi-js/kimi-voices.js +4 -3
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
| 1 |
# Virtual Kimi App Changelog
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
# [1.1.5] - 2025-09-03
|
| 4 |
|
| 5 |
### Bug Fixes
|
|
|
|
| 1 |
# Virtual Kimi App Changelog
|
| 2 |
|
| 3 |
+
# [1.1.6] - 2025-09-04
|
| 4 |
+
|
| 5 |
+
### Bug Fixes
|
| 6 |
+
|
| 7 |
+
- Fixed a bug where sliders refused the value 0 (0 was treated as falsy and reset to defaults).
|
| 8 |
+
- Removed crossfade transition from video playback to avoid visual glitches during video changes.
|
| 9 |
+
|
| 10 |
# [1.1.5] - 2025-09-03
|
| 11 |
|
| 12 |
### Bug Fixes
|
index.html
CHANGED
|
@@ -37,44 +37,25 @@
|
|
| 37 |
content="Virtual AI companion with evolving personality and advanced voice recognition.">
|
| 38 |
<meta property="twitter:image" content="kimi-icons/virtualkimi-logo.png">
|
| 39 |
|
| 40 |
-
<!-- Schema.org
|
| 41 |
<script type="application/ld+json">
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
"@type": "WebPage",
|
| 45 |
-
"name": "Virtual Kimi - Virtual AI Companion",
|
| 46 |
-
"description": "Virtual Kimi, your virtual AI girlfriend and companion with an evolving personality, multi-provider AI support, advanced voice recognition and immersive interface.",
|
| 47 |
-
"url": "https://virtualkimi.com/virtual-kimi-app/index.html",
|
| 48 |
-
"mainEntity": {
|
| 49 |
"@type": "SoftwareApplication",
|
| 50 |
-
"@id": "https://virtualkimi.com/virtual-kimi-app/#app",
|
| 51 |
"name": "Virtual Kimi",
|
| 52 |
-
"
|
|
|
|
| 53 |
"applicationCategory": "AI Companion",
|
| 54 |
"operatingSystem": "Web Browser",
|
| 55 |
-
"
|
| 56 |
-
"@type": "Offer",
|
| 57 |
-
"price": "0",
|
| 58 |
-
"priceCurrency": "USD"
|
| 59 |
-
},
|
| 60 |
-
"creator": {
|
| 61 |
-
"@type": "Person",
|
| 62 |
-
"name": "Jean & Kimi"
|
| 63 |
-
},
|
| 64 |
"dateCreated": "2025-07-16",
|
| 65 |
-
"dateModified": "2025-09-
|
| 66 |
-
"version": "v1.1.
|
| 67 |
-
"
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
"Premium LLM integration",
|
| 71 |
-
"5 customizable visual themes",
|
| 72 |
-
"Persistent memory",
|
| 73 |
-
"Intelligent affection system"
|
| 74 |
-
]
|
| 75 |
}
|
| 76 |
-
|
| 77 |
-
</script>
|
| 78 |
|
| 79 |
<!-- Favicon -->
|
| 80 |
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
@@ -1087,8 +1068,8 @@
|
|
| 1087 |
<h3><i class="fas fa-code"></i> Technical Information</h3>
|
| 1088 |
<div class="tech-info">
|
| 1089 |
<p><strong>Created date :</strong> July 16, 2025</p>
|
| 1090 |
-
<p><strong>Version :</strong> v1.1.
|
| 1091 |
-
<p><strong>Last update :</strong> September
|
| 1092 |
<p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
|
| 1093 |
API</p>
|
| 1094 |
<p><strong>Status :</strong> ✅ Stable and functional</p>
|
|
|
|
| 37 |
content="Virtual AI companion with evolving personality and advanced voice recognition.">
|
| 38 |
<meta property="twitter:image" content="kimi-icons/virtualkimi-logo.png">
|
| 39 |
|
| 40 |
+
<!-- Minimal Schema.org JSON-LD for SoftwareApplication -->
|
| 41 |
<script type="application/ld+json">
|
| 42 |
+
{
|
| 43 |
+
"@context": "https://schema.org",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
"@type": "SoftwareApplication",
|
|
|
|
| 45 |
"name": "Virtual Kimi",
|
| 46 |
+
"url": "https://virtualkimi.com/virtual-kimi-app/index.html",
|
| 47 |
+
"description": "Virtual AI girlfriend and companion with evolving personality and voice interface.",
|
| 48 |
"applicationCategory": "AI Companion",
|
| 49 |
"operatingSystem": "Web Browser",
|
| 50 |
+
"author": { "@type": "Person", "name": "Jean & Kimi" },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
"dateCreated": "2025-07-16",
|
| 52 |
+
"dateModified": "2025-09-04",
|
| 53 |
+
"version": "v1.1.6",
|
| 54 |
+
"logo": { "@type": "ImageObject", "url": "https://virtualkimi.com/kimi-icons/virtualkimi-logo.png" },
|
| 55 |
+
"screenshot": ["https://virtualkimi.com/kimi-icons/virtualkimi-preview1.jpg","https://virtualkimi.com/kimi-icons/virtualkimi-preview2.jpg"],
|
| 56 |
+
"sameAs": ["https://x.com/virtualkimi","https://www.youtube.com/@VirtualKimi","https://github.com/virtualkimi"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
+
</script>
|
|
|
|
| 59 |
|
| 60 |
<!-- Favicon -->
|
| 61 |
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
|
|
| 1068 |
<h3><i class="fas fa-code"></i> Technical Information</h3>
|
| 1069 |
<div class="tech-info">
|
| 1070 |
<p><strong>Created date :</strong> July 16, 2025</p>
|
| 1071 |
+
<p><strong>Version :</strong> v1.1.6</p>
|
| 1072 |
+
<p><strong>Last update :</strong> September 04, 2025</p>
|
| 1073 |
<p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
|
| 1074 |
API</p>
|
| 1075 |
<p><strong>Status :</strong> ✅ Stable and functional</p>
|
kimi-css/kimi-style.css
CHANGED
|
@@ -119,9 +119,6 @@
|
|
| 119 |
--mic-pulse-color: rgba(39, 174, 96, 0.5);
|
| 120 |
--mic-pulse-listening-color: rgba(39, 174, 96, 0.4);
|
| 121 |
|
| 122 |
-
/* Video crossfade timing */
|
| 123 |
-
--video-fade-duration: 400ms;
|
| 124 |
-
|
| 125 |
/* Cards & Stats */
|
| 126 |
--card-bg: rgba(255, 255, 255, 0.02);
|
| 127 |
--card-border: rgba(255, 255, 255, 0.05);
|
|
@@ -899,16 +896,9 @@ body {
|
|
| 899 |
height: 100%;
|
| 900 |
object-fit: contain;
|
| 901 |
opacity: 0;
|
| 902 |
-
transition: opacity var(--video-fade-duration) cubic-bezier(0.4, 0, 0.2, 1);
|
| 903 |
background-color: #1a1a1a;
|
| 904 |
-
will-change: opacity;
|
| 905 |
backface-visibility: hidden;
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
.bg-video.transitioning {
|
| 909 |
-
opacity: 0;
|
| 910 |
-
transition: opacity var(--video-fade-duration) cubic-bezier(0.4, 0, 0.2, 1);
|
| 911 |
-
pointer-events: none;
|
| 912 |
}
|
| 913 |
|
| 914 |
.content-overlay {
|
|
|
|
| 119 |
--mic-pulse-color: rgba(39, 174, 96, 0.5);
|
| 120 |
--mic-pulse-listening-color: rgba(39, 174, 96, 0.4);
|
| 121 |
|
|
|
|
|
|
|
|
|
|
| 122 |
/* Cards & Stats */
|
| 123 |
--card-bg: rgba(255, 255, 255, 0.02);
|
| 124 |
--card-border: rgba(255, 255, 255, 0.05);
|
|
|
|
| 896 |
height: 100%;
|
| 897 |
object-fit: contain;
|
| 898 |
opacity: 0;
|
|
|
|
| 899 |
background-color: #1a1a1a;
|
|
|
|
| 900 |
backface-visibility: hidden;
|
| 901 |
+
transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 902 |
}
|
| 903 |
|
| 904 |
.content-overlay {
|
kimi-icons/preview1.jpg
ADDED
|
|
kimi-icons/preview2.jpg
ADDED
|
|
kimi-icons/virtualkimi-preview1.jpg
ADDED
|
|
kimi-icons/virtualkimi-preview2.jpg
ADDED
|
|
kimi-js/kimi-config.js
CHANGED
|
@@ -113,10 +113,8 @@ window.KIMI_CONFIG.validate = function (value, type) {
|
|
| 113 |
try {
|
| 114 |
const range = this.RANGES[type];
|
| 115 |
if (!range) return { valid: true, value };
|
| 116 |
-
|
| 117 |
const numValue = parseFloat(value);
|
| 118 |
if (isNaN(numValue)) return { valid: false, value: this.DEFAULTS[type] };
|
| 119 |
-
|
| 120 |
const clampedValue = Math.max(range.min, Math.min(range.max, numValue));
|
| 121 |
return { valid: true, value: clampedValue };
|
| 122 |
} catch (error) {
|
|
|
|
| 113 |
try {
|
| 114 |
const range = this.RANGES[type];
|
| 115 |
if (!range) return { valid: true, value };
|
|
|
|
| 116 |
const numValue = parseFloat(value);
|
| 117 |
if (isNaN(numValue)) return { valid: false, value: this.DEFAULTS[type] };
|
|
|
|
| 118 |
const clampedValue = Math.max(range.min, Math.min(range.max, numValue));
|
| 119 |
return { valid: true, value: clampedValue };
|
| 120 |
} catch (error) {
|
kimi-js/kimi-module.js
CHANGED
|
@@ -1442,6 +1442,23 @@ async function sendMessage() {
|
|
| 1442 |
}
|
| 1443 |
|
| 1444 |
function setupSettingsListeners(kimiDB, kimiMemory) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1445 |
const voiceRateSlider = document.getElementById("voice-rate");
|
| 1446 |
const voicePitchSlider = document.getElementById("voice-pitch");
|
| 1447 |
const voiceVolumeSlider = document.getElementById("voice-volume");
|
|
@@ -1526,7 +1543,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
|
|
| 1526 |
if (voiceRateSlider) {
|
| 1527 |
const listener = e => {
|
| 1528 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceRate");
|
| 1529 |
-
const value =
|
| 1530 |
|
| 1531 |
document.getElementById("voice-rate-value").textContent = value;
|
| 1532 |
e.target.value = value; // Ensure slider shows validated value
|
|
@@ -1538,7 +1555,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
|
|
| 1538 |
if (voicePitchSlider) {
|
| 1539 |
const listener = e => {
|
| 1540 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voicePitch");
|
| 1541 |
-
const value =
|
| 1542 |
|
| 1543 |
document.getElementById("voice-pitch-value").textContent = value;
|
| 1544 |
e.target.value = value;
|
|
@@ -1550,7 +1567,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
|
|
| 1550 |
if (voiceVolumeSlider) {
|
| 1551 |
const listener = e => {
|
| 1552 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceVolume");
|
| 1553 |
-
const value =
|
| 1554 |
|
| 1555 |
document.getElementById("voice-volume-value").textContent = value;
|
| 1556 |
e.target.value = value;
|
|
@@ -1613,7 +1630,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
|
|
| 1613 |
if (llmTemperatureSlider) {
|
| 1614 |
const listener = e => {
|
| 1615 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTemperature");
|
| 1616 |
-
const value =
|
| 1617 |
|
| 1618 |
document.getElementById("llm-temperature-value").textContent = value;
|
| 1619 |
e.target.value = value;
|
|
@@ -1625,7 +1642,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
|
|
| 1625 |
if (llmMaxTokensSlider) {
|
| 1626 |
const listener = e => {
|
| 1627 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmMaxTokens");
|
| 1628 |
-
const value =
|
| 1629 |
|
| 1630 |
document.getElementById("llm-max-tokens-value").textContent = value;
|
| 1631 |
e.target.value = value;
|
|
@@ -1637,7 +1654,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
|
|
| 1637 |
if (llmTopPSlider) {
|
| 1638 |
const listener = e => {
|
| 1639 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTopP");
|
| 1640 |
-
const value =
|
| 1641 |
|
| 1642 |
document.getElementById("llm-top-p-value").textContent = value;
|
| 1643 |
e.target.value = value;
|
|
@@ -1649,7 +1666,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
|
|
| 1649 |
if (llmFrequencyPenaltySlider) {
|
| 1650 |
const listener = e => {
|
| 1651 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmFrequencyPenalty");
|
| 1652 |
-
const value =
|
| 1653 |
|
| 1654 |
document.getElementById("llm-frequency-penalty-value").textContent = value;
|
| 1655 |
e.target.value = value;
|
|
@@ -1661,7 +1678,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
|
|
| 1661 |
if (llmPresencePenaltySlider) {
|
| 1662 |
const listener = e => {
|
| 1663 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmPresencePenalty");
|
| 1664 |
-
const value =
|
| 1665 |
|
| 1666 |
document.getElementById("llm-presence-penalty-value").textContent = value;
|
| 1667 |
e.target.value = value;
|
|
@@ -1697,7 +1714,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
|
|
| 1697 |
if (interfaceOpacitySlider) {
|
| 1698 |
const listener = e => {
|
| 1699 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "interfaceOpacity");
|
| 1700 |
-
const value =
|
| 1701 |
|
| 1702 |
document.getElementById("interface-opacity-value").textContent = value;
|
| 1703 |
e.target.value = value;
|
|
|
|
| 1442 |
}
|
| 1443 |
|
| 1444 |
function setupSettingsListeners(kimiDB, kimiMemory) {
|
| 1445 |
+
// ---------------------------------------------------------------------------
|
| 1446 |
+
// Slider value coercion utilities
|
| 1447 |
+
// Ensures that numeric sliders preserve explicit 0 instead of falling back
|
| 1448 |
+
// to defaults via the logical OR (||) operator. We only fall back when the
|
| 1449 |
+
// parsed value is NaN or validation returns undefined (never when value === 0).
|
| 1450 |
+
// Use coerceFloat / coerceInt in all handlers to standardize behavior.
|
| 1451 |
+
// ---------------------------------------------------------------------------
|
| 1452 |
+
const coerceFloat = (raw, fallback, validationValue) => {
|
| 1453 |
+
if (validationValue !== undefined) return validationValue;
|
| 1454 |
+
const parsed = parseFloat(raw);
|
| 1455 |
+
return Number.isNaN(parsed) ? fallback : parsed;
|
| 1456 |
+
};
|
| 1457 |
+
const coerceInt = (raw, fallback, validationValue) => {
|
| 1458 |
+
if (validationValue !== undefined) return validationValue;
|
| 1459 |
+
const parsed = parseInt(raw, 10);
|
| 1460 |
+
return Number.isNaN(parsed) ? fallback : parsed;
|
| 1461 |
+
};
|
| 1462 |
const voiceRateSlider = document.getElementById("voice-rate");
|
| 1463 |
const voicePitchSlider = document.getElementById("voice-pitch");
|
| 1464 |
const voiceVolumeSlider = document.getElementById("voice-volume");
|
|
|
|
| 1543 |
if (voiceRateSlider) {
|
| 1544 |
const listener = e => {
|
| 1545 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceRate");
|
| 1546 |
+
const value = coerceFloat(e.target.value, 1.1, validation?.value);
|
| 1547 |
|
| 1548 |
document.getElementById("voice-rate-value").textContent = value;
|
| 1549 |
e.target.value = value; // Ensure slider shows validated value
|
|
|
|
| 1555 |
if (voicePitchSlider) {
|
| 1556 |
const listener = e => {
|
| 1557 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voicePitch");
|
| 1558 |
+
const value = coerceFloat(e.target.value, 1.1, validation?.value);
|
| 1559 |
|
| 1560 |
document.getElementById("voice-pitch-value").textContent = value;
|
| 1561 |
e.target.value = value;
|
|
|
|
| 1567 |
if (voiceVolumeSlider) {
|
| 1568 |
const listener = e => {
|
| 1569 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceVolume");
|
| 1570 |
+
const value = coerceFloat(e.target.value, 0.8, validation?.value);
|
| 1571 |
|
| 1572 |
document.getElementById("voice-volume-value").textContent = value;
|
| 1573 |
e.target.value = value;
|
|
|
|
| 1630 |
if (llmTemperatureSlider) {
|
| 1631 |
const listener = e => {
|
| 1632 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTemperature");
|
| 1633 |
+
const value = coerceFloat(e.target.value, 0.9, validation?.value);
|
| 1634 |
|
| 1635 |
document.getElementById("llm-temperature-value").textContent = value;
|
| 1636 |
e.target.value = value;
|
|
|
|
| 1642 |
if (llmMaxTokensSlider) {
|
| 1643 |
const listener = e => {
|
| 1644 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmMaxTokens");
|
| 1645 |
+
const value = coerceInt(e.target.value, 400, validation?.value);
|
| 1646 |
|
| 1647 |
document.getElementById("llm-max-tokens-value").textContent = value;
|
| 1648 |
e.target.value = value;
|
|
|
|
| 1654 |
if (llmTopPSlider) {
|
| 1655 |
const listener = e => {
|
| 1656 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTopP");
|
| 1657 |
+
const value = coerceFloat(e.target.value, 0.9, validation?.value);
|
| 1658 |
|
| 1659 |
document.getElementById("llm-top-p-value").textContent = value;
|
| 1660 |
e.target.value = value;
|
|
|
|
| 1666 |
if (llmFrequencyPenaltySlider) {
|
| 1667 |
const listener = e => {
|
| 1668 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmFrequencyPenalty");
|
| 1669 |
+
const value = coerceFloat(e.target.value, 0.9, validation?.value);
|
| 1670 |
|
| 1671 |
document.getElementById("llm-frequency-penalty-value").textContent = value;
|
| 1672 |
e.target.value = value;
|
|
|
|
| 1678 |
if (llmPresencePenaltySlider) {
|
| 1679 |
const listener = e => {
|
| 1680 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmPresencePenalty");
|
| 1681 |
+
const value = coerceFloat(e.target.value, 0.8, validation?.value);
|
| 1682 |
|
| 1683 |
document.getElementById("llm-presence-penalty-value").textContent = value;
|
| 1684 |
e.target.value = value;
|
|
|
|
| 1714 |
if (interfaceOpacitySlider) {
|
| 1715 |
const listener = e => {
|
| 1716 |
const validation = window.KimiValidationUtils?.validateRange(e.target.value, "interfaceOpacity");
|
| 1717 |
+
const value = coerceFloat(e.target.value, 0.8, validation?.value);
|
| 1718 |
|
| 1719 |
document.getElementById("interface-opacity-value").textContent = value;
|
| 1720 |
e.target.value = value;
|
kimi-js/kimi-plugin-manager.js
CHANGED
|
@@ -15,6 +15,16 @@ class KimiPluginManager {
|
|
| 15 |
path.startsWith("kimi-plugins/")
|
| 16 |
);
|
| 17 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
async loadPlugins() {
|
| 19 |
const pluginDirs = await this.getPluginDirs();
|
| 20 |
this.plugins = [];
|
|
@@ -27,22 +37,17 @@ class KimiPluginManager {
|
|
| 27 |
|
| 28 |
// Basic manifest validation and path sanitization (deny external or absolute URLs)
|
| 29 |
const validTypes = new Set(["theme", "voice", "behavior"]);
|
| 30 |
-
|
| 31 |
-
typeof p === "string" &&
|
| 32 |
-
/^[-a-zA-Z0-9_\/.]+$/.test(p) &&
|
| 33 |
-
!p.startsWith("/") &&
|
| 34 |
-
!p.includes("..") &&
|
| 35 |
-
!/^https?:\/\//i.test(p);
|
| 36 |
|
| 37 |
if (!manifest.name || !manifest.type || !validTypes.has(manifest.type)) {
|
| 38 |
console.warn(`Invalid plugin manifest in ${dir}: missing name or invalid type`);
|
| 39 |
continue;
|
| 40 |
}
|
| 41 |
-
if (manifest.style && !
|
| 42 |
console.warn(`Blocked unsafe style path in ${dir}: ${manifest.style}`);
|
| 43 |
delete manifest.style;
|
| 44 |
}
|
| 45 |
-
if (manifest.main && !
|
| 46 |
console.warn(`Blocked unsafe main path in ${dir}: ${manifest.main}`);
|
| 47 |
delete manifest.main;
|
| 48 |
}
|
|
|
|
| 15 |
path.startsWith("kimi-plugins/")
|
| 16 |
);
|
| 17 |
}
|
| 18 |
+
// New: validate file name inside a plugin directory (relative path only)
|
| 19 |
+
isValidPluginFileName(file) {
|
| 20 |
+
return (
|
| 21 |
+
typeof file === "string" &&
|
| 22 |
+
/^[-a-zA-Z0-9_\/.]+$/.test(file) &&
|
| 23 |
+
!file.startsWith("/") &&
|
| 24 |
+
!file.includes("..") &&
|
| 25 |
+
!/^https?:\/:/i.test(file)
|
| 26 |
+
);
|
| 27 |
+
}
|
| 28 |
async loadPlugins() {
|
| 29 |
const pluginDirs = await this.getPluginDirs();
|
| 30 |
this.plugins = [];
|
|
|
|
| 37 |
|
| 38 |
// Basic manifest validation and path sanitization (deny external or absolute URLs)
|
| 39 |
const validTypes = new Set(["theme", "voice", "behavior"]);
|
| 40 |
+
// DEPRECATION: inlined isSafePath replaced by isValidPluginFileName()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
if (!manifest.name || !manifest.type || !validTypes.has(manifest.type)) {
|
| 43 |
console.warn(`Invalid plugin manifest in ${dir}: missing name or invalid type`);
|
| 44 |
continue;
|
| 45 |
}
|
| 46 |
+
if (manifest.style && !this.isValidPluginFileName(manifest.style)) {
|
| 47 |
console.warn(`Blocked unsafe style path in ${dir}: ${manifest.style}`);
|
| 48 |
delete manifest.style;
|
| 49 |
}
|
| 50 |
+
if (manifest.main && !this.isValidPluginFileName(manifest.main)) {
|
| 51 |
console.warn(`Blocked unsafe main path in ${dir}: ${manifest.main}`);
|
| 52 |
delete manifest.main;
|
| 53 |
}
|
kimi-js/kimi-utils.js
CHANGED
|
@@ -20,24 +20,10 @@ window.KimiValidationUtils = {
|
|
| 20 |
return div.innerHTML;
|
| 21 |
},
|
| 22 |
validateRange(value, key) {
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
llmTemperature: { min: 0, max: 1, def: 0.9 },
|
| 28 |
-
llmMaxTokens: { min: 1, max: 8192, def: 400 },
|
| 29 |
-
llmTopP: { min: 0, max: 1, def: 0.9 },
|
| 30 |
-
llmFrequencyPenalty: { min: 0, max: 2, def: 0.9 },
|
| 31 |
-
llmPresencePenalty: { min: 0, max: 2, def: 0.8 },
|
| 32 |
-
interfaceOpacity: { min: 0.1, max: 1, def: 0.8 }
|
| 33 |
-
};
|
| 34 |
-
const b = bounds[key] || { min: 0, max: 100, def: 0 };
|
| 35 |
-
const v = window.KimiSecurityUtils
|
| 36 |
-
? window.KimiSecurityUtils.validateRange(value, b.min, b.max, b.def)
|
| 37 |
-
: isNaN(parseFloat(value))
|
| 38 |
-
? b.def
|
| 39 |
-
: Math.max(b.min, Math.min(b.max, parseFloat(value)));
|
| 40 |
-
return { value: v, clamped: v !== parseFloat(value) };
|
| 41 |
}
|
| 42 |
};
|
| 43 |
|
|
@@ -90,6 +76,40 @@ const KimiProviderPlaceholders = {
|
|
| 90 |
window.KimiProviderPlaceholders = KimiProviderPlaceholders;
|
| 91 |
export { KimiProviderUtils, KimiProviderPlaceholders };
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
// Performance utility functions for debouncing and throttling
|
| 94 |
window.KimiPerformanceUtils = {
|
| 95 |
debounce: function (func, wait, immediate = false, context = null) {
|
|
@@ -201,12 +221,8 @@ class KimiSecurityUtils {
|
|
| 201 |
|
| 202 |
switch (type) {
|
| 203 |
case "html":
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
.replace(/</g, "<")
|
| 207 |
-
.replace(/>/g, ">")
|
| 208 |
-
.replace(/"/g, """)
|
| 209 |
-
.replace(/'/g, "'");
|
| 210 |
case "number":
|
| 211 |
const num = parseFloat(input);
|
| 212 |
return isNaN(num) ? 0 : num;
|
|
@@ -225,12 +241,6 @@ class KimiSecurityUtils {
|
|
| 225 |
}
|
| 226 |
}
|
| 227 |
|
| 228 |
-
static validateRange(value, min, max, defaultValue = 0) {
|
| 229 |
-
const num = parseFloat(value);
|
| 230 |
-
if (isNaN(num)) return defaultValue;
|
| 231 |
-
return Math.max(min, Math.min(max, num));
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
static validateApiKey(key) {
|
| 235 |
if (!key || typeof key !== "string") return false;
|
| 236 |
if (window.KIMI_VALIDATORS && typeof window.KIMI_VALIDATORS.validateApiKey === "function") {
|
|
@@ -521,6 +531,42 @@ class KimiOverlayManager {
|
|
| 521 |
open(name) {
|
| 522 |
const el = this.overlays[name];
|
| 523 |
if (el) el.classList.add("visible");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
}
|
| 525 |
close(name) {
|
| 526 |
const el = this.overlays[name];
|
|
|
|
| 20 |
return div.innerHTML;
|
| 21 |
},
|
| 22 |
validateRange(value, key) {
|
| 23 |
+
if (!window.KimiRange) {
|
| 24 |
+
throw new Error("KimiRange not initialized before validateRange call");
|
| 25 |
+
}
|
| 26 |
+
return window.KimiRange.clamp(key, value);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
}
|
| 28 |
};
|
| 29 |
|
|
|
|
| 76 |
window.KimiProviderPlaceholders = KimiProviderPlaceholders;
|
| 77 |
export { KimiProviderUtils, KimiProviderPlaceholders };
|
| 78 |
|
| 79 |
+
// Unified range management (central source of truth for numeric clamping)
|
| 80 |
+
// Keys map UI/logic identifiers to CONFIG constant names.
|
| 81 |
+
window.KimiRange = {
|
| 82 |
+
KEY_MAP: {
|
| 83 |
+
voiceRate: "VOICE_RATE",
|
| 84 |
+
voicePitch: "VOICE_PITCH",
|
| 85 |
+
voiceVolume: "VOICE_VOLUME",
|
| 86 |
+
llmTemperature: "LLM_TEMPERATURE",
|
| 87 |
+
llmMaxTokens: "LLM_MAX_TOKENS",
|
| 88 |
+
llmTopP: "LLM_TOP_P",
|
| 89 |
+
llmFrequencyPenalty: "LLM_FREQUENCY_PENALTY",
|
| 90 |
+
llmPresencePenalty: "LLM_PRESENCE_PENALTY",
|
| 91 |
+
interfaceOpacity: "INTERFACE_OPACITY"
|
| 92 |
+
},
|
| 93 |
+
getBounds(key) {
|
| 94 |
+
try {
|
| 95 |
+
const configKey = this.KEY_MAP[key];
|
| 96 |
+
if (configKey && window.KIMI_CONFIG && window.KIMI_CONFIG.RANGES && window.KIMI_CONFIG.RANGES[configKey]) {
|
| 97 |
+
const range = window.KIMI_CONFIG.RANGES[configKey];
|
| 98 |
+
const def = window.KIMI_CONFIG.DEFAULTS?.[configKey] ?? range.min;
|
| 99 |
+
return { min: range.min, max: range.max, def };
|
| 100 |
+
}
|
| 101 |
+
} catch {}
|
| 102 |
+
return { min: 0, max: 100, def: 0 };
|
| 103 |
+
},
|
| 104 |
+
clamp(key, value) {
|
| 105 |
+
const b = this.getBounds(key);
|
| 106 |
+
const num = parseFloat(value);
|
| 107 |
+
if (isNaN(num)) return { value: b.def, clamped: true };
|
| 108 |
+
const v = Math.max(b.min, Math.min(b.max, num));
|
| 109 |
+
return { value: v, clamped: v !== num };
|
| 110 |
+
}
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
// Performance utility functions for debouncing and throttling
|
| 114 |
window.KimiPerformanceUtils = {
|
| 115 |
debounce: function (func, wait, immediate = false, context = null) {
|
|
|
|
| 221 |
|
| 222 |
switch (type) {
|
| 223 |
case "html":
|
| 224 |
+
// Reuse centralized escape logic (removes duplication with KimiValidationUtils.escapeHtml)
|
| 225 |
+
return window.KimiValidationUtils?.escapeHtml(input) || input;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
case "number":
|
| 227 |
const num = parseFloat(input);
|
| 228 |
return isNaN(num) ? 0 : num;
|
|
|
|
| 241 |
}
|
| 242 |
}
|
| 243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
static validateApiKey(key) {
|
| 245 |
if (!key || typeof key !== "string") return false;
|
| 246 |
if (window.KIMI_VALIDATORS && typeof window.KIMI_VALIDATORS.validateApiKey === "function") {
|
|
|
|
| 531 |
open(name) {
|
| 532 |
const el = this.overlays[name];
|
| 533 |
if (el) el.classList.add("visible");
|
| 534 |
+
// Special handling: opening settings overlay sometimes causes active video to freeze (browser rendering stall)
|
| 535 |
+
if (name === "settings-overlay") {
|
| 536 |
+
const kv = window.kimiVideo;
|
| 537 |
+
if (kv && kv.activeVideo) {
|
| 538 |
+
// Short delay so layout / repaint settles before forcing playback
|
| 539 |
+
setTimeout(() => {
|
| 540 |
+
try {
|
| 541 |
+
const v = kv.activeVideo;
|
| 542 |
+
if (!v) return;
|
| 543 |
+
// If ended -> immediately cycle neutral to avoid static frame
|
| 544 |
+
if (v.ended) {
|
| 545 |
+
if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
|
| 546 |
+
} else {
|
| 547 |
+
// Near-end ( <400ms rest ) -> preemptively rotate to avoid stuck on last frame
|
| 548 |
+
if (v.duration && !isNaN(v.duration) && v.duration - v.currentTime < 0.4) {
|
| 549 |
+
if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
|
| 550 |
+
} else if (v.paused) {
|
| 551 |
+
v.play().catch(() => {});
|
| 552 |
+
}
|
| 553 |
+
}
|
| 554 |
+
// Restart freeze watchdog if available
|
| 555 |
+
if (typeof kv._startFreezeWatchdog === "function") kv._startFreezeWatchdog();
|
| 556 |
+
} catch {}
|
| 557 |
+
}, 50);
|
| 558 |
+
// Deferred recheck (covers cases where autoplay is blocked after overlay animation)
|
| 559 |
+
setTimeout(() => {
|
| 560 |
+
try {
|
| 561 |
+
const v = kv.activeVideo;
|
| 562 |
+
if (!v) return;
|
| 563 |
+
if (!v.ended && (v.paused || v.readyState < 2)) {
|
| 564 |
+
v.play().catch(() => {});
|
| 565 |
+
}
|
| 566 |
+
} catch {}
|
| 567 |
+
}, 600);
|
| 568 |
+
}
|
| 569 |
+
}
|
| 570 |
}
|
| 571 |
close(name) {
|
| 572 |
const el = this.overlays[name];
|
kimi-js/kimi-videos.js
CHANGED
|
@@ -60,54 +60,13 @@ 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 |
-
|
| 66 |
-
static crossfadeVideos(fromVideo, toVideo, duration = 300, onComplete) {
|
| 67 |
-
// Resolve duration from CSS variable if present
|
| 68 |
try {
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
// Convert CSS time to ms number if needed (e.g., '300ms' or '0.3s')
|
| 72 |
-
if (cssDur.endsWith("ms")) duration = parseFloat(cssDur);
|
| 73 |
-
else if (cssDur.endsWith("s")) duration = Math.round(parseFloat(cssDur) * 1000);
|
| 74 |
}
|
| 75 |
} catch {}
|
| 76 |
-
|
| 77 |
-
// Preload and strict synchronization
|
| 78 |
-
const easing = "ease-in-out";
|
| 79 |
-
fromVideo.style.transition = `opacity ${duration}ms ${easing}`;
|
| 80 |
-
toVideo.style.transition = `opacity ${duration}ms ${easing}`;
|
| 81 |
-
// Prepare target video (opacity 0, top z-index)
|
| 82 |
-
toVideo.style.opacity = "0";
|
| 83 |
-
toVideo.style.zIndex = "2";
|
| 84 |
-
fromVideo.style.zIndex = "1";
|
| 85 |
-
|
| 86 |
-
// Start target video slightly before the crossfade
|
| 87 |
-
const startTarget = () => {
|
| 88 |
-
if (toVideo.paused) toVideo.play().catch(() => {});
|
| 89 |
-
// Lance le fondu croisé
|
| 90 |
-
setTimeout(() => {
|
| 91 |
-
fromVideo.style.opacity = "0";
|
| 92 |
-
toVideo.style.opacity = "1";
|
| 93 |
-
}, 20);
|
| 94 |
-
// After transition, adjust z-index and call the callback
|
| 95 |
-
setTimeout(() => {
|
| 96 |
-
fromVideo.style.zIndex = "1";
|
| 97 |
-
toVideo.style.zIndex = "2";
|
| 98 |
-
if (onComplete) onComplete();
|
| 99 |
-
}, duration + 30);
|
| 100 |
-
};
|
| 101 |
-
|
| 102 |
-
// If target video is not ready, wait for canplay
|
| 103 |
-
if (toVideo.readyState < 3) {
|
| 104 |
-
toVideo.addEventListener("canplay", startTarget, { once: true });
|
| 105 |
-
toVideo.load();
|
| 106 |
-
} else {
|
| 107 |
-
startTarget();
|
| 108 |
-
}
|
| 109 |
-
// Ensure source video is playing
|
| 110 |
-
if (fromVideo.paused) fromVideo.play().catch(() => {});
|
| 111 |
}
|
| 112 |
|
| 113 |
//Centralized video element creation utility.
|
|
@@ -119,7 +78,6 @@ class KimiVideoManager {
|
|
| 119 |
video.muted = true;
|
| 120 |
video.playsinline = true;
|
| 121 |
video.preload = "auto";
|
| 122 |
-
video.style.opacity = "0";
|
| 123 |
video.innerHTML =
|
| 124 |
'<source src="" type="video/mp4" /><span data-i18n="video_not_supported">Your browser does not support the video tag.</span>';
|
| 125 |
return video;
|
|
@@ -1376,10 +1334,11 @@ class KimiVideoManager {
|
|
| 1376 |
const fromVideo = this.activeVideo;
|
| 1377 |
const toVideo = this.inactiveVideo;
|
| 1378 |
|
| 1379 |
-
|
| 1380 |
-
|
| 1381 |
-
|
| 1382 |
-
|
|
|
|
| 1383 |
fromVideo.classList.remove("active");
|
| 1384 |
toVideo.classList.add("active");
|
| 1385 |
|
|
@@ -1397,22 +1356,20 @@ class KimiVideoManager {
|
|
| 1397 |
const src = this.activeVideo?.querySelector("source")?.getAttribute("src");
|
| 1398 |
const info = { context: this.currentContext, emotion: this.currentEmotion };
|
| 1399 |
console.log("🎬 VideoManager: Now playing:", src, info);
|
| 1400 |
-
// Recompute autoTransitionDuration from actual duration if available (C)
|
| 1401 |
try {
|
| 1402 |
const d = this.activeVideo.duration;
|
| 1403 |
if (!isNaN(d) && d > 0.5) {
|
| 1404 |
-
// Keep 1s headroom before natural end for auto scheduling
|
| 1405 |
const target = Math.max(1000, d * 1000 - 1100);
|
| 1406 |
this.autoTransitionDuration = target;
|
| 1407 |
} else {
|
| 1408 |
-
this.autoTransitionDuration = 9900;
|
| 1409 |
}
|
| 1410 |
-
// Dynamic neutral prefetch to widen diversity without burst
|
| 1411 |
this._prefetchNeutralDynamic();
|
| 1412 |
} catch {}
|
| 1413 |
} catch {}
|
| 1414 |
this._switchInProgress = false;
|
| 1415 |
this.setupEventListenersForContext(this.currentContext);
|
|
|
|
| 1416 |
})
|
| 1417 |
.catch(error => {
|
| 1418 |
console.warn("Failed to play video:", error);
|
|
@@ -1428,7 +1385,7 @@ class KimiVideoManager {
|
|
| 1428 |
this.setupEventListenersForContext(this.currentContext);
|
| 1429 |
});
|
| 1430 |
} else {
|
| 1431 |
-
// Non-promise
|
| 1432 |
this._switchInProgress = false;
|
| 1433 |
try {
|
| 1434 |
const d = this.activeVideo.duration;
|
|
@@ -1441,8 +1398,82 @@ class KimiVideoManager {
|
|
| 1441 |
this._prefetchNeutralDynamic();
|
| 1442 |
} catch {}
|
| 1443 |
this.setupEventListenersForContext(this.currentContext);
|
|
|
|
| 1444 |
}
|
| 1445 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1446 |
}
|
| 1447 |
|
| 1448 |
_prefetchNeutralDynamic() {
|
|
|
|
| 60 |
this._consecutiveErrorCount = 0;
|
| 61 |
// Track per-video load attempts to adapt timeouts & avoid faux échecs
|
| 62 |
this._videoAttempts = new Map();
|
|
|
|
| 63 |
|
| 64 |
+
// Ensure the initially active video is visible (remove any stale inline opacity)
|
|
|
|
|
|
|
| 65 |
try {
|
| 66 |
+
if (this.activeVideo && this.activeVideo.style && this.activeVideo.classList.contains("active")) {
|
| 67 |
+
this.activeVideo.style.opacity = ""; // rely purely on CSS class
|
|
|
|
|
|
|
|
|
|
| 68 |
}
|
| 69 |
} catch {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
//Centralized video element creation utility.
|
|
|
|
| 78 |
video.muted = true;
|
| 79 |
video.playsinline = true;
|
| 80 |
video.preload = "auto";
|
|
|
|
| 81 |
video.innerHTML =
|
| 82 |
'<source src="" type="video/mp4" /><span data-i18n="video_not_supported">Your browser does not support the video tag.</span>';
|
| 83 |
return video;
|
|
|
|
| 1334 |
const fromVideo = this.activeVideo;
|
| 1335 |
const toVideo = this.inactiveVideo;
|
| 1336 |
|
| 1337 |
+
const finalizeSwap = () => {
|
| 1338 |
+
// Clear any inline opacity to rely solely on class-based visibility
|
| 1339 |
+
fromVideo.style.opacity = "";
|
| 1340 |
+
toVideo.style.opacity = "";
|
| 1341 |
+
|
| 1342 |
fromVideo.classList.remove("active");
|
| 1343 |
toVideo.classList.add("active");
|
| 1344 |
|
|
|
|
| 1356 |
const src = this.activeVideo?.querySelector("source")?.getAttribute("src");
|
| 1357 |
const info = { context: this.currentContext, emotion: this.currentEmotion };
|
| 1358 |
console.log("🎬 VideoManager: Now playing:", src, info);
|
|
|
|
| 1359 |
try {
|
| 1360 |
const d = this.activeVideo.duration;
|
| 1361 |
if (!isNaN(d) && d > 0.5) {
|
|
|
|
| 1362 |
const target = Math.max(1000, d * 1000 - 1100);
|
| 1363 |
this.autoTransitionDuration = target;
|
| 1364 |
} else {
|
| 1365 |
+
this.autoTransitionDuration = 9900;
|
| 1366 |
}
|
|
|
|
| 1367 |
this._prefetchNeutralDynamic();
|
| 1368 |
} catch {}
|
| 1369 |
} catch {}
|
| 1370 |
this._switchInProgress = false;
|
| 1371 |
this.setupEventListenersForContext(this.currentContext);
|
| 1372 |
+
this._startFreezeWatchdog();
|
| 1373 |
})
|
| 1374 |
.catch(error => {
|
| 1375 |
console.warn("Failed to play video:", error);
|
|
|
|
| 1385 |
this.setupEventListenersForContext(this.currentContext);
|
| 1386 |
});
|
| 1387 |
} else {
|
| 1388 |
+
// Non-promise fallback
|
| 1389 |
this._switchInProgress = false;
|
| 1390 |
try {
|
| 1391 |
const d = this.activeVideo.duration;
|
|
|
|
| 1398 |
this._prefetchNeutralDynamic();
|
| 1399 |
} catch {}
|
| 1400 |
this.setupEventListenersForContext(this.currentContext);
|
| 1401 |
+
this._startFreezeWatchdog();
|
| 1402 |
}
|
| 1403 |
+
};
|
| 1404 |
+
|
| 1405 |
+
// Ensure target video is at start and attempt playback ahead of swap
|
| 1406 |
+
try {
|
| 1407 |
+
toVideo.currentTime = 0;
|
| 1408 |
+
} catch {}
|
| 1409 |
+
const ready = toVideo.readyState >= 2; // HAVE_CURRENT_DATA
|
| 1410 |
+
if (!ready) {
|
| 1411 |
+
const onReady = () => {
|
| 1412 |
+
toVideo.removeEventListener("canplay", onReady);
|
| 1413 |
+
finalizeSwap();
|
| 1414 |
+
};
|
| 1415 |
+
toVideo.addEventListener("canplay", onReady, { once: true });
|
| 1416 |
+
// Trigger load if not already
|
| 1417 |
+
try {
|
| 1418 |
+
toVideo.load();
|
| 1419 |
+
} catch {}
|
| 1420 |
+
// Also try to play (some browsers will start buffering more aggressively)
|
| 1421 |
+
toVideo.play().catch(() => {});
|
| 1422 |
+
} else {
|
| 1423 |
+
// Already ready -> swap immediately
|
| 1424 |
+
toVideo.play().catch(() => {});
|
| 1425 |
+
finalizeSwap();
|
| 1426 |
+
}
|
| 1427 |
+
}
|
| 1428 |
+
|
| 1429 |
+
// Watchdog to detect freeze when a 10s clip reaches end but 'ended' listener may not fire (browser quirk)
|
| 1430 |
+
_startFreezeWatchdog() {
|
| 1431 |
+
clearInterval(this._freezeInterval);
|
| 1432 |
+
const v = this.activeVideo;
|
| 1433 |
+
if (!v) return;
|
| 1434 |
+
const CHECK_MS = 1000;
|
| 1435 |
+
this._lastProgressTime = Date.now();
|
| 1436 |
+
let lastTime = v.currentTime;
|
| 1437 |
+
// Stalled detection via progress event
|
| 1438 |
+
const onStalled = () => {
|
| 1439 |
+
this._lastProgressTime = Date.now();
|
| 1440 |
+
};
|
| 1441 |
+
v.addEventListener("timeupdate", onStalled);
|
| 1442 |
+
v.addEventListener("progress", onStalled);
|
| 1443 |
+
this._freezeInterval = setInterval(() => {
|
| 1444 |
+
if (v !== this.activeVideo) return; // switched
|
| 1445 |
+
const dur = v.duration || 9.9; // assume 9.9s
|
| 1446 |
+
const nearEnd = v.currentTime >= dur - 0.25; // last 250ms
|
| 1447 |
+
const progressed = v.currentTime !== lastTime;
|
| 1448 |
+
if (progressed) {
|
| 1449 |
+
lastTime = v.currentTime;
|
| 1450 |
+
this._lastProgressTime = Date.now();
|
| 1451 |
+
}
|
| 1452 |
+
// If near end and not auto-transitioned within 500ms, trigger manual neutral
|
| 1453 |
+
if (nearEnd && Date.now() - this._lastProgressTime > 600) {
|
| 1454 |
+
// Ensure we are not already neutral cycling
|
| 1455 |
+
if (this.currentContext === "neutral") {
|
| 1456 |
+
// Pick another neutral to animate
|
| 1457 |
+
try {
|
| 1458 |
+
this.returnToNeutral();
|
| 1459 |
+
} catch {}
|
| 1460 |
+
} else {
|
| 1461 |
+
if (!this._processPendingSwitches()) this.returnToNeutral();
|
| 1462 |
+
}
|
| 1463 |
+
}
|
| 1464 |
+
// Extra safety: if video paused unexpectedly before end
|
| 1465 |
+
if (!v.paused && !v.ended && Date.now() - this._lastProgressTime > 4000) {
|
| 1466 |
+
try {
|
| 1467 |
+
v.play().catch(() => {});
|
| 1468 |
+
} catch {}
|
| 1469 |
+
}
|
| 1470 |
+
// Cleanup if naturally ended (ended handler will schedule next)
|
| 1471 |
+
if (v.ended) {
|
| 1472 |
+
clearInterval(this._freezeInterval);
|
| 1473 |
+
v.removeEventListener("timeupdate", onStalled);
|
| 1474 |
+
v.removeEventListener("progress", onStalled);
|
| 1475 |
+
}
|
| 1476 |
+
}, CHECK_MS);
|
| 1477 |
}
|
| 1478 |
|
| 1479 |
_prefetchNeutralDynamic() {
|
kimi-js/kimi-voices.js
CHANGED
|
@@ -498,9 +498,10 @@ class KimiVoiceManager {
|
|
| 498 |
getVoicePreference(paramType, options = {}) {
|
| 499 |
// Hierarchy: options > memory.preferences > kimiMemory.preferences > DOM element > default
|
| 500 |
const defaults = {
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
|
|
|
| 504 |
};
|
| 505 |
|
| 506 |
const elementIds = {
|
|
|
|
| 498 |
getVoicePreference(paramType, options = {}) {
|
| 499 |
// Hierarchy: options > memory.preferences > kimiMemory.preferences > DOM element > default
|
| 500 |
const defaults = {
|
| 501 |
+
// Use nullish coalescing to preserve explicit 0 values in config
|
| 502 |
+
rate: window.KIMI_CONFIG?.DEFAULTS?.VOICE_RATE ?? 1.1,
|
| 503 |
+
pitch: window.KIMI_CONFIG?.DEFAULTS?.VOICE_PITCH ?? 1.1,
|
| 504 |
+
volume: window.KIMI_CONFIG?.DEFAULTS?.VOICE_VOLUME ?? 0.8
|
| 505 |
};
|
| 506 |
|
| 507 |
const elementIds = {
|