feat(vida): jeito de gente e console com o porquê de cada decisão

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>
This commit is contained in:
julio
2026-10-11 03:37:07 -03:00
co-authored by Claude Opus 5.5
parent a99aa897c4
commit 2933407b9e
27 changed files with 1248 additions and 33 deletions
+18 -1
View File
@@ -1,5 +1,7 @@
'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) {
@@ -9,6 +11,13 @@ async function prepare(c) {
}
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});
@@ -25,9 +34,17 @@ self.onmessage = async ({data}) => {
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}))),
ms:performance.now()-began,timestamp:data.timestamp});
sorriso,ms:performance.now()-began,timestamp:data.timestamp});
} catch(e) { self.postMessage({tipo:'erro',erro:e.message || String(e)}); }
finally { if (bitmap) bitmap.close(); }
};