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:
@@ -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" />
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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());
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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
@@ -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'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)."""
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,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()
|
||||||
@@ -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,18 +621,19 @@ 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
|
||||||
|
if anotar:
|
||||||
self.anotar('acao', 'Lembrou da reação "' + r['receita'].get('nome', '?') + '"', agora)
|
self.anotar('acao', 'Lembrou da reação "' + r['receita'].get('nome', '?') + '"', agora)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -486,23 +641,28 @@ class Vida:
|
|||||||
"""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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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). */
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user