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
+2 -2
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.carinha.rosto" package="br.carinha.rosto"
android:versionCode="13" android:versionCode="14"
android:versionName="0.9.1"> android:versionName="0.10.0">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
+118 -5
View File
@@ -595,6 +595,10 @@ function atividade(nome, segundos, receita) {
if (!window.MotorRosto || !window.MotorRosto.atividade) return; if (!window.MotorRosto || !window.MotorRosto.atividade) return;
if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; } if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; }
if (r.dormindo) { r.dormindo = false; animar("surpreso"); } if (r.dormindo) { r.dormindo = false; animar("surpreso"); }
lembrarDoDia(nome, receita);
const agora = performance.now() / 1000, dur = Number(segundos) || 0;
HUMANO.oops = dur > 20 && Math.random() < 0.35 ? { ini: agora + dur * (0.3 + 0.4 * Math.random()) } : null;
if (HUMANO.oops) HUMANO.oops.fim = HUMANO.oops.ini + 2.2;
if (window.MotorRosto.atividade(nome, segundos, receita) && nome === "cantar") tocarSom("musica"); if (window.MotorRosto.atividade(nome, segundos, receita) && nome === "cantar") tocarSom("musica");
} }
@@ -618,6 +622,91 @@ function tocarExpressaoCriada(texto) {
if (rc.maos && rc.maos !== "nenhuma" && window.MotorRosto && window.MotorRosto.maos) window.MotorRosto.maos(rc.maos); if (rc.maos && rc.maos !== "nenhuma" && window.MotorRosto && window.MotorRosto.maos) window.MotorRosto.maos(rc.maos);
} }
/* ------------------------------------------------------------- jeito de gente
Acordar grogue, sonhar dormindo, hesitar antes de agir, errar na atividade e ficar sem graça,
olhar nos olhos e desviar, sorrir de volta para quem sorri e reagir à luz do ambiente. */
const HUMANO = {
dormiaAntes: false, dormiuEm: 0, grogueIni: 0, grogueAte: 0, bocejou: true,
sonhoProx: 0, sonhoAte: 0, sonhos: ['estrela', 'bola', 'nota', 'coracao', 'livro', 'flor'],
oops: null, // {ini, fim}: um tropeço no meio da atividade
evita: { ate: 0, prox: 0, dx: 0, dy: 0 }, // desvio do olhar durante o contato visual
sorriso: { desde: 0, ultimo: 0 },
luz: { lux: null, media: null, sonolentoAte: 0, ofuscadoAte: 0, mudou: 0 },
};
const OBJETO_DA_ATIVIDADE = { bola: 'bola', ler: 'livro', cafe: 'xicara', cantar: 'nota', violao: 'nota', pintar: 'pincel',
regar: 'flor', telescopio: 'estrela', malhar: 'halter', bolhas: 'bolha', videogame: 'controle', dancar: 'estrela',
meditar: 'coracao', cozinhar: 'chama', computador: 'controle', pescar: 'bolha', pipa: 'estrela', skate: 'estrela',
corda: 'estrela' };
function lembrarDoDia(nome, receita) { // o que ele fez hoje vira assunto dos sonhos
const obj = receita && Array.isArray(receita.objetos) && receita.objetos[0] ? receita.objetos[0].tipo : OBJETO_DA_ATIVIDADE[nome];
if (obj) HUMANO.sonhos = [obj].concat(HUMANO.sonhos.filter((o) => o !== obj)).slice(0, 8);
}
function falarFrase(texto, segundos) { // "dizer ..." do modo vida: legenda + voz de robô
if (window.carinhaSemServidor || !texto || r.dormindo) return;
if (window.MotorRosto) window.MotorRosto.faixa('legenda', { texto, segundos: segundos || Math.max(4, Math.min(14, texto.length / 9)) });
falar(texto);
}
function hesitar(fn, gesto) { // às vezes pensa um instante antes de agir
if (['surpreso', 'piscar', 'piscar2'].indexOf(gesto) >= 0 || Math.random() > 0.3) return fn();
setTimeout(fn, 150 + Math.random() * 450);
}
function humanoTick(t) {
// acordou depois de dormir um tempo: fica grogue (olhos pesados, lento) e boceja
if (r.dormindo && !HUMANO.dormiaAntes) HUMANO.dormiuEm = t;
if (!r.dormindo && HUMANO.dormiaAntes && t - HUMANO.dormiuEm > 180) {
HUMANO.grogueIni = t; HUMANO.grogueAte = t + 30 + Math.random() * 20; HUMANO.bocejou = false;
}
HUMANO.dormiaAntes = r.dormindo;
if (!HUMANO.bocejou && t - HUMANO.grogueIni > 1.6 && !r.dormindo && !r.kf) { HUMANO.bocejou = true; animar('bocejar'); }
// dormindo (com a tela acesa): de vez em quando sonha com algo do dia
if (r.dormindo && !SONO.apagada && !window.carinhaSemServidor) {
if (!HUMANO.sonhoProx) HUMANO.sonhoProx = t + 40 + Math.random() * 60;
if (t >= HUMANO.sonhoProx) {
const dur = 6 + Math.random() * 4;
HUMANO.sonhoAte = t + dur; HUMANO.sonhoProx = t + dur + 45 + Math.random() * 90;
const obj = HUMANO.sonhos[Math.floor(Math.random() * Math.min(4, HUMANO.sonhos.length))];
if (window.MotorRosto && window.MotorRosto.sonho) window.MotorRosto.sonho(obj, dur);
}
} else HUMANO.sonhoProx = 0;
}
function grogue(t) { // 1 = acabou de acordar, 0 = esperto
return t < HUMANO.grogueAte ? Math.min(1, (HUMANO.grogueAte - t) / (HUMANO.grogueAte - HUMANO.grogueIni)) : 0;
}
function piscarHumano(t) { animar(grogue(t) > 0.2 || t < HUMANO.luz.sonolentoAte ? 'piscar2' : 'piscar'); }
function desvioDoOlhar(t) { // olha nos olhos 2 a 5 s, desvia um instante e volta
const ev = HUMANO.evita;
if (t >= ev.prox) {
const dura = Math.random() < 0.7 ? 0.6 + Math.random() * 0.7 : 0;
ev.ate = t + dura; ev.prox = ev.ate + 2 + Math.random() * 3;
ev.dx = (Math.random() < 0.5 ? -1 : 1) * (8 + Math.random() * 8); ev.dy = 3 + Math.random() * 5;
}
return t < ev.ate ? ev : null;
}
function contagio(t, agora) { // sorriu para ele (rastreamento do painel): sorri de volta
const S = HUMANO.sorriso;
if (t.tipo !== 'rosto' || typeof t.sorriso !== 'number' || r.dormindo || ATIV) { S.desde = 0; return; }
if (t.sorriso > 0.55) { if (!S.desde) S.desde = agora; } else S.desde = 0;
if (S.desde && agora - S.desde > 0.6 && agora - S.ultimo > 20 && !r.kf) {
S.ultimo = agora; S.desde = 0;
animar(t.sorriso > 0.8 ? 'rir' : 'feliz');
}
}
function luzAmbiente(lux) { // sensor de luz do celular (lux)
lux = Number(lux);
if (!Number.isFinite(lux) || lux < 0) return;
const L = HUMANO.luz, t = performance.now() / 1000, antes = L.media;
L.lux = lux;
L.media = antes === null ? lux : antes + (lux - antes) * 0.15;
if (antes === null || t - L.mudou < 8) return;
if (lux < 3 && antes > 25) { // apagaram a luz: fica com sono
L.mudou = t; L.sonolentoAte = t + 60; L.media = lux;
if (!r.dormindo && !r.kf) animar('bocejar');
} else if (lux > 80 && antes < 8) { // acenderam a luz de repente: aperta os olhos
L.mudou = t; L.ofuscadoAte = t + 1.8; L.media = lux;
if (r.dormindo && !sonoRespeitado() && PERFIL !== PERFIS.calmo) r.dormindo = false;
}
}
function repouso(t) { // pose calma: olha devagar, "respira" function repouso(t) { // pose calma: olha devagar, "respira"
if (r.dormindo) { if (r.dormindo) {
r.forma = "dormir"; r.forma2 = "dormir"; r.boca = "sono"; r.forma = "dormir"; r.forma2 = "dormir"; r.boca = "sono";
@@ -626,6 +715,7 @@ function repouso(t) { // pose calma: olha devagar, "respira
r.esc = 1.0 + 0.03 * Math.sin(t * 1.4); r.esc = 1.0 + 0.03 * Math.sin(t * 1.4);
r.dx = 0; r.dx = 0;
r.dy = 3.0 * Math.sin(t * 1.4); r.dy = 3.0 * Math.sin(t * 1.4);
if (t < HUMANO.sonhoAte) { r.dx = 3.2 * Math.sin(t * 6.5); r.boca = 'sorriso'; } // sonhando
return; return;
} }
r.forma = "aberto"; r.forma2 = null; r.forma = "aberto"; r.forma2 = null;
@@ -638,7 +728,7 @@ function repouso(t) { // pose calma: olha devagar, "respira
const longe = Math.random() < 0.25; const longe = Math.random() < 0.25;
OLHAR.ax = (Math.random() * 2 - 1) * (longe ? 18 : 7); OLHAR.ax = (Math.random() * 2 - 1) * (longe ? 18 : 7);
OLHAR.ay = (Math.random() * 2 - 1) * (longe ? 8 : 3); OLHAR.ay = (Math.random() * 2 - 1) * (longe ? 8 : 3);
OLHAR.prox = t + 1.2 + Math.random() * 3; OLHAR.prox = t + (1.2 + Math.random() * 3) * (1 + 2 * grogue(t)); // grogue: olhar lento
} }
OLHAR.x += (OLHAR.ax - OLHAR.x) * 0.3; OLHAR.x += (OLHAR.ax - OLHAR.x) * 0.3;
OLHAR.y += (OLHAR.ay - OLHAR.y) * 0.3; OLHAR.y += (OLHAR.ay - OLHAR.y) * 0.3;
@@ -651,9 +741,23 @@ function repouso(t) { // pose calma: olha devagar, "respira
if (ATIV.forma) r.forma = ATIV.forma; if (ATIV.forma) r.forma = ATIV.forma;
if (ATIV.boca) r.boca = ATIV.boca; if (ATIV.boca) r.boca = ATIV.boca;
if (ATIV.ab) r.ab = ATIV.ab; if (ATIV.ab) r.ab = ATIV.ab;
const o = HUMANO.oops; // errou: susto e depois fica sem graça
if (o && t >= o.ini && t < o.fim) {
if (t - o.ini < 0.5) { r.forma = 'surpreso'; r.boca = 'aberta'; r.ab = 1.1; }
else { r.forma = 'feliz'; r.boca = 'sorriso'; r.cor = lerpCor(r.cor, ROSA, 0.5); r.dy += 6; }
}
} else if (ZANGA >= 0.7) { // alegria caindo: emburrado (a cor escolhida fica) } else if (ZANGA >= 0.7) { // alegria caindo: emburrado (a cor escolhida fica)
r.forma = "bravo"; r.forma = "bravo";
} }
const g = grogue(t);
if (g > 0) { // acabou de acordar: olhos pesados, cor apagada, olhar baixo
r.ab = Math.min(r.ab, 1 - 0.5 * g);
r.cor = lerpCor(r.cor, corEscura(0.7), 0.35 * g);
if (g > 0.5 && !ATIV) r.boca = 'sono';
r.dx *= 1 - 0.5 * g; r.dy = r.dy * (1 - 0.5 * g) + 3 * g;
}
if (t < HUMANO.luz.sonolentoAte) r.ab = Math.min(r.ab, 0.78);
if (t < HUMANO.luz.ofuscadoAte) { r.ab = Math.min(r.ab, 0.3); r.forma = 'feliz'; }
} }
function desenhar() { function desenhar() {
@@ -667,7 +771,9 @@ function desenhar() {
const seguindo = TRACK.alvo && tAgora - TRACK.visto < .35 && !r.dormindo; const seguindo = TRACK.alvo && tAgora - TRACK.visto < .35 && !r.dormindo;
const mirando = (MOV.ativo && (tAgora - MOV.ultimoMov) < 1.2) || !!ATIV; const mirando = (MOV.ativo && (tAgora - MOV.ultimoMov) < 1.2) || !!ATIV;
const mover = seguindo ? .55 : (r.kf !== null || r.dormindo) ? 0.25 : (mirando ? 0.25 : 0.0); const mover = seguindo ? .55 : (r.kf !== null || r.dormindo) ? 0.25 : (mirando ? 0.25 : 0.0);
const dx = seguindo ? MOV.olharX : r.dx, dy = seguindo ? MOV.olharY : r.dy; const ev = seguindo && TRACK.alvo.tipo === 'rosto' ? desvioDoOlhar(tAgora) : null;
const dx = seguindo ? (ev ? MOV.olharX * 0.3 + ev.dx : MOV.olharX) : r.dx;
const dy = seguindo ? (ev ? MOV.olharY * 0.3 + ev.dy : MOV.olharY) : r.dy;
const olhos = [[E1, ab, r.forma, +1], [E2, ab2, f2, -1]]; const olhos = [[E1, ab, r.forma, +1], [E2, ab2, f2, -1]];
const comMotor = ROSTO.motor && window.MotorRosto; const comMotor = ROSTO.motor && window.MotorRosto;
OLHOS.forEach((g) => { g.style.display = comMotor ? "none" : ""; }); OLHOS.forEach((g) => { g.style.display = comMotor ? "none" : ""; });
@@ -751,6 +857,7 @@ function contarAprendi(texto) {
function trata(ordem) { function trata(ordem) {
const bruta = (ordem || "").trim(); const bruta = (ordem || "").trim();
if (/^aprendi /i.test(bruta)) { contarAprendi(bruta.slice(8).trim()); return; } // sem minúsculas: é uma frase if (/^aprendi /i.test(bruta)) { contarAprendi(bruta.slice(8).trim()); return; } // sem minúsculas: é uma frase
if (/^dizer /i.test(bruta)) { falarFrase(bruta.slice(6).trim()); return; }
ordem = bruta.toLowerCase(); ordem = bruta.toLowerCase();
if (!ordem) return; if (!ordem) return;
if(ordem==='recarregar') { if(ordem==='recarregar') {
@@ -797,14 +904,16 @@ function trata(ordem) {
if (aviso) { aviso.classList.remove("cutucado"); void aviso.offsetWidth; aviso.classList.add("cutucado"); } if (aviso) { aviso.classList.remove("cutucado"); void aviso.offsetWidth; aviso.classList.add("cutucado"); }
return; return;
} }
r.dormindo = false; animar("surpreso"); const longo = r.dormindo && performance.now() / 1000 - HUMANO.dormiuEm > 180;
r.dormindo = false;
if (!longo) animar("surpreso"); // sono longo: acorda devagar, grogue (humanoTick)
} else if (ordem === "aleatorio") { } else if (ordem === "aleatorio") {
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]); animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
} else if (SEQS[ordem]) { } else if (SEQS[ordem]) {
if (window.MotorRosto) { if (window.MotorRosto) {
if (ordem === "musica") window.MotorRosto.faixa("equalizador", { segundos: 2.3 }); if (ordem === "musica") window.MotorRosto.faixa("equalizador", { segundos: 2.3 });
} }
animar(ordem); hesitar(() => animar(ordem), ordem);
} else if (ordem === "calma") { } else if (ordem === "calma") {
// E-Stop do painel: interrompe a animação e volta ao rosto neutro. // E-Stop do painel: interrompe a animação e volta ao rosto neutro.
r.fila = []; r.kf = null; r.boca = "neutra"; r.dormindo = false; r.fila = []; r.kf = null; r.boca = "neutra"; r.dormindo = false;
@@ -1121,6 +1230,7 @@ function rastrear(t) {
(r.dormindo && !configRecebida?.cameraSono)) return; (r.dormindo && !configRecebida?.cameraSono)) return;
const agora=performance.now()/1000; const agora=performance.now()/1000;
TRACK.alvo=t; TRACK.visto=agora; MOV.ativo=true;MOV.visto=agora;MOV.cx=t.x;MOV.cy=t.y; TRACK.alvo=t; TRACK.visto=agora; MOV.ativo=true;MOV.visto=agora;MOV.cx=t.x;MOV.cy=t.y;
contagio(t, agora);
MOV.olharX+=((t.x-.5)*2*MOV.olharMax-MOV.olharX)*.65; MOV.olharX+=((t.x-.5)*2*MOV.olharMax-MOV.olharX)*.65;
MOV.olharY+=((t.y-.5)*2*MOV.olharMax*.6-MOV.olharY)*.65; MOV.olharY+=((t.y-.5)*2*MOV.olharMax*.6-MOV.olharY)*.65;
if(t.atividade===true) { if(t.atividade===true) {
@@ -1306,6 +1416,7 @@ const AUDIO = {
}; };
if (window.eilik) window.eilik.audio = AUDIO; // so depois do const: sem TDZ if (window.eilik) window.eilik.audio = AUDIO; // so depois do const: sem TDZ
if (window.eilik) window.eilik.camera = CAM; // so depois do const: sem TDZ if (window.eilik) window.eilik.camera = CAM; // so depois do const: sem TDZ
if (window.eilik) window.eilik.luz = luzAmbiente; // sensor de luz do celular (o app chama)
let configRecebida = null; let configRecebida = null;
function configurarApp(c) { function configurarApp(c) {
@@ -1353,6 +1464,7 @@ async function sincronizarApp() {
bateria: window.carinhaBateria, bateria: window.carinhaBateria,
aparelho: window.carinhaAparelho, aparelho: window.carinhaAparelho,
video: window.carinhaVideo, video: window.carinhaVideo,
luz: HUMANO.luz.lux,
}) }); }) });
} }
} catch (e) { /* servidor ausente: animação continua */ } } catch (e) { /* servidor ausente: animação continua */ }
@@ -1448,6 +1560,7 @@ function laco(t) {
checarSilencio(t / 1000); checarSilencio(t / 1000);
atualizarFeicao(t / 1000); atualizarFeicao(t / 1000);
humanoTick(t / 1000);
ATIV = window.MotorRosto && window.MotorRosto.olharAtividade && !r.dormindo ? window.MotorRosto.olharAtividade(t / 1000) : null; ATIV = window.MotorRosto && window.MotorRosto.olharAtividade && !r.dormindo ? window.MotorRosto.olharAtividade(t / 1000) : null;
if (r.dormindo && window.MotorRosto && window.MotorRosto.atividadeAtual && window.MotorRosto.atividadeAtual()) atividade(null); if (r.dormindo && window.MotorRosto && window.MotorRosto.atividadeAtual && window.MotorRosto.atividadeAtual()) atividade(null);
@@ -1461,7 +1574,7 @@ function laco(t) {
if (r.dormindo) { if (r.dormindo) {
proxPiscar = proxGesto = t + 3600000; proxPiscar = proxGesto = t + 3600000;
} else if (t >= proxPiscar) { } else if (t >= proxPiscar) {
animar("piscar"); piscarHumano(t / 1000);
proxPiscar = t + 1600 + Math.random() * 3500; proxPiscar = t + 1600 + Math.random() * 3500;
} else if (AUTO && t >= proxGesto && !ATIV) { } else if (AUTO && t >= proxGesto && !ATIV) {
animar(gestoSolto()); animar(gestoSolto());
+30 -1
View File
@@ -287,11 +287,13 @@
olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i])); olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i]));
desenharMaos(ctx, o, olhos, tela.height); desenharMaos(ctx, o, olhos, tela.height);
desenharZzz(ctx, o, olhos); desenharZzz(ctx, o, olhos);
desenharSonho(ctx, o, olhos);
} else { } else {
cctx.clearRect(0, 0, camada.width, camada.height); cctx.clearRect(0, 0, camada.width, camada.height);
olhos.forEach((p, i) => olhoPlano(cctx, o, p, estilo, estado[i])); olhos.forEach((p, i) => olhoPlano(cctx, o, p, estilo, estado[i]));
desenharMaos(cctx, o, olhos, tela.height); desenharMaos(cctx, o, olhos, tela.height);
desenharZzz(cctx, o, olhos); desenharZzz(cctx, o, olhos);
desenharSonho(cctx, o, olhos);
ctx.save(); ctx.save();
if (estilo.brilho) { ctx.shadowColor = css(o.cor, 0.7); ctx.shadowBlur = Math.min(60, tela.height * 0.05); } if (estilo.brilho) { ctx.shadowColor = css(o.cor, 0.7); ctx.shadowBlur = Math.min(60, tela.height * 0.05); }
ctx.drawImage(camada, 0, 0); ctx.drawImage(camada, 0, 0);
@@ -421,6 +423,33 @@
} }
// ------------------------------------------------------------------ Zzz // ------------------------------------------------------------------ Zzz
// ----------------------------------------------------------- sonho
// Dormindo, às vezes aparece bem fraquinho um balão de pensamento com algo do dia (a bola, a estrela...).
const sonho = { obj: null, t0: 0, dur: 0 };
function definirSonho(obj, segundos) {
sonho.obj = obj || null; sonho.t0 = performance.now() / 1000; sonho.dur = segundos || 7;
}
function desenharSonho(c, o, olhos) {
if (!o.dormindo || !sonho.obj) return;
const k = (o.t - sonho.t0) / sonho.dur;
if (k < 0) return;
if (k >= 1) { sonho.obj = null; return; }
const surge = Math.min(1, k * sonho.dur / 1.2, (1 - k) * sonho.dur / 1.2); // aparece e some devagar
const e = olhos.slice().sort((a, b) => a.cx - b.cx)[0]; // olho da esquerda na tela
const R = e.ry * 0.62, bx = e.cx - e.ry * 0.1;
const by = Math.max(R * 1.2, e.cy - e.ry * 1.75) + Math.sin(o.t * 1.3) * e.ry * 0.05; // nunca sai da tela
c.save();
c.strokeStyle = css(o.cor); c.fillStyle = css(o.cor, 0.08); c.lineWidth = Math.max(1.5, R * 0.06);
c.globalAlpha = 0.45 * surge;
for (const [f, rr] of [[0.25, 0.09], [0.55, 0.15]]) { // bolinhas subindo até o balão
c.beginPath(); c.arc(e.cx + e.ry * 0.3 - f * e.ry * 0.3, e.cy - e.ry * 0.75 + (by + R - e.cy + e.ry * 0.75) * f, R * rr * 1.3, 0, Math.PI * 2); c.stroke();
}
c.beginPath(); c.arc(bx, by, R, 0, Math.PI * 2); c.fill(); c.stroke();
c.globalAlpha = 0.55 * surge;
desenharObjeto(c, sonho.obj, bx, by, R * 1.15, o.t, o.cor, Math.sin(o.t * 0.8) * 0.15, 0);
c.restore();
}
function desenharZzz(c, o, olhos, fator) { function desenharZzz(c, o, olhos, fator) {
fator = fator || 1; fator = fator || 1;
if (!o.dormindo) return; if (!o.dormindo) return;
@@ -1385,7 +1414,7 @@
return ((!base.lente && FORMATOS[formato]) || base).h / 2; return ((!base.lente && FORMATOS[formato]) || base).h / 2;
}, },
maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } }, maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } },
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS), faixa: definirFaixa, sonho: definirSonho, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS),
/** Atividades (momentos dele): iniciar/parar, para onde olhar e a camada da cena. */ /** Atividades (momentos dele): iniciar/parar, para onde olhar e a camada da cena. */
atividade: (nome, segundos, receita) => (nome ? iniciarAtividade(nome, segundos, receita) : (pararAtividade(), false)), atividade: (nome, segundos, receita) => (nome ? iniciarAtividade(nome, segundos, receita) : (pararAtividade(), false)),
atividadeAtual: () => (ativ.nome && !ativ.fim ? ativ.nome : null), atividadeAtual: () => (ativ.nome && !ativ.fim ? ativ.nome : null),
@@ -0,0 +1,44 @@
package br.carinha.rosto;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
/**
* Sensor de luz do celular (lux). A página decide o que fazer (apagaram a luz: sono; acenderam de
* repente: aperta os olhos). Manda no máximo a cada INTERVALO e só quando muda de verdade.
*/
final class Luz implements SensorEventListener {
interface Ouvinte { void luz(float lux); }
private static final long INTERVALO = 700; // ms
private final SensorManager gerente;
private final Sensor sensor;
private final Ouvinte ouvinte;
private long enviadoEm;
private float enviado = -1;
Luz(Context c, Ouvinte ouvinte) {
gerente = (SensorManager) c.getSystemService(Context.SENSOR_SERVICE);
sensor = gerente == null ? null : gerente.getDefaultSensor(Sensor.TYPE_LIGHT);
this.ouvinte = ouvinte;
}
void ligar() { if (sensor != null) gerente.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL); }
void desligar() { if (sensor != null) gerente.unregisterListener(this); }
@Override public void onSensorChanged(SensorEvent e) {
float lux = e.values[0];
long agora = System.currentTimeMillis();
boolean mudou = enviado < 0 || Math.abs(lux - enviado) > Math.max(2f, enviado * 0.15f);
if (!mudou || agora - enviadoEm < INTERVALO) return;
enviado = lux;
enviadoEm = agora;
ouvinte.luz(lux);
}
@Override public void onAccuracyChanged(Sensor s, int precisao) { }
}
@@ -109,6 +109,7 @@ public class MainActivity extends Activity {
private String token = ""; // CARINHA_TOKEN do servidor; vazio = servidor aberto private String token = ""; // CARINHA_TOKEN do servidor; vazio = servidor aberto
private final CerebroLocal cerebroLocal = new CerebroLocal(); private final CerebroLocal cerebroLocal = new CerebroLocal();
private Descoberta descoberta; // acha a placa na rede quando o endereço salvo não responde private Descoberta descoberta; // acha a placa na rede quando o endereço salvo não responde
private Luz luz; // sensor de luz: a página reage a apagar/acender a luz
private boolean jaCaiuParaLocal = false; private boolean jaCaiuParaLocal = false;
private boolean jaRecarregou = false; private boolean jaRecarregou = false;
private boolean usandoLocal = false; // servidor caiu: ficamos na copia do APK private boolean usandoLocal = false; // servidor caiu: ficamos na copia do APK
@@ -193,6 +194,11 @@ public class MainActivity extends Activity {
cerebroLocal.chave = token; cerebroLocal.chave = token;
cerebroLocal.iniciar(this, servidor); cerebroLocal.iniciar(this, servidor);
descoberta = new Descoberta(this); descoberta = new Descoberta(this);
luz = new Luz(this, new Luz.Ouvinte() {
@Override public void luz(final float lux) {
if (emPrimeiroPlano) web.evaluateJavascript("window.eilik&&window.eilik.luz&&window.eilik.luz(" + fmt(lux) + ")", null);
}
});
remoto = new ControleRemoto(new Runnable() { remoto = new ControleRemoto(new Runnable() {
@Override public void run() { @Override public void run() {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@@ -867,6 +873,7 @@ public class MainActivity extends Activity {
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
emPrimeiroPlano = true; emPrimeiroPlano = true;
luz.ligar();
ultimoUsoTela = System.currentTimeMillis(); ultimoUsoTela = System.currentTimeMillis();
remoto.servidor(servidor); remoto.servidor(servidor);
remoto.iniciar(); remoto.iniciar();
@@ -890,6 +897,7 @@ public class MainActivity extends Activity {
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
emPrimeiroPlano = false; emPrimeiroPlano = false;
luz.desligar();
remoto.parar(); remoto.parar();
rtcWeb.evaluateJavascript("window.rtcParar&&window.rtcParar()", null); rtcWeb.evaluateJavascript("window.rtcParar&&window.rtcParar()", null);
rtcAtivo = false; rtcAtivo = false;
+3
View File
@@ -135,6 +135,7 @@ class Aprendizado:
self.pode_ia = lambda provedor: False self.pode_ia = lambda provedor: False
self.decisoes = [] # [{horario, tipo, texto, motivo}] mais nova primeiro self.decisoes = [] # [{horario, tipo, texto, motivo}] mais nova primeiro
self.sono_avisado = 0.0 self.sono_avisado = 0.0
self.console = lambda origem, texto, motivo='', nivel='info', agora=None: None
# horário da madrugada (Ajustes do celular): o main liga; None = madrugada silenciosa desligada # horário da madrugada (Ajustes do celular): o main liga; None = madrugada silenciosa desligada
self.noite = lambda: None self.noite = lambda: None
self.mudar_noite = lambda ini, fim: None self.mudar_noite = lambda ini, fim: None
@@ -345,6 +346,7 @@ class Aprendizado:
with self.lock: with self.lock:
self.decisoes.insert(0, dict(horario=agora or time.time(), tipo=tipo, texto=texto, motivo=motivo)) self.decisoes.insert(0, dict(horario=agora or time.time(), tipo=tipo, texto=texto, motivo=motivo))
del self.decisoes[MAX_DECISOES:] del self.decisoes[MAX_DECISOES:]
self.console('aprendizado', texto, motivo, agora=agora)
def porque_chegada(self, agora): def porque_chegada(self, agora):
t = time.localtime(agora + 3600) t = time.localtime(agora + 3600)
@@ -548,6 +550,7 @@ class Aprendizado:
self.decisoes.insert(0, dict( self.decisoes.insert(0, dict(
horario=fim_do_dia, tipo='sono', texto='Mudou o horário de dormir para %dh às %dh' % (novo_ini, novo_fim), horario=fim_do_dia, tipo='sono', texto='Mudou o horário de dormir para %dh às %dh' % (novo_ini, novo_fim),
motivo='; '.join(motivos) + ' (o horário escolhido é %dh às %dh)' % tuple(self.sono_base))) motivo='; '.join(motivos) + ' (o horário escolhido é %dh às %dh)' % tuple(self.sono_base)))
self.console('aprendizado', self.decisoes[0]['texto'], self.decisoes[0]['motivo'])
return licoes return licoes
# ------------------------------------------------------------------ semana: IA e plano # ------------------------------------------------------------------ semana: IA e plano
+118 -5
View File
@@ -595,6 +595,10 @@ function atividade(nome, segundos, receita) {
if (!window.MotorRosto || !window.MotorRosto.atividade) return; if (!window.MotorRosto || !window.MotorRosto.atividade) return;
if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; } if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; }
if (r.dormindo) { r.dormindo = false; animar("surpreso"); } if (r.dormindo) { r.dormindo = false; animar("surpreso"); }
lembrarDoDia(nome, receita);
const agora = performance.now() / 1000, dur = Number(segundos) || 0;
HUMANO.oops = dur > 20 && Math.random() < 0.35 ? { ini: agora + dur * (0.3 + 0.4 * Math.random()) } : null;
if (HUMANO.oops) HUMANO.oops.fim = HUMANO.oops.ini + 2.2;
if (window.MotorRosto.atividade(nome, segundos, receita) && nome === "cantar") tocarSom("musica"); if (window.MotorRosto.atividade(nome, segundos, receita) && nome === "cantar") tocarSom("musica");
} }
@@ -618,6 +622,91 @@ function tocarExpressaoCriada(texto) {
if (rc.maos && rc.maos !== "nenhuma" && window.MotorRosto && window.MotorRosto.maos) window.MotorRosto.maos(rc.maos); if (rc.maos && rc.maos !== "nenhuma" && window.MotorRosto && window.MotorRosto.maos) window.MotorRosto.maos(rc.maos);
} }
/* ------------------------------------------------------------- jeito de gente
Acordar grogue, sonhar dormindo, hesitar antes de agir, errar na atividade e ficar sem graça,
olhar nos olhos e desviar, sorrir de volta para quem sorri e reagir à luz do ambiente. */
const HUMANO = {
dormiaAntes: false, dormiuEm: 0, grogueIni: 0, grogueAte: 0, bocejou: true,
sonhoProx: 0, sonhoAte: 0, sonhos: ['estrela', 'bola', 'nota', 'coracao', 'livro', 'flor'],
oops: null, // {ini, fim}: um tropeço no meio da atividade
evita: { ate: 0, prox: 0, dx: 0, dy: 0 }, // desvio do olhar durante o contato visual
sorriso: { desde: 0, ultimo: 0 },
luz: { lux: null, media: null, sonolentoAte: 0, ofuscadoAte: 0, mudou: 0 },
};
const OBJETO_DA_ATIVIDADE = { bola: 'bola', ler: 'livro', cafe: 'xicara', cantar: 'nota', violao: 'nota', pintar: 'pincel',
regar: 'flor', telescopio: 'estrela', malhar: 'halter', bolhas: 'bolha', videogame: 'controle', dancar: 'estrela',
meditar: 'coracao', cozinhar: 'chama', computador: 'controle', pescar: 'bolha', pipa: 'estrela', skate: 'estrela',
corda: 'estrela' };
function lembrarDoDia(nome, receita) { // o que ele fez hoje vira assunto dos sonhos
const obj = receita && Array.isArray(receita.objetos) && receita.objetos[0] ? receita.objetos[0].tipo : OBJETO_DA_ATIVIDADE[nome];
if (obj) HUMANO.sonhos = [obj].concat(HUMANO.sonhos.filter((o) => o !== obj)).slice(0, 8);
}
function falarFrase(texto, segundos) { // "dizer ..." do modo vida: legenda + voz de robô
if (window.carinhaSemServidor || !texto || r.dormindo) return;
if (window.MotorRosto) window.MotorRosto.faixa('legenda', { texto, segundos: segundos || Math.max(4, Math.min(14, texto.length / 9)) });
falar(texto);
}
function hesitar(fn, gesto) { // às vezes pensa um instante antes de agir
if (['surpreso', 'piscar', 'piscar2'].indexOf(gesto) >= 0 || Math.random() > 0.3) return fn();
setTimeout(fn, 150 + Math.random() * 450);
}
function humanoTick(t) {
// acordou depois de dormir um tempo: fica grogue (olhos pesados, lento) e boceja
if (r.dormindo && !HUMANO.dormiaAntes) HUMANO.dormiuEm = t;
if (!r.dormindo && HUMANO.dormiaAntes && t - HUMANO.dormiuEm > 180) {
HUMANO.grogueIni = t; HUMANO.grogueAte = t + 30 + Math.random() * 20; HUMANO.bocejou = false;
}
HUMANO.dormiaAntes = r.dormindo;
if (!HUMANO.bocejou && t - HUMANO.grogueIni > 1.6 && !r.dormindo && !r.kf) { HUMANO.bocejou = true; animar('bocejar'); }
// dormindo (com a tela acesa): de vez em quando sonha com algo do dia
if (r.dormindo && !SONO.apagada && !window.carinhaSemServidor) {
if (!HUMANO.sonhoProx) HUMANO.sonhoProx = t + 40 + Math.random() * 60;
if (t >= HUMANO.sonhoProx) {
const dur = 6 + Math.random() * 4;
HUMANO.sonhoAte = t + dur; HUMANO.sonhoProx = t + dur + 45 + Math.random() * 90;
const obj = HUMANO.sonhos[Math.floor(Math.random() * Math.min(4, HUMANO.sonhos.length))];
if (window.MotorRosto && window.MotorRosto.sonho) window.MotorRosto.sonho(obj, dur);
}
} else HUMANO.sonhoProx = 0;
}
function grogue(t) { // 1 = acabou de acordar, 0 = esperto
return t < HUMANO.grogueAte ? Math.min(1, (HUMANO.grogueAte - t) / (HUMANO.grogueAte - HUMANO.grogueIni)) : 0;
}
function piscarHumano(t) { animar(grogue(t) > 0.2 || t < HUMANO.luz.sonolentoAte ? 'piscar2' : 'piscar'); }
function desvioDoOlhar(t) { // olha nos olhos 2 a 5 s, desvia um instante e volta
const ev = HUMANO.evita;
if (t >= ev.prox) {
const dura = Math.random() < 0.7 ? 0.6 + Math.random() * 0.7 : 0;
ev.ate = t + dura; ev.prox = ev.ate + 2 + Math.random() * 3;
ev.dx = (Math.random() < 0.5 ? -1 : 1) * (8 + Math.random() * 8); ev.dy = 3 + Math.random() * 5;
}
return t < ev.ate ? ev : null;
}
function contagio(t, agora) { // sorriu para ele (rastreamento do painel): sorri de volta
const S = HUMANO.sorriso;
if (t.tipo !== 'rosto' || typeof t.sorriso !== 'number' || r.dormindo || ATIV) { S.desde = 0; return; }
if (t.sorriso > 0.55) { if (!S.desde) S.desde = agora; } else S.desde = 0;
if (S.desde && agora - S.desde > 0.6 && agora - S.ultimo > 20 && !r.kf) {
S.ultimo = agora; S.desde = 0;
animar(t.sorriso > 0.8 ? 'rir' : 'feliz');
}
}
function luzAmbiente(lux) { // sensor de luz do celular (lux)
lux = Number(lux);
if (!Number.isFinite(lux) || lux < 0) return;
const L = HUMANO.luz, t = performance.now() / 1000, antes = L.media;
L.lux = lux;
L.media = antes === null ? lux : antes + (lux - antes) * 0.15;
if (antes === null || t - L.mudou < 8) return;
if (lux < 3 && antes > 25) { // apagaram a luz: fica com sono
L.mudou = t; L.sonolentoAte = t + 60; L.media = lux;
if (!r.dormindo && !r.kf) animar('bocejar');
} else if (lux > 80 && antes < 8) { // acenderam a luz de repente: aperta os olhos
L.mudou = t; L.ofuscadoAte = t + 1.8; L.media = lux;
if (r.dormindo && !sonoRespeitado() && PERFIL !== PERFIS.calmo) r.dormindo = false;
}
}
function repouso(t) { // pose calma: olha devagar, "respira" function repouso(t) { // pose calma: olha devagar, "respira"
if (r.dormindo) { if (r.dormindo) {
r.forma = "dormir"; r.forma2 = "dormir"; r.boca = "sono"; r.forma = "dormir"; r.forma2 = "dormir"; r.boca = "sono";
@@ -626,6 +715,7 @@ function repouso(t) { // pose calma: olha devagar, "respira
r.esc = 1.0 + 0.03 * Math.sin(t * 1.4); r.esc = 1.0 + 0.03 * Math.sin(t * 1.4);
r.dx = 0; r.dx = 0;
r.dy = 3.0 * Math.sin(t * 1.4); r.dy = 3.0 * Math.sin(t * 1.4);
if (t < HUMANO.sonhoAte) { r.dx = 3.2 * Math.sin(t * 6.5); r.boca = 'sorriso'; } // sonhando
return; return;
} }
r.forma = "aberto"; r.forma2 = null; r.forma = "aberto"; r.forma2 = null;
@@ -638,7 +728,7 @@ function repouso(t) { // pose calma: olha devagar, "respira
const longe = Math.random() < 0.25; const longe = Math.random() < 0.25;
OLHAR.ax = (Math.random() * 2 - 1) * (longe ? 18 : 7); OLHAR.ax = (Math.random() * 2 - 1) * (longe ? 18 : 7);
OLHAR.ay = (Math.random() * 2 - 1) * (longe ? 8 : 3); OLHAR.ay = (Math.random() * 2 - 1) * (longe ? 8 : 3);
OLHAR.prox = t + 1.2 + Math.random() * 3; OLHAR.prox = t + (1.2 + Math.random() * 3) * (1 + 2 * grogue(t)); // grogue: olhar lento
} }
OLHAR.x += (OLHAR.ax - OLHAR.x) * 0.3; OLHAR.x += (OLHAR.ax - OLHAR.x) * 0.3;
OLHAR.y += (OLHAR.ay - OLHAR.y) * 0.3; OLHAR.y += (OLHAR.ay - OLHAR.y) * 0.3;
@@ -651,9 +741,23 @@ function repouso(t) { // pose calma: olha devagar, "respira
if (ATIV.forma) r.forma = ATIV.forma; if (ATIV.forma) r.forma = ATIV.forma;
if (ATIV.boca) r.boca = ATIV.boca; if (ATIV.boca) r.boca = ATIV.boca;
if (ATIV.ab) r.ab = ATIV.ab; if (ATIV.ab) r.ab = ATIV.ab;
const o = HUMANO.oops; // errou: susto e depois fica sem graça
if (o && t >= o.ini && t < o.fim) {
if (t - o.ini < 0.5) { r.forma = 'surpreso'; r.boca = 'aberta'; r.ab = 1.1; }
else { r.forma = 'feliz'; r.boca = 'sorriso'; r.cor = lerpCor(r.cor, ROSA, 0.5); r.dy += 6; }
}
} else if (ZANGA >= 0.7) { // alegria caindo: emburrado (a cor escolhida fica) } else if (ZANGA >= 0.7) { // alegria caindo: emburrado (a cor escolhida fica)
r.forma = "bravo"; r.forma = "bravo";
} }
const g = grogue(t);
if (g > 0) { // acabou de acordar: olhos pesados, cor apagada, olhar baixo
r.ab = Math.min(r.ab, 1 - 0.5 * g);
r.cor = lerpCor(r.cor, corEscura(0.7), 0.35 * g);
if (g > 0.5 && !ATIV) r.boca = 'sono';
r.dx *= 1 - 0.5 * g; r.dy = r.dy * (1 - 0.5 * g) + 3 * g;
}
if (t < HUMANO.luz.sonolentoAte) r.ab = Math.min(r.ab, 0.78);
if (t < HUMANO.luz.ofuscadoAte) { r.ab = Math.min(r.ab, 0.3); r.forma = 'feliz'; }
} }
function desenhar() { function desenhar() {
@@ -667,7 +771,9 @@ function desenhar() {
const seguindo = TRACK.alvo && tAgora - TRACK.visto < .35 && !r.dormindo; const seguindo = TRACK.alvo && tAgora - TRACK.visto < .35 && !r.dormindo;
const mirando = (MOV.ativo && (tAgora - MOV.ultimoMov) < 1.2) || !!ATIV; const mirando = (MOV.ativo && (tAgora - MOV.ultimoMov) < 1.2) || !!ATIV;
const mover = seguindo ? .55 : (r.kf !== null || r.dormindo) ? 0.25 : (mirando ? 0.25 : 0.0); const mover = seguindo ? .55 : (r.kf !== null || r.dormindo) ? 0.25 : (mirando ? 0.25 : 0.0);
const dx = seguindo ? MOV.olharX : r.dx, dy = seguindo ? MOV.olharY : r.dy; const ev = seguindo && TRACK.alvo.tipo === 'rosto' ? desvioDoOlhar(tAgora) : null;
const dx = seguindo ? (ev ? MOV.olharX * 0.3 + ev.dx : MOV.olharX) : r.dx;
const dy = seguindo ? (ev ? MOV.olharY * 0.3 + ev.dy : MOV.olharY) : r.dy;
const olhos = [[E1, ab, r.forma, +1], [E2, ab2, f2, -1]]; const olhos = [[E1, ab, r.forma, +1], [E2, ab2, f2, -1]];
const comMotor = ROSTO.motor && window.MotorRosto; const comMotor = ROSTO.motor && window.MotorRosto;
OLHOS.forEach((g) => { g.style.display = comMotor ? "none" : ""; }); OLHOS.forEach((g) => { g.style.display = comMotor ? "none" : ""; });
@@ -751,6 +857,7 @@ function contarAprendi(texto) {
function trata(ordem) { function trata(ordem) {
const bruta = (ordem || "").trim(); const bruta = (ordem || "").trim();
if (/^aprendi /i.test(bruta)) { contarAprendi(bruta.slice(8).trim()); return; } // sem minúsculas: é uma frase if (/^aprendi /i.test(bruta)) { contarAprendi(bruta.slice(8).trim()); return; } // sem minúsculas: é uma frase
if (/^dizer /i.test(bruta)) { falarFrase(bruta.slice(6).trim()); return; }
ordem = bruta.toLowerCase(); ordem = bruta.toLowerCase();
if (!ordem) return; if (!ordem) return;
if(ordem==='recarregar') { if(ordem==='recarregar') {
@@ -797,14 +904,16 @@ function trata(ordem) {
if (aviso) { aviso.classList.remove("cutucado"); void aviso.offsetWidth; aviso.classList.add("cutucado"); } if (aviso) { aviso.classList.remove("cutucado"); void aviso.offsetWidth; aviso.classList.add("cutucado"); }
return; return;
} }
r.dormindo = false; animar("surpreso"); const longo = r.dormindo && performance.now() / 1000 - HUMANO.dormiuEm > 180;
r.dormindo = false;
if (!longo) animar("surpreso"); // sono longo: acorda devagar, grogue (humanoTick)
} else if (ordem === "aleatorio") { } else if (ordem === "aleatorio") {
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]); animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
} else if (SEQS[ordem]) { } else if (SEQS[ordem]) {
if (window.MotorRosto) { if (window.MotorRosto) {
if (ordem === "musica") window.MotorRosto.faixa("equalizador", { segundos: 2.3 }); if (ordem === "musica") window.MotorRosto.faixa("equalizador", { segundos: 2.3 });
} }
animar(ordem); hesitar(() => animar(ordem), ordem);
} else if (ordem === "calma") { } else if (ordem === "calma") {
// E-Stop do painel: interrompe a animação e volta ao rosto neutro. // E-Stop do painel: interrompe a animação e volta ao rosto neutro.
r.fila = []; r.kf = null; r.boca = "neutra"; r.dormindo = false; r.fila = []; r.kf = null; r.boca = "neutra"; r.dormindo = false;
@@ -1121,6 +1230,7 @@ function rastrear(t) {
(r.dormindo && !configRecebida?.cameraSono)) return; (r.dormindo && !configRecebida?.cameraSono)) return;
const agora=performance.now()/1000; const agora=performance.now()/1000;
TRACK.alvo=t; TRACK.visto=agora; MOV.ativo=true;MOV.visto=agora;MOV.cx=t.x;MOV.cy=t.y; TRACK.alvo=t; TRACK.visto=agora; MOV.ativo=true;MOV.visto=agora;MOV.cx=t.x;MOV.cy=t.y;
contagio(t, agora);
MOV.olharX+=((t.x-.5)*2*MOV.olharMax-MOV.olharX)*.65; MOV.olharX+=((t.x-.5)*2*MOV.olharMax-MOV.olharX)*.65;
MOV.olharY+=((t.y-.5)*2*MOV.olharMax*.6-MOV.olharY)*.65; MOV.olharY+=((t.y-.5)*2*MOV.olharMax*.6-MOV.olharY)*.65;
if(t.atividade===true) { if(t.atividade===true) {
@@ -1306,6 +1416,7 @@ const AUDIO = {
}; };
if (window.eilik) window.eilik.audio = AUDIO; // so depois do const: sem TDZ if (window.eilik) window.eilik.audio = AUDIO; // so depois do const: sem TDZ
if (window.eilik) window.eilik.camera = CAM; // so depois do const: sem TDZ if (window.eilik) window.eilik.camera = CAM; // so depois do const: sem TDZ
if (window.eilik) window.eilik.luz = luzAmbiente; // sensor de luz do celular (o app chama)
let configRecebida = null; let configRecebida = null;
function configurarApp(c) { function configurarApp(c) {
@@ -1353,6 +1464,7 @@ async function sincronizarApp() {
bateria: window.carinhaBateria, bateria: window.carinhaBateria,
aparelho: window.carinhaAparelho, aparelho: window.carinhaAparelho,
video: window.carinhaVideo, video: window.carinhaVideo,
luz: HUMANO.luz.lux,
}) }); }) });
} }
} catch (e) { /* servidor ausente: animação continua */ } } catch (e) { /* servidor ausente: animação continua */ }
@@ -1448,6 +1560,7 @@ function laco(t) {
checarSilencio(t / 1000); checarSilencio(t / 1000);
atualizarFeicao(t / 1000); atualizarFeicao(t / 1000);
humanoTick(t / 1000);
ATIV = window.MotorRosto && window.MotorRosto.olharAtividade && !r.dormindo ? window.MotorRosto.olharAtividade(t / 1000) : null; ATIV = window.MotorRosto && window.MotorRosto.olharAtividade && !r.dormindo ? window.MotorRosto.olharAtividade(t / 1000) : null;
if (r.dormindo && window.MotorRosto && window.MotorRosto.atividadeAtual && window.MotorRosto.atividadeAtual()) atividade(null); if (r.dormindo && window.MotorRosto && window.MotorRosto.atividadeAtual && window.MotorRosto.atividadeAtual()) atividade(null);
@@ -1461,7 +1574,7 @@ function laco(t) {
if (r.dormindo) { if (r.dormindo) {
proxPiscar = proxGesto = t + 3600000; proxPiscar = proxGesto = t + 3600000;
} else if (t >= proxPiscar) { } else if (t >= proxPiscar) {
animar("piscar"); piscarHumano(t / 1000);
proxPiscar = t + 1600 + Math.random() * 3500; proxPiscar = t + 1600 + Math.random() * 3500;
} else if (AUTO && t >= proxGesto && !ATIV) { } else if (AUTO && t >= proxGesto && !ATIV) {
animar(gestoSolto()); animar(gestoSolto());
+3
View File
@@ -39,6 +39,7 @@ class Cerebro:
try:self.ativo=json.loads(self.path.read_text())['ativo'] is True try:self.ativo=json.loads(self.path.read_text())['ativo'] is True
except (OSError,ValueError,KeyError,TypeError):pass except (OSError,ValueError,KeyError,TypeError):pass
threading.Thread(target=self.loop,daemon=True).start() threading.Thread(target=self.loop,daemon=True).start()
console = staticmethod(lambda origem, texto, motivo='', nivel='info', agora=None: None) # o main troca pelo console do painel
def cabecalhos(self): def cabecalhos(self):
h={'Content-Type':'application/json'} h={'Content-Type':'application/json'}
if self.chave:h['Authorization']='Bearer '+self.chave if self.chave:h['Authorization']='Bearer '+self.chave
@@ -100,8 +101,10 @@ class Cerebro:
with self.controle.lock: with self.controle.lock:
if time.monotonic()-self.controle.status_at<5 and not self.controle.estado.get('dormindo') and not self.controle.estado.get('atividade'): if time.monotonic()-self.controle.status_at<5 and not self.controle.estado.get('dormindo') and not self.controle.estado.get('atividade'):
self.enviar(result['gesto']) self.enviar(result['gesto'])
self.console('ia','IA local escolheu: '+result['gesto'],'evento "%s", respondeu em %.1f s'%(event,result['segundos']))
except Exception as e: except Exception as e:
with self.lock:self.error=str(e)[:200] with self.lock:self.error=str(e)[:200]
self.console('ia','IA local não respondeu',str(e)[:200],'detalhe')
finally: finally:
self.last=time.monotonic() self.last=time.monotonic()
with self.lock:self.busy=False with self.lock:self.busy=False
+50
View File
@@ -0,0 +1,50 @@
"""Console: tudo o que o Carinha está fazendo e por quê, ao vivo.
Cada parte do servidor registra o que fez e o motivo (modo vida, sentidos, IA local, aprendizado,
ideias do dia, celular). As linhas ficam na memória (as últimas MAX_LINHAS) e vão para o painel pelo
SSE ('console'). Nível 'detalhe' é o que se repete muito (humor reenviado, disparo solto do sensor):
o painel esconde por padrão.
"""
import json
import threading
import time
from collections import deque
MAX_LINHAS = 400
ORIGENS = ('vida', 'sentidos', 'celular', 'ia', 'aprendizado', 'ideias', 'sistema')
class Console:
def __init__(self):
self.lock = threading.Lock()
self.linhas = deque(maxlen=MAX_LINHAS)
self.seq = 0
self.publicar = lambda tipo, dados: None
def registrar(self, origem, texto, motivo='', nivel='info', agora=None):
if origem not in ORIGENS:
origem = 'sistema'
with self.lock:
self.seq += 1
linha = dict(id=self.seq, horario=agora or time.time(), origem=origem, texto=str(texto)[:240],
motivo=str(motivo or '')[:300], nivel='detalhe' if nivel == 'detalhe' else 'info')
self.linhas.append(linha)
try:
self.publicar('console', linha)
except Exception:
pass
return linha
def status(self, desde=0):
with self.lock:
return dict(linhas=[l for l in self.linhas if l['id'] > desde], seq=self.seq, origens=list(ORIGENS))
def rotas(self, h, metodo):
rota = h.path.split('?', 1)[0]
if rota != '/console':
return False
if metodo != 'GET':
h.enviar(json.dumps(dict(erro='Método inválido')), codigo=400)
return True
h.enviar(json.dumps(self.status(), ensure_ascii=False))
return True
+19
View File
@@ -105,6 +105,8 @@ class ControlePC:
self.rtc.encerrar() self.rtc.encerrar()
return novo.copy() return novo.copy()
console = staticmethod(lambda origem, texto, motivo='', nivel='info', agora=None: None) # o main troca pelo console do painel
def resumo(self, host=''): def resumo(self, host=''):
"""Estado do celular para o painel (GET /app/status, /estado e evento 'telemetria').""" """Estado do celular para o painel (GET /app/status, /estado e evento 'telemetria')."""
with self.lock: with self.lock:
@@ -160,12 +162,14 @@ class ControlePC:
if not isinstance(dados, dict): if not isinstance(dados, dict):
raise ValueError('Estado deve ser objeto JSON') raise ValueError('Estado deve ser objeto JSON')
with self.lock: with self.lock:
antes = self.estado
self.estado = dados self.estado = dados
self.celular_ip = ip_informado(dados) or h.client_address[0] self.celular_ip = ip_informado(dados) or h.client_address[0]
if self.recarga["id"] and str(dados.get("tela_atualizada", "")) == self.recarga["id"]: if self.recarga["id"] and str(dados.get("tela_atualizada", "")) == self.recarga["id"]:
self.recarga["confirmada"] = True self.recarga["confirmada"] = True
self.status_at = time.monotonic() self.status_at = time.monotonic()
self.publicar('telemetria', self.resumo(h.headers.get("Host", "").split(":")[0])) self.publicar('telemetria', self.resumo(h.headers.get("Host", "").split(":")[0]))
self.mudancas(antes, dados)
enviar({'ok': True}) enviar({'ok': True})
else: else:
enviar(self.resumo(h.headers.get("Host", "").split(":")[0])) enviar(self.resumo(h.headers.get("Host", "").split(":")[0]))
@@ -176,6 +180,21 @@ class ControlePC:
enviar({'erro': str(e)}, 503) enviar({'erro': str(e)}, 503)
return True return True
def mudancas(self, antes, agora):
"""O que mudou no celular desde o último status: vai para o console do painel."""
if not antes:
self.console('celular', 'Celular conectado', 'rosto %s' % agora.get('rosto', '?'))
return
if bool(antes.get('dormindo')) != bool(agora.get('dormindo')):
self.console('celular', 'O rosto dormiu' if agora.get('dormindo') else 'O rosto acordou')
if antes.get('atividade') != agora.get('atividade'):
if agora.get('atividade'):
self.console('celular', 'Começou a atividade: %s' % agora['atividade'])
else:
self.console('celular', 'Terminou a atividade: %s' % antes['atividade'])
if bool(antes.get('tela_apagada')) != bool(agora.get('tela_apagada')):
self.console('celular', 'Apagou a tela (dormindo há um tempo)' if agora.get('tela_apagada') else 'Acendeu a tela')
def registrar_recusa(self, rota, motivo): def registrar_recusa(self, rota, motivo):
"""Pedido do celular recusado: aparece no journal (no máximo 1 linha por minuto).""" """Pedido do celular recusado: aparece no journal (no máximo 1 linha por minuto)."""
agora = time.monotonic() agora = time.monotonic()
+1 -1
View File
@@ -27,7 +27,7 @@ Set-Location $PSScriptRoot -ErrorAction Stop
# chaves, vendor/ (modelos já instalados na placa) e o fonte do painel (só web/dist vai). # chaves, vendor/ (modelos já instalados na placa) e o fonte do painel (só web/dist vai).
$arquivos = @( $arquivos = @(
'kiosk-server.py', 'comum.py', 'eventos.py', 'controle_pc.py', 'sinalizacao.py', 'servos.py', 'braco.py', 'kiosk-server.py', 'comum.py', 'eventos.py', 'controle_pc.py', 'sinalizacao.py', 'servos.py', 'braco.py',
'cerebro.py', 'conversa.py', 'gemini.py', 'treinamento.py', 'figurinhas.py', 'api_config.py', 'historico.py', 'vida.py', 'criacao.py', 'inspiracao.py', 'aprendizado.py', 'cerebro.py', 'conversa.py', 'gemini.py', 'treinamento.py', 'figurinhas.py', 'api_config.py', 'historico.py', 'vida.py', 'criacao.py', 'inspiracao.py', 'console.py', 'aprendizado.py',
'carinha.html', 'camera.html', 'reacoes-render.js', 'rastreador-worker.js', 'rosto-motor.js', 'voz-robo.js', 'version.json', 'carinha.html', 'camera.html', 'reacoes-render.js', 'rastreador-worker.js', 'rosto-motor.js', 'voz-robo.js', 'version.json',
'obter-rastreamento.py', 'android/app/assets/qualidade-auto.js' 'obter-rastreamento.py', 'android/app/assets/qualidade-auto.js'
) )
+4
View File
@@ -56,6 +56,7 @@ class Historico:
ativo=False) # ativo = pino em nível alto agora (o HC-SR505 segura ~8 s e rearma) ativo=False) # ativo = pino em nível alto agora (o HC-SR505 segura ~8 s e rearma)
self.processo_pir = None self.processo_pir = None
self.subidas_pir = [] # disparos ainda não confirmados (horários) self.subidas_pir = [] # disparos ainda não confirmados (horários)
self.console = lambda origem, texto, motivo='', nivel='info', agora=None: None
self.subiu_em = None # quando o pino subiu da última vez (ligado direto desde então) self.subiu_em = None # quando o pino subiu da última vez (ligado direto desde então)
with self.db() as db: with self.db() as db:
db.execute('CREATE TABLE IF NOT EXISTS eventos(id INTEGER PRIMARY KEY, horario REAL, tipo TEXT, evento TEXT, nivel REAL, foto TEXT)') db.execute('CREATE TABLE IF NOT EXISTS eventos(id INTEGER PRIMARY KEY, horario REAL, tipo TEXT, evento TEXT, nivel REAL, foto TEXT)')
@@ -163,6 +164,9 @@ class Historico:
confirmou = len(self.subidas_pir) >= quantos confirmou = len(self.subidas_pir) >= quantos
if confirmou: if confirmou:
self.confirmar_pir(agora) self.confirmar_pir(agora)
else:
self.console('sentidos', 'Disparo solto do sensor de presença', 'sensibilidade %s: ainda não confirmou (%d de %d)' % (
self.config['sensibilidade_pir'], len(self.subidas_pir), quantos), 'detalhe', agora)
def checar_pir(self, agora=None): def checar_pir(self, agora=None):
"""Ligado direto por bastante tempo = alguém se mexendo na frente (o sensor foi redisparando).""" """Ligado direto por bastante tempo = alguém se mexendo na frente (o sensor foi redisparando)."""
+4
View File
@@ -90,6 +90,7 @@ class Inspiracao:
self.madrugada = lambda agora: False self.madrugada = lambda agora: False
self.pode_gemini = lambda: False self.pode_gemini = lambda: False
self.baixar = _baixar # trocado nos testes self.baixar = _baixar # trocado nos testes
self.console = lambda origem, texto, motivo='', nivel='info', agora=None: None
self.ocupado = False self.ocupado = False
self.config = json.loads(json.dumps(PADRAO)) self.config = json.loads(json.dumps(PADRAO))
self.hoje = self._dia_vazio('') self.hoje = self._dia_vazio('')
@@ -326,11 +327,13 @@ class Inspiracao:
item = self.criacao.item(ident) item = self.criacao.item(ident)
if item: if item:
self.anotar('ideia', 'Teve uma ideia nova: %s %s (%s)' % (item['emoji'], item['nome'], item['motivo']), agora) self.anotar('ideia', 'Teve uma ideia nova: %s %s (%s)' % (item['emoji'], item['nome'], item['motivo']), agora)
self.console('ideias', 'Criou a atividade %s %s' % (item['emoji'], item['nome']), item['motivo'], agora=agora)
try: try:
self.criacao.avisar() # a grade do estúdio mostra as novidades self.criacao.avisar() # a grade do estúdio mostra as novidades
except Exception: except Exception:
pass pass
except Exception as e: except Exception as e:
self.console('ideias', 'Não conseguiu criar as ideias do dia', str(e)[:200], agora=agora)
with self.lock: with self.lock:
self.hoje['erro'] = str(e)[:200] self.hoje['erro'] = str(e)[:200]
if not manual: if not manual:
@@ -404,6 +407,7 @@ class Inspiracao:
chave = str(item['id']) chave = str(item['id'])
self.hoje['contadas'][chave] = self.hoje['contadas'].get(chave, 0) + 1 self.hoje['contadas'][chave] = self.hoje['contadas'].get(chave, 0) + 1
self.anotar('aprendi', 'Contou o que aprendeu: ' + item['aprendi'], agora) self.anotar('aprendi', 'Contou o que aprendeu: ' + item['aprendi'], agora)
self.console('ideias', 'Contou o que aprendeu', item['aprendi'], agora=agora)
self.salvar() self.salvar()
return dict(ok=True) return dict(ok=True)
+28 -3
View File
@@ -45,6 +45,7 @@ from vida import Vida
from criacao import Criacao from criacao import Criacao
from inspiracao import Inspiracao from inspiracao import Inspiracao
from aprendizado import Aprendizado from aprendizado import Aprendizado
from console import Console
TRAVA = threading.Lock() TRAVA = threading.Lock()
@@ -73,12 +74,13 @@ ARQUIVO_APARENCIA = "aparencia.json"
# Arquivos servidos como estão: o rosto do celular e o rastreamento do painel. # Arquivos servidos como estão: o rosto do celular e o rastreamento do painel.
ESTATICOS = {"/carinha.html", "/camera.html", "/reacoes-render.js", "/rastreador-worker.js", "/rosto-motor.js", "/voz-robo.js", ESTATICOS = {"/carinha.html", "/camera.html", "/reacoes-render.js", "/rastreador-worker.js", "/rosto-motor.js", "/voz-robo.js",
"/vendor/mediapipe/vision_bundle.mjs", "/vendor/mediapipe/face.tflite", "/vendor/mediapipe/hands.task", "/vendor/mediapipe/vision_bundle.mjs", "/vendor/mediapipe/face.tflite", "/vendor/mediapipe/hands.task",
"/vendor/mediapipe/expressao.task",
"/vendor/mediapipe/wasm/vision_wasm_internal.js", "/vendor/mediapipe/wasm/vision_wasm_internal.wasm", "/vendor/mediapipe/wasm/vision_wasm_internal.js", "/vendor/mediapipe/wasm/vision_wasm_internal.wasm",
"/vendor/mediapipe/wasm/vision_wasm_nosimd_internal.js", "/vendor/mediapipe/wasm/vision_wasm_nosimd_internal.wasm"} "/vendor/mediapipe/wasm/vision_wasm_nosimd_internal.js", "/vendor/mediapipe/wasm/vision_wasm_nosimd_internal.wasm"}
# Rotas que não exigem token: só o necessário para a página do rosto abrir e o app se achar. # Rotas que não exigem token: só o necessário para a página do rosto abrir e o app se achar.
PUBLICAS_GET = {"/app/health", "/faces", "/gestos", "/config", "/ia/modelo", "/app.apk"} PUBLICAS_GET = {"/app/health", "/faces", "/gestos", "/config", "/ia/modelo", "/app.apk"}
PREFIXOS_API = ("/eventos", "/estado", "/fila", "/rtc/", "/conversa/", "/treinamento/", "/historico/", "/vida/", "/aprendizado/", "/criacao/", PREFIXOS_API = ("/eventos", "/console", "/estado", "/fila", "/rtc/", "/conversa/", "/treinamento/", "/historico/", "/vida/", "/aprendizado/", "/criacao/",
"/app/", "/braco/", "/servos/", "/ia/", "/api/", "/gesto/", "/face/", "/diag/") "/app/", "/braco/", "/servos/", "/ia/", "/api/", "/gesto/", "/face/", "/diag/")
@@ -122,10 +124,26 @@ EVENTOS = Eventos()
ESTOP = {"ativo": False, "desde": None} ESTOP = {"ativo": False, "desde": None}
CONSOLE = Console()
# ordens que se repetem (o modo vida reenvia o humor a cada minuto): no console, só em "detalhes"
ORDENS_DETALHE = ("humor ", "ritmo ", "zanga ", "sono ")
def enfileirar(ordem): def enfileirar(ordem):
"""Caminho único de uma ordem para o rosto: fila do celular + evento para os painéis.""" """Caminho único de uma ordem para o rosto: fila do celular + evento para os painéis."""
FILA.adicionar(ordem) FILA.adicionar(ordem)
EVENTOS.publicar("expressao", {"ordem": ordem}) EVENTOS.publicar("expressao", {"ordem": ordem})
if ordem.startswith("dizer "):
CONSOLE.registrar("celular", "Falou: " + ordem[6:])
elif ordem.startswith(("atividade_criada ", "expressao_criada ")):
try:
nome = json.loads(ordem.split(" ", 1)[1]).get("nome", "?")
except ValueError:
nome = "?"
CONSOLE.registrar("celular", "Mandou ao rosto a criação: " + nome)
elif not ordem.startswith("aprendi "):
CONSOLE.registrar("celular", "Mandou ao rosto: " + ordem,
nivel="detalhe" if ordem.startswith(ORDENS_DETALHE) else "info")
def carregar_aparencia(): def carregar_aparencia():
@@ -264,7 +282,7 @@ class Handler(SimpleHTTPRequestHandler):
def modulos(self): def modulos(self):
s = self.server s = self.server
return [getattr(s, nome) for nome in ("api_config", "historico", "vida", "aprendizado", "inspiracao", "criacao", "treinamento", "conversa", "cerebro", return [getattr(s, nome) for nome in ("console", "api_config", "historico", "vida", "aprendizado", "inspiracao", "criacao", "treinamento", "conversa", "cerebro",
"braco", "servos", "controle") if hasattr(s, nome)] "braco", "servos", "controle") if hasattr(s, nome)]
def estado(self): def estado(self):
@@ -486,7 +504,8 @@ class Servidor(ThreadingHTTPServer):
def ligar_eventos(servidor): def ligar_eventos(servidor):
for nome in ("controle", "servos", "cerebro", "conversa", "treinamento", "historico", "vida", "criacao", "inspiracao", "aprendizado"): for nome in ("controle", "servos", "cerebro", "conversa", "treinamento", "historico", "vida", "criacao", "inspiracao", "aprendizado",
"console"):
if hasattr(servidor, nome): if hasattr(servidor, nome):
getattr(servidor, nome).publicar = EVENTOS.publicar getattr(servidor, nome).publicar = EVENTOS.publicar
@@ -560,6 +579,12 @@ def main():
def nome_criada(ident): def nome_criada(ident):
return next((a['nome'] for a in servidor.criacao.atividades() if a['id'] == ident), None) return next((a['nome'] for a in servidor.criacao.atividades() if a['id'] == ident), None)
servidor.aprendizado = aprendizado = Aprendizado(Path.cwd() / 'aprendizado.json', nome_criada) servidor.aprendizado = aprendizado = Aprendizado(Path.cwd() / 'aprendizado.json', nome_criada)
# Console: tudo o que ele faz e por quê (cada parte registra o motivo da decisão)
servidor.console = CONSOLE
for parte in (vida, aprendizado, inspiracao, servidor.cerebro, servidor.historico, servidor.controle):
parte.console = CONSOLE.registrar
vida.dia_anterior = lambda: aprendizado.historico[-1] if aprendizado.historico else None
CONSOLE.registrar("sistema", "Servidor iniciado", "versão " + versao())
aprendizado.anotar = lambda tipo, texto, agora=None: (vida.anotar(tipo, texto, agora), vida.avisar()) aprendizado.anotar = lambda tipo, texto, agora=None: (vida.anotar(tipo, texto, agora), vida.avisar())
vida.aprendizado = aprendizado vida.aprendizado = aprendizado
# a análise da semana usa a mesma IA do estúdio (Gemini, IA local ou o S10) # a análise da semana usa a mesma IA do estúdio (Gemini, IA local ou o S10)
+2
View File
@@ -36,6 +36,8 @@ def main():
'LICENSE':'https://raw.githubusercontent.com/google-ai-edge/mediapipe/master/LICENSE', 'LICENSE':'https://raw.githubusercontent.com/google-ai-edge/mediapipe/master/LICENSE',
'face.tflite':'https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite', 'face.tflite':'https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite',
'hands.task':'https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task', 'hands.task':'https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task',
# expressão do rosto (sorriso) para o contágio emocional
'expressao.task':'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task',
}.items(): }.items():
data = download(url); (dest/name).write_bytes(data) data = download(url); (dest/name).write_bytes(data)
files[name] = {'url':url, 'sha256':hashlib.sha256(data).hexdigest()} files[name] = {'url':url, 'sha256':hashlib.sha256(data).hexdigest()}
+18 -1
View File
@@ -1,5 +1,7 @@
'use strict'; 'use strict';
let face = null, hands = null, vision = null; 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; let lastTime = 0;
async function prepare(c) { async function prepare(c) {
if (!vision) { if (!vision) {
@@ -9,6 +11,13 @@ async function prepare(c) {
} }
if (c.rosto && !face) face = await vision.module.FaceDetector.createFromOptions(vision.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}); 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, { if (c.maos && !hands) hands = await vision.module.HandLandmarker.createFromOptions(vision.files, {
baseOptions:{modelAssetPath:'/vendor/mediapipe/hands.task',delegate:'CPU'},runningMode:'VIDEO',numHands:2, baseOptions:{modelAssetPath:'/vendor/mediapipe/hands.task',delegate:'CPU'},runningMode:'VIDEO',numHands:2,
minHandDetectionConfidence:.6,minHandPresenceConfidence:.6,minTrackingConfidence:.5}); 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, 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})) 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:[]}; 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}))), 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)}); } } catch(e) { self.postMessage({tipo:'erro',erro:e.message || String(e)}); }
finally { if (bitmap) bitmap.close(); } finally { if (bitmap) bitmap.close(); }
}; };
+30 -1
View File
@@ -287,11 +287,13 @@
olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i])); olhos.forEach((p, i) => olhoLente(ctx, o, p, estilo, estado[i]));
desenharMaos(ctx, o, olhos, tela.height); desenharMaos(ctx, o, olhos, tela.height);
desenharZzz(ctx, o, olhos); desenharZzz(ctx, o, olhos);
desenharSonho(ctx, o, olhos);
} else { } else {
cctx.clearRect(0, 0, camada.width, camada.height); cctx.clearRect(0, 0, camada.width, camada.height);
olhos.forEach((p, i) => olhoPlano(cctx, o, p, estilo, estado[i])); olhos.forEach((p, i) => olhoPlano(cctx, o, p, estilo, estado[i]));
desenharMaos(cctx, o, olhos, tela.height); desenharMaos(cctx, o, olhos, tela.height);
desenharZzz(cctx, o, olhos); desenharZzz(cctx, o, olhos);
desenharSonho(cctx, o, olhos);
ctx.save(); ctx.save();
if (estilo.brilho) { ctx.shadowColor = css(o.cor, 0.7); ctx.shadowBlur = Math.min(60, tela.height * 0.05); } if (estilo.brilho) { ctx.shadowColor = css(o.cor, 0.7); ctx.shadowBlur = Math.min(60, tela.height * 0.05); }
ctx.drawImage(camada, 0, 0); ctx.drawImage(camada, 0, 0);
@@ -421,6 +423,33 @@
} }
// ------------------------------------------------------------------ Zzz // ------------------------------------------------------------------ Zzz
// ----------------------------------------------------------- sonho
// Dormindo, às vezes aparece bem fraquinho um balão de pensamento com algo do dia (a bola, a estrela...).
const sonho = { obj: null, t0: 0, dur: 0 };
function definirSonho(obj, segundos) {
sonho.obj = obj || null; sonho.t0 = performance.now() / 1000; sonho.dur = segundos || 7;
}
function desenharSonho(c, o, olhos) {
if (!o.dormindo || !sonho.obj) return;
const k = (o.t - sonho.t0) / sonho.dur;
if (k < 0) return;
if (k >= 1) { sonho.obj = null; return; }
const surge = Math.min(1, k * sonho.dur / 1.2, (1 - k) * sonho.dur / 1.2); // aparece e some devagar
const e = olhos.slice().sort((a, b) => a.cx - b.cx)[0]; // olho da esquerda na tela
const R = e.ry * 0.62, bx = e.cx - e.ry * 0.1;
const by = Math.max(R * 1.2, e.cy - e.ry * 1.75) + Math.sin(o.t * 1.3) * e.ry * 0.05; // nunca sai da tela
c.save();
c.strokeStyle = css(o.cor); c.fillStyle = css(o.cor, 0.08); c.lineWidth = Math.max(1.5, R * 0.06);
c.globalAlpha = 0.45 * surge;
for (const [f, rr] of [[0.25, 0.09], [0.55, 0.15]]) { // bolinhas subindo até o balão
c.beginPath(); c.arc(e.cx + e.ry * 0.3 - f * e.ry * 0.3, e.cy - e.ry * 0.75 + (by + R - e.cy + e.ry * 0.75) * f, R * rr * 1.3, 0, Math.PI * 2); c.stroke();
}
c.beginPath(); c.arc(bx, by, R, 0, Math.PI * 2); c.fill(); c.stroke();
c.globalAlpha = 0.55 * surge;
desenharObjeto(c, sonho.obj, bx, by, R * 1.15, o.t, o.cor, Math.sin(o.t * 0.8) * 0.15, 0);
c.restore();
}
function desenharZzz(c, o, olhos, fator) { function desenharZzz(c, o, olhos, fator) {
fator = fator || 1; fator = fator || 1;
if (!o.dormindo) return; if (!o.dormindo) return;
@@ -1385,7 +1414,7 @@
return ((!base.lente && FORMATOS[formato]) || base).h / 2; return ((!base.lente && FORMATOS[formato]) || base).h / 2;
}, },
maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } }, maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } },
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS), faixa: definirFaixa, sonho: definirSonho, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS),
/** Atividades (momentos dele): iniciar/parar, para onde olhar e a camada da cena. */ /** Atividades (momentos dele): iniciar/parar, para onde olhar e a camada da cena. */
atividade: (nome, segundos, receita) => (nome ? iniciarAtividade(nome, segundos, receita) : (pararAtividade(), false)), atividade: (nome, segundos, receita) => (nome ? iniciarAtividade(nome, segundos, receita) : (pararAtividade(), false)),
atividadeAtual: () => (ativ.nome && !ativ.fim ? ativ.nome : null), atividadeAtual: () => (ativ.nome && !ativ.fim ? ativ.nome : null),
+158
View File
@@ -0,0 +1,158 @@
"""Jeito de gente do modo vida: reencontro e saudade, bom dia lembrando de ontem, pedidos com motivo,
largar atividade no meio, personalidade e o console com o porquê de cada decisão."""
import random
import sys
import tempfile
import time
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from console import Console # noqa: E402
from vida import Vida # noqa: E402
DIA = time.mktime((2026, 10, 12, 9, 0, 0, 0, 0, -1)) # uma segunda, 9h
class HumanoTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.v = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False, rng=random.Random(1))
self.ordens, self.linhas = [], []
self.v.enfileirar = self.ordens.append
self.v.console = lambda origem, texto, motivo='', nivel='info', agora=None: self.linhas.append((origem, texto, motivo, nivel))
self.v.celular_online = lambda: True
self.v.celular_dormindo = lambda: False
self.v.madrugada = lambda agora: False
self.perto = False
self.v.alguem_perto = lambda: self.perto
def tearDown(self):
self.tmp.cleanup()
def falas(self):
return [o[6:] for o in self.ordens if o.startswith('dizer ')]
def chegar(self, agora, ausencia):
self.v.ultimo_perto = agora - ausencia
self.v.perto_antes = False
self.perto = True
self.v.tick(agora)
def test_reencontro_cresce_com_o_tempo_fora(self):
self.v.saudou_dia = '2026-10-12' # bom dia já foi dado
self.chegar(DIA, 5 * 60) # 5 min fora: nada
self.assertEqual(self.falas(), [])
self.assertNotIn('acenar', self.ordens)
self.chegar(DIA + 100, 20 * 60) # 20 min: acena
self.assertIn('acenar', self.ordens)
self.chegar(DIA + 200, 2 * 3600) # 2 h: comemora
self.assertEqual(self.falas()[-1], 'Oi! Você voltou!')
social = self.v.necessidades['social']
self.chegar(DIA + 300, 6 * 3600) # 6 h: saudade
self.assertEqual(self.falas()[-1], 'Senti sua falta!')
self.assertIn('coracao', self.ordens)
self.assertGreater(self.v.necessidades['social'], social)
self.assertTrue(any('ninguém por perto havia 6h00' in m for _, _, m, _ in self.linhas))
def test_bom_dia_lembra_de_ontem(self):
ontem = dict(presenca=[None] * 24, respostas=0, atos=8)
ontem['presenca'][1] = 0.6 # ficaram acordados até as 2h
self.v.dia_anterior = lambda: ontem
self.v.acaso = random.Random(0)
self.chegar(DIA, 8 * 3600)
fala = self.falas()[0]
self.assertTrue(fala.startswith('Bom dia! '), fala)
self.assertIn(fala[9:], ['Ontem a gente ficou acordado até as 2h, hein!',
'Ontem ninguém brincou comigo... hoje vai ser diferente?'])
self.assertEqual(self.v.saudou_dia, '2026-10-12')
self.chegar(DIA + 3600, 30 * 60) # mesmo dia: não repete o bom dia
self.assertEqual(len([f for f in self.falas() if f.startswith('Bom dia')]), 1)
# de madrugada não faz festa
self.v.madrugada = lambda agora: True
n = len(self.falas())
self.chegar(DIA + 7200, 6 * 3600)
self.assertEqual(len(self.falas()), n)
def test_pede_o_que_falta_com_motivo_e_intervalo(self):
self.v.saudou_dia = '2026-10-12'
self.perto, self.v.perto_antes, self.v.visto_em = True, True, DIA
self.v.atividade_em = DIA # sem folga para atividades
self.v.necessidades.update(social=30, diversao=80, energia=80, alegria=70)
self.v.acaso = random.Random(3)
pedido = None
for i in range(40):
if self.v.agir(DIA + i) == 'pedido':
pedido = DIA + i
break
self.assertIsNotNone(pedido)
self.assertEqual(self.falas()[-1], 'Tô carente... me faz um carinho?')
self.assertIn('social 30', self.v.porque)
self.assertEqual(self.v.diario[0]['tipo'], 'pedido')
n = len(self.falas())
for i in range(40): # menos de 20 min depois: não pede de novo
self.v.agir(pedido + 60 + i)
self.assertEqual(len(self.falas()), n)
def test_larga_atividade_no_meio_e_conta(self):
self.v.acaso = random.Random(0)
largou = None
for i in range(60):
self.v.desistencia = None
self.v.comecar_atividade(DIA + i, 'ler', 200)
if self.v.desistencia:
largou = self.v.desistencia
break
self.assertIsNotNone(largou)
segundos = int(self.ordens[-1].split()[-1])
self.assertLess(segundos, 110) # mandou ao celular a duração encurtada
self.v.tick(largou[0] + 1)
self.assertIn('bocejar', self.ordens)
self.assertIn('Largou no meio: cansou de ler um livro', [d['texto'] for d in self.v.diario[:3]])
def test_personalidade_muda_o_jeito_e_amadurece(self):
self.assertEqual(self.v.jeito('rir'), 1.0) # neutro nos testes
self.v.personalidade['brincadeira'] = 0.9
self.assertGreater(self.v.jeito('rir'), 1.4)
self.v.personalidade['timidez'] = 0.2
self.assertLess(self.v.jeito('timido'), 0.7)
antes = self.v.personalidade['timidez']
for _ in range(10):
self.v.estimulo('carinho', DIA)
self.assertLess(self.v.personalidade['timidez'], antes)
self.v.salvar()
outra = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False, rng=random.Random(1))
self.assertEqual(outra.personalidade, self.v.personalidade)
self.assertIn('personalidade', self.v.status())
def test_cada_decisao_vai_para_o_console_com_o_porque(self):
self.v.necessidades['energia'] = 10
self.v.proximo_ato = 0
self.v.ultimo_tick = DIA
self.v.tick(DIA)
origem, texto, motivo, _ = self.linhas[-1]
self.assertEqual((origem, texto), ('vida', 'Ficou exausto e foi dormir'))
self.assertIn('energia 10: abaixo de 20', motivo)
self.assertIn('energia 10 · alegria', motivo)
self.v.estimulo('carinho', DIA)
self.assertIn(('sentidos', 'Recebeu carinho'), [(o, t) for o, t, _, _ in self.linhas])
class ConsoleTest(unittest.TestCase):
def test_guarda_as_ultimas_e_publica(self):
c = Console()
publicados = []
c.publicar = lambda tipo, dados: publicados.append((tipo, dados))
for i in range(450):
c.registrar('vida', 'ato %d' % i, 'motivo')
c.registrar('inventada', 'vira sistema', nivel='qualquer')
st = c.status()
self.assertEqual(len(st['linhas']), 400)
self.assertEqual(st['linhas'][-1]['origem'], 'sistema')
self.assertEqual(st['linhas'][-1]['nivel'], 'info')
self.assertEqual(publicados[-1][0], 'console')
self.assertEqual(len(c.status(desde=449)['linhas']), 2)
if __name__ == '__main__':
unittest.main()
+207 -8
View File
@@ -43,6 +43,8 @@ ESTIMULOS = {
'brincar': (dict(diversao=25, alegria=8, energia=-5), 'alegre'), 'brincar': (dict(diversao=25, alegria=8, energia=-5), 'alegre'),
'alimentar': (dict(energia=30, alegria=4), 'feliz'), 'alimentar': (dict(energia=30, alegria=4), 'feliz'),
} }
NOME_ESTIMULO = {'pir': 'O sensor de presença viu alguém', 'movimento': 'A câmera viu movimento',
'som': 'Ouviu um barulho', 'conversa': 'Alguém conversou com ele'}
CUIDADOS = {'carinho': 'Recebeu carinho', 'cutucar': 'Levou uma cutucada', 'brincar': 'Brincou', CUIDADOS = {'carinho': 'Recebeu carinho', 'cutucar': 'Levou uma cutucada', 'brincar': 'Brincou',
'alimentar': 'Ganhou energia', 'ninar': 'Foi posto para dormir', 'acordar': 'Foi acordado'} 'alimentar': 'Ganhou energia', 'ninar': 'Foi posto para dormir', 'acordar': 'Foi acordado'}
@@ -112,6 +114,16 @@ PERIODOS = [(5, 12, 'de manhã', ['um sol nascendo', 'um pãozinho', 'uma xícar
(0, 5, 'de madrugada', ['uma lua', 'uma vela', 'uma coruja'])] (0, 5, 'de madrugada', ['uma lua', 'uma vela', 'uma coruja'])]
MAX_DIARIO = 60 MAX_DIARIO = 60
# Personalidade: traços de 0 a 1 que mudam o jeito de tudo (0,5 = neutro) e amadurecem com o convívio.
TRACOS = ('curiosidade', 'brincadeira', 'timidez')
GESTOS_TRACO = {'brincadeira': {'rir', 'comemorar', 'alegre', 'musica', 'piscadinha'},
'curiosidade': {'olhar_esq', 'olhar_dir', 'pensar', 'espiar', 'confuso'},
'timidez': {'timido', 'esconder', 'espiar'}}
# Reencontro: quanto mais tempo a pessoa ficou fora, maior a festa.
AUSENCIA_ACENO, AUSENCIA_OI, AUSENCIA_SAUDADE = 10 * 60, 60 * 60, 4 * 3600
INTERVALO_PEDIDO = 20 * 60 # no mínimo 20 min entre um pedido ("brinca comigo?") e outro
DESISTE = 0.12 # chance de largar uma atividade no meio porque ficou chato
GESTOS_VIDA = {g for atos in ATOS.values() for g, _, _ in atos} GESTOS_VIDA = {g for atos in ATOS.values() for g, _, _ in atos}
@@ -149,6 +161,17 @@ class Vida:
self.aprendi_pendente = lambda: None # curiosidade de hoje que ainda dá para contar self.aprendi_pendente = lambda: None # curiosidade de hoje que ainda dá para contar
self.contar_aprendi = lambda ident, agora=None: None self.contar_aprendi = lambda ident, agora=None: None
self.aprendizado = None # o que ele aprende com o dia a dia (aprendizado.py) self.aprendizado = None # o que ele aprende com o dia a dia (aprendizado.py)
self.console = lambda origem, texto, motivo='', nivel='info', agora=None: None # console do painel
self.dia_anterior = lambda: None # resumo de ontem (aprendizado): para lembrar no bom dia
# sorteios dos comportamentos "humanos" ficam à parte: não mexem na sequência do rng principal
self.acaso = random.Random() if rng is None else random.Random(7)
self.porque = '' # motivo da última decisão (vai para o console)
self.ultima = None # {horario, texto, porque}: a última decisão de fato
self.perto_antes = False
self.ultimo_perto = time.time()
self.pediu_em = 0.0
self.desistencia = None # (quando, o que) se largou a atividade no meio
self.figurinha_mostrada = 0.0
agora = time.time() agora = time.time()
self.config = dict(PADRAO) self.config = dict(PADRAO)
@@ -157,6 +180,10 @@ class Vida:
self.diario = [] self.diario = []
self.emojis = dict(dia='', total=0, ultimo=0.0) self.emojis = dict(dia='', total=0, ultimo=0.0)
self.dormindo_proprio = False self.dormindo_proprio = False
# cada Carinha nasce com um jeito um pouco diferente (nos testes, neutro)
self.personalidade = ({k: 0.5 for k in TRACOS} if rng is not None else
{k: round(self.acaso.uniform(0.35, 0.65), 2) for k in TRACOS})
self.saudou_dia = ''
try: try:
salvo = json.loads(self.caminho.read_text(encoding='utf-8')) salvo = json.loads(self.caminho.read_text(encoding='utf-8'))
self.config.update(self.validar(salvo.get('config', {}))) self.config.update(self.validar(salvo.get('config', {})))
@@ -165,6 +192,11 @@ class Vida:
self.necessidades[k] = _limitar(float(salvo['necessidades'][k])) self.necessidades[k] = _limitar(float(salvo['necessidades'][k]))
self.nascimento = float(salvo.get('nascimento', agora)) self.nascimento = float(salvo.get('nascimento', agora))
self.diario = [d for d in salvo.get('diario', []) if isinstance(d, dict)][:MAX_DIARIO] self.diario = [d for d in salvo.get('diario', []) if isinstance(d, dict)][:MAX_DIARIO]
if isinstance(salvo.get('personalidade'), dict):
for k in TRACOS:
if isinstance(salvo['personalidade'].get(k), (int, float)):
self.personalidade[k] = max(0.0, min(1.0, float(salvo['personalidade'][k])))
self.saudou_dia = str(salvo.get('saudou_dia', ''))
if isinstance(salvo.get('emojis'), dict): if isinstance(salvo.get('emojis'), dict):
self.emojis.update({k: salvo['emojis'][k] for k in ('dia', 'total', 'ultimo') if k in salvo['emojis']}) self.emojis.update({k: salvo['emojis'][k] for k in ('dia', 'total', 'ultimo') if k in salvo['emojis']})
except (OSError, ValueError, TypeError, AttributeError): except (OSError, ValueError, TypeError, AttributeError):
@@ -222,7 +254,8 @@ class Vida:
def salvar(self): def salvar(self):
with self.lock: with self.lock:
dados = dict(config=self.config, necessidades=self.necessidades, nascimento=self.nascimento, dados = dict(config=self.config, necessidades=self.necessidades, nascimento=self.nascimento,
diario=self.diario, emojis=self.emojis) diario=self.diario, emojis=self.emojis, personalidade=self.personalidade,
saudou_dia=self.saudou_dia)
self.salvo_em = time.time() self.salvo_em = time.time()
try: try:
gravar_atomico(self.caminho, dados) gravar_atomico(self.caminho, dados)
@@ -274,7 +307,9 @@ class Vida:
nascimento=self.nascimento, idade_dias=int((agora - self.nascimento) // 86400), nascimento=self.nascimento, idade_dias=int((agora - self.nascimento) // 86400),
emojis_hoje=self.emojis_hoje(agora), ultimo_emoji=self.emojis.get('ultimo') or None, emojis_hoje=self.emojis_hoje(agora), ultimo_emoji=self.emojis.get('ultimo') or None,
zanga=self.zanga(), sono_profundo=self.sono_profundo, zanga=self.zanga(), sono_profundo=self.sono_profundo,
pode_criar=self.motivo_sem_emoji(agora) or '', diario=self.diario[:30]) pode_criar=self.motivo_sem_emoji(agora) or '', diario=self.diario[:30],
personalidade=dict(self.personalidade), porque=self.porque, ultima=self.ultima,
proximo_ato_em=max(0, round(self.proximo_ato - agora)))
def avisar(self): def avisar(self):
self.publicado_em = time.time() self.publicado_em = time.time()
@@ -323,6 +358,10 @@ class Vida:
self.interagiu_em = agora # alguém quis ele acordado (sensor não conta) self.interagiu_em = agora # alguém quis ele acordado (sensor não conta)
if self.aprendizado: if self.aprendizado:
self.aprendizado.resposta(tipo, agora) self.aprendizado.resposta(tipo, agora)
self.amadurecer(tipo)
self.console('sentidos', CUIDADOS.get(tipo) or NOME_ESTIMULO.get(tipo, tipo),
', '.join('%s %+d' % (k, v) for k, v in efeito.items()),
'detalhe' if tipo in ('som', 'movimento') else 'info', agora)
if gesto and not self.bloqueado(): if gesto and not self.bloqueado():
if self.dormindo(): if self.dormindo():
self.acordar(agora, None) self.acordar(agora, None)
@@ -390,12 +429,108 @@ class Vida:
self.anotar('atividade', 'Foi fazer %s %s%s' % (item['emoji'], item['nome'], motivo), agora) self.anotar('atividade', 'Foi fazer %s %s%s' % (item['emoji'], item['nome'], motivo), agora)
return 'atividade ' + nome return 'atividade ' + nome
nome = self.rng.choice(CALMAS) # apagaram a criação no meio do caminho nome = self.rng.choice(CALMAS) # apagaram a criação no meio do caminho
if nome != 'dormir' and segundos > 60 and self.acaso.random() < DESISTE * (1.3 - self.personalidade['curiosidade']):
segundos = segundos * self.acaso.uniform(0.3, 0.5) # vai largar no meio: ficou chato
self.desistencia = (agora + segundos, NOME_ATIVIDADE.get(nome, 'a atividade'))
self.enfileirar('atividade %s %d' % (nome, segundos)) self.enfileirar('atividade %s %d' % (nome, segundos))
self.atividade_em = agora self.atividade_em = agora
self.ultima_atividade = nome self.ultima_atividade = nome
self.anotar('atividade', NOME_ATIVIDADE.get(nome, 'Foi fazer algo'), agora) self.anotar('atividade', NOME_ATIVIDADE.get(nome, 'Foi fazer algo'), agora)
return 'atividade ' + nome return 'atividade ' + nome
def dizer(self, texto, gesto=None):
"""Uma fala curta: o gesto e a frase em legenda, com a voz de robô."""
if gesto:
self.enfileirar(gesto)
self.enfileirar('dizer ' + texto)
def resumo(self):
n = self.necessidades
return 'energia %d · alegria %d · social %d · diversão %d · %s' % (
n['energia'], n['alegria'], n['social'], n['diversao'], HUMORES[self.humor()][1].lower())
@staticmethod
def tempo(seg):
return '%d min' % round(seg / 60) if seg < 3600 else '%dh%02d' % (seg // 3600, seg % 3600 // 60)
def saudacao(self, agora):
"""Bom dia com uma lembrança de ontem (do que ele viveu, sem IA)."""
hora = time.localtime(agora).tm_hour
prefixo = 'Bom dia' if 5 <= hora < 12 else 'Boa tarde' if hora < 18 else 'Boa noite'
fatos = []
ontem = None
try:
ontem = self.dia_anterior()
except Exception:
pass
if isinstance(ontem, dict):
pres = ontem.get('presenca') or []
tarde = [hh for hh in range(0, 5) if hh < len(pres) and pres[hh] is not None and pres[hh] >= 0.3]
if tarde:
fatos.append('Ontem a gente ficou acordado até as %dh, hein!' % (max(tarde) + 1))
elif len(pres) == 24 and pres[23] is not None and pres[23] >= 0.3:
fatos.append('Ontem a gente ficou até tarde, hein!')
if (ontem.get('respostas') or 0) >= 5:
fatos.append('Ontem foi um dia cheio de carinho. Obrigado!')
elif (ontem.get('atos') or 0) >= 5 and not ontem.get('respostas'):
fatos.append('Ontem ninguém brincou comigo... hoje vai ser diferente?')
if not fatos:
fatos.append('Acordei cheio de energia!' if self.necessidades['energia'] >= 80 else 'Que bom te ver!')
return '%s! %s' % (prefixo, self.acaso.choice(fatos))
def reencontro(self, agora, ausencia):
"""Alguém apareceu: a reação depende de quanto tempo ficou fora (e de quem ele é)."""
if self.dormindo() or self.madrugada(agora) or self.bloqueado():
return None
n, tim = self.necessidades, self.personalidade['timidez']
dia = time.strftime('%Y-%m-%d', time.localtime(agora))
motivo = 'ninguém por perto havia ' + self.tempo(ausencia)
if self.saudou_dia != dia and time.localtime(agora).tm_hour >= 5 and ausencia >= AUSENCIA_ACENO:
self.saudou_dia = dia
frase = self.saudacao(agora)
self.dizer(frase, 'timido' if tim > 0.65 else 'acenar')
ganho, texto, motivo = 10, 'Deu bom dia: "%s"' % frase, 'primeiro encontro do dia; ' + motivo
elif ausencia >= AUSENCIA_SAUDADE:
self.dizer('Senti sua falta!', 'comemorar')
self.enfileirar('coracao')
ganho, texto = 20, 'Fez festa: estava com saudade'
n['alegria'] = _limitar(n['alegria'] + 10)
elif ausencia >= AUSENCIA_OI:
self.dizer('Oi! Você voltou!', 'timido' if tim > 0.65 else 'comemorar')
ganho, texto = 10, 'Comemorou que alguém voltou'
elif ausencia >= AUSENCIA_ACENO:
self.enfileirar('acenar')
ganho, texto = 4, 'Acenou para quem voltou'
else:
return None
with self.lock:
n['social'] = _limitar(n['social'] + ganho)
self.anotar('social', texto, agora)
self.console('vida', texto, motivo, agora=agora)
self.avisar()
return texto
def pedir_algo(self, agora):
"""Iniciativa com motivo: pede o que está faltando (ou mostra o que fez)."""
n = self.necessidades
if n['social'] < 35:
frase, gesto, porque = 'Tô carente... me faz um carinho?', 'coracao', 'social %d' % n['social']
elif n['diversao'] < 40:
frase, gesto, porque = 'Brinca comigo?', 'acenar', 'diversão %d' % n['diversao']
elif (float(self.emojis.get('ultimo') or 0) > self.figurinha_mostrada and agora - float(self.emojis['ultimo']) < 3 * 3600
and self.tocar_reacao(agora, anotar=False)):
self.figurinha_mostrada = float(self.emojis['ultimo'])
frase, gesto, porque = 'Olha o que eu desenhei!', None, 'criou uma figurinha e ainda não tinha mostrado'
elif n['energia'] < 35:
frase, gesto, porque = 'Tô com soninho...', 'bocejar', 'energia %d' % n['energia']
else:
return None
self.pediu_em = agora
self.dizer(frase, gesto)
self.anotar('pedido', 'Pediu: "%s"' % frase, agora)
self.porque = 'alguém por perto e ' + porque
return 'pedido'
def pir_acorda(self, agora=None): def pir_acorda(self, agora=None):
"""Disparo do PIR com o rosto dormindo: acorda? No ritmo calmo precisa de vários em 2 min.""" """Disparo do PIR com o rosto dormindo: acorda? No ritmo calmo precisa de vários em 2 min."""
agora = agora or time.time() agora = agora or time.time()
@@ -423,8 +558,27 @@ class Vida:
"""Quanto o aprendizado multiplica a chance de um ato (1 = sem opinião).""" """Quanto o aprendizado multiplica a chance de um ato (1 = sem opinião)."""
return self.aprendizado.peso(nome) if self.aprendizado else 1.0 return self.aprendizado.peso(nome) if self.aprendizado else 1.0
def jeito(self, gesto):
"""Quanto a personalidade puxa um gesto (1 = neutro): brincalhão ri mais, tímido se esconde mais."""
f = 1.0
for traco, gestos in GESTOS_TRACO.items():
if gesto in gestos:
f *= 1 + (self.personalidade[traco] - 0.5) * 1.2
return f
def amadurecer(self, tipo):
"""O convívio muda o jeito devagar: carinho deixa menos tímido, brincar deixa mais brincalhão."""
p = self.personalidade
with self.lock:
if tipo in ('carinho', 'conversa', 'brincar'):
p['timidez'] = round(max(0.1, p['timidez'] - 0.003), 3)
if tipo == 'brincar':
p['brincadeira'] = round(min(0.95, p['brincadeira'] + 0.002), 3)
if tipo == 'conversa':
p['curiosidade'] = round(min(0.95, p['curiosidade'] + 0.001), 3)
def escolher(self, opcoes): def escolher(self, opcoes):
opcoes = [(g, p * self.gosto(g), t) for g, p, t in opcoes] opcoes = [(g, p * self.gosto(g) * self.jeito(g), t) for g, p, t in opcoes]
total = sum(p for _, p, _ in opcoes) total = sum(p for _, p, _ in opcoes)
x = self.rng.uniform(0, total) x = self.rng.uniform(0, total)
for gesto, peso, texto in opcoes: for gesto, peso, texto in opcoes:
@@ -467,42 +621,48 @@ class Vida:
self.anotar('emoji', 'Teve uma ideia e desenhou ' + tema, agora) self.anotar('emoji', 'Teve uma ideia e desenhou ' + tema, agora)
return True return True
def tocar_reacao(self, agora): def tocar_reacao(self, agora, anotar=True):
try: try:
reacoes = [r for r in self.treinamento.status()['reacoes'] if r.get('ativa', True) and r['feedback'] >= 0] reacoes = [r for r in self.treinamento.status()['reacoes'] if r.get('ativa', True) and r['feedback'] >= 0]
except Exception: except Exception:
return False return False
if not reacoes: if not reacoes:
return False return False
r = self.rng.choice(reacoes[:40]) r = self.rng.choice(reacoes[:40]) if anotar else reacoes[0] # para mostrar: a mais nova
try: try:
self.treinamento.testar(r['id']) self.treinamento.testar(r['id'])
except ValueError: except ValueError:
return False return False
self.anotar('acao', 'Lembrou da reação "' + r['receita'].get('nome', '?') + '"', agora) if anotar:
self.anotar('acao', 'Lembrou da reação "' + r['receita'].get('nome', '?') + '"', agora)
return True return True
def agir(self, agora): def agir(self, agora):
"""Um ato: o que um bichinho com essas necessidades faria agora.""" """Um ato: o que um bichinho com essas necessidades faria agora."""
n = self.necessidades n = self.necessidades
perto = self.alguem_perto() perto = self.alguem_perto()
self.porque = ''
if self.dormindo(): if self.dormindo():
# de madrugada nunca acorda sozinho (nem com gente se mexendo na cama); de dia, só descansado # de madrugada nunca acorda sozinho (nem com gente se mexendo na cama); de dia, só descansado
if n['energia'] >= DESCANSADO and not self.madrugada(agora): if n['energia'] >= DESCANSADO and not self.madrugada(agora):
self.acordar(agora, 'Acordou descansado') self.acordar(agora, 'Acordou descansado')
self.porque = 'energia %d: descansou' % n['energia']
return 'acordar' return 'acordar'
# aprendeu a rotina: acorda do cochilo um pouco antes da hora em que costuma ter gente # aprendeu a rotina: acorda do cochilo um pouco antes da hora em que costuma ter gente
if (self.aprendizado and not self.sono_profundo and not self.madrugada(agora) and n['energia'] >= 50 if (self.aprendizado and not self.sono_profundo and not self.madrugada(agora) and n['energia'] >= 50
and self.aprendizado.chega_logo(agora)): and self.aprendizado.chega_logo(agora)):
self.acordar(agora, 'Acordou: daqui a pouco costuma chegar alguém') self.acordar(agora, 'Acordou: daqui a pouco costuma chegar alguém')
self.aprendizado.decidir('acordar', 'Acordou do cochilo antes da hora', self.aprendizado.porque_chegada(agora), agora) self.porque = self.aprendizado.porque_chegada(agora)
self.aprendizado.decidir('acordar', 'Acordou do cochilo antes da hora', self.porque, agora)
return 'acordar' return 'acordar'
return None return None
if self.config['noite'] and self.madrugada(agora) and agora - self.interagiu_em > PEDIU_ATENCAO: if self.config['noite'] and self.madrugada(agora) and agora - self.interagiu_em > PEDIU_ATENCAO:
self.dormir(agora, 'Ficou tarde, foi dormir') self.dormir(agora, 'Ficou tarde, foi dormir')
self.porque = 'madrugada e ninguém interagiu nos últimos 10 min'
return 'dormir' return 'dormir'
if n['energia'] < SONO: if n['energia'] < SONO:
self.dormir(agora, 'Ficou exausto e foi dormir', profundo=True) self.dormir(agora, 'Ficou exausto e foi dormir', profundo=True)
self.porque = 'energia %d: abaixo de %d, sono pesado até descansar' % (n['energia'], SONO)
return 'dormir' return 'dormir'
humor = self.humor() humor = self.humor()
if self.celular_atividade(): if self.celular_atividade():
@@ -511,9 +671,11 @@ class Vida:
folga = agora - self.atividade_em > 90 # um respiro entre uma atividade e outra folga = agora - self.atividade_em > 90 # um respiro entre uma atividade e outra
# cansado: um cochilo na caminha de vez em quando (recupera energia sem o sono pesado) # cansado: um cochilo na caminha de vez em quando (recupera energia sem o sono pesado)
if dia and folga and n['energia'] < COCHILO and self.rng.random() < 0.35: if dia and folga and n['energia'] < COCHILO and self.rng.random() < 0.35:
self.porque = 'energia %d: abaixo de %d, um cochilo' % (n['energia'], COCHILO)
return self.comecar_atividade(agora, 'dormir', self.rng.uniform(120, 240)) return self.comecar_atividade(agora, 'dormir', self.rng.uniform(120, 240))
# entediado: vai fazer alguma coisa por alguns minutos (pode ser interrompido) # entediado: vai fazer alguma coisa por alguns minutos (pode ser interrompido)
if dia and folga and n['diversao'] < TEDIO and self.rng.random() < 0.75: if dia and folga and n['diversao'] < TEDIO and self.rng.random() < 0.75:
self.porque = 'diversão %d: entediado (abaixo de %d)' % (n['diversao'], TEDIO)
return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(120, 300)) return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(120, 300))
# ninguém olhando há um tempo: vai cuidar da vida dele (ler, pintar, música...) # ninguém olhando há um tempo: vai cuidar da vida dele (ler, pintar, música...)
if perto: if perto:
@@ -523,8 +685,9 @@ class Vida:
if (dia and folga and sozinho and self.aprendizado and n['energia'] < 85 if (dia and folga and sozinho and self.aprendizado and n['energia'] < 85
and self.aprendizado.casa_vazia(agora) and self.rng.random() < 0.3): and self.aprendizado.casa_vazia(agora) and self.rng.random() < 0.3):
segundos = self.rng.uniform(600, 1200) segundos = self.rng.uniform(600, 1200)
self.porque = self.aprendizado.porque_vazia(agora)
self.aprendizado.decidir('cochilo', 'Tirou um cochilo de %d min para guardar energia' % round(segundos / 60), self.aprendizado.decidir('cochilo', 'Tirou um cochilo de %d min para guardar energia' % round(segundos / 60),
self.aprendizado.porque_vazia(agora), agora) self.porque, agora)
return self.comecar_atividade(agora, 'dormir', segundos) return self.comecar_atividade(agora, 'dormir', segundos)
# alguém por perto e ele aprendeu algo hoje: "Olha o que eu aprendi hoje!" # alguém por perto e ele aprendeu algo hoje: "Olha o que eu aprendi hoje!"
if dia and perto and humor != 'bravo': if dia and perto and humor != 'bravo':
@@ -533,26 +696,41 @@ class Vida:
self.contar_aprendi(item['id'], agora) self.contar_aprendi(item['id'], agora)
with self.lock: with self.lock:
n['social'] = _limitar(n['social'] + 5) n['social'] = _limitar(n['social'] + 5)
self.porque = 'alguém por perto e aprendeu "%s" hoje' % item.get('nome', '?')
return 'aprendi' return 'aprendi'
# iniciativa: com gente por perto, pede o que está faltando (tímido pede menos)
if (dia and perto and not sozinho and agora - self.pediu_em > INTERVALO_PEDIDO
and self.acaso.random() < 0.25 * (1.3 - self.personalidade['timidez'])):
pedido = self.pedir_algo(agora)
if pedido:
return pedido
if dia and folga and sozinho and self.rng.random() < 0.4: if dia and folga and sozinho and self.rng.random() < 0.4:
self.porque = 'ninguém por perto há %s: vai cuidar da vida dele' % self.tempo(agora - self.visto_em)
return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(60, 180)) return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(60, 180))
# bem: de vez em quando, uma atividade curtinha por vontade própria # bem: de vez em quando, uma atividade curtinha por vontade própria
if dia and folga and humor in ('feliz', 'tranquilo') and self.rng.random() < 0.1: if dia and folga and humor in ('feliz', 'tranquilo') and self.rng.random() < 0.1:
self.porque = 'de bem (%s): vontade própria' % humor
return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(40, 90)) return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(40, 90))
# habilidade: inventar uma figurinha nova (raro, com limite por dia e intervalo) # habilidade: inventar uma figurinha nova (raro, com limite por dia e intervalo)
if not self.motivo_sem_emoji(agora) and self.celular_online() and self.rng.random() < 0.08: if not self.motivo_sem_emoji(agora) and self.celular_online() and self.rng.random() < 0.08:
if self.criar_figurinha(agora, humor): if self.criar_figurinha(agora, humor):
self.porque = 'teve uma ideia (até %d figurinhas por dia)' % self.config['max_emojis_dia']
return 'figurinha' return 'figurinha'
# às vezes lembra de uma reação da biblioteca # às vezes lembra de uma reação da biblioteca
if self.treinamento is not None and humor in ('feliz', 'tranquilo', 'entediado') and self.rng.random() < 0.15: if self.treinamento is not None and humor in ('feliz', 'tranquilo', 'entediado') and self.rng.random() < 0.15:
if self.tocar_reacao(agora): if self.tocar_reacao(agora):
with self.lock: with self.lock:
n['diversao'] = _limitar(n['diversao'] + 8) n['diversao'] = _limitar(n['diversao'] + 8)
self.porque = 'humor %s: lembrou de uma reação da biblioteca' % humor
return 'reacao' return 'reacao'
lista = ATOS['carente_perto'] if humor == 'carente' and perto else ATOS.get(humor, ATOS['tranquilo']) lista = ATOS['carente_perto'] if humor == 'carente' and perto else ATOS.get(humor, ATOS['tranquilo'])
if sozinho: # sem plateia: nada de acenar ou ficar tímido if sozinho: # sem plateia: nada de acenar ou ficar tímido
lista = [a for a in lista if a[0] not in PARA_ALGUEM] or ATOS['tranquilo'][:3] lista = [a for a in lista if a[0] not in PARA_ALGUEM] or ATOS['tranquilo'][:3]
gesto, texto = self.escolher(lista) gesto, texto = self.escolher(lista)
self.porque = 'humor %s%s%s' % (
humor, ' com gente por perto' if perto else ', sozinho' if sozinho else '',
'; gosto %.1f×, jeito %.1f×' % (self.gosto(gesto), self.jeito(gesto))
if (self.gosto(gesto), round(self.jeito(gesto), 1)) != (1.0, 1.0) else '')
if self.aprendizado and perto: if self.aprendizado and perto:
self.aprendizado.escolheu(gesto, agora) self.aprendizado.escolheu(gesto, agora)
self.enfileirar(gesto) self.enfileirar(gesto)
@@ -588,6 +766,21 @@ class Vida:
self.sono_profundo = False self.sono_profundo = False
self.enfileirar('sono leve') self.enfileirar('sono leve')
self.anotar('sono', 'Acordou antes de descansar', agora) self.anotar('sono', 'Acordou antes de descansar', agora)
perto = self.alguem_perto()
if perto:
if not self.perto_antes:
self.reencontro(agora, agora - self.ultimo_perto)
self.ultimo_perto = agora
self.perto_antes = perto
if self.desistencia and agora >= self.desistencia[0]:
_, oque = self.desistencia
self.desistencia = None
if not self.dormindo():
self.enfileirar('bocejar')
acao = oque[4:] if oque.startswith('Foi ') else 'a atividade'
texto = 'Largou no meio: cansou de ' + acao
self.anotar('acao', texto, agora)
self.console('vida', texto, 'perdeu a graça (curiosidade %.2f)' % self.personalidade['curiosidade'], agora=agora)
if not self.bloqueado(): if not self.bloqueado():
# o celular escolhe os gestos soltos pelo humor; reenvia de tempos em tempos (pode ter reiniciado) # o celular escolhe os gestos soltos pelo humor; reenvia de tempos em tempos (pode ter reiniciado)
zanga = self.zanga() zanga = self.zanga()
@@ -600,8 +793,14 @@ class Vida:
self.humor_enviado = (humor, agora) self.humor_enviado = (humor, agora)
self.zanga_enviada = zanga self.zanga_enviada = zanga
if agora >= self.proximo_ato: if agora >= self.proximo_ato:
antes = self.diario[0]['horario'] if self.diario else None
ato = self.agir(agora) ato = self.agir(agora)
self.aprender_ato(ato, agora) self.aprender_ato(ato, agora)
if ato:
novo = self.diario[0] if self.diario and self.diario[0]['horario'] != antes else None
self.ultima = dict(horario=agora, texto=novo['texto'] if novo else ato, porque=self.porque)
self.console('vida', novo['texto'] if novo else ato,
(self.porque + ' · ' if self.porque else '') + self.resumo(), agora=agora)
lo, hi = RITMOS[self.config['ritmo']] lo, hi = RITMOS[self.config['ritmo']]
self.proximo_ato = agora + self.rng.uniform(lo, hi) self.proximo_ato = agora + self.rng.uniform(lo, hi)
if ato or agora - self.publicado_em > 30 or humor != self.humor(): if ato or agora - self.publicado_em > 30 or humor != self.humor():
+4 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Bell, Bot, Command, Hand, MessageCircle, Moon, Settings, Smile, Sun, Video } from '@lucide/svelte'; import { Bell, Bot, Command, Hand, MessageCircle, Moon, Settings, Smile, Sun, Terminal, Video } from '@lucide/svelte';
import { onMount, type Component } from 'svelte'; import { onMount, type Component } from 'svelte';
import Avisos from './lib/componentes/Avisos.svelte'; import Avisos from './lib/componentes/Avisos.svelte';
import EStop from './lib/componentes/EStop.svelte'; import EStop from './lib/componentes/EStop.svelte';
@@ -21,6 +21,7 @@
import Guia from './telas/Guia.svelte'; import Guia from './telas/Guia.svelte';
import Criar from './telas/Criar.svelte'; import Criar from './telas/Criar.svelte';
import DiaADia from './telas/DiaADia.svelte'; import DiaADia from './telas/DiaADia.svelte';
import Console from './telas/Console.svelte';
import Ajuda from './lib/componentes/Ajuda.svelte'; import Ajuda from './lib/componentes/Ajuda.svelte';
import { TOPICO_DA_TELA } from './lib/guia'; import { TOPICO_DA_TELA } from './lib/guia';
@@ -35,10 +36,12 @@
guia: Guia, guia: Guia,
criar: Criar, criar: Criar,
'dia-a-dia': DiaADia, 'dia-a-dia': DiaADia,
console: Console,
}; };
const ICONES: Record<string, Component<any>> = { const ICONES: Record<string, Component<any>> = {
'ao-vivo': Video, 'ao-vivo': Video,
eventos: Bell, eventos: Bell,
console: Terminal,
conversa: MessageCircle, conversa: MessageCircle,
reacoes: Smile, reacoes: Smile,
movimento: Hand, movimento: Hand,
+1 -1
View File
@@ -188,7 +188,7 @@ function abrirEventos() {
if (estado.conexao !== 'token') estado.conexao = 'caiu'; if (estado.conexao !== 'token') estado.conexao = 'caiu';
}; };
for (const tipo of ['expressao', 'telemetria', 'app_config', 'aparencia', 'motores', 'estop', 'conversa', 'ia', for (const tipo of ['expressao', 'telemetria', 'app_config', 'aparencia', 'motores', 'estop', 'conversa', 'ia',
'reacao', 'treinamento', 'historico', 'pir', 'vida', 'aprendizado', 'criacao', 'inspiracao']) { 'reacao', 'treinamento', 'historico', 'pir', 'vida', 'aprendizado', 'criacao', 'inspiracao', 'console']) {
fonte.addEventListener(tipo, (e) => { fonte.addEventListener(tipo, (e) => {
const dados = JSON.parse((e as MessageEvent).data); const dados = JSON.parse((e as MessageEvent).data);
tratadores[tipo]?.(dados); tratadores[tipo]?.(dados);
+33 -2
View File
@@ -229,6 +229,37 @@ export const TOPICOS: Record<string, TopicoGuia> = {
{ emoji: '⭐', nome: 'Favoritos', texto: 'A estrela põe expressões e atividades criadas na grade de favoritos do Ao vivo (e nas teclas 1 a 9).' }, { emoji: '⭐', nome: 'Favoritos', texto: 'A estrela põe expressões e atividades criadas na grade de favoritos do Ao vivo (e nas teclas 1 a 9).' },
], ],
}, },
console: {
titulo: 'Console',
emoji: '🖥️',
onde: 'Console (menu lateral)',
resumo:
'Tudo o que ele faz, ao vivo, com o porquê. Em cima, o agora: humor, necessidades, o que está fazendo, quando decide de novo e o jeito dele. Embaixo, cada decisão com o motivo (ex.: "Ficou exausto e foi dormir, porque energia 12: abaixo de 20"). Os filtros escolhem de onde vêm as linhas; "Detalhes" mostra também o que se repete muito (humor reenviado, disparo solto do sensor). Pausar segura a tela para ler.',
itens: [
{ emoji: '🫀', nome: 'Modo vida', texto: 'As decisões dele: dormir, atividades, gestos, pedidos, reencontros.' },
{ emoji: '👀', nome: 'Sentidos', texto: 'Carinho, toque, conversa, presença, som e movimento, com o efeito nas necessidades.' },
{ emoji: '📱', nome: 'Celular', texto: 'O que foi mandado ao rosto e o que o rosto fez (dormiu, acordou, começou atividade).' },
{ emoji: '🧠', nome: 'IA local / Aprendizado / Ideias', texto: 'Escolhas da IA do S10, decisões tiradas da rotina e as ideias do dia.' },
],
},
jeito: {
titulo: 'Jeito de gente',
emoji: '🧒',
onde: 'Modo vida e rosto',
resumo: 'O que faz ele parecer alguém, e não um programa.',
itens: [
{ emoji: '🤗', nome: 'Saudade', texto: 'A festa depende do tempo fora: 10 min, um aceno; 1 h, "Oi! Você voltou!"; 4 h ou mais, "Senti sua falta!".' },
{ emoji: '☀️', nome: 'Bom dia com lembrança', texto: 'No primeiro encontro do dia lembra de ontem: "ontem a gente ficou acordado até as 2h, hein!".' },
{ emoji: '🥱', nome: 'Acordar grogue', texto: 'Depois de dormir um tempo, acorda com os olhos pesados, lento, e boceja antes de ficar esperto.' },
{ emoji: '💭', nome: 'Sonhar', texto: 'Dormindo, às vezes aparece bem fraco um balão com algo do dia (a bola, as estrelas) e ele sorri.' },
{ emoji: '🙈', nome: 'Imperfeição', texto: 'Hesita antes de alguns gestos, erra no meio da atividade e fica sem graça, e às vezes larga uma atividade porque ficou chato.' },
{ emoji: '🙋', nome: 'Pedidos', texto: 'Com alguém perto, pede o que falta ("Brinca comigo?", "Me faz um carinho?") ou mostra a figurinha que desenhou. No máximo um pedido a cada 20 min.' },
{ emoji: '👁️', nome: 'Contato visual', texto: 'Com o rastreamento de rosto, olha nos olhos alguns segundos, desvia um instante e volta.' },
{ emoji: '😊', nome: 'Sorriso de volta', texto: 'Com o Ao vivo aberto e o rastreamento de rosto ligado, se você sorri ele sorri (ou ri) de volta.' },
{ emoji: '💡', nome: 'Luz', texto: 'Apagaram a luz: boceja e fica com sono. Acenderam de repente: aperta os olhos.' },
{ emoji: '🧬', nome: 'Personalidade', texto: 'Curioso, brincalhão e tímido em medidas próprias. Muda o jeito de tudo e amadurece: carinho deixa menos tímido, brincar deixa mais brincalhão.' },
],
},
ideias: { ideias: {
titulo: 'Ideias do dia', titulo: 'Ideias do dia',
emoji: '💡', emoji: '💡',
@@ -371,13 +402,13 @@ export const TOPICOS: Record<string, TopicoGuia> = {
/** Ordem do tutorial (página Guia). */ /** Ordem do tutorial (página Guia). */
export const ORDEM_GUIA = [ export const ORDEM_GUIA = [
'inicio', 'vida', 'aprendizado', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'atividades', 'criar', 'ideias', 'aparencia', 'inicio', 'vida', 'aprendizado', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'atividades', 'jeito', 'console', 'criar', 'ideias', 'aparencia',
'personalidade', 'acontecimentos', 'ligacoes', 'eventos', 'presenca', 'tela', 'sensores', 'sensibilidade', 'personalidade', 'acontecimentos', 'ligacoes', 'eventos', 'presenca', 'tela', 'sensores', 'sensibilidade',
'reacoes', 'conversa', 'movimento', 'atalhos', 'reacoes', 'conversa', 'movimento', 'atalhos',
]; ];
/** Tópico de cada tela, para o ⓘ do topo. */ /** Tópico de cada tela, para o ⓘ do topo. */
export const TOPICO_DA_TELA: Record<string, string> = { export const TOPICO_DA_TELA: Record<string, string> = {
'ao-vivo': 'inicio', eventos: 'eventos', conversa: 'conversa', reacoes: 'reacoes', 'ao-vivo': 'inicio', console: 'console', eventos: 'eventos', conversa: 'conversa', reacoes: 'reacoes',
movimento: 'movimento', ajustes: 'tela', rosto: 'aparencia', guia: 'inicio', criar: 'criar', 'dia-a-dia': 'aprendizado', movimento: 'movimento', ajustes: 'tela', rosto: 'aparencia', guia: 'inicio', criar: 'criar', 'dia-a-dia': 'aprendizado',
}; };
+2
View File
@@ -18,6 +18,8 @@ export interface AlvoRastreio {
x: number; x: number;
y: number; y: number;
atividade?: boolean; atividade?: boolean;
/** 0 a 1: o quanto a pessoa está sorrindo (só rosto; o celular sorri de volta) */
sorriso?: number;
} }
const valido = (p: Ponto | undefined) => !!p && Number.isFinite(p.x) && Number.isFinite(p.y); const valido = (p: Ponto | undefined) => !!p && Number.isFinite(p.x) && Number.isFinite(p.y);
+2
View File
@@ -11,6 +11,7 @@ interface Resultado {
hands: Ponto[][]; hands: Ponto[][];
ms: number; ms: number;
timestamp: number; timestamp: number;
sorriso?: number | null;
} }
const LIGACOES = [[0, 1], [1, 2], [2, 3], [3, 4], [0, 5], [5, 6], [6, 7], [7, 8], [5, 9], [9, 10], [10, 11], [11, 12], const LIGACOES = [[0, 1], [1, 2], [2, 3], [3, 4], [0, 5], [5, 6], [6, 7], [7, 8], [5, 9], [9, 10], [10, 11], [11, 12],
@@ -146,6 +147,7 @@ export class Rastreamento {
const anterior = this.alvo; const anterior = this.alvo;
const alvo = escolherAlvo(r.faces, r.hands, anterior); const alvo = escolherAlvo(r.faces, r.hands, anterior);
if (alvo) alvo.atividade = !!anterior && alvo.tipo === anterior.tipo && Math.hypot(alvo.x - anterior.x, alvo.y - anterior.y) > 0.008; if (alvo) alvo.atividade = !!anterior && alvo.tipo === anterior.tipo && Math.hypot(alvo.x - anterior.x, alvo.y - anterior.y) > 0.008;
if (alvo && alvo.tipo === 'rosto' && typeof r.sorriso === 'number') alvo.sorriso = Math.round(r.sorriso * 100) / 100;
this.alvo = alvo; this.alvo = alvo;
this.alvoEm = r.timestamp; this.alvoEm = r.timestamp;
this.intervalo = Math.max(50, Math.min(200, r.ms * 1.2)); this.intervalo = Math.max(50, Math.min(200, r.ms * 1.2));
+2 -1
View File
@@ -1,9 +1,10 @@
/** Rotas por hash (#/conversa): o servidor só precisa entregar index.html em "/". */ /** Rotas por hash (#/conversa): o servidor só precisa entregar index.html em "/". */
export type Rota = 'ao-vivo' | 'eventos' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto' | 'guia' | 'criar' | 'dia-a-dia'; export type Rota = 'ao-vivo' | 'eventos' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto' | 'guia' | 'criar' | 'dia-a-dia' | 'console';
export const ROTAS: { id: Rota; titulo: string; atalho: string }[] = [ export const ROTAS: { id: Rota; titulo: string; atalho: string }[] = [
{ id: 'ao-vivo', titulo: 'Ao vivo', atalho: 'g v' }, { id: 'ao-vivo', titulo: 'Ao vivo', atalho: 'g v' },
{ id: 'console', titulo: 'Console', atalho: 'g k' },
{ id: 'eventos', titulo: 'Eventos', atalho: 'g e' }, { id: 'eventos', titulo: 'Eventos', atalho: 'g e' },
{ id: 'conversa', titulo: 'Conversa', atalho: 'g c' }, { id: 'conversa', titulo: 'Conversa', atalho: 'g c' },
{ id: 'reacoes', titulo: 'Reações', atalho: 'g r' }, { id: 'reacoes', titulo: 'Reações', atalho: 'g r' },
+9 -1
View File
@@ -104,7 +104,15 @@ export interface Vida {
sono_profundo?: boolean; sono_profundo?: boolean;
/** '' quando pode inventar figurinha agora; senão, o motivo. */ /** '' quando pode inventar figurinha agora; senão, o motivo. */
pode_criar: string; pode_criar: string;
diario: { horario: number; tipo: 'acao' | 'sono' | 'emoji' | 'cuidado' | 'vida' | 'atividade' | 'aprendizado' | 'aprendi'; texto: string }[]; diario: { horario: number; tipo: 'acao' | 'sono' | 'emoji' | 'cuidado' | 'vida' | 'atividade' | 'aprendizado' | 'aprendi' | 'social' | 'pedido' | 'ideia'; texto: string }[];
/** Traços de 0 a 1 (curiosidade, brincadeira, timidez): mudam o jeito e amadurecem com o convívio. */
personalidade?: Record<string, number>;
/** Motivo da última decisão do modo vida. */
porque?: string;
/** A última decisão de fato (o que fez e por quê). */
ultima?: { horario: number; texto: string; porque: string } | null;
/** Segundos até a próxima decisão. */
proximo_ato_em?: number;
} }
/** O que ele aprendeu com o dia a dia (aprendizado.py). */ /** O que ele aprendeu com o dia a dia (aprendizado.py). */
+348
View File
@@ -0,0 +1,348 @@
<script lang="ts">
import { Pause, Play, Search, Trash2 } from '@lucide/svelte';
import { onMount } from 'svelte';
import { pedir } from '../lib/api';
import { avisos } from '../lib/avisos.svelte';
import { estado, ouvir } from '../lib/estado.svelte';
// Console: tudo o que ele está fazendo e por quê, ao vivo. Em cima o "agora"; embaixo cada decisão
// com o motivo (modo vida, sentidos, celular, IA local, aprendizado, ideias do dia).
type Origem = 'vida' | 'sentidos' | 'celular' | 'ia' | 'aprendizado' | 'ideias' | 'sistema';
interface Linha { id: number; horario: number; origem: Origem; texto: string; motivo: string; nivel: 'info' | 'detalhe' }
const ORIGENS: { id: Origem; nome: string }[] = [
{ id: 'vida', nome: 'Modo vida' }, { id: 'sentidos', nome: 'Sentidos' }, { id: 'celular', nome: 'Celular' },
{ id: 'ia', nome: 'IA local' }, { id: 'aprendizado', nome: 'Aprendizado' }, { id: 'ideias', nome: 'Ideias' },
{ id: 'sistema', nome: 'Sistema' },
];
const MAX = 600;
let linhas = $state<Linha[]>([]);
let pausado = $state(false);
let guardadas = $state<Linha[]>([]); // chegaram com o console pausado
let detalhes = $state(false);
let busca = $state('');
let ligadas = $state<Record<Origem, boolean>>({ vida: true, sentidos: true, celular: true, ia: true, aprendizado: true, ideias: true, sistema: true });
let vidaEm = $state(Date.now());
onMount(() => {
pedir<{ linhas: Linha[] }>('/console').then((d) => (linhas = d.linhas.slice(-MAX).reverse())).catch((e) => avisos.erro(e));
return ouvir('console', (l: Linha) => {
if (pausado) guardadas = [l, ...guardadas].slice(0, MAX);
else linhas = [l, ...linhas].slice(0, MAX);
});
});
$effect(() => {
estado.vida; // a contagem do próximo ato parte de quando o status chegou
vidaEm = Date.now();
});
function alternarPausa() {
pausado = !pausado;
if (!pausado && guardadas.length) {
linhas = [...guardadas, ...linhas].slice(0, MAX);
guardadas = [];
}
}
const visiveis = $derived.by(() => {
const q = busca.trim().toLowerCase();
return linhas.filter((l) => ligadas[l.origem] && (detalhes || l.nivel !== 'detalhe') &&
(!q || (l.texto + ' ' + l.motivo).toLowerCase().includes(q)));
});
const hora = (s: number) => new Date(s * 1000).toLocaleTimeString('pt-BR');
// ---------------------------------------------------------------- agora
const v = $derived(estado.vida);
const cel = $derived(estado.app);
const proximo = $derived(v?.proximo_ato_em != null ? Math.max(0, Math.round(v.proximo_ato_em - (estado.agora - vidaEm) / 1000)) : null);
const fazendo = $derived.by(() => {
if (!cel?.online) return 'celular desconectado';
const e = cel.estado ?? {};
if (e.atividade) return 'fazendo: ' + e.atividade;
if (e.dormindo) return v?.sono_profundo ? 'dormindo (sono pesado)' : 'dormindo';
return 'acordado, de bobeira';
});
const noite = $derived(estado.appConfig ? `${estado.appConfig.noiteInicio ?? 23}h às ${estado.appConfig.noiteFim ?? 7}h` : '');
const NEC: { id: 'energia' | 'alegria' | 'social' | 'diversao'; nome: string }[] = [
{ id: 'energia', nome: 'Energia' }, { id: 'alegria', nome: 'Alegria' }, { id: 'social', nome: 'Social' }, { id: 'diversao', nome: 'Diversão' },
];
const TRACO: Record<string, string> = { curiosidade: 'Curioso', brincadeira: 'Brincalhão', timidez: 'Tímido' };
</script>
<div class="console">
<section class="agora" aria-label="O que ele está fazendo agora">
<div class="quem">
<span class="emoji" aria-hidden="true">{v?.emoji ?? '…'}</span>
<div>
<strong>{v?.nome_humor ?? 'Carregando'}</strong>
<small>{fazendo}</small>
</div>
</div>
<div class="necessidades">
{#each NEC as n (n.id)}
{@const valor = v?.necessidades[n.id] ?? 0}
<div class="nec">
<span>{n.nome}</span>
<span class="barra" aria-hidden="true"><span style:width="{valor}%" class:baixa={valor < 35}></span></span>
<span class="mono">{valor}</span>
</div>
{/each}
</div>
<dl class="fatos">
<div><dt>Próxima decisão</dt><dd class="mono">{!v?.config.ligado ? 'modo vida desligado' : proximo === null ? '—' : `em ${proximo} s`}</dd></div>
<div><dt>Última decisão</dt><dd>{#if v?.ultima}{v.ultima.texto}<small class="porque">{hora(v.ultima.horario)}{v.ultima.porque ? ' · ' + v.ultima.porque : ''}</small>{:else}—{/if}</dd></div>
<div><dt>Presença</dt><dd>{estado.pir?.ativo ? 'sensor vendo alguém' : estado.pir?.presenca?.estado === 'ausente' ? 'ninguém por perto' : 'ninguém agora'}</dd></div>
<div><dt>Madrugada</dt><dd>{noite}</dd></div>
{#if v?.personalidade}
<div><dt>Jeito</dt><dd>{Object.entries(v.personalidade).map(([k, x]) => `${TRACO[k] ?? k} ${Math.round(x * 100)}%`).join(' · ')}</dd></div>
{/if}
</dl>
</section>
<section class="terminal" aria-label="Registro ao vivo">
<div class="barra-ferramentas">
<div class="filtros" role="group" aria-label="Mostrar">
{#each ORIGENS as o (o.id)}
<button class="chip o-{o.id}" aria-pressed={ligadas[o.id]} onclick={() => (ligadas[o.id] = !ligadas[o.id])}>{o.nome}</button>
{/each}
</div>
<div class="acoes">
<label class="busca"><Search size={14} /><input type="search" placeholder="Procurar" bind:value={busca} aria-label="Procurar no console" /></label>
<label class="detalhes"><input type="checkbox" bind:checked={detalhes} /> Detalhes</label>
<button class="btn pequeno" onclick={alternarPausa} aria-pressed={pausado}>
{#if pausado}<Play size={14} /> Continuar{guardadas.length ? ` (${guardadas.length})` : ''}{:else}<Pause size={14} /> Pausar{/if}
</button>
<button class="btn pequeno fantasma" onclick={() => { linhas = []; guardadas = []; }} aria-label="Limpar a tela"><Trash2 size={14} /></button>
</div>
</div>
<ol class="linhas" aria-live="polite">
{#each visiveis as l (l.id)}
<li class:detalhe={l.nivel === 'detalhe'}>
<time class="mono">{hora(l.horario)}</time>
<span class="tag o-{l.origem}">{ORIGENS.find((o) => o.id === l.origem)?.nome ?? l.origem}</span>
<span class="texto">{l.texto}{#if l.motivo}<span class="motivo">porque {l.motivo}</span>{/if}</span>
</li>
{:else}
<li class="vazio">Nada por aqui ainda. Cada decisão dele aparece aqui na hora, com o motivo.</li>
{/each}
</ol>
</section>
</div>
<style>
.console {
display: grid;
gap: var(--e-4);
}
.agora {
display: grid;
gap: var(--e-4);
padding: var(--e-4);
border: 1px solid var(--borda);
border-radius: var(--raio-lg);
background: var(--superficie);
}
@media (min-width: 1024px) {
.agora {
grid-template-columns: minmax(180px, 0.8fr) minmax(220px, 1fr) minmax(0, 2fr);
align-items: start;
}
}
.quem {
display: flex;
gap: var(--e-3);
align-items: center;
}
.quem .emoji {
font-size: 2.4rem;
line-height: 1;
}
.quem strong {
display: block;
font-size: var(--t-lg);
}
.quem small {
color: var(--texto-2);
}
.necessidades {
display: grid;
gap: 6px;
}
.nec {
display: grid;
grid-template-columns: 64px 1fr 28px;
gap: var(--e-2);
align-items: center;
font-size: var(--t-xs);
color: var(--texto-2);
}
.barra {
height: 6px;
border-radius: var(--raio-pill);
background: var(--superficie-3);
overflow: hidden;
}
.barra span {
display: block;
height: 100%;
background: var(--destaque);
transition: width var(--transicao);
}
.barra span.baixa {
background: var(--aviso);
}
.nec .mono {
text-align: right;
font-variant-numeric: tabular-nums;
}
.fatos {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--e-2) var(--e-4);
margin: 0;
}
.fatos div {
min-width: 0;
}
.fatos dt {
font-size: var(--t-xs);
color: var(--texto-3);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.fatos dd {
margin: 2px 0 0;
font-size: var(--t-sm);
overflow-wrap: anywhere;
}
.porque {
display: block;
color: var(--texto-3);
font-size: var(--t-xs);
}
.terminal {
border: 1px solid var(--borda);
border-radius: var(--raio-lg);
background: var(--fundo-2);
overflow: hidden;
}
.barra-ferramentas {
display: flex;
flex-wrap: wrap;
gap: var(--e-2) var(--e-3);
justify-content: space-between;
align-items: center;
padding: var(--e-3);
border-bottom: 1px solid var(--borda);
background: var(--superficie);
}
.filtros, .acoes {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.chip {
min-height: 28px;
padding: 0 var(--e-3);
border: 1px solid var(--borda);
border-radius: var(--raio-pill);
background: transparent;
color: var(--texto-3);
font: 500 var(--t-xs) var(--fonte);
cursor: pointer;
}
.chip[aria-pressed='true'] {
color: var(--texto);
border-color: var(--cor);
background: color-mix(in srgb, var(--cor) 14%, transparent);
}
.busca {
display: inline-flex;
gap: 6px;
align-items: center;
padding: 0 var(--e-2);
border: 1px solid var(--borda);
border-radius: var(--raio-sm);
color: var(--texto-3);
}
.busca input {
width: 140px;
min-height: 28px;
border: 0;
background: transparent;
color: var(--texto);
font: inherit;
font-size: var(--t-xs);
outline: none;
}
.detalhes {
display: inline-flex;
gap: 4px;
align-items: center;
font-size: var(--t-xs);
color: var(--texto-2);
}
.linhas {
list-style: none;
margin: 0;
padding: var(--e-2) 0;
max-height: min(70vh, 900px);
overflow-y: auto;
font-size: var(--t-sm);
}
.linhas li {
display: grid;
grid-template-columns: 72px 104px minmax(0, 1fr);
gap: var(--e-3);
align-items: baseline;
padding: 5px var(--e-4);
border-bottom: 1px solid color-mix(in srgb, var(--borda) 45%, transparent);
}
@media (max-width: 640px) {
.linhas li {
grid-template-columns: 64px minmax(0, 1fr);
}
.linhas li .texto {
grid-column: 1 / -1;
}
}
.linhas li.detalhe {
opacity: 0.6;
}
.linhas li.vazio {
display: block;
color: var(--texto-3);
}
time {
color: var(--texto-3);
font-size: var(--t-xs);
font-variant-numeric: tabular-nums;
}
.tag {
justify-self: start;
padding: 1px 8px;
border-radius: var(--raio-pill);
font-size: 11px;
font-weight: 600;
color: color-mix(in srgb, var(--cor) 70%, var(--texto));
background: color-mix(in srgb, var(--cor) 16%, transparent);
white-space: nowrap;
}
.texto {
overflow-wrap: anywhere;
}
.motivo {
display: block;
margin-top: 1px;
font-size: var(--t-xs);
color: var(--texto-3);
}
.o-vida { --cor: var(--destaque); }
.o-sentidos { --cor: var(--ok); }
.o-celular { --cor: #7aa2ff; }
.o-ia { --cor: #c084fc; }
.o-aprendizado { --cor: #fb923c; }
.o-ideias { --cor: var(--aviso); }
.o-sistema { --cor: var(--texto-3); }
</style>