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 = {
|