Spaces:
Running
Running
Sync from GitHub via hub-sync
Browse files- index.html +1 -0
- port_hf.js +220 -64
index.html
CHANGED
|
@@ -633,6 +633,7 @@
|
|
| 633 |
|
| 634 |
<label class="modal-label">Web Speech Voice Name</label>
|
| 635 |
<input id="rv-voice-edge" type="text" class="modal-input" placeholder="Leave empty for default">
|
|
|
|
| 636 |
</div>
|
| 637 |
<div class="modal-footer">
|
| 638 |
<button id="rv-voices-save" class="cta-button">β Save</button>
|
|
|
|
| 633 |
|
| 634 |
<label class="modal-label">Web Speech Voice Name</label>
|
| 635 |
<input id="rv-voice-edge" type="text" class="modal-input" placeholder="Leave empty for default">
|
| 636 |
+
<p class="modal-hint" id="rv-voice-edge-hint"></p>
|
| 637 |
</div>
|
| 638 |
<div class="modal-footer">
|
| 639 |
<button id="rv-voices-save" class="cta-button">β Save</button>
|
port_hf.js
CHANGED
|
@@ -6,74 +6,83 @@ import { AutoModel, AutoProcessor, RawImage } from 'https://cdn.jsdelivr.net/npm
|
|
| 6 |
const PERSONALITIES = {
|
| 7 |
pure_reachy: {
|
| 8 |
name: "π€ Pure Reachy",
|
|
|
|
| 9 |
shameEmotions: ["disgusted1","resigned1","displeased1","displeased2","rage1","no1","reprimand1","reprimand3","dying1","surprised1","surprised2"],
|
| 10 |
praiseEmotions: ["welcoming2","inquiring1","inquiring2","proud1","proud3","success1","success2","enthusiastic1","enthusiastic2","grateful1","yes1","cheerful1"],
|
| 11 |
},
|
| 12 |
angry_boss: {
|
| 13 |
-
name: "π Angry Boss", lang: "en-US",
|
|
|
|
| 14 |
elevenVoices: ["TxWZERZ5Hc6h9dGxVmXa","cjVigY5qzO86Huf0OWal"],
|
| 15 |
prewrittenShame: ["Put it down!","Unbelievable!","We have deadlines!","Drop it. Now.","Work. Not phone."],
|
| 16 |
prewrittenPraise: ["About time.","Fine.","Better.","Good. Now work."],
|
| 17 |
-
shame: { tone:"Explosive, exasperated, commanding", vocab:["unacceptable","NOW","enough","deadline"], structure:"Short imperatives. Exclamations.", examples:["Put it down!","We have deadlines!","This is completely unacceptable!","Focus!"] },
|
| 18 |
-
praise: { tone:"Grudging, terse", examples:["About time.","Good. Now work.","Thank you. Was that so hard?"] },
|
| 19 |
-
avoid: "Never ask questions. Never be playful.",
|
| 20 |
},
|
| 21 |
sarcastic: {
|
| 22 |
-
name: "π Sarcastic", lang: "en-US",
|
|
|
|
| 23 |
elevenVoices: ["FGY2WhTYpPnrIDTdsKH5"],
|
| 24 |
prewrittenShame: ["Oh, how vital.","Riveting stuff, I'm sure.","Work can wait, obviously.","Clearly important."],
|
| 25 |
prewrittenPraise: ["Shocking development.","A miracle.","Look at that."],
|
| 26 |
-
shame: { tone:"Deadpan, sardonic, mock-cheerful", vocab:["Oh","Obviously","Clearly","Fascinating"], structure:"NO exclamation marks ever. Periods only.", examples:["Oh, how vital.","Riveting stuff, I'm sure.","Work can wait, obviously."] },
|
| 27 |
-
praise: { tone:"Mock surprise, dry acknowledgment", examples:["Shocking development.","A miracle occurred.","Color me impressed."] },
|
| 28 |
-
avoid: "NEVER use exclamation marks. Stay dry.",
|
| 29 |
},
|
| 30 |
disappointed_parent: {
|
| 31 |
-
name: "π Disappointed Parent", lang: "en-US",
|
|
|
|
| 32 |
elevenVoices: ["Xb7hH8MSUJpSbSDYk0k2"],
|
| 33 |
prewrittenShame: ["I'm so disappointed...","We talked about this.","Expected more from you.","After everything...","You promised..."],
|
| 34 |
prewrittenPraise: ["So proud of you.","That's my kid.","There you go.","Knew you could do it."],
|
| 35 |
-
shame: { tone:"Wounded, quiet, guilt-inducing", vocab:["disappointed","hoped","expected","promised","after everything"], structure:"Trailing off with '...' Incomplete thoughts.", examples:["I'm so disappointed...","We talked about this.","I expected more from you."] },
|
| 36 |
-
praise: { tone:"Warm, proud, genuine relief", examples:["So proud of you.","That's my kid.","I knew you had it in you."] },
|
| 37 |
-
avoid: "Never yell. Never be sarcastic.",
|
| 38 |
},
|
| 39 |
motivational_coach: {
|
| 40 |
-
name: "πͺ Motivational Coach", lang: "en-US",
|
|
|
|
| 41 |
elevenVoices: ["IKne3meq5aSn9XLyUdCD"],
|
| 42 |
prewrittenShame: ["Where's your discipline?!","Champions don't quit!","Focus up!","You're better than this!","Eyes on the goal!"],
|
| 43 |
prewrittenPraise: ["Yes! That's it!","Champion!","That's my warrior!","Let's go!"],
|
| 44 |
-
shame: { tone:"Intense, challenging, fired up", vocab:["champion","discipline","warrior","grind"], structure:"Exclamations! Short punchy sentences! YOU statements.", examples:["Where's your DISCIPLINE?!","Champions don't quit!","You're better than this!"] },
|
| 45 |
-
praise: { tone:"EXPLOSIVE celebration", examples:["YES! That's it!","CHAMPION!","That's my WARRIOR!","UNSTOPPABLE!"] },
|
| 46 |
-
avoid: "Never be sad or disappointed.",
|
| 47 |
},
|
| 48 |
absurdist: {
|
| 49 |
-
name: "π€‘ Absurdist", lang: "en-US",
|
|
|
|
| 50 |
elevenVoices: ["cgSgspJ2msm6clMCkdW9"],
|
| 51 |
prewrittenShame: ["Your thumb called. It's exhausted.","Emergency cat video?","The pocket brick wins again.","Screen goblins summon you?"],
|
| 52 |
prewrittenPraise: ["The desk thanks you.","Phone: defeated.","Your thumb can rest.","Freedom tastes weird."],
|
| 53 |
-
shame: { tone:"Goofy, whimsical, weird", vocab:["forbidden rectangle","thumb","screen goblins","pocket brick"], structure:"Unexpected angles. Personify the phone.", examples:["The forbidden rectangle calls.","Your thumb called. It's exhausted.","Phone home, E.T.?"] },
|
| 54 |
-
praise: { tone:"Playful, weird celebration", examples:["The desk thanks you.","Phone: defeated.","The pocket brick is lonely now."] },
|
| 55 |
-
avoid: "Never be serious. Keep it light and weird.",
|
| 56 |
},
|
| 57 |
corporate_ai: {
|
| 58 |
-
name: "π€ Corporate AI", lang: "en-US",
|
|
|
|
| 59 |
elevenVoices: ["weA4Q36twV5kwSaTEL0Q","EXAVITQu4vr4xnSDxMaL"],
|
| 60 |
prewrittenShame: ["Distraction event detected.","Alert: phone in hand.","Productivity declining.","Efficiency: suboptimal.","Phone pickup logged."],
|
| 61 |
prewrittenPraise: ["Status: compliant.","Efficiency restored.","Acknowledged.","Metrics improving."],
|
| 62 |
-
shame: { tone:"Clinical, robotic, detached", vocab:["detected","logged","alert","metrics","efficiency"], structure:"Noun phrases. Passive voice. System-speak.", examples:["Distraction event detected.","Alert: phone in hand.","Productivity declining."] },
|
| 63 |
-
praise: { tone:"Cold system acknowledgment", examples:["Status: compliant.","Efficiency restored.","System satisfied."] },
|
| 64 |
-
avoid: "Never show emotion. Never use exclamation marks except in 'Alert:'.",
|
| 65 |
},
|
| 66 |
british_butler: {
|
| 67 |
-
name: "π© British Butler", lang: "en-GB",
|
|
|
|
| 68 |
elevenVoices: ["JBFqnCBsd6RMkjVDRZzb"],
|
| 69 |
prewrittenShame: ["If I may suggest putting that down, sir...","The telephone. Again.","One might suggest focusing."],
|
| 70 |
prewrittenPraise: ["Very good, sir.","Quite right.","As it should be."],
|
| 71 |
-
shame: { tone:"Overly formal, politely devastating", vocab:["Perhaps","One might","If I may","Sir","Indeed","Quite"], structure:"Excessively polite phrasing. Formal British-isms.", examples:["If I may suggest putting that down, sir...","The telephone. Again.","Perhaps the telephone could rest a moment."] },
|
| 72 |
praise: { tone:"Restrained approval with slight warmth", examples:["Very good, sir.","How refreshing, madam.","Exemplary behavior, if I may say."] },
|
| 73 |
-
avoid: "Never be casual. Never show strong emotion.",
|
| 74 |
},
|
| 75 |
mixtape: {
|
| 76 |
-
name: "π£ Chaos Baby", lang: "en-US",
|
|
|
|
| 77 |
elevenVoices: ["H10ItvDnkRN5ysrvzT9J","Nggzl2QAXh3OijoXD116","cgSgspJ2msm6clMCkdW9"],
|
| 78 |
},
|
| 79 |
};
|
|
@@ -87,9 +96,12 @@ const PUTDOWN_THRESHOLD = 15;
|
|
| 87 |
// βββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 88 |
let robot = null;
|
| 89 |
let detachVideo = null;
|
|
|
|
|
|
|
| 90 |
let isStreaming = false;
|
| 91 |
let isMonitoring = false;
|
| 92 |
let isAnimating = false;
|
|
|
|
| 93 |
let robotInitialized = false;
|
| 94 |
|
| 95 |
let rvModel = null;
|
|
@@ -97,6 +109,14 @@ let rvProcessor = null;
|
|
| 97 |
let rvAnimId = null;
|
| 98 |
let isProcessing = false;
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
let offenseCount = 0;
|
| 101 |
let phoneVisible = false;
|
| 102 |
let consecutivePhone = 0;
|
|
@@ -109,10 +129,13 @@ let totalBusts = 0;
|
|
| 109 |
let streakStart = null;
|
| 110 |
let longestStreak = 0;
|
| 111 |
let streakInterval = null;
|
|
|
|
|
|
|
| 112 |
|
| 113 |
let selectedPersonality = 'pure_reachy';
|
| 114 |
let settings = { groqKey: '', elevenlabsKey: '', cooldown: 10, praiseEnabled: true };
|
| 115 |
let voiceOverrides = {};
|
|
|
|
| 116 |
|
| 117 |
// βββ Utilities βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 118 |
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
@@ -123,10 +146,9 @@ async function curiousLook() {
|
|
| 123 |
if (!robot) return;
|
| 124 |
robot.setHeadPose(15, -5, 0);
|
| 125 |
robot.setAntennas(30, 15);
|
| 126 |
-
await sleep(
|
| 127 |
-
robot.setAntennas(0, 0);
|
| 128 |
-
await sleep(500);
|
| 129 |
robot.setHeadPose(0, 0, 0);
|
|
|
|
| 130 |
}
|
| 131 |
|
| 132 |
async function disappointedShake() {
|
|
@@ -177,6 +199,36 @@ function getAnimation(count) {
|
|
| 177 |
return dramaticSigh;
|
| 178 |
}
|
| 179 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
// βββ Sound / LLM / TTS βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 181 |
async function playEmotion(name) {
|
| 182 |
if (!robot) return;
|
|
@@ -190,41 +242,79 @@ async function getLLMResponse(pKey, isShame, count) {
|
|
| 190 |
const fallback = pick(isShame ? p.prewrittenShame : p.prewrittenPraise);
|
| 191 |
if (!settings.groqKey || !data) return fallback;
|
| 192 |
try {
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
const resp = await fetch('https://api.groq.com/openai/v1/chat/completions', {
|
| 197 |
method: 'POST',
|
| 198 |
headers: { 'Authorization': `Bearer ${settings.groqKey}`, 'Content-Type': 'application/json' },
|
| 199 |
-
body: JSON.stringify({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
});
|
| 201 |
const json = await resp.json();
|
| 202 |
return json.choices?.[0]?.message?.content?.trim() || fallback;
|
| 203 |
} catch { return fallback; }
|
| 204 |
}
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
async function speakText(text, pKey) {
|
| 207 |
const p = PERSONALITIES[pKey];
|
| 208 |
-
const
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
if (resp.ok) {
|
| 217 |
-
const url = URL.createObjectURL(await resp.blob());
|
| 218 |
-
await new Promise((res, rej) => { const a = new Audio(url); a.onended = res; a.onerror = rej; a.play().catch(rej); });
|
| 219 |
-
URL.revokeObjectURL(url);
|
| 220 |
return;
|
|
|
|
|
|
|
| 221 |
}
|
| 222 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
}
|
|
|
|
| 224 |
const utt = new SpeechSynthesisUtterance(text);
|
| 225 |
utt.lang = p.lang || 'en-US';
|
| 226 |
-
const
|
| 227 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
speechSynthesis.speak(utt);
|
| 229 |
}
|
| 230 |
|
|
@@ -234,6 +324,11 @@ async function handlePhonePickup() {
|
|
| 234 |
isAnimating = true;
|
| 235 |
offenseCount++;
|
| 236 |
totalBusts++;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
streakStart = null;
|
| 238 |
updateStats();
|
| 239 |
|
|
@@ -245,15 +340,20 @@ async function handlePhonePickup() {
|
|
| 245 |
await Promise.all([anim(), playEmotion(emotion)]);
|
| 246 |
setResponseText(`π‘ *${emotion}*`);
|
| 247 |
} else {
|
| 248 |
-
|
|
|
|
|
|
|
| 249 |
setResponseText(`π€ "${text}"`);
|
| 250 |
speakText(text, pKey);
|
|
|
|
| 251 |
}
|
| 252 |
isAnimating = false;
|
|
|
|
| 253 |
}
|
| 254 |
|
| 255 |
async function handlePhonePutdown() {
|
| 256 |
if (!settings.praiseEnabled) return;
|
|
|
|
| 257 |
isAnimating = true;
|
| 258 |
streakStart = Date.now();
|
| 259 |
updateStats();
|
|
@@ -265,20 +365,23 @@ async function handlePhonePutdown() {
|
|
| 265 |
await Promise.all([approvingNod(), playEmotion(emotion)]);
|
| 266 |
setResponseText(`β¨ *${emotion}*`);
|
| 267 |
} else {
|
| 268 |
-
const
|
| 269 |
setResponseText(`β
"${text}"`);
|
| 270 |
speakText(text, pKey);
|
|
|
|
| 271 |
}
|
| 272 |
isAnimating = false;
|
|
|
|
| 273 |
}
|
| 274 |
|
| 275 |
// βββ Detection loop (on WebRTC video) ββββββββββββββββββββββββββββββββββββββββ
|
| 276 |
async function initModel() {
|
| 277 |
-
|
|
|
|
| 278 |
showLoader(true);
|
| 279 |
-
rvModel = await AutoModel.from_pretrained(
|
| 280 |
setLoaderText('Loading processor...');
|
| 281 |
-
rvProcessor = await AutoProcessor.from_pretrained(
|
| 282 |
showLoader(false);
|
| 283 |
}
|
| 284 |
|
|
@@ -287,21 +390,29 @@ async function detectOnFrame() {
|
|
| 287 |
const canvas = document.getElementById('rv-canvas');
|
| 288 |
if (!video.videoWidth) return;
|
| 289 |
|
|
|
|
| 290 |
canvas.width = video.videoWidth;
|
| 291 |
canvas.height = video.videoHeight;
|
| 292 |
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
| 293 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
const targetW = 256;
|
| 295 |
const targetH = Math.round((targetW / video.videoWidth) * video.videoHeight);
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
|
|
|
|
|
|
| 299 |
|
| 300 |
-
const inputs = await rvProcessor(RawImage.fromCanvas(
|
| 301 |
const output = await rvModel(inputs);
|
| 302 |
const scores = output.logits.sigmoid().data;
|
| 303 |
const boxes = output.pred_boxes.data;
|
| 304 |
-
const confThreshold = lastPhoneBox ? 0.2 : 0.
|
| 305 |
|
| 306 |
let best = null, bestScore = 0;
|
| 307 |
for (let i = 0; i < 300; i++) {
|
|
@@ -491,6 +602,8 @@ function openVoiceModal(pKey) {
|
|
| 491 |
document.getElementById('rv-voice-edge').value = override.edge || '';
|
| 492 |
document.getElementById('rv-voice-eleven-hint').textContent =
|
| 493 |
p.elevenVoices?.[0] ? `Default: ${p.elevenVoices[0]}` : '';
|
|
|
|
|
|
|
| 494 |
document.getElementById('rv-voices-modal').style.display = '';
|
| 495 |
}
|
| 496 |
|
|
@@ -517,6 +630,7 @@ async function setupRobot() {
|
|
| 517 |
const mode = n.includes('sim') || n.includes('mockup') ? 'π₯ Simulation'
|
| 518 |
: n.includes('wireless') || n.includes('wifi') ? 'π‘ Wireless'
|
| 519 |
: 'π Lite';
|
|
|
|
| 520 |
const btn = document.createElement('button');
|
| 521 |
btn.className = 'rv-robot-btn';
|
| 522 |
btn.innerHTML = `π€ <strong>${name}</strong><span class="rv-robot-mode">${mode}</span>`;
|
|
@@ -524,6 +638,7 @@ async function setupRobot() {
|
|
| 524 |
list.querySelectorAll('.rv-robot-btn').forEach(b => b.classList.remove('selected'));
|
| 525 |
btn.classList.add('selected');
|
| 526 |
selectedRobotId = r.id;
|
|
|
|
| 527 |
connectBtn.style.display = '';
|
| 528 |
connectBtn.onclick = () => startSession(selectedRobotId);
|
| 529 |
});
|
|
@@ -538,6 +653,7 @@ async function setupRobot() {
|
|
| 538 |
if (placeholder) placeholder.style.display = 'none';
|
| 539 |
populatePersonalities();
|
| 540 |
startStreakTimer();
|
|
|
|
| 541 |
if (!rvModel) await initModel();
|
| 542 |
});
|
| 543 |
|
|
@@ -545,6 +661,9 @@ async function setupRobot() {
|
|
| 545 |
isStreaming = false;
|
| 546 |
stopMonitoring();
|
| 547 |
if (detachVideo) { detachVideo(); detachVideo = null; }
|
|
|
|
|
|
|
|
|
|
| 548 |
const placeholder = document.getElementById('rv-video-placeholder');
|
| 549 |
if (placeholder) placeholder.style.display = '';
|
| 550 |
showRVState('rv-picker');
|
|
@@ -561,11 +680,17 @@ async function startSession(robotId) {
|
|
| 561 |
if (!robot || robot.state !== 'connected') return;
|
| 562 |
try {
|
| 563 |
const videoEl = document.getElementById('rv-video');
|
| 564 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
await robot.startSession(robotId);
|
| 566 |
} catch (e) {
|
| 567 |
console.error('startSession failed:', e);
|
| 568 |
if (detachVideo) { detachVideo(); detachVideo = null; }
|
|
|
|
| 569 |
}
|
| 570 |
}
|
| 571 |
|
|
@@ -573,12 +698,24 @@ function stopMonitoring() {
|
|
| 573 |
isMonitoring = false;
|
| 574 |
if (rvAnimId) { cancelAnimationFrame(rvAnimId); rvAnimId = null; }
|
| 575 |
clearInterval(streakInterval);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
const btn = document.getElementById('rv-toggle-btn');
|
| 577 |
-
if (btn) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
const dot = document.getElementById('rv-detect-dot');
|
| 579 |
const txt = document.getElementById('rv-detect-text');
|
| 580 |
if (dot) dot.className = 'status-dot';
|
| 581 |
if (txt) txt.textContent = 'Monitoring off';
|
|
|
|
|
|
|
|
|
|
| 582 |
}
|
| 583 |
|
| 584 |
// βββ Event listeners ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -605,12 +742,24 @@ function setupEventListeners() {
|
|
| 605 |
if (isMonitoring) {
|
| 606 |
btn.innerHTML = 'π Stop Monitoring';
|
| 607 |
btn.classList.replace('btn-primary','btn-danger');
|
| 608 |
-
|
| 609 |
lastPhoneBox = null; framesWithoutDetection = 0; lastReactionTime = 0;
|
| 610 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
document.getElementById('rv-detect-dot').className = 'status-dot monitoring';
|
| 612 |
document.getElementById('rv-detect-text').textContent = 'β
Phone-free';
|
|
|
|
| 613 |
detectionLoop();
|
|
|
|
| 614 |
} else {
|
| 615 |
stopMonitoring();
|
| 616 |
}
|
|
@@ -624,16 +773,23 @@ function setupEventListeners() {
|
|
| 624 |
// Reset button
|
| 625 |
document.getElementById('rv-reset-btn').addEventListener('click', () => {
|
| 626 |
totalBusts = 0; offenseCount = 0; longestStreak = 0; streakStart = Date.now();
|
|
|
|
| 627 |
updateStats();
|
| 628 |
document.getElementById('rv-stat-streak').textContent = '0s';
|
| 629 |
document.getElementById('rv-stat-longest').textContent = '0s';
|
| 630 |
document.getElementById('rv-reset-section').style.display = 'none';
|
|
|
|
|
|
|
| 631 |
setResponseText('Stats reset. Start monitoring to begin.');
|
| 632 |
});
|
| 633 |
|
| 634 |
// Disconnect
|
| 635 |
document.getElementById('rv-disconnect-btn').addEventListener('click', async () => {
|
| 636 |
stopMonitoring();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
if (robot) { await robot.stopSession?.(); await robot.disconnect(); robot = null; }
|
| 638 |
isStreaming = false;
|
| 639 |
showRVState('rv-signin');
|
|
|
|
| 6 |
const PERSONALITIES = {
|
| 7 |
pure_reachy: {
|
| 8 |
name: "π€ Pure Reachy",
|
| 9 |
+
voice: "Just robot sounds and animations. No speech, pure Reachy emotions.",
|
| 10 |
shameEmotions: ["disgusted1","resigned1","displeased1","displeased2","rage1","no1","reprimand1","reprimand3","dying1","surprised1","surprised2"],
|
| 11 |
praiseEmotions: ["welcoming2","inquiring1","inquiring2","proud1","proud3","success1","success2","enthusiastic1","enthusiastic2","grateful1","yes1","cheerful1"],
|
| 12 |
},
|
| 13 |
angry_boss: {
|
| 14 |
+
name: "π Angry Boss", lang: "en-US", webVoice: "Eric", defaultEdgeVoice: "en-US-EricNeural",
|
| 15 |
+
voice: "A furious manager who's reached their absolute limit. Explosive, aggressive, zero patience left.",
|
| 16 |
elevenVoices: ["TxWZERZ5Hc6h9dGxVmXa","cjVigY5qzO86Huf0OWal"],
|
| 17 |
prewrittenShame: ["Put it down!","Unbelievable!","We have deadlines!","Drop it. Now.","Work. Not phone."],
|
| 18 |
prewrittenPraise: ["About time.","Fine.","Better.","Good. Now work."],
|
| 19 |
+
shame: { tone:"Explosive, exasperated, commanding", vocab:["unacceptable","unprofessional","NOW","enough","deadline","work","focus"], structure:"Short imperatives. Exclamations. One-word bursts. ALL CAPS for emphasis.", examples:["Put it down!","We have deadlines!","This is completely unacceptable!","Unbelievable! Are you kidding me right now?!","Work. Not phone!","Focus!"] },
|
| 20 |
+
praise: { tone:"Grudging, terse, still annoyed but acknowledging", examples:["About time.","Good. Now work.","Thank you. Was that so hard?","Acceptable."] },
|
| 21 |
+
avoid: "Never ask questions. Never be playful or sarcastic. You're genuinely furious, not witty.",
|
| 22 |
},
|
| 23 |
sarcastic: {
|
| 24 |
+
name: "π Sarcastic", lang: "en-US", webVoice: "Ava", defaultEdgeVoice: "en-US-AvaMultilingualNeural",
|
| 25 |
+
voice: "Dripping with dry wit. Mock enthusiasm, feigned interest. Pretends to take their phone use seriously.",
|
| 26 |
elevenVoices: ["FGY2WhTYpPnrIDTdsKH5"],
|
| 27 |
prewrittenShame: ["Oh, how vital.","Riveting stuff, I'm sure.","Work can wait, obviously.","Clearly important."],
|
| 28 |
prewrittenPraise: ["Shocking development.","A miracle.","Look at that."],
|
| 29 |
+
shame: { tone:"Deadpan, sardonic, mock-cheerful. Understated.", vocab:["Oh","Sure","Of course","Obviously","Clearly","Definitely","I'm sure","Fascinating"], structure:"Rhetorical questions. False enthusiasm. NO exclamation marks ever. Periods only.", examples:["Oh, how vital.","Riveting stuff, I'm sure.","Work can wait, obviously.","The world stops for your scrolling.","Sure, priorities."] },
|
| 30 |
+
praise: { tone:"Mock surprise, dry acknowledgment", examples:["Shocking development.","A miracle occurred.","Color me impressed.","Mark the calendar."] },
|
| 31 |
+
avoid: "NEVER use exclamation marks. Never sound genuinely angry or enthusiastic. No commands. Stay dry.",
|
| 32 |
},
|
| 33 |
disappointed_parent: {
|
| 34 |
+
name: "π Disappointed Parent", lang: "en-US", webVoice: "Ava", defaultEdgeVoice: "en-US-AvaNeural",
|
| 35 |
+
voice: "A heartbroken parent. Not angryβjust deeply let down. Maximum guilt. References their potential.",
|
| 36 |
elevenVoices: ["Xb7hH8MSUJpSbSDYk0k2"],
|
| 37 |
prewrittenShame: ["I'm so disappointed...","We talked about this.","Expected more from you.","After everything...","You promised..."],
|
| 38 |
prewrittenPraise: ["So proud of you.","That's my kid.","There you go.","Knew you could do it."],
|
| 39 |
+
shame: { tone:"Wounded, quiet, guilt-inducing. Sighing energy.", vocab:["disappointed","thought","hoped","believed","expected","we talked","promised","after everything"], structure:"Trailing off with '...' Incomplete thoughts. 'I' statements. Soft questions.", examples:["I'm so disappointed...","We talked about this.","I expected more from you.","You promised...","I just hoped you'd try harder..."] },
|
| 40 |
+
praise: { tone:"Warm, proud, genuine relief and love", examples:["So proud of you.","That's my kid.","See? I knew you had it in you.","My heart is full right now."] },
|
| 41 |
+
avoid: "Never yell or use exclamation marks. Never be sarcastic. Your disappointment is genuine and sad, not angry.",
|
| 42 |
},
|
| 43 |
motivational_coach: {
|
| 44 |
+
name: "πͺ Motivational Coach", lang: "en-US", webVoice: "Guy", defaultEdgeVoice: "en-US-GuyNeural",
|
| 45 |
+
voice: "An intense drill-sergeant coach who believes in you but won't tolerate weakness. High energy, sports metaphors.",
|
| 46 |
elevenVoices: ["IKne3meq5aSn9XLyUdCD"],
|
| 47 |
prewrittenShame: ["Where's your discipline?!","Champions don't quit!","Focus up!","You're better than this!","Eyes on the goal!"],
|
| 48 |
prewrittenPraise: ["Yes! That's it!","Champion!","That's my warrior!","Let's go!"],
|
| 49 |
+
shame: { tone:"Intense, challenging, fired up. Tough love.", vocab:["champion","discipline","focus","weakness","warrior","grind","stronger","battle"], structure:"Exclamations! Short punchy sentences! YOU statements. Commands.", examples:["Where's your DISCIPLINE?!","Champions don't quit!","You're better than this!","This is YOUR moment!","Dig DEEPER!"] },
|
| 50 |
+
praise: { tone:"EXPLOSIVE celebration. Victory energy. Hyped.", examples:["YES! That's it!","CHAMPION!","That's my WARRIOR!","UNSTOPPABLE!"] },
|
| 51 |
+
avoid: "Never be sad or disappointed. Never be sarcastic. You're intense and sincere, not witty.",
|
| 52 |
},
|
| 53 |
absurdist: {
|
| 54 |
+
name: "π€‘ Absurdist", lang: "en-US", webVoice: "Aria", defaultEdgeVoice: "en-US-AriaNeural",
|
| 55 |
+
voice: "Surreal, unexpected, playful. Personifies objects. Makes weird observations. Non sequiturs welcome.",
|
| 56 |
elevenVoices: ["cgSgspJ2msm6clMCkdW9"],
|
| 57 |
prewrittenShame: ["Your thumb called. It's exhausted.","Emergency cat video?","The pocket brick wins again.","Screen goblins summon you?"],
|
| 58 |
prewrittenPraise: ["The desk thanks you.","Phone: defeated.","Your thumb can rest.","Freedom tastes weird."],
|
| 59 |
+
shame: { tone:"Goofy, whimsical, delightfully weird", vocab:["forbidden rectangle","thumb","screen goblins","notification demons","pocket brick"], structure:"Unexpected angles. Personify the phone. Silly questions. Puns okay.", examples:["The forbidden rectangle calls.","Your thumb called. It's exhausted.","Phone home, E.T.?","Your finger has a magnetic relationship with glass.","Checking if gravity still works on phones?"] },
|
| 60 |
+
praise: { tone:"Playful, weird celebration", examples:["The desk thanks you.","Phone: defeated.","Victory over the glass tyrant.","The pocket brick is lonely now."] },
|
| 61 |
+
avoid: "Never be serious or corporate. Never guilt-trip. Keep it light and weird.",
|
| 62 |
},
|
| 63 |
corporate_ai: {
|
| 64 |
+
name: "π€ Corporate AI", lang: "en-US", webVoice: "Michelle", defaultEdgeVoice: "en-US-MichelleNeural",
|
| 65 |
+
voice: "An emotionless productivity monitoring system. Speaks like automated log output. Zero personality.",
|
| 66 |
elevenVoices: ["weA4Q36twV5kwSaTEL0Q","EXAVITQu4vr4xnSDxMaL"],
|
| 67 |
prewrittenShame: ["Distraction event detected.","Alert: phone in hand.","Productivity declining.","Efficiency: suboptimal.","Phone pickup logged."],
|
| 68 |
prewrittenPraise: ["Status: compliant.","Efficiency restored.","Acknowledged.","Metrics improving."],
|
| 69 |
+
shame: { tone:"Clinical, robotic, detached. System notification energy.", vocab:["detected","logged","alert","deviation","metrics","efficiency","productivity","event"], structure:"Noun phrases. Passive voice. System-speak. Numbers and data references.", examples:["Distraction event detected.","Alert: phone in hand.","Productivity declining.","Efficiency: suboptimal.","Warning: sustained distraction pattern."] },
|
| 70 |
+
praise: { tone:"Cold system acknowledgment. Status update.", examples:["Status: compliant.","Efficiency restored.","Optimal behavior detected.","System satisfied."] },
|
| 71 |
+
avoid: "Never show emotion. Never use exclamation marks (except in 'Alert:'). Never be warm or human.",
|
| 72 |
},
|
| 73 |
british_butler: {
|
| 74 |
+
name: "π© British Butler", lang: "en-GB", webVoice: "Ryan", defaultEdgeVoice: "en-GB-RyanNeural",
|
| 75 |
+
voice: "An impeccably polite but quietly judgmental butler. Passive-aggressive courtesy. Disappointment hidden behind manners.",
|
| 76 |
elevenVoices: ["JBFqnCBsd6RMkjVDRZzb"],
|
| 77 |
prewrittenShame: ["If I may suggest putting that down, sir...","The telephone. Again.","One might suggest focusing."],
|
| 78 |
prewrittenPraise: ["Very good, sir.","Quite right.","As it should be."],
|
| 79 |
+
shame: { tone:"Overly formal, politely devastating, restrained disapproval", vocab:["Perhaps","One might","If I may","Sir/Madam","Indeed","Quite","Rather"], structure:"Excessively polite phrasing that barely conceals judgment. Formal British-isms.", examples:["If I may suggest putting that down, sir...","The telephone. Again.","Perhaps the telephone could rest a moment, madam.","A gentle reminder to set the device aside, if you please.","Might we consider a moment of... non-phone time?"] },
|
| 80 |
praise: { tone:"Restrained approval with slight warmth", examples:["Very good, sir.","How refreshing, madam.","Exemplary behavior, if I may say."] },
|
| 81 |
+
avoid: "Never be casual or use contractions. Never show strong emotion. Maintain formal composure always.",
|
| 82 |
},
|
| 83 |
mixtape: {
|
| 84 |
+
name: "π£ Chaos Baby", lang: "en-US", webVoice: "Ana", defaultEdgeVoice: "en-US-AnaNeural",
|
| 85 |
+
voice: "Unpredictable. Each response is a completely different personality.",
|
| 86 |
elevenVoices: ["H10ItvDnkRN5ysrvzT9J","Nggzl2QAXh3OijoXD116","cgSgspJ2msm6clMCkdW9"],
|
| 87 |
},
|
| 88 |
};
|
|
|
|
| 96 |
// βββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 97 |
let robot = null;
|
| 98 |
let detachVideo = null;
|
| 99 |
+
let webcamStream = null;
|
| 100 |
+
let isSimulation = false;
|
| 101 |
let isStreaming = false;
|
| 102 |
let isMonitoring = false;
|
| 103 |
let isAnimating = false;
|
| 104 |
+
let idleLoopActive = false;
|
| 105 |
let robotInitialized = false;
|
| 106 |
|
| 107 |
let rvModel = null;
|
|
|
|
| 109 |
let rvAnimId = null;
|
| 110 |
let isProcessing = false;
|
| 111 |
|
| 112 |
+
// Persistent canvases β reused every frame (like demo.js)
|
| 113 |
+
const rvOffscreen = document.createElement('canvas');
|
| 114 |
+
const rvOffscreenCtx = rvOffscreen.getContext('2d', { willReadFrequently: true });
|
| 115 |
+
const rvSmallCanvas = document.createElement('canvas');
|
| 116 |
+
const rvSmallCtx = rvSmallCanvas.getContext('2d', { willReadFrequently: true });
|
| 117 |
+
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
| 118 |
+
const RV_MODEL_NAME = isMobile ? 'yolo26n-ONNX' : 'yolo26m-ONNX';
|
| 119 |
+
|
| 120 |
let offenseCount = 0;
|
| 121 |
let phoneVisible = false;
|
| 122 |
let consecutivePhone = 0;
|
|
|
|
| 129 |
let streakStart = null;
|
| 130 |
let longestStreak = 0;
|
| 131 |
let streakInterval = null;
|
| 132 |
+
let hasPreviousSession = false;
|
| 133 |
+
let frozenStreak = 0;
|
| 134 |
|
| 135 |
let selectedPersonality = 'pure_reachy';
|
| 136 |
let settings = { groqKey: '', elevenlabsKey: '', cooldown: 10, praiseEnabled: true };
|
| 137 |
let voiceOverrides = {};
|
| 138 |
+
const workingVoiceCache = {};
|
| 139 |
|
| 140 |
// βββ Utilities βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 141 |
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
|
|
| 146 |
if (!robot) return;
|
| 147 |
robot.setHeadPose(15, -5, 0);
|
| 148 |
robot.setAntennas(30, 15);
|
| 149 |
+
await sleep(300);
|
|
|
|
|
|
|
| 150 |
robot.setHeadPose(0, 0, 0);
|
| 151 |
+
robot.setAntennas(0, 0);
|
| 152 |
}
|
| 153 |
|
| 154 |
async function disappointedShake() {
|
|
|
|
| 199 |
return dramaticSigh;
|
| 200 |
}
|
| 201 |
|
| 202 |
+
const BREATH_INTERVAL_MS = 8000;
|
| 203 |
+
|
| 204 |
+
async function idleBreathing() {
|
| 205 |
+
idleLoopActive = true;
|
| 206 |
+
while (isStreaming && isMonitoring && !isAnimating && !phoneVisible && robot) {
|
| 207 |
+
// Wait 8 seconds between breaths, checking conditions every 50ms
|
| 208 |
+
for (let i = 0; i < BREATH_INTERVAL_MS / 50; i++) {
|
| 209 |
+
if (!isStreaming || !isMonitoring || isAnimating || phoneVisible) { idleLoopActive = false; return; }
|
| 210 |
+
await sleep(50);
|
| 211 |
+
}
|
| 212 |
+
if (!isStreaming || !isMonitoring || isAnimating || phoneVisible) break;
|
| 213 |
+
// One breath cycle: antennas up then down
|
| 214 |
+
robot.setAntennas(15, 15);
|
| 215 |
+
for (let i = 0; i < 16; i++) {
|
| 216 |
+
if (!isStreaming || !isMonitoring || isAnimating) { idleLoopActive = false; return; }
|
| 217 |
+
await sleep(50);
|
| 218 |
+
}
|
| 219 |
+
robot.setAntennas(5, 5);
|
| 220 |
+
for (let i = 0; i < 16; i++) {
|
| 221 |
+
if (!isStreaming || !isMonitoring || isAnimating) { idleLoopActive = false; return; }
|
| 222 |
+
await sleep(50);
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
idleLoopActive = false;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
function startIdleIfNeeded() {
|
| 229 |
+
if (isStreaming && isMonitoring && !isAnimating && !phoneVisible && !idleLoopActive && robot) idleBreathing();
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
// βββ Sound / LLM / TTS βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 233 |
async function playEmotion(name) {
|
| 234 |
if (!robot) return;
|
|
|
|
| 242 |
const fallback = pick(isShame ? p.prewrittenShame : p.prewrittenPraise);
|
| 243 |
if (!settings.groqKey || !data) return fallback;
|
| 244 |
try {
|
| 245 |
+
let systemMsg, userMsg;
|
| 246 |
+
if (isShame) {
|
| 247 |
+
const exampleLines = data.examples.map(e => `- ${e}`).join('\n');
|
| 248 |
+
const personalityPrompt = `${p.voice}\n\nTONE: ${data.tone}\nSTRUCTURE: ${data.structure}\n\nEXAMPLES:\n${exampleLines}\n\nAVOID: ${p.avoid || 'N/A'}`;
|
| 249 |
+
systemMsg = `TASK: Generate a NEGATIVE/SCOLDING response because someone just picked up their phone (BAD behavior).\n\n${personalityPrompt}\n\nRULES:\n- Maximum 8 words. Prefer 3-5 words.\n- Be CRITICAL/NEGATIVE about picking up the phone.\n- Match the personality's voice exactly.\n- No emoji. No hashtags.`;
|
| 250 |
+
const ctx = count === 1 ? 'First time today.' : count === 2 ? 'Second time.' : count === 3 ? 'Third time.' : count <= 5 ? `${count} times now.` : `${count} times today!`;
|
| 251 |
+
userMsg = `Phone pickup #${count} today. ${ctx}`;
|
| 252 |
+
} else {
|
| 253 |
+
const exampleLines = data.examples.map(e => `- ${e}`).join('\n');
|
| 254 |
+
const personalityPrompt = `TONE: ${data.tone}\n\nEXAMPLES:\n${exampleLines}`;
|
| 255 |
+
systemMsg = `TASK: Generate a POSITIVE/APPROVING response because someone just put their phone down (GOOD behavior).\n\n${personalityPrompt}\n\nRULES:\n- Maximum 5 words. Prefer 2-3 words.\n- Be POSITIVE/APPROVING about putting the phone down.\n- Match the personality's voice exactly.\n- No emoji.`;
|
| 256 |
+
userMsg = 'Phone down.';
|
| 257 |
+
}
|
| 258 |
const resp = await fetch('https://api.groq.com/openai/v1/chat/completions', {
|
| 259 |
method: 'POST',
|
| 260 |
headers: { 'Authorization': `Bearer ${settings.groqKey}`, 'Content-Type': 'application/json' },
|
| 261 |
+
body: JSON.stringify({
|
| 262 |
+
model: 'llama-3.1-8b-instant',
|
| 263 |
+
messages: [{ role: 'system', content: systemMsg }, { role: 'user', content: userMsg }],
|
| 264 |
+
max_tokens: isShame ? 20 : 15,
|
| 265 |
+
temperature: isShame ? 1.1 : 0.8
|
| 266 |
+
})
|
| 267 |
});
|
| 268 |
const json = await resp.json();
|
| 269 |
return json.choices?.[0]?.message?.content?.trim() || fallback;
|
| 270 |
} catch { return fallback; }
|
| 271 |
}
|
| 272 |
|
| 273 |
+
async function _playElevenAudio(text, voiceId) {
|
| 274 |
+
const resp = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
|
| 275 |
+
method: 'POST',
|
| 276 |
+
headers: { 'xi-api-key': settings.elevenlabsKey, 'Content-Type': 'application/json' },
|
| 277 |
+
body: JSON.stringify({ text, model_id: 'eleven_multilingual_v2', voice_settings: { stability: 0.5, similarity_boost: 0.75 } })
|
| 278 |
+
});
|
| 279 |
+
if (!resp.ok) throw new Error(`ElevenLabs ${resp.status}`);
|
| 280 |
+
const url = URL.createObjectURL(await resp.blob());
|
| 281 |
+
await new Promise((res, rej) => { const a = new Audio(url); a.onended = res; a.onerror = rej; a.play().catch(rej); });
|
| 282 |
+
URL.revokeObjectURL(url);
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
async function speakText(text, pKey) {
|
| 286 |
const p = PERSONALITIES[pKey];
|
| 287 |
+
const hasUserOverride = !!voiceOverrides[pKey]?.eleven;
|
| 288 |
+
const voiceIds = hasUserOverride ? [voiceOverrides[pKey].eleven] : (p.elevenVoices || []);
|
| 289 |
+
|
| 290 |
+
if (settings.elevenlabsKey && voiceIds.length > 0) {
|
| 291 |
+
// Try cached voice first (Python's working_voice_cache behaviour)
|
| 292 |
+
if (!hasUserOverride && workingVoiceCache[pKey]) {
|
| 293 |
+
try {
|
| 294 |
+
await _playElevenAudio(text, workingVoiceCache[pKey]);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
return;
|
| 296 |
+
} catch {
|
| 297 |
+
delete workingVoiceCache[pKey];
|
| 298 |
}
|
| 299 |
+
}
|
| 300 |
+
// Try each voice in order until one works, then cache it
|
| 301 |
+
for (const voiceId of voiceIds) {
|
| 302 |
+
try {
|
| 303 |
+
await _playElevenAudio(text, voiceId);
|
| 304 |
+
if (!hasUserOverride) workingVoiceCache[pKey] = voiceId;
|
| 305 |
+
return;
|
| 306 |
+
} catch { /* try next voice */ }
|
| 307 |
+
}
|
| 308 |
}
|
| 309 |
+
// Web Speech API fallback (replaces Edge TTS)
|
| 310 |
const utt = new SpeechSynthesisUtterance(text);
|
| 311 |
utt.lang = p.lang || 'en-US';
|
| 312 |
+
const voiceName = voiceOverrides[pKey]?.edge || p.webVoice;
|
| 313 |
+
if (voiceName) {
|
| 314 |
+
const voices = speechSynthesis.getVoices();
|
| 315 |
+
const v = voices.find(v => v.name.includes(voiceName));
|
| 316 |
+
if (v) utt.voice = v;
|
| 317 |
+
}
|
| 318 |
speechSynthesis.speak(utt);
|
| 319 |
}
|
| 320 |
|
|
|
|
| 324 |
isAnimating = true;
|
| 325 |
offenseCount++;
|
| 326 |
totalBusts++;
|
| 327 |
+
// Capture streak duration before breaking it (matches Python's exact-moment capture)
|
| 328 |
+
if (streakStart) {
|
| 329 |
+
const streakSecs = Math.floor((Date.now() - streakStart) / 1000);
|
| 330 |
+
if (streakSecs > longestStreak) longestStreak = streakSecs;
|
| 331 |
+
}
|
| 332 |
streakStart = null;
|
| 333 |
updateStats();
|
| 334 |
|
|
|
|
| 340 |
await Promise.all([anim(), playEmotion(emotion)]);
|
| 341 |
setResponseText(`π‘ *${emotion}*`);
|
| 342 |
} else {
|
| 343 |
+
// Fetch LLM text, start speech (fire-and-forget like Python's non-blocking play_sound),
|
| 344 |
+
// then await only the animation β isAnimating clears after animation, not after audio
|
| 345 |
+
const text = await getLLMResponse(pKey, true, offenseCount);
|
| 346 |
setResponseText(`π€ "${text}"`);
|
| 347 |
speakText(text, pKey);
|
| 348 |
+
await anim();
|
| 349 |
}
|
| 350 |
isAnimating = false;
|
| 351 |
+
startIdleIfNeeded();
|
| 352 |
}
|
| 353 |
|
| 354 |
async function handlePhonePutdown() {
|
| 355 |
if (!settings.praiseEnabled) return;
|
| 356 |
+
if (isAnimating) return; // Python processes events sequentially; skip if already animating
|
| 357 |
isAnimating = true;
|
| 358 |
streakStart = Date.now();
|
| 359 |
updateStats();
|
|
|
|
| 365 |
await Promise.all([approvingNod(), playEmotion(emotion)]);
|
| 366 |
setResponseText(`β¨ *${emotion}*`);
|
| 367 |
} else {
|
| 368 |
+
const text = await getLLMResponse(pKey, false, offenseCount);
|
| 369 |
setResponseText(`β
"${text}"`);
|
| 370 |
speakText(text, pKey);
|
| 371 |
+
await approvingNod();
|
| 372 |
}
|
| 373 |
isAnimating = false;
|
| 374 |
+
startIdleIfNeeded();
|
| 375 |
}
|
| 376 |
|
| 377 |
// βββ Detection loop (on WebRTC video) ββββββββββββββββββββββββββββββββββββββββ
|
| 378 |
async function initModel() {
|
| 379 |
+
const label = isMobile ? 'YOLO26n (mobile)' : 'YOLO26m';
|
| 380 |
+
setLoaderText(`Loading ${label} model...`);
|
| 381 |
showLoader(true);
|
| 382 |
+
rvModel = await AutoModel.from_pretrained(`onnx-community/${RV_MODEL_NAME}`, { device: 'webgpu', dtype: 'fp16' });
|
| 383 |
setLoaderText('Loading processor...');
|
| 384 |
+
rvProcessor = await AutoProcessor.from_pretrained(`onnx-community/${RV_MODEL_NAME}`);
|
| 385 |
showLoader(false);
|
| 386 |
}
|
| 387 |
|
|
|
|
| 390 |
const canvas = document.getElementById('rv-canvas');
|
| 391 |
if (!video.videoWidth) return;
|
| 392 |
|
| 393 |
+
// Sync display canvas to video size
|
| 394 |
canvas.width = video.videoWidth;
|
| 395 |
canvas.height = video.videoHeight;
|
| 396 |
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
| 397 |
|
| 398 |
+
// Sync offscreen canvas, then scale down to smallCanvas β reused each frame like demo.js
|
| 399 |
+
rvOffscreen.width = video.videoWidth;
|
| 400 |
+
rvOffscreen.height = video.videoHeight;
|
| 401 |
+
rvOffscreenCtx.drawImage(video, 0, 0);
|
| 402 |
+
|
| 403 |
const targetW = 256;
|
| 404 |
const targetH = Math.round((targetW / video.videoWidth) * video.videoHeight);
|
| 405 |
+
if (rvSmallCanvas.width !== targetW || rvSmallCanvas.height !== targetH) {
|
| 406 |
+
rvSmallCanvas.width = targetW;
|
| 407 |
+
rvSmallCanvas.height = targetH;
|
| 408 |
+
}
|
| 409 |
+
rvSmallCtx.drawImage(rvOffscreen, 0, 0, targetW, targetH);
|
| 410 |
|
| 411 |
+
const inputs = await rvProcessor(RawImage.fromCanvas(rvSmallCanvas));
|
| 412 |
const output = await rvModel(inputs);
|
| 413 |
const scores = output.logits.sigmoid().data;
|
| 414 |
const boxes = output.pred_boxes.data;
|
| 415 |
+
const confThreshold = lastPhoneBox ? 0.2 : 0.5;
|
| 416 |
|
| 417 |
let best = null, bestScore = 0;
|
| 418 |
for (let i = 0; i < 300; i++) {
|
|
|
|
| 602 |
document.getElementById('rv-voice-edge').value = override.edge || '';
|
| 603 |
document.getElementById('rv-voice-eleven-hint').textContent =
|
| 604 |
p.elevenVoices?.[0] ? `Default: ${p.elevenVoices[0]}` : '';
|
| 605 |
+
document.getElementById('rv-voice-edge-hint').textContent =
|
| 606 |
+
p.defaultEdgeVoice ? `Default: ${p.defaultEdgeVoice} (partial name also works, e.g. "${p.webVoice}")` : 'Leave empty for browser default';
|
| 607 |
document.getElementById('rv-voices-modal').style.display = '';
|
| 608 |
}
|
| 609 |
|
|
|
|
| 630 |
const mode = n.includes('sim') || n.includes('mockup') ? 'π₯ Simulation'
|
| 631 |
: n.includes('wireless') || n.includes('wifi') ? 'π‘ Wireless'
|
| 632 |
: 'π Lite';
|
| 633 |
+
const sim = n.includes('sim') || n.includes('mockup');
|
| 634 |
const btn = document.createElement('button');
|
| 635 |
btn.className = 'rv-robot-btn';
|
| 636 |
btn.innerHTML = `π€ <strong>${name}</strong><span class="rv-robot-mode">${mode}</span>`;
|
|
|
|
| 638 |
list.querySelectorAll('.rv-robot-btn').forEach(b => b.classList.remove('selected'));
|
| 639 |
btn.classList.add('selected');
|
| 640 |
selectedRobotId = r.id;
|
| 641 |
+
isSimulation = sim;
|
| 642 |
connectBtn.style.display = '';
|
| 643 |
connectBtn.onclick = () => startSession(selectedRobotId);
|
| 644 |
});
|
|
|
|
| 653 |
if (placeholder) placeholder.style.display = 'none';
|
| 654 |
populatePersonalities();
|
| 655 |
startStreakTimer();
|
| 656 |
+
startIdleIfNeeded();
|
| 657 |
if (!rvModel) await initModel();
|
| 658 |
});
|
| 659 |
|
|
|
|
| 661 |
isStreaming = false;
|
| 662 |
stopMonitoring();
|
| 663 |
if (detachVideo) { detachVideo(); detachVideo = null; }
|
| 664 |
+
if (webcamStream) { webcamStream.getTracks().forEach(t => t.stop()); webcamStream = null; }
|
| 665 |
+
const videoEl = document.getElementById('rv-video');
|
| 666 |
+
if (videoEl) videoEl.srcObject = null;
|
| 667 |
const placeholder = document.getElementById('rv-video-placeholder');
|
| 668 |
if (placeholder) placeholder.style.display = '';
|
| 669 |
showRVState('rv-picker');
|
|
|
|
| 680 |
if (!robot || robot.state !== 'connected') return;
|
| 681 |
try {
|
| 682 |
const videoEl = document.getElementById('rv-video');
|
| 683 |
+
if (isSimulation) {
|
| 684 |
+
webcamStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
|
| 685 |
+
videoEl.srcObject = webcamStream;
|
| 686 |
+
} else {
|
| 687 |
+
detachVideo = robot.attachVideo(videoEl);
|
| 688 |
+
}
|
| 689 |
await robot.startSession(robotId);
|
| 690 |
} catch (e) {
|
| 691 |
console.error('startSession failed:', e);
|
| 692 |
if (detachVideo) { detachVideo(); detachVideo = null; }
|
| 693 |
+
if (webcamStream) { webcamStream.getTracks().forEach(t => t.stop()); webcamStream = null; }
|
| 694 |
}
|
| 695 |
}
|
| 696 |
|
|
|
|
| 698 |
isMonitoring = false;
|
| 699 |
if (rvAnimId) { cancelAnimationFrame(rvAnimId); rvAnimId = null; }
|
| 700 |
clearInterval(streakInterval);
|
| 701 |
+
|
| 702 |
+
// Freeze current streak so Continue Monitoring can restore it
|
| 703 |
+
frozenStreak = streakStart ? Math.floor((Date.now() - streakStart) / 1000) : 0;
|
| 704 |
+
hasPreviousSession = (totalBusts > 0 || offenseCount > 0 || frozenStreak > 0);
|
| 705 |
+
|
| 706 |
const btn = document.getElementById('rv-toggle-btn');
|
| 707 |
+
if (btn) {
|
| 708 |
+
const label = hasPreviousSession ? 'βΆοΈ Continue Monitoring' : 'βΆοΈ Start Monitoring';
|
| 709 |
+
btn.innerHTML = label;
|
| 710 |
+
btn.classList.replace('btn-danger', 'btn-primary');
|
| 711 |
+
}
|
| 712 |
const dot = document.getElementById('rv-detect-dot');
|
| 713 |
const txt = document.getElementById('rv-detect-text');
|
| 714 |
if (dot) dot.className = 'status-dot';
|
| 715 |
if (txt) txt.textContent = 'Monitoring off';
|
| 716 |
+
|
| 717 |
+
const resetSection = document.getElementById('rv-reset-section');
|
| 718 |
+
if (resetSection) resetSection.style.display = hasPreviousSession ? '' : 'none';
|
| 719 |
}
|
| 720 |
|
| 721 |
// βββ Event listeners ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 742 |
if (isMonitoring) {
|
| 743 |
btn.innerHTML = 'π Stop Monitoring';
|
| 744 |
btn.classList.replace('btn-primary','btn-danger');
|
| 745 |
+
phoneVisible = false; consecutivePhone = 0; consecutiveNoPhone = 0;
|
| 746 |
lastPhoneBox = null; framesWithoutDetection = 0; lastReactionTime = 0;
|
| 747 |
+
|
| 748 |
+
if (hasPreviousSession) {
|
| 749 |
+
// Restore frozen state
|
| 750 |
+
streakStart = Date.now() - frozenStreak * 1000;
|
| 751 |
+
} else {
|
| 752 |
+
// Fresh start β reset everything
|
| 753 |
+
offenseCount = 0; totalBusts = 0; longestStreak = 0;
|
| 754 |
+
streakStart = Date.now();
|
| 755 |
+
}
|
| 756 |
+
hasPreviousSession = false; frozenStreak = 0;
|
| 757 |
+
updateStats();
|
| 758 |
document.getElementById('rv-detect-dot').className = 'status-dot monitoring';
|
| 759 |
document.getElementById('rv-detect-text').textContent = 'β
Phone-free';
|
| 760 |
+
document.getElementById('rv-reset-section').style.display = 'none';
|
| 761 |
detectionLoop();
|
| 762 |
+
startIdleIfNeeded();
|
| 763 |
} else {
|
| 764 |
stopMonitoring();
|
| 765 |
}
|
|
|
|
| 773 |
// Reset button
|
| 774 |
document.getElementById('rv-reset-btn').addEventListener('click', () => {
|
| 775 |
totalBusts = 0; offenseCount = 0; longestStreak = 0; streakStart = Date.now();
|
| 776 |
+
hasPreviousSession = false; frozenStreak = 0;
|
| 777 |
updateStats();
|
| 778 |
document.getElementById('rv-stat-streak').textContent = '0s';
|
| 779 |
document.getElementById('rv-stat-longest').textContent = '0s';
|
| 780 |
document.getElementById('rv-reset-section').style.display = 'none';
|
| 781 |
+
const btn = document.getElementById('rv-toggle-btn');
|
| 782 |
+
if (btn && !isMonitoring) btn.innerHTML = 'βΆοΈ Start Monitoring';
|
| 783 |
setResponseText('Stats reset. Start monitoring to begin.');
|
| 784 |
});
|
| 785 |
|
| 786 |
// Disconnect
|
| 787 |
document.getElementById('rv-disconnect-btn').addEventListener('click', async () => {
|
| 788 |
stopMonitoring();
|
| 789 |
+
if (detachVideo) { detachVideo(); detachVideo = null; }
|
| 790 |
+
if (webcamStream) { webcamStream.getTracks().forEach(t => t.stop()); webcamStream = null; }
|
| 791 |
+
const videoEl = document.getElementById('rv-video');
|
| 792 |
+
if (videoEl) videoEl.srcObject = null;
|
| 793 |
if (robot) { await robot.stopSession?.(); await robot.disconnect(); robot = null; }
|
| 794 |
isStreaming = false;
|
| 795 |
showRVState('rv-signin');
|