Spaces:
Running
Running
File size: 9,476 Bytes
9aa357b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
// ===== KIMI VIDEO CONTROLLER =====
class KimiVideoController {
constructor(videoManager) {
this.videoManager = videoManager;
this._lastNegativeAt = 0;
this._negativeCooldownMs = 8000; // avoid spamming negative videos
this._negativeStickUntil = 0; // timestamp until which negative stays sticky
this._baseNegativeStickMs = 3000; // minimal stickiness
this._positiveDebounceMs = 5000; // block positive too soon after negative
this._lastCategory = "neutral";
this._lastSwitchAt = 0;
this._suppressPositiveUntil = 0; // timestamp blocking positive transitions
// Dynamic hostility series tracking
this._hostileTimestamps = []; // epoch ms of negative triggers
this._maxSeriesWindowMs = 60000; // 60s sliding window
this._minCooldownMs = 3000; // lower bound
this._maxCooldownMs = 15000; // upper bound
}
// ===== SINGLE DECISION FUNCTION =====
playVideo(trigger, text = "") {
// 1. DECISION BASE
let category = "neutral";
const now = Date.now();
if (this._isDancing(text)) {
category = "dancing";
} else if (trigger === "user") {
// Analyze user message directly for immediate negative reaction
let userEmo = null;
try {
userEmo = window.kimiEmotionSystem?.analyzeEmotionValidated?.(text) || null;
} catch {}
if (userEmo === "negative") {
const severity = this._computeHostileSeverity(text);
if (now - this._lastNegativeAt > this._negativeCooldownMs) {
category = "speakingNegative";
this._lastNegativeAt = now;
this._negativeStickUntil = now + this._stickDurationForSeverity(severity);
this._registerNegative(now, severity);
} else if (now < this._negativeStickUntil) {
category = "speakingNegative"; // still sticky
}
}
} else if (trigger === "tts") {
// Prefer centralized emotion analysis if available
let emo = null;
try {
if (window.kimiEmotionSystem?.analyzeEmotionValidated) {
emo = window.kimiEmotionSystem.analyzeEmotionValidated(text || "");
} else if (window.kimiEmotionSystem?.analyzeEmotion) {
emo = window.kimiEmotionSystem.analyzeEmotion(text || "");
}
} catch {}
if (emo === "negative") {
const severity = this._computeHostileSeverity(text);
if (now - this._lastNegativeAt > this._negativeCooldownMs) {
category = "speakingNegative";
this._lastNegativeAt = now;
this._negativeStickUntil = now + this._stickDurationForSeverity(severity);
this._registerNegative(now, severity);
} else if (now < this._negativeStickUntil) {
category = "speakingNegative";
} else {
// cooldown active; degrade to neutral
category = "neutral";
}
} else {
// Block positive if still inside debounce window after last negative
if (now < this._suppressPositiveUntil && now < this._negativeStickUntil) {
category = "speakingNegative"; // keep negative sticky
} else if (now < this._suppressPositiveUntil) {
category = "neutral"; // soft neutral instead of immediate positive
} else {
category = "speakingPositive";
}
}
} else if (trigger === "listening") {
category = "listening";
}
// Sticky guard: if trying to leave negative before stick time -> stay
if (this._lastCategory === "speakingNegative" && category !== "speakingNegative" && Date.now() < this._negativeStickUntil) {
category = "speakingNegative";
}
// Neutral dedupe & cascade suppression (< 500ms repeated neutrals)
if (category === "neutral" && this._lastCategory === "neutral") {
const since = now - this._lastSwitchAt;
if (since < 500) {
if (window.KIMI_DEBUG_EMOTION) console.debug("[EMO] Skip rapid neutral cascade", { since });
return;
}
}
this._commitCategory(category, now);
if (window.KIMI_DEBUG_EMOTION) {
console.debug("[EMO] category=", category, {
lastNegativeAt: this._lastNegativeAt,
stickUntil: this._negativeStickUntil,
now,
debounceRemaining: Math.max(0, this._positiveDebounceMs - (now - this._lastNegativeAt)),
suppressPositiveMs: Math.max(0, this._suppressPositiveUntil - now)
});
}
}
// ===== SIMPLE HELPERS =====
_isDancing(text) {
if (!text) return false;
if (window.hasKeywordCategory && window.hasKeywordCategory("dancing", text)) return true;
// Fallback minimal legacy list (should rarely be used)
const words = ["dance", "dancing"];
return words.some(w => text.toLowerCase().includes(w));
}
// _isNegative deprecated: centralized emotion system handles polarity
// Severity = proportion of hostile keywords length found relative to text tokens
_computeHostileSeverity(text) {
if (!text) return 0;
try {
const lang = window.KIMI_LAST_LANG || "en";
const raw = text.toLowerCase();
const tokens = raw.split(/\s+/).filter(Boolean);
if (!tokens.length) return 0;
const hostile = (window.KIMI_CONTEXT_KEYWORDS?.[lang]?.hostile || []).concat(window.KIMI_CONTEXT_KEYWORDS?.en?.hostile || []);
// Prebuild boundary regex list once per call
const patterns = hostile.map(h => {
const esc = String(h)
.trim()
.toLowerCase()
.replace(/[-/\\^$*+?.()|[\]{}]/g, r => "\\" + r);
return new RegExp(`\\b${esc}\\b`, "i");
});
let hits = 0;
for (const p of patterns) {
if (p.test(raw)) hits++;
}
const severity = hits / tokens.length;
return severity > 1 ? 1 : severity;
} catch {
return 0;
}
}
_commitCategory(category, now) {
if (this.videoManager?.switchToContext) {
this.videoManager.switchToContext(category, category, null, null, null, false);
}
this._lastCategory = category;
this._lastSwitchAt = now;
}
_stickDurationForSeverity(sev) {
if (sev >= 0.4) return 6000;
if (sev >= 0.25) return 4500;
if (sev >= 0.15) return 3800;
return this._baseNegativeStickMs;
}
_registerNegative(now, severity) {
try {
this._hostileTimestamps.push({ t: now, s: severity });
// purge old
const cutoff = now - this._maxSeriesWindowMs;
this._hostileTimestamps = this._hostileTimestamps.filter(e => e.t >= cutoff);
this._updateDynamicCooldown(now);
} catch {}
}
_updateDynamicCooldown(now) {
const entries = this._hostileTimestamps;
if (!entries.length) return;
// Compute weighted intensity: sum(severity * freshnessWeight)
// freshnessWeight = 1 - age/maxWindow
const cutoff = now - this._maxSeriesWindowMs;
let weighted = 0;
for (const e of entries) {
const age = Math.min(this._maxSeriesWindowMs, Math.max(0, now - e.t));
const w = 1 - age / this._maxSeriesWindowMs;
weighted += e.s * w;
}
// Normalize to rough 0..N scale. If user spams many insults severity .3 → weighted ~ >1
// Map weighted to cooldown via inverse relation: plus d'hostilité récente => cooldown plus long & debounce plus long
// Clamp weighted to 0..2 for mapping
const clamped = Math.min(2, weighted);
const ratio = clamped / 2; // 0..1
// Interpolate cooldown
const newCooldown = Math.round(this._minCooldownMs + (this._maxCooldownMs - this._minCooldownMs) * ratio);
this._negativeCooldownMs = newCooldown;
// Optionally also scale positive debounce (bounded 3000..8000)
this._positiveDebounceMs = 3000 + Math.round(5000 * ratio);
if (window.KIMI_DEBUG_EMOTION) {
console.debug("[EMO] dynamicCooldown", { weighted, ratio, newCooldown, positiveDebounce: this._positiveDebounceMs, series: entries.length });
}
}
}
// ===== SIMPLE API =====
function initializeVideoController(videoManager) {
const controller = new KimiVideoController(videoManager);
return controller;
}
// ES6 exports
export { KimiVideoController, initializeVideoController };
// Global exposure
window.KimiVideoController = KimiVideoController;
window.initializeVideoController = initializeVideoController;
|