Console (console.py + tela Console no painel):
- cada parte registra o que fez e o motivo: modo vida (com necessidades
e humor), sentidos, celular (ordens e o que o rosto fez), IA local,
aprendizado e ideias do dia; últimas 400 linhas, ao vivo pelo SSE;
- tela com o "agora" (humor, necessidades, o que está fazendo, próxima
decisão, última decisão, jeito), filtros, detalhes, pausa e busca.
Jeito de gente:
- saudade: a festa cresce com o tempo fora (aceno, "Oi! Você voltou!",
"Senti sua falta!"); bom dia lembrando de ontem;
- pedidos com motivo ("Brinca comigo?", "Me faz um carinho?", mostrar a
figurinha nova), no máximo 1 a cada 20 min;
- larga atividade no meio quando fica chato; personalidade (curiosidade,
brincadeira, timidez) que pesa nos gestos e amadurece com o convívio;
- no rosto: acordar grogue, sonhar com algo do dia (balão no motor),
hesitar antes de gestos, errar na atividade e ficar sem graça, olhar
nos olhos e desviar, sorrir de volta (FaceLandmarker no rastreador do
painel) e reagir à luz (sensor de luz no app 0.10.0);
- ordem "dizer" com legenda e voz de robô.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
51 lines
2.7 KiB
JavaScript
51 lines
2.7 KiB
JavaScript
'use strict';
|
|
let face = null, hands = null, vision = null;
|
|
// expressão (sorriso) para o contágio emocional: modelo opcional, roda 1 quadro a cada 4
|
|
let expressao = null, semExpressao = false, quadro = 0, sorriso = null;
|
|
let lastTime = 0;
|
|
async function prepare(c) {
|
|
if (!vision) {
|
|
const module = await import('/vendor/mediapipe/vision_bundle.mjs');
|
|
const files = await module.FilesetResolver.forVisionTasks('/vendor/mediapipe/wasm');
|
|
vision = {module,files};
|
|
}
|
|
if (c.rosto && !face) face = await vision.module.FaceDetector.createFromOptions(vision.files, {
|
|
baseOptions:{modelAssetPath:'/vendor/mediapipe/face.tflite',delegate:'CPU'},runningMode:'VIDEO',minDetectionConfidence:.6});
|
|
if (c.rosto && !expressao && !semExpressao) {
|
|
try {
|
|
expressao = await vision.module.FaceLandmarker.createFromOptions(vision.files, {
|
|
baseOptions:{modelAssetPath:'/vendor/mediapipe/expressao.task',delegate:'CPU'},runningMode:'VIDEO',numFaces:1,
|
|
outputFaceBlendshapes:true});
|
|
} catch (e) { semExpressao = true; } // sem o modelo: segue só com a posição do rosto
|
|
}
|
|
if (c.maos && !hands) hands = await vision.module.HandLandmarker.createFromOptions(vision.files, {
|
|
baseOptions:{modelAssetPath:'/vendor/mediapipe/hands.task',delegate:'CPU'},runningMode:'VIDEO',numHands:2,
|
|
minHandDetectionConfidence:.6,minHandPresenceConfidence:.6,minTrackingConfidence:.5});
|
|
}
|
|
self.onmessage = async ({data}) => {
|
|
const bitmap = data.bitmap;
|
|
try {
|
|
await prepare(data.config);
|
|
if (!bitmap) { self.postMessage({tipo:'pronto'}); return; }
|
|
const began = performance.now();
|
|
const stamp = Math.max(data.timestamp,lastTime+1); lastTime=stamp;
|
|
const faces = data.config.rosto ? face.detectForVideo(bitmap,stamp).detections.map(d=>({
|
|
x:d.boundingBox.originX/bitmap.width,y:d.boundingBox.originY/bitmap.height,
|
|
w:d.boundingBox.width/bitmap.width,h:d.boundingBox.height/bitmap.height,
|
|
score:d.categories[0]?.score || 0,pontos:d.keypoints.map(p=>({x:p.x,y:p.y}))
|
|
})) : [];
|
|
if (!faces.length) sorriso = null;
|
|
else if (expressao && quadro++ % 4 === 0) {
|
|
const b = (expressao.detectForVideo(bitmap,stamp).faceBlendshapes || [])[0];
|
|
if (b) {
|
|
const s = {}; for (const c of b.categories) s[c.categoryName] = c.score;
|
|
sorriso = ((s.mouthSmileLeft || 0) + (s.mouthSmileRight || 0)) / 2;
|
|
}
|
|
}
|
|
const result = data.config.maos ? hands.detectForVideo(bitmap,stamp) : {landmarks:[]};
|
|
self.postMessage({tipo:'resultado',faces,hands:result.landmarks.map(h=>h.map(p=>({x:p.x,y:p.y}))),
|
|
sorriso,ms:performance.now()-began,timestamp:data.timestamp});
|
|
} catch(e) { self.postMessage({tipo:'erro',erro:e.message || String(e)}); }
|
|
finally { if (bitmap) bitmap.close(); }
|
|
};
|