feat(vida): aprendizado com o dia a dia (gosto, rotina da casa e sono)
O Carinha passa a melhorar as interações com o que vive em casa, sem IA e sem internet (aprendizado.py): - gosto: gesto ou atividade feito com alguém por perto é uma tentativa; carinho, brincadeira ou conversa até 60 s depois contam como "gostaram" (cutucada vale meio). A chance de cada ato vai de 0,5x a 2x, relativa à média, e só depois de 3 tentativas; - rotina: fração de minutos com gente em cada hora (média móvel entre dias). Antes de uma hora movimentada, acorda do cochilo 15 min antes; sono pesado e madrugada continuam respeitados; - sono: PIR acordou e ninguém ficou nem interagiu em 3 min = à toa. Se repete na mesma hora, ali pede 1 ou 2 disparos a mais para acordar; - à meia-noite fecha o dia: lições em primeira pessoa no diário e o passado pesa 15% menos a cada dia (o gosto muda); - rotas /aprendizado/status|config|esquecer e SSE 'aprendizado'; - painel: cartão "O que ele aprendeu" no Ao vivo (rotina em 24 barras, faz mais / faz menos, lições, esquecer com confirmação) e tópico no guia. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+363
@@ -0,0 +1,363 @@
|
|||||||
|
"""Aprendizado: o Carinha melhora as interações com o que vive no dia a dia.
|
||||||
|
|
||||||
|
Sem IA e sem internet: só conta o que acontece em casa e ajusta o modo vida aos poucos.
|
||||||
|
- Gosto: cada gesto ou atividade feito com alguém por perto vira uma tentativa; um carinho, uma
|
||||||
|
brincadeira ou uma conversa logo depois conta como "gostaram". Os que agradam saem mais, os
|
||||||
|
ignorados saem menos (no máximo o dobro ou a metade da chance).
|
||||||
|
- Rotina: em que horas costuma ter gente por perto. Um pouco antes da hora de sempre, ele acorda
|
||||||
|
de um cochilo para estar de pé quando a pessoa chegar.
|
||||||
|
- Sono: quando o sensor acorda o rosto e ninguém fica nem interage, foi à toa. Se isso se repete
|
||||||
|
na mesma hora, ele passa a pedir mais movimento para acordar nesse horário.
|
||||||
|
À meia-noite ele fecha o dia: o que aprendeu vira lição no diário e o que ficou para trás pesa
|
||||||
|
menos (o gosto das pessoas muda). "Esquecer" zera tudo.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from comum import gravar_atomico, ler_json
|
||||||
|
|
||||||
|
PADRAO = dict(ligado=True)
|
||||||
|
JANELA = 60 # s depois de um ato em que a resposta de alguém conta para ele
|
||||||
|
PESO_RESPOSTA = {'carinho': 1.0, 'brincar': 1.0, 'conversa': 1.0, 'alimentar': 0.6, 'cutucar': 0.5}
|
||||||
|
ESQUECE = 0.85 # todo dia, o que aprendeu antes pesa um pouco menos
|
||||||
|
MEMORIA_ROTINA = 0.3 # quanto um dia novo pesa na rotina aprendida
|
||||||
|
MIN_TENTATIVAS = 3 # com menos que isso, ainda não tem opinião sobre um ato
|
||||||
|
FAIXA = (0.5, 2.0) # quanto o gosto pode mudar a chance de um ato
|
||||||
|
FALSO_ALARME = 180 # s que esperar para ver se acordar pelo sensor valeu a pena
|
||||||
|
PRESENCA_FICOU = 0.3 # fração da espera com alguém por perto para o acordar ter valido
|
||||||
|
MIN_MINUTOS_HORA = 10 # minutos observados numa hora para ela entrar na rotina
|
||||||
|
ROTINA_CHEIA, ROTINA_VAZIA = 0.5, 0.3
|
||||||
|
AVISO_CHEGADA = 15 * 60 # s antes da hora movimentada em que ele acorda do cochilo
|
||||||
|
MAX_LICOES = 30
|
||||||
|
|
||||||
|
NOMES = {
|
||||||
|
'alegre': 'saltitar', 'musica': 'cantarolar', 'feliz': 'sorrir', 'piscadinha': 'piscadinha',
|
||||||
|
'coracao': 'coraçãozinho', 'rir': 'dar risada', 'comemorar': 'comemorar', 'olhar_esq': 'olhar em volta',
|
||||||
|
'olhar_dir': 'olhar em volta', 'piscar2': 'piscar devagar', 'pensar': 'ficar pensativo', 'timido': 'ficar tímido',
|
||||||
|
'espiar': 'espiar', 'bocejar': 'bocejar', 'confuso': 'ficar confuso', 'triste': 'ficar tristinho',
|
||||||
|
'olhar_baixo': 'olhar para baixo', 'acenar': 'acenar', 'surpreso': 'fazer cara de "oi!"',
|
||||||
|
'esconder': 'esconder o rosto', 'bravo': 'ficar emburrado', 'tonto': 'ficar tonto',
|
||||||
|
'ler': 'ler', 'cafe': 'tomar café', 'meditar': 'meditar', 'telescopio': 'olhar as estrelas', 'pescar': 'pescar',
|
||||||
|
'computador': 'mexer no computador', 'regar': 'regar a plantinha', 'bola': 'jogar bola', 'malhar': 'malhar',
|
||||||
|
'corda': 'pular corda', 'skate': 'andar de skate', 'dancar': 'dançar', 'pipa': 'soltar pipa', 'pintar': 'pintar',
|
||||||
|
'violao': 'tocar violão', 'cantar': 'cantar', 'cozinhar': 'cozinhar', 'bolhas': 'fazer bolhas de sabão',
|
||||||
|
'videogame': 'jogar videogame', 'dormir': 'cochilar',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _dia(agora):
|
||||||
|
return time.strftime('%Y-%m-%d', time.localtime(agora))
|
||||||
|
|
||||||
|
|
||||||
|
def _hora(agora):
|
||||||
|
return time.localtime(agora).tm_hour
|
||||||
|
|
||||||
|
|
||||||
|
def _faixas(horas):
|
||||||
|
"""[18, 19, 20, 22] -> [(18, 21), (22, 23)]: horas seguidas viram uma faixa (fim aberto)."""
|
||||||
|
faixas = []
|
||||||
|
for h in sorted(horas):
|
||||||
|
if faixas and faixas[-1][1] == h:
|
||||||
|
faixas[-1] = (faixas[-1][0], h + 1)
|
||||||
|
else:
|
||||||
|
faixas.append((h, h + 1))
|
||||||
|
return faixas
|
||||||
|
|
||||||
|
|
||||||
|
def _texto_faixas(faixas):
|
||||||
|
partes = ['das %dh às %dh' % (a, b % 24) for a, b in faixas]
|
||||||
|
return partes[0] if len(partes) == 1 else ', '.join(partes[:-1]) + ' e ' + partes[-1]
|
||||||
|
|
||||||
|
|
||||||
|
class Aprendizado:
|
||||||
|
def __init__(self, caminho, nomes=None):
|
||||||
|
self.caminho = Path(caminho)
|
||||||
|
self.lock = threading.RLock()
|
||||||
|
self.publicar = lambda tipo, dados: None
|
||||||
|
self.anotar = lambda tipo, texto, agora=None: None # diário do modo vida
|
||||||
|
self.nomes = nomes or (lambda ident: None) # nome das atividades criadas por IA
|
||||||
|
self.config = dict(PADRAO)
|
||||||
|
self.itens = {} # nome -> {'t': tentativas, 's': agradou}
|
||||||
|
self.rotina = [None] * 24 # chance de ter gente por perto em cada hora
|
||||||
|
self.falsos = [0.0] * 24 # acordou à toa nessa hora (média dos dias)
|
||||||
|
self.dias = 0
|
||||||
|
self.licoes = []
|
||||||
|
self.rotina_avisada = []
|
||||||
|
self.hoje = self._dia_vazio(_dia(time.time()))
|
||||||
|
self.pendentes = [] # atos esperando resposta: (nome, horario)
|
||||||
|
self.acordou_em = None # acordou pelo sensor: esperando para ver se valeu
|
||||||
|
self.presenca_depois = [0, 0] # (com gente, total) nas observações depois de acordar
|
||||||
|
self.minuto = None
|
||||||
|
self.salvo_em = 0.0
|
||||||
|
try:
|
||||||
|
salvo = json.loads(self.caminho.read_text(encoding='utf-8'))
|
||||||
|
self.config.update(self.validar(salvo.get('config', {})))
|
||||||
|
self.itens = {k: dict(t=float(v['t']), s=float(v['s'])) for k, v in salvo.get('itens', {}).items()}
|
||||||
|
if len(salvo.get('rotina', [])) == 24:
|
||||||
|
self.rotina = [None if v is None else float(v) for v in salvo['rotina']]
|
||||||
|
if len(salvo.get('falsos', [])) == 24:
|
||||||
|
self.falsos = [float(v) for v in salvo['falsos']]
|
||||||
|
self.dias = int(salvo.get('dias', 0))
|
||||||
|
self.licoes = [l for l in salvo.get('licoes', []) if isinstance(l, dict)][:MAX_LICOES]
|
||||||
|
self.rotina_avisada = list(salvo.get('rotina_avisada', []))
|
||||||
|
if isinstance(salvo.get('hoje'), dict) and salvo['hoje'].get('dia'):
|
||||||
|
self.hoje.update(salvo['hoje'])
|
||||||
|
except (OSError, ValueError, TypeError, KeyError, AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _dia_vazio(dia):
|
||||||
|
return dict(dia=dia, observado=[0] * 24, presenca=[0] * 24, falsos=[0] * 24, acordou=[0] * 24,
|
||||||
|
atos=0, respostas={}, contagem={})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validar(dados):
|
||||||
|
if not isinstance(dados, dict):
|
||||||
|
raise ValueError('Esperava um objeto')
|
||||||
|
for chave, valor in dados.items():
|
||||||
|
if chave not in PADRAO:
|
||||||
|
raise ValueError('Ajuste desconhecido: ' + chave)
|
||||||
|
if not isinstance(valor, bool):
|
||||||
|
raise ValueError(chave + ' precisa ser verdadeiro ou falso')
|
||||||
|
return dict(dados)
|
||||||
|
|
||||||
|
def configurar(self, dados):
|
||||||
|
limpo = self.validar(dados)
|
||||||
|
with self.lock:
|
||||||
|
self.config.update(limpo)
|
||||||
|
self.salvar()
|
||||||
|
self.avisar()
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def esquecer(self):
|
||||||
|
with self.lock:
|
||||||
|
self.itens, self.rotina, self.falsos = {}, [None] * 24, [0.0] * 24
|
||||||
|
self.dias, self.licoes, self.rotina_avisada = 0, [], []
|
||||||
|
self.hoje = self._dia_vazio(_dia(time.time()))
|
||||||
|
self.pendentes, self.acordou_em = [], None
|
||||||
|
self.anotar('aprendizado', 'Esqueceu tudo o que tinha aprendido e começou do zero')
|
||||||
|
self.salvar()
|
||||||
|
self.avisar()
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def salvar(self):
|
||||||
|
with self.lock:
|
||||||
|
dados = dict(config=self.config, itens=self.itens, rotina=self.rotina, falsos=self.falsos, dias=self.dias,
|
||||||
|
licoes=self.licoes, rotina_avisada=self.rotina_avisada, hoje=self.hoje)
|
||||||
|
self.salvo_em = time.time()
|
||||||
|
try:
|
||||||
|
gravar_atomico(self.caminho, dados)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ o que acontece no dia
|
||||||
|
def ligado(self):
|
||||||
|
return self.config['ligado']
|
||||||
|
|
||||||
|
def observar(self, agora, perto):
|
||||||
|
"""A cada tick do modo vida: conta os minutos com gente por perto e fecha o dia à meia-noite."""
|
||||||
|
if not self.ligado():
|
||||||
|
return
|
||||||
|
self.virar_dia(agora)
|
||||||
|
with self.lock:
|
||||||
|
self.pendentes = [(n, t) for n, t in self.pendentes if agora - t <= JANELA]
|
||||||
|
if self.acordou_em is not None:
|
||||||
|
self.presenca_depois[0] += 1 if perto else 0
|
||||||
|
self.presenca_depois[1] += 1
|
||||||
|
if agora - self.acordou_em >= FALSO_ALARME:
|
||||||
|
com_gente, total = self.presenca_depois
|
||||||
|
if total and com_gente / total < PRESENCA_FICOU:
|
||||||
|
self.hoje['falsos'][_hora(self.acordou_em)] += 1
|
||||||
|
self.acordou_em = None
|
||||||
|
minuto = int(agora // 60)
|
||||||
|
if minuto == self.minuto:
|
||||||
|
return
|
||||||
|
self.minuto = minuto
|
||||||
|
h = _hora(agora)
|
||||||
|
self.hoje['observado'][h] += 1
|
||||||
|
if perto:
|
||||||
|
self.hoje['presenca'][h] += 1
|
||||||
|
if agora - self.salvo_em > 300:
|
||||||
|
self.salvar()
|
||||||
|
|
||||||
|
def ato(self, nome, agora, perto):
|
||||||
|
"""Ele fez um gesto ou uma atividade. Só conta com plateia: sozinho ninguém pode gostar."""
|
||||||
|
if not self.ligado() or not perto or not nome:
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
self.pendentes.append((nome, agora))
|
||||||
|
self.hoje['atos'] += 1
|
||||||
|
item = self.itens.setdefault(nome, dict(t=0.0, s=0.0))
|
||||||
|
item['t'] += 1
|
||||||
|
c = self.hoje['contagem'].setdefault(nome, [0, 0.0])
|
||||||
|
c[0] += 1
|
||||||
|
|
||||||
|
def resposta(self, tipo, agora):
|
||||||
|
"""Carinho, brincadeira, conversa...: o último ato de pouco antes agradou."""
|
||||||
|
if not self.ligado() or tipo not in PESO_RESPOSTA:
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
self.hoje['respostas'][tipo] = self.hoje['respostas'].get(tipo, 0) + 1
|
||||||
|
if self.acordou_em is not None: # alguém quis ele acordado: valeu a pena
|
||||||
|
self.acordou_em = None
|
||||||
|
recentes = [(n, t) for n, t in self.pendentes if agora - t <= JANELA]
|
||||||
|
if not recentes:
|
||||||
|
return
|
||||||
|
nome, _ = recentes[-1]
|
||||||
|
self.pendentes = [p for p in self.pendentes if p[0] != nome] # cada ato agrada uma vez
|
||||||
|
item = self.itens.setdefault(nome, dict(t=1.0, s=0.0))
|
||||||
|
item['s'] = min(item['t'], item['s'] + PESO_RESPOSTA[tipo])
|
||||||
|
c = self.hoje['contagem'].setdefault(nome, [1, 0.0])
|
||||||
|
c[1] += PESO_RESPOSTA[tipo]
|
||||||
|
|
||||||
|
def acordou_pelo_sensor(self, agora):
|
||||||
|
if not self.ligado():
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
self.hoje['acordou'][_hora(agora)] += 1
|
||||||
|
self.acordou_em = agora
|
||||||
|
self.presenca_depois = [0, 0]
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ o que muda no comportamento
|
||||||
|
def peso(self, nome):
|
||||||
|
"""Multiplica a chance de um ato: acima de 1 se costuma agradar, abaixo se é ignorado."""
|
||||||
|
if not self.ligado():
|
||||||
|
return 1.0
|
||||||
|
with self.lock:
|
||||||
|
item = self.itens.get(nome)
|
||||||
|
if not item or item['t'] < MIN_TENTATIVAS:
|
||||||
|
return 1.0
|
||||||
|
total_t = sum(i['t'] for i in self.itens.values())
|
||||||
|
total_s = sum(i['s'] for i in self.itens.values())
|
||||||
|
media = (total_s + 1) / (total_t + 2)
|
||||||
|
taxa = (item['s'] + 1) / (item['t'] + 2)
|
||||||
|
return round(max(FAIXA[0], min(FAIXA[1], taxa / media)), 2)
|
||||||
|
|
||||||
|
def extra_pir(self, agora):
|
||||||
|
"""Disparos a mais do PIR para acordar nesta hora (acordou à toa aqui outras vezes)."""
|
||||||
|
return self._extra(self.falsos[_hora(agora)]) if self.ligado() else 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extra(falsos):
|
||||||
|
return 2 if falsos >= 1.5 else 1 if falsos >= 0.5 else 0
|
||||||
|
|
||||||
|
def chega_logo(self, agora):
|
||||||
|
"""Falta pouco para a hora em que costuma ter gente (e agora costuma estar vazio)?"""
|
||||||
|
if not self.ligado():
|
||||||
|
return False
|
||||||
|
t = time.localtime(agora)
|
||||||
|
prox, atual = self.rotina[(t.tm_hour + 1) % 24], self.rotina[t.tm_hour]
|
||||||
|
falta = 3600 - (t.tm_min * 60 + t.tm_sec)
|
||||||
|
return (prox is not None and prox >= ROTINA_CHEIA and (atual is None or atual < ROTINA_VAZIA)
|
||||||
|
and falta <= AVISO_CHEGADA)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ fechar o dia
|
||||||
|
def virar_dia(self, agora):
|
||||||
|
dia = _dia(agora)
|
||||||
|
if self.hoje['dia'] == dia:
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
licoes = self.fechar_dia()
|
||||||
|
self.hoje = self._dia_vazio(dia)
|
||||||
|
self.pendentes, self.acordou_em = [], None
|
||||||
|
for texto in licoes:
|
||||||
|
self.anotar('aprendizado', texto, agora)
|
||||||
|
self.salvar()
|
||||||
|
self.avisar()
|
||||||
|
|
||||||
|
def nome(self, ident):
|
||||||
|
if ident.startswith('criado:'):
|
||||||
|
try:
|
||||||
|
nome = self.nomes(int(ident[7:]))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
nome = None
|
||||||
|
return 'fazer ' + nome if nome else 'fazer uma atividade nova'
|
||||||
|
return NOMES.get(ident, ident)
|
||||||
|
|
||||||
|
def fechar_dia(self):
|
||||||
|
"""Junta o dia na memória e devolve as lições (frases curtas, em primeira pessoa)."""
|
||||||
|
hoje, licoes = self.hoje, []
|
||||||
|
# rotina: média móvel da fração de minutos com gente em cada hora
|
||||||
|
for h in range(24):
|
||||||
|
if hoje['observado'][h] >= MIN_MINUTOS_HORA:
|
||||||
|
frac = hoje['presenca'][h] / hoje['observado'][h]
|
||||||
|
antes = self.rotina[h]
|
||||||
|
self.rotina[h] = round(frac if antes is None else antes + (frac - antes) * MEMORIA_ROTINA, 3)
|
||||||
|
self.falsos[h] = round(self.falsos[h] + (hoje['falsos'][h] - self.falsos[h]) * MEMORIA_ROTINA, 3)
|
||||||
|
observou = sum(hoje['observado']) >= 60
|
||||||
|
if observou:
|
||||||
|
self.dias += 1
|
||||||
|
# gosto: o que agradou e o que foi ignorado hoje
|
||||||
|
contagem = hoje['contagem']
|
||||||
|
gostou = sorted(((s, n) for n, (t, s) in contagem.items() if s >= 1), reverse=True)
|
||||||
|
if gostou:
|
||||||
|
nomes = [self.nome(n) for _, n in gostou[:2]]
|
||||||
|
licoes.append('Aprendi que vocês gostam de me ver ' + ' e '.join(nomes) + '. Vou fazer mais!')
|
||||||
|
ignorado = sorted(((t, n) for n, (t, s) in contagem.items() if t >= 4 and s == 0), reverse=True)
|
||||||
|
if ignorado and sum(hoje['respostas'].values()):
|
||||||
|
licoes.append('Tentei %s %d vezes e ninguém ligou. Vou fazer menos.' % (self.nome(ignorado[0][1]), ignorado[0][0]))
|
||||||
|
if observou and hoje['atos'] >= 5 and not sum(hoje['respostas'].values()):
|
||||||
|
licoes.append('Hoje ninguém brincou comigo. Amanhã eu tento coisas diferentes.')
|
||||||
|
# rotina: quando passa a ter um padrão (ou ele muda), conta
|
||||||
|
if self.dias >= 2:
|
||||||
|
cheias = [h for h in range(24) if self.rotina[h] is not None and self.rotina[h] >= ROTINA_CHEIA]
|
||||||
|
if cheias and cheias != self.rotina_avisada:
|
||||||
|
licoes.append('Percebi que vocês costumam estar por perto %s. Vou estar acordado nessa hora.'
|
||||||
|
% _texto_faixas(_faixas(cheias)))
|
||||||
|
self.rotina_avisada = cheias
|
||||||
|
# sono: acordou à toa várias vezes na mesma hora
|
||||||
|
piores = sorted(((n, h) for h, n in enumerate(hoje['falsos']) if n >= 2), reverse=True)
|
||||||
|
if piores:
|
||||||
|
n, h = piores[0]
|
||||||
|
licoes.append('Acordei à toa %d vezes por volta das %dh. Nessa hora vou precisar de mais movimento para acordar.'
|
||||||
|
% (n, h))
|
||||||
|
# o gosto muda: o passado pesa menos a cada dia
|
||||||
|
for nome in list(self.itens):
|
||||||
|
item = self.itens[nome]
|
||||||
|
item['t'] = round(item['t'] * ESQUECE, 3)
|
||||||
|
item['s'] = round(item['s'] * ESQUECE, 3)
|
||||||
|
if item['t'] < 0.5:
|
||||||
|
del self.itens[nome]
|
||||||
|
for texto in licoes:
|
||||||
|
self.licoes.insert(0, dict(dia=hoje['dia'], texto=texto))
|
||||||
|
del self.licoes[MAX_LICOES:]
|
||||||
|
return licoes
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ painel
|
||||||
|
def status(self):
|
||||||
|
with self.lock:
|
||||||
|
gosto = []
|
||||||
|
for nome, item in self.itens.items():
|
||||||
|
if item['t'] >= MIN_TENTATIVAS:
|
||||||
|
gosto.append(dict(id=nome, nome=self.nome(nome), tentativas=round(item['t'], 1),
|
||||||
|
agradou=round(item['s'], 1), peso=self.peso(nome)))
|
||||||
|
gosto.sort(key=lambda g: -g['peso'])
|
||||||
|
hoje = self.hoje
|
||||||
|
return dict(config=dict(self.config), dias=self.dias, rotina=list(self.rotina),
|
||||||
|
sono=[self._extra(f) if self.ligado() else 0 for f in self.falsos],
|
||||||
|
gosto=gosto, licoes=self.licoes[:12],
|
||||||
|
hoje=dict(dia=hoje['dia'], atos=hoje['atos'], respostas=dict(hoje['respostas']),
|
||||||
|
acordou=sum(hoje['acordou']), a_toa=sum(hoje['falsos'])))
|
||||||
|
|
||||||
|
def avisar(self):
|
||||||
|
self.publicar('aprendizado', self.status())
|
||||||
|
|
||||||
|
def rotas(self, h, metodo):
|
||||||
|
rota = h.path.split('?', 1)[0]
|
||||||
|
if not rota.startswith('/aprendizado/'):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
if metodo == 'GET' and rota == '/aprendizado/status':
|
||||||
|
resultado = self.status()
|
||||||
|
elif metodo == 'POST' and rota == '/aprendizado/config':
|
||||||
|
resultado = self.configurar(ler_json(h, 1024))
|
||||||
|
elif metodo == 'POST' and rota == '/aprendizado/esquecer':
|
||||||
|
resultado = self.esquecer()
|
||||||
|
else:
|
||||||
|
raise ValueError('Rota inválida')
|
||||||
|
h.enviar(json.dumps(resultado, ensure_ascii=False))
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
h.enviar(json.dumps(dict(erro=str(e)), ensure_ascii=False), codigo=400)
|
||||||
|
return True
|
||||||
+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',
|
'cerebro.py', 'conversa.py', 'gemini.py', 'treinamento.py', 'figurinhas.py', 'api_config.py', 'historico.py', 'vida.py', 'criacao.py', 'inspiracao.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'
|
||||||
)
|
)
|
||||||
|
|||||||
+12
-3
@@ -44,6 +44,7 @@ from historico import Historico
|
|||||||
from vida import Vida
|
from vida import Vida
|
||||||
from criacao import Criacao
|
from criacao import Criacao
|
||||||
from inspiracao import Inspiracao
|
from inspiracao import Inspiracao
|
||||||
|
from aprendizado import Aprendizado
|
||||||
|
|
||||||
TRAVA = threading.Lock()
|
TRAVA = threading.Lock()
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ ESTATICOS = {"/carinha.html", "/camera.html", "/reacoes-render.js", "/rastreador
|
|||||||
|
|
||||||
# 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/", "/criacao/",
|
PREFIXOS_API = ("/eventos", "/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/")
|
||||||
|
|
||||||
|
|
||||||
@@ -263,7 +264,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", "inspiracao", "criacao", "treinamento", "conversa", "cerebro",
|
return [getattr(s, nome) for nome in ("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):
|
||||||
@@ -291,6 +292,8 @@ class Handler(SimpleHTTPRequestHandler):
|
|||||||
dados["pir"] = dict(s.historico.pir, presenca=dict(s.historico.presenca))
|
dados["pir"] = dict(s.historico.pir, presenca=dict(s.historico.presenca))
|
||||||
if hasattr(s, "vida"):
|
if hasattr(s, "vida"):
|
||||||
dados["vida"] = s.vida.status()
|
dados["vida"] = s.vida.status()
|
||||||
|
if hasattr(s, "aprendizado"):
|
||||||
|
dados["aprendizado"] = s.aprendizado.status()
|
||||||
return dados
|
return dados
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
@@ -483,7 +486,7 @@ class Servidor(ThreadingHTTPServer):
|
|||||||
|
|
||||||
|
|
||||||
def ligar_eventos(servidor):
|
def ligar_eventos(servidor):
|
||||||
for nome in ("controle", "servos", "cerebro", "conversa", "treinamento", "historico", "vida", "criacao", "inspiracao"):
|
for nome in ("controle", "servos", "cerebro", "conversa", "treinamento", "historico", "vida", "criacao", "inspiracao", "aprendizado"):
|
||||||
if hasattr(servidor, nome):
|
if hasattr(servidor, nome):
|
||||||
getattr(servidor, nome).publicar = EVENTOS.publicar
|
getattr(servidor, nome).publicar = EVENTOS.publicar
|
||||||
|
|
||||||
@@ -553,6 +556,12 @@ def main():
|
|||||||
vida.do_dia = lambda: inspiracao.hoje['ids']
|
vida.do_dia = lambda: inspiracao.hoje['ids']
|
||||||
vida.aprendi_pendente = inspiracao.pendente
|
vida.aprendi_pendente = inspiracao.pendente
|
||||||
vida.contar_aprendi = lambda ident, agora=None: inspiracao.contar(ident, agora)
|
vida.contar_aprendi = lambda ident, agora=None: inspiracao.contar(ident, agora)
|
||||||
|
# Aprendizado: com o dia a dia (carinhos, conversas, presença, sensor) ele ajusta o que faz e quando
|
||||||
|
def nome_criada(ident):
|
||||||
|
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)
|
||||||
|
aprendizado.anotar = lambda tipo, texto, agora=None: (vida.anotar(tipo, texto, agora), vida.avisar())
|
||||||
|
vida.aprendizado = aprendizado
|
||||||
# E-Stop parado, ou modo presença com ninguém em casa (dormindo em silêncio): não faz nada
|
# E-Stop parado, ou modo presença com ninguém em casa (dormindo em silêncio): não faz nada
|
||||||
vida.bloqueado = lambda: ESTOP["ativo"] or servidor.historico.presenca.get("estado") == "ausente"
|
vida.bloqueado = lambda: ESTOP["ativo"] or servidor.historico.presenca.get("estado") == "ausente"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
"""Aprendizado com o dia a dia: gosto pelos atos, rotina da casa, sono e o fechamento do dia."""
|
||||||
|
import http.client
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
from aprendizado import Aprendizado # noqa: E402
|
||||||
|
from vida import Vida # noqa: E402
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location('kiosk_aprendizado', ROOT / 'kiosk-server.py')
|
||||||
|
kiosk = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(kiosk)
|
||||||
|
|
||||||
|
|
||||||
|
def em(dia, hora, minuto=0, segundo=0):
|
||||||
|
return time.mktime((2026, 10, dia, hora, minuto, segundo, 0, 0, -1))
|
||||||
|
|
||||||
|
|
||||||
|
class AprendizadoTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.caminho = Path(self.tmp.name) / 'aprendizado.json'
|
||||||
|
self.a = Aprendizado(self.caminho)
|
||||||
|
self.a.hoje = self.a._dia_vazio('2026-10-06')
|
||||||
|
self.diario = []
|
||||||
|
self.a.anotar = lambda tipo, texto, agora=None: self.diario.append((tipo, texto))
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.tmp.cleanup()
|
||||||
|
|
||||||
|
def fazer(self, nome, vezes, resposta=None, inicio=None):
|
||||||
|
t = inicio or em(6, 15)
|
||||||
|
for i in range(vezes):
|
||||||
|
self.a.ato(nome, t + i * 120, perto=True)
|
||||||
|
if resposta:
|
||||||
|
self.a.resposta(resposta, t + i * 120 + 20)
|
||||||
|
|
||||||
|
def test_gosto_sobe_com_carinho_e_desce_quando_ignorado(self):
|
||||||
|
self.fazer('rir', 5, 'carinho')
|
||||||
|
self.fazer('bocejar', 5, inicio=em(6, 16))
|
||||||
|
self.assertGreater(self.a.peso('rir'), 1.0)
|
||||||
|
self.assertLess(self.a.peso('bocejar'), 1.0)
|
||||||
|
self.assertLessEqual(self.a.peso('rir'), 2.0)
|
||||||
|
self.assertGreaterEqual(self.a.peso('bocejar'), 0.5)
|
||||||
|
|
||||||
|
def test_sem_opiniao_com_poucas_tentativas_ou_sozinho(self):
|
||||||
|
self.fazer('rir', 2, 'carinho')
|
||||||
|
self.assertEqual(self.a.peso('rir'), 1.0)
|
||||||
|
for i in range(10):
|
||||||
|
self.a.ato('pensar', em(6, 15) + i, perto=False) # sem plateia ninguém pode gostar
|
||||||
|
self.assertEqual(self.a.peso('pensar'), 1.0)
|
||||||
|
self.assertNotIn('pensar', self.a.itens)
|
||||||
|
|
||||||
|
def test_resposta_fora_da_janela_nao_conta(self):
|
||||||
|
for i in range(4):
|
||||||
|
self.a.ato('acenar', em(6, 15) + i * 300, perto=True)
|
||||||
|
self.a.resposta('carinho', em(6, 15) + i * 300 + 200) # 200 s depois: já era outra coisa
|
||||||
|
self.assertEqual(self.a.itens['acenar']['s'], 0)
|
||||||
|
|
||||||
|
def test_cada_ato_agrada_uma_vez(self):
|
||||||
|
self.a.ato('rir', em(6, 15), perto=True)
|
||||||
|
for _ in range(5):
|
||||||
|
self.a.resposta('carinho', em(6, 15) + 10)
|
||||||
|
self.assertEqual(self.a.itens['rir']['s'], 1.0)
|
||||||
|
|
||||||
|
def test_fechar_o_dia_gera_licoes_e_esquece_um_pouco(self):
|
||||||
|
self.fazer('rir', 4, 'carinho')
|
||||||
|
self.fazer('bocejar', 5, inicio=em(6, 17))
|
||||||
|
t_antes = self.a.itens['rir']['t']
|
||||||
|
self.a.observar(em(7, 0, 1), perto=False) # meia-noite: fecha o dia 6
|
||||||
|
textos = [t for _, t in self.diario]
|
||||||
|
self.assertTrue(any('gostam de me ver dar risada' in t for t in textos), textos)
|
||||||
|
self.assertTrue(any('Tentei bocejar 5 vezes e ninguém ligou' in t for t in textos), textos)
|
||||||
|
self.assertAlmostEqual(self.a.itens['rir']['t'], t_antes * 0.85, places=2)
|
||||||
|
self.assertEqual(self.a.hoje['dia'], '2026-10-07')
|
||||||
|
self.assertTrue(self.caminho.exists())
|
||||||
|
|
||||||
|
def test_rotina_da_casa_e_acordar_antes_de_chegarem(self):
|
||||||
|
for dia in (6, 7, 8):
|
||||||
|
for minuto in range(60):
|
||||||
|
self.a.observar(em(dia, 17, minuto), perto=False) # 17h: casa vazia
|
||||||
|
self.a.observar(em(dia, 18, minuto), perto=True) # 18h: chegou gente
|
||||||
|
self.a.observar(em(9, 0, 1), perto=False)
|
||||||
|
self.assertGreaterEqual(self.a.rotina[18], 0.5)
|
||||||
|
self.assertLess(self.a.rotina[17], 0.3)
|
||||||
|
self.assertTrue(any('costumam estar por perto das 18h às 19h' in t for _, t in self.diario), self.diario)
|
||||||
|
self.assertTrue(self.a.chega_logo(em(9, 17, 50)))
|
||||||
|
self.assertFalse(self.a.chega_logo(em(9, 17, 10))) # ainda falta muito
|
||||||
|
self.assertFalse(self.a.chega_logo(em(9, 12, 50)))
|
||||||
|
|
||||||
|
def test_acordar_a_toa_pede_mais_movimento_naquela_hora(self):
|
||||||
|
for dia in (6, 7, 8, 9):
|
||||||
|
for vez in range(2):
|
||||||
|
t = em(dia, 22, vez * 20)
|
||||||
|
self.a.acordou_pelo_sensor(t)
|
||||||
|
for s in range(0, 200, 5):
|
||||||
|
self.a.observar(t + s, perto=s < 10) # mexeu e sumiu: foi à toa
|
||||||
|
self.a.observar(em(dia + 1, 0, 1), perto=False)
|
||||||
|
self.assertGreaterEqual(self.a.extra_pir(em(10, 22, 30)), 1)
|
||||||
|
self.assertEqual(self.a.extra_pir(em(10, 15)), 0)
|
||||||
|
self.assertTrue(any('Acordei à toa 2 vezes por volta das 22h' in t for _, t in self.diario), self.diario)
|
||||||
|
|
||||||
|
def test_acordar_com_carinho_valeu_a_pena(self):
|
||||||
|
t = em(6, 22)
|
||||||
|
self.a.acordou_pelo_sensor(t)
|
||||||
|
self.a.observar(t + 5, perto=True)
|
||||||
|
self.a.resposta('carinho', t + 30)
|
||||||
|
for s in range(35, 200, 5):
|
||||||
|
self.a.observar(t + s, perto=False)
|
||||||
|
self.assertEqual(self.a.hoje['falsos'][22], 0)
|
||||||
|
|
||||||
|
def test_desligado_nao_aprende_nem_muda_nada(self):
|
||||||
|
self.a.configurar(dict(ligado=False))
|
||||||
|
self.fazer('rir', 6, 'carinho')
|
||||||
|
self.assertEqual(self.a.peso('rir'), 1.0)
|
||||||
|
self.assertEqual(self.a.extra_pir(em(6, 22)), 0)
|
||||||
|
self.assertFalse(self.a.chega_logo(em(6, 17, 50)))
|
||||||
|
|
||||||
|
def test_esquecer_e_persistencia(self):
|
||||||
|
self.fazer('rir', 5, 'carinho')
|
||||||
|
self.fazer('bocejar', 5, inicio=em(6, 17)) # o gosto é relativo: precisa de algo menos querido
|
||||||
|
self.a.salvar()
|
||||||
|
outro = Aprendizado(self.caminho)
|
||||||
|
self.assertGreater(outro.peso('rir'), 1.0)
|
||||||
|
outro.anotar = lambda *a, **k: None
|
||||||
|
outro.esquecer()
|
||||||
|
self.assertEqual(outro.peso('rir'), 1.0)
|
||||||
|
self.assertEqual(Aprendizado(self.caminho).itens, {})
|
||||||
|
|
||||||
|
def test_validar(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.a.configurar(dict(ligado='sim'))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.a.configurar(dict(outro=True))
|
||||||
|
|
||||||
|
|
||||||
|
class VidaComAprendizadoTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.a = Aprendizado(Path(self.tmp.name) / 'aprendizado.json')
|
||||||
|
self.a.hoje = self.a._dia_vazio('2026-10-06')
|
||||||
|
self.v = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False, rng=random.Random(3))
|
||||||
|
self.v.aprendizado = self.a
|
||||||
|
self.ordens = []
|
||||||
|
self.v.enfileirar = self.ordens.append
|
||||||
|
self.v.ultimo_tick = em(6, 15)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.tmp.cleanup()
|
||||||
|
|
||||||
|
def test_gesto_que_agrada_sai_mais(self):
|
||||||
|
for i in range(8):
|
||||||
|
self.a.ato('rir', em(6, 15) + i * 100, perto=True)
|
||||||
|
self.a.resposta('carinho', em(6, 15) + i * 100 + 5)
|
||||||
|
self.a.ato('feliz', em(6, 15) + i * 100 + 50, perto=True)
|
||||||
|
opcoes = [('rir', 1, ''), ('feliz', 1, '')]
|
||||||
|
saiu = [self.v.escolher(opcoes)[0] for _ in range(400)]
|
||||||
|
self.assertGreater(saiu.count('rir'), saiu.count('feliz') * 2)
|
||||||
|
|
||||||
|
def test_carinho_e_atos_com_plateia_chegam_ao_aprendizado(self):
|
||||||
|
self.v.alguem_perto = lambda: True
|
||||||
|
self.v.celular_online = lambda: True
|
||||||
|
self.v.proximo_ato = 0
|
||||||
|
self.v.necessidades.update(energia=90, social=90, diversao=90, alegria=90)
|
||||||
|
ato = None
|
||||||
|
for i in range(30):
|
||||||
|
ato = self.v.tick(em(6, 15) + i * 200)
|
||||||
|
if ato in self.a.itens:
|
||||||
|
break
|
||||||
|
self.assertIn(ato, self.a.itens)
|
||||||
|
self.v.estimulo('carinho', em(6, 15) + i * 200 + 10)
|
||||||
|
self.assertGreater(self.a.itens[ato]['s'], 0)
|
||||||
|
|
||||||
|
def test_pir_pede_mais_quando_acordou_a_toa_naquela_hora(self):
|
||||||
|
self.a.falsos[22] = 1.0
|
||||||
|
self.assertFalse(self.v.pir_acorda(em(6, 22)))
|
||||||
|
self.assertTrue(self.v.pir_acorda(em(6, 22) + 10))
|
||||||
|
self.assertEqual(self.a.acordou_em, em(6, 22) + 10)
|
||||||
|
self.assertTrue(self.v.pir_acorda(em(6, 15)))
|
||||||
|
|
||||||
|
def test_acorda_do_cochilo_antes_de_chegarem(self):
|
||||||
|
self.a.rotina[18], self.a.rotina[17] = 0.8, 0.1
|
||||||
|
self.v.dormindo_proprio = True
|
||||||
|
self.v.necessidades['energia'] = 70
|
||||||
|
self.assertEqual(self.v.agir(em(6, 17, 50)), 'acordar')
|
||||||
|
self.assertIn('acordar', self.ordens)
|
||||||
|
|
||||||
|
def test_sono_profundo_nao_e_interrompido_pela_rotina(self):
|
||||||
|
self.a.rotina[18], self.a.rotina[17] = 0.8, 0.1
|
||||||
|
self.v.dormindo_proprio = True
|
||||||
|
self.v.sono_profundo = True
|
||||||
|
self.v.necessidades['energia'] = 70
|
||||||
|
self.assertIsNone(self.v.agir(em(6, 17, 50)))
|
||||||
|
|
||||||
|
|
||||||
|
class RotasTest(unittest.TestCase):
|
||||||
|
def test_status_config_e_esquecer(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
kiosk.ARQUIVO_APARENCIA = str(Path(tmp) / 'a.json')
|
||||||
|
s = kiosk.Servidor(('127.0.0.1', 0), kiosk.Handler)
|
||||||
|
s.aprendizado = Aprendizado(Path(tmp) / 'ap.json')
|
||||||
|
kiosk.ligar_eventos(s)
|
||||||
|
threading.Thread(target=s.serve_forever, daemon=True).start()
|
||||||
|
try:
|
||||||
|
def pedir(metodo, rota, corpo=None):
|
||||||
|
c = http.client.HTTPConnection('127.0.0.1', s.server_port, timeout=5)
|
||||||
|
dados = json.dumps(corpo).encode() if corpo is not None else None
|
||||||
|
c.request(metodo, rota, body=dados, headers={'Content-Type': 'application/json'} if dados else {})
|
||||||
|
r = c.getresponse()
|
||||||
|
return r.status, json.loads(r.read())
|
||||||
|
cod, st = pedir('GET', '/aprendizado/status')
|
||||||
|
self.assertEqual(cod, 200)
|
||||||
|
self.assertEqual(len(st['rotina']), 24)
|
||||||
|
cod, st = pedir('POST', '/aprendizado/config', dict(ligado=False))
|
||||||
|
self.assertEqual((cod, st['config']['ligado']), (200, False))
|
||||||
|
cod, st = pedir('POST', '/aprendizado/config', dict(ligado='x'))
|
||||||
|
self.assertEqual(cod, 400)
|
||||||
|
cod, st = pedir('POST', '/aprendizado/esquecer', {})
|
||||||
|
self.assertEqual((cod, st['dias']), (200, 0))
|
||||||
|
cod, est = pedir('GET', '/estado')
|
||||||
|
self.assertIn('aprendizado', est)
|
||||||
|
finally:
|
||||||
|
s.shutdown()
|
||||||
|
kiosk.EVENTOS.fechar()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -112,6 +112,7 @@ 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
|
||||||
|
GESTOS_VIDA = {g for atos in ATOS.values() for g, _, _ in atos}
|
||||||
|
|
||||||
|
|
||||||
def _limitar(v):
|
def _limitar(v):
|
||||||
@@ -147,6 +148,7 @@ class Vida:
|
|||||||
self.do_dia = lambda: [] # ids das ideias de hoje
|
self.do_dia = lambda: [] # ids das ideias de hoje
|
||||||
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)
|
||||||
|
|
||||||
agora = time.time()
|
agora = time.time()
|
||||||
self.config = dict(PADRAO)
|
self.config = dict(PADRAO)
|
||||||
@@ -319,6 +321,8 @@ class Vida:
|
|||||||
self.anotar('cuidado', CUIDADOS[tipo], agora)
|
self.anotar('cuidado', CUIDADOS[tipo], agora)
|
||||||
if tipo in CUIDADOS or tipo == 'conversa':
|
if tipo in CUIDADOS or tipo == 'conversa':
|
||||||
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:
|
||||||
|
self.aprendizado.resposta(tipo, 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)
|
||||||
@@ -369,7 +373,10 @@ class Vida:
|
|||||||
else:
|
else:
|
||||||
lista = ATIVAS + CRIATIVAS * 2 + CALMAS
|
lista = ATIVAS + CRIATIVAS * 2 + CALMAS
|
||||||
opcoes = [a for a in lista if a != self.ultima_atividade] or lista
|
opcoes = [a for a in lista if a != self.ultima_atividade] or lista
|
||||||
return self.rng.choice(opcoes)
|
pesos = [self.gosto(a) for a in opcoes]
|
||||||
|
if all(p == 1.0 for p in pesos):
|
||||||
|
return self.rng.choice(opcoes)
|
||||||
|
return self.rng.choices(opcoes, weights=pesos)[0] # as que agradam saem mais
|
||||||
|
|
||||||
def comecar_atividade(self, agora, nome, segundos):
|
def comecar_atividade(self, agora, nome, segundos):
|
||||||
if nome.startswith('criado:'):
|
if nome.startswith('criado:'):
|
||||||
@@ -394,8 +401,12 @@ class Vida:
|
|||||||
agora = agora or time.time()
|
agora = agora or time.time()
|
||||||
self.pir_eventos = [t for t in self.pir_eventos if agora - t < 120] + [agora]
|
self.pir_eventos = [t for t in self.pir_eventos if agora - t < 120] + [agora]
|
||||||
precisa = PIR_ACORDAR[self.config['ritmo']] if self.config['ligado'] else 1
|
precisa = PIR_ACORDAR[self.config['ritmo']] if self.config['ligado'] else 1
|
||||||
|
if self.aprendizado and self.config['ligado']:
|
||||||
|
precisa += self.aprendizado.extra_pir(agora) # nesta hora já acordou à toa outras vezes
|
||||||
if len(self.pir_eventos) >= precisa:
|
if len(self.pir_eventos) >= precisa:
|
||||||
self.pir_eventos = []
|
self.pir_eventos = []
|
||||||
|
if self.aprendizado and self.config['ligado']:
|
||||||
|
self.aprendizado.acordou_pelo_sensor(agora)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -406,7 +417,12 @@ class Vida:
|
|||||||
return hora >= ini or hora < fim
|
return hora >= ini or hora < fim
|
||||||
|
|
||||||
# ------------------------------------------------------------------ decidir o que fazer
|
# ------------------------------------------------------------------ decidir o que fazer
|
||||||
|
def gosto(self, nome):
|
||||||
|
"""Quanto o aprendizado multiplica a chance de um ato (1 = sem opinião)."""
|
||||||
|
return self.aprendizado.peso(nome) if self.aprendizado else 1.0
|
||||||
|
|
||||||
def escolher(self, opcoes):
|
def escolher(self, opcoes):
|
||||||
|
opcoes = [(g, p * self.gosto(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:
|
||||||
@@ -473,6 +489,11 @@ class Vida:
|
|||||||
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')
|
||||||
return 'acordar'
|
return 'acordar'
|
||||||
|
# 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
|
||||||
|
and self.aprendizado.chega_logo(agora)):
|
||||||
|
self.acordar(agora, 'Acordou: daqui a pouco costuma chegar alguém')
|
||||||
|
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')
|
||||||
@@ -529,11 +550,25 @@ class Vida:
|
|||||||
self.anotar('acao', texto, agora)
|
self.anotar('acao', texto, agora)
|
||||||
return gesto
|
return gesto
|
||||||
|
|
||||||
|
def aprender_ato(self, ato, agora):
|
||||||
|
"""Gestos e atividades feitos com plateia entram no aprendizado (para ver se agradam)."""
|
||||||
|
if not self.aprendizado or not ato:
|
||||||
|
return
|
||||||
|
if ato.startswith('atividade '):
|
||||||
|
nome = ato[len('atividade '):]
|
||||||
|
elif ato in GESTOS_VIDA:
|
||||||
|
nome = ato
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
self.aprendizado.ato(nome, agora, self.alguem_perto())
|
||||||
|
|
||||||
def tick(self, agora=None):
|
def tick(self, agora=None):
|
||||||
agora = agora or time.time()
|
agora = agora or time.time()
|
||||||
self.passar_tempo(agora)
|
self.passar_tempo(agora)
|
||||||
if not self.config['ligado']:
|
if not self.config['ligado']:
|
||||||
return None
|
return None
|
||||||
|
if self.aprendizado:
|
||||||
|
self.aprendizado.observar(agora, self.alguem_perto())
|
||||||
ato = None
|
ato = None
|
||||||
humor = self.humor()
|
humor = self.humor()
|
||||||
# acordaram ele de propósito (toque, painel) antes de descansar: o sono pesado acaba aqui
|
# acordaram ele de propósito (toque, painel) antes de descansar: o sono pesado acaba aqui
|
||||||
@@ -554,6 +589,7 @@ class Vida:
|
|||||||
self.zanga_enviada = zanga
|
self.zanga_enviada = zanga
|
||||||
if agora >= self.proximo_ato:
|
if agora >= self.proximo_ato:
|
||||||
ato = self.agir(agora)
|
ato = self.agir(agora)
|
||||||
|
self.aprender_ato(ato, 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():
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/** Aprendizado com o dia a dia: ligar/desligar e esquecer. O estado chega pelo SSE 'aprendizado'. */
|
||||||
|
import { postar } from './api';
|
||||||
|
import { avisos } from './avisos.svelte';
|
||||||
|
import { estado } from './estado.svelte';
|
||||||
|
import type { Aprendizado } from './tipos';
|
||||||
|
|
||||||
|
export async function configurarAprendizado(ligado: boolean) {
|
||||||
|
const antes = estado.aprendizado;
|
||||||
|
if (antes) estado.aprendizado = { ...antes, config: { ...antes.config, ligado } };
|
||||||
|
try {
|
||||||
|
estado.aprendizado = await postar<Aprendizado>('/aprendizado/config', { ligado });
|
||||||
|
} catch (e) {
|
||||||
|
estado.aprendizado = antes;
|
||||||
|
avisos.erro(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function esquecerAprendizado() {
|
||||||
|
try {
|
||||||
|
estado.aprendizado = await postar<Aprendizado>('/aprendizado/esquecer', {});
|
||||||
|
avisos.ok('Ele esqueceu o que tinha aprendido e começa do zero');
|
||||||
|
} catch (e) {
|
||||||
|
avisos.erro(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { GraduationCap } from '@lucide/svelte';
|
||||||
|
import { configurarAprendizado, esquecerAprendizado } from '../aprendizado.svelte';
|
||||||
|
import { estado } from '../estado.svelte';
|
||||||
|
import Grupo from './Grupo.svelte';
|
||||||
|
import Interruptor from './Interruptor.svelte';
|
||||||
|
import Linha from './Linha.svelte';
|
||||||
|
|
||||||
|
// O que ele aprendeu com o dia a dia: rotina da casa (24 h), do que gostam, lições e o sono.
|
||||||
|
const a = $derived(estado.aprendizado);
|
||||||
|
let confirmar = $state(false);
|
||||||
|
|
||||||
|
const horaAgora = $derived(new Date(estado.agora).getHours());
|
||||||
|
const temRotina = $derived(!!a && a.rotina.some((v) => v !== null));
|
||||||
|
const faz_mais = $derived(a ? a.gosto.filter((g) => g.peso > 1.05).slice(0, 4) : []);
|
||||||
|
const faz_menos = $derived(a ? [...a.gosto].reverse().filter((g) => g.peso < 0.95).slice(0, 3) : []);
|
||||||
|
const respostas = $derived(a ? Object.values(a.hoje.respostas).reduce((s, n) => s + n, 0) : 0);
|
||||||
|
|
||||||
|
const detalhe = $derived.by(() => {
|
||||||
|
if (!a) return '';
|
||||||
|
if (!a.config.ligado) return 'Desligado: ele faz tudo como veio de fábrica';
|
||||||
|
const dias = a.dias === 0 ? 'primeiro dia observando' : a.dias === 1 ? '1 dia observado' : `${a.dias} dias observados`;
|
||||||
|
return `${dias} · hoje: ${a.hoje.atos} ${a.hoje.atos === 1 ? 'gesto' : 'gestos'} com alguém perto, ${respostas} ${respostas === 1 ? 'carinho ou conversa' : 'carinhos ou conversas'}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const resumoRotina = $derived.by(() => {
|
||||||
|
if (!a) return '';
|
||||||
|
const cheias = a.rotina.flatMap((v, h) => (v !== null && v >= 0.5 ? [h] : []));
|
||||||
|
return cheias.length ? 'Costuma ter gente às ' + cheias.map((h) => h + 'h').join(', ') : 'Ainda sem um horário marcado';
|
||||||
|
});
|
||||||
|
|
||||||
|
function dia(iso: string) {
|
||||||
|
const [, m, d] = iso.split('-');
|
||||||
|
return `${d}/${m}`;
|
||||||
|
}
|
||||||
|
function vezes(peso: number) {
|
||||||
|
return '×' + peso.toLocaleString('pt-BR', { maximumFractionDigits: 1 });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Grupo
|
||||||
|
titulo="O que ele aprendeu"
|
||||||
|
ajuda="aprendizado"
|
||||||
|
rodape="Ele conta gestos feitos com alguém por perto e o que veio logo depois (carinho, conversa). À meia-noite junta o dia e escreve as lições."
|
||||||
|
>
|
||||||
|
{#if !a}
|
||||||
|
<p class="vazio">Carregando…</p>
|
||||||
|
{:else}
|
||||||
|
<Linha icone={GraduationCap} cor="#30b0c7" rotulo="Aprender com o dia a dia" {detalhe}>
|
||||||
|
{#snippet acessorio()}
|
||||||
|
<Interruptor rotulo="Aprender com o dia a dia" checked={a.config.ligado} onchange={(l) => configurarAprendizado(l)} />
|
||||||
|
{/snippet}
|
||||||
|
</Linha>
|
||||||
|
|
||||||
|
{#if a.config.ligado}
|
||||||
|
<section class="bloco" aria-labelledby="ap-rotina">
|
||||||
|
<h3 id="ap-rotina">Rotina da casa <span>{temRotina ? resumoRotina : 'aparece depois da primeira meia-noite'}</span></h3>
|
||||||
|
<div class="rotina" role="img" aria-label={resumoRotina}>
|
||||||
|
{#each a.rotina as v, h (h)}
|
||||||
|
<span
|
||||||
|
class="hora"
|
||||||
|
class:agora={h === horaAgora}
|
||||||
|
class:sem={v === null}
|
||||||
|
title={`${h}h: ${v === null ? 'sem dados' : Math.round(v * 100) + '% do tempo com gente'}${a.sono[h] ? ' · acordou à toa aqui' : ''}`}
|
||||||
|
>
|
||||||
|
<span class="barra" style="--v: {v ?? 0}"></span>
|
||||||
|
{#if a.sono[h]}<span class="lua" aria-hidden="true">☾</span>{/if}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="eixo mono" aria-hidden="true"><span>0h</span><span>6h</span><span>12h</span><span>18h</span><span>24h</span></div>
|
||||||
|
{#if a.sono.some((n) => n > 0)}
|
||||||
|
<p class="nota"><span aria-hidden="true">☾</span> Horas em que acordou à toa: ali o sensor precisa de mais movimento para acordá-lo.</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if faz_mais.length || faz_menos.length}
|
||||||
|
<section class="bloco gosto" aria-labelledby="ap-gosto">
|
||||||
|
<h3 id="ap-gosto">Do que vocês gostam</h3>
|
||||||
|
<div class="colunas">
|
||||||
|
<ul>
|
||||||
|
<li class="titulo">Faz mais</li>
|
||||||
|
{#each faz_mais as g (g.id)}
|
||||||
|
<li class="mais"><span>{g.nome}</span><b class="mono">{vezes(g.peso)}</b></li>
|
||||||
|
{:else}
|
||||||
|
<li class="nada">Nada se destacou ainda</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
<li class="titulo">Faz menos</li>
|
||||||
|
{#each faz_menos as g (g.id)}
|
||||||
|
<li class="menos"><span>{g.nome}</span><b class="mono">{vezes(g.peso)}</b></li>
|
||||||
|
{:else}
|
||||||
|
<li class="nada">Nada foi ignorado</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<section class="bloco" aria-labelledby="ap-licoes">
|
||||||
|
<h3 id="ap-licoes">Lições</h3>
|
||||||
|
{#if a.licoes.length}
|
||||||
|
<ol class="licoes">
|
||||||
|
{#each a.licoes.slice(0, 5) as l (l.dia + l.texto)}
|
||||||
|
<li><time class="mono">{dia(l.dia)}</time> <span>{l.texto}</span></li>
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
|
{:else}
|
||||||
|
<p class="nota">A primeira lição chega à meia-noite. Faça carinho ou converse logo depois de um gesto que você gostou.</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="esquecer">
|
||||||
|
{#if confirmar}
|
||||||
|
<span>Apagar rotina, gostos e lições?</span>
|
||||||
|
<button class="btn pequeno perigo" onclick={() => { confirmar = false; esquecerAprendizado(); }}>Esquecer tudo</button>
|
||||||
|
<button class="btn fantasma pequeno" onclick={() => (confirmar = false)}>Cancelar</button>
|
||||||
|
{:else}
|
||||||
|
<button class="btn fantasma pequeno" onclick={() => (confirmar = true)} disabled={!a.dias && !a.gosto.length && !a.licoes.length}>
|
||||||
|
Esquecer o que aprendeu
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Grupo>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.vazio,
|
||||||
|
.nota {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--texto-3);
|
||||||
|
font-size: var(--t-xs);
|
||||||
|
}
|
||||||
|
.vazio {
|
||||||
|
padding: var(--e-3) var(--e-4);
|
||||||
|
}
|
||||||
|
.bloco {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--e-2);
|
||||||
|
padding: var(--e-3) var(--e-4);
|
||||||
|
border-top: 1px solid var(--borda);
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--e-2);
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--t-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--texto-3);
|
||||||
|
}
|
||||||
|
h3 span {
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
.rotina {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(24, minmax(0, 1fr));
|
||||||
|
align-items: end;
|
||||||
|
gap: 2px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.hora {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--superficie-2);
|
||||||
|
}
|
||||||
|
.barra {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(var(--v) * 100%);
|
||||||
|
min-height: 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #30b0c7;
|
||||||
|
transition: height 600ms ease;
|
||||||
|
}
|
||||||
|
.sem .barra {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.agora {
|
||||||
|
outline: 1.5px solid var(--texto-2);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.lua {
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #5e5ce6;
|
||||||
|
}
|
||||||
|
.eixo {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: var(--texto-3);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.colunas {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
||||||
|
gap: var(--e-2) var(--e-4);
|
||||||
|
}
|
||||||
|
.colunas ul {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
font-size: var(--t-sm);
|
||||||
|
}
|
||||||
|
.colunas li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--e-2);
|
||||||
|
color: var(--texto-2);
|
||||||
|
}
|
||||||
|
.colunas b {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--t-xs);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.mais b {
|
||||||
|
color: var(--ok, #34c759);
|
||||||
|
}
|
||||||
|
.menos b {
|
||||||
|
color: var(--texto-3);
|
||||||
|
}
|
||||||
|
.colunas .titulo {
|
||||||
|
color: var(--texto-3);
|
||||||
|
font-size: var(--t-xs);
|
||||||
|
}
|
||||||
|
.nada {
|
||||||
|
color: var(--texto-3);
|
||||||
|
font-size: var(--t-xs);
|
||||||
|
}
|
||||||
|
.licoes {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
font-size: var(--t-sm);
|
||||||
|
}
|
||||||
|
.licoes li {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--e-2);
|
||||||
|
color: var(--texto);
|
||||||
|
}
|
||||||
|
.licoes time {
|
||||||
|
flex: none;
|
||||||
|
padding-top: 2px;
|
||||||
|
color: var(--texto-3);
|
||||||
|
font-size: var(--t-xs);
|
||||||
|
}
|
||||||
|
.esquecer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--e-2);
|
||||||
|
padding: var(--e-2) var(--e-3) var(--e-3);
|
||||||
|
border-top: 1px solid var(--borda);
|
||||||
|
font-size: var(--t-sm);
|
||||||
|
color: var(--texto-2);
|
||||||
|
}
|
||||||
|
.esquecer span {
|
||||||
|
padding-left: var(--e-1);
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.barra {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -296,6 +296,9 @@
|
|||||||
.diario li.emoji {
|
.diario li.emoji {
|
||||||
color: #af52de;
|
color: #af52de;
|
||||||
}
|
}
|
||||||
|
.diario li.aprendizado {
|
||||||
|
color: #30b0c7;
|
||||||
|
}
|
||||||
.diario li.cuidado {
|
.diario li.cuidado {
|
||||||
color: var(--texto);
|
color: var(--texto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { ErroApi, pedir, quandoNegado, urlEventos } from './api';
|
import { ErroApi, pedir, quandoNegado, urlEventos } from './api';
|
||||||
import { avisos } from './avisos.svelte';
|
import { avisos } from './avisos.svelte';
|
||||||
import type { AppConfig, Conversa, EstadoServidor, IA, Motores, Reacao, Telemetria, Vida } from './tipos';
|
import type { Aprendizado, AppConfig, Conversa, EstadoServidor, IA, Motores, Reacao, Telemetria, Vida } from './tipos';
|
||||||
|
|
||||||
export type Conexao = 'conectando' | 'aberta' | 'caiu' | 'token';
|
export type Conexao = 'conectando' | 'aberta' | 'caiu' | 'token';
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ class Estado {
|
|||||||
treinamento = $state({ ocupado: false, erro: '' });
|
treinamento = $state({ ocupado: false, erro: '' });
|
||||||
api = $state<EstadoServidor['api'] | null>(null);
|
api = $state<EstadoServidor['api'] | null>(null);
|
||||||
vida = $state<Vida | null>(null);
|
vida = $state<Vida | null>(null);
|
||||||
|
aprendizado = $state<Aprendizado | null>(null);
|
||||||
feed = $state<ItemFeed[]>([]);
|
feed = $state<ItemFeed[]>([]);
|
||||||
/** Sensor PIR da placa: último disparo (s, epoch) e se está ligado. */
|
/** Sensor PIR da placa: último disparo (s, epoch) e se está ligado. */
|
||||||
pir = $state<{ disponivel: boolean; ultimo: number | null; ativo?: boolean; presenca?: { estado: 'presente' | 'ausente'; desde: number; ultimo: number } } | null>(null);
|
pir = $state<{ disponivel: boolean; ultimo: number | null; ativo?: boolean; presenca?: { estado: 'presente' | 'ausente'; desde: number; ultimo: number } } | null>(null);
|
||||||
@@ -110,6 +111,7 @@ function aplicarFoto(f: EstadoServidor) {
|
|||||||
if (f.treinamento) estado.treinamento = f.treinamento;
|
if (f.treinamento) estado.treinamento = f.treinamento;
|
||||||
if (f.api) estado.api = f.api;
|
if (f.api) estado.api = f.api;
|
||||||
if (f.vida) estado.vida = f.vida;
|
if (f.vida) estado.vida = f.vida;
|
||||||
|
if (f.aprendizado) estado.aprendizado = f.aprendizado;
|
||||||
if (f.pir) estado.pir = { disponivel: f.pir.disponivel, ultimo: f.pir.ultimo, ativo: (f.pir as any).ativo, presenca: (f.pir as any).presenca };
|
if (f.pir) estado.pir = { disponivel: f.pir.disponivel, ultimo: f.pir.ultimo, ativo: (f.pir as any).ativo, presenca: (f.pir as any).presenca };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +160,7 @@ const tratadores: Record<string, (d: any) => void> = {
|
|||||||
if (novo && novo.horario !== estado.vida?.diario[0]?.horario && novo.tipo !== 'acao') noFeed('vida', d.emoji + ' ' + novo.texto);
|
if (novo && novo.horario !== estado.vida?.diario[0]?.horario && novo.tipo !== 'acao') noFeed('vida', d.emoji + ' ' + novo.texto);
|
||||||
estado.vida = d;
|
estado.vida = d;
|
||||||
},
|
},
|
||||||
|
aprendizado: (d: Aprendizado) => (estado.aprendizado = d),
|
||||||
pir: (d: { ultimo: number; ativo?: boolean; presenca?: any }) => (estado.pir = { disponivel: true, ultimo: d.ultimo, ativo: d.ativo, presenca: d.presenca }),
|
pir: (d: { ultimo: number; ativo?: boolean; presenca?: any }) => (estado.pir = { disponivel: true, ultimo: d.ultimo, ativo: d.ativo, presenca: d.presenca }),
|
||||||
historico: (d) => {
|
historico: (d) => {
|
||||||
const n = d?.novo;
|
const n = d?.novo;
|
||||||
@@ -185,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', 'criacao', 'inspiracao']) {
|
'reacao', 'treinamento', 'historico', 'pir', 'vida', 'aprendizado', 'criacao', 'inspiracao']) {
|
||||||
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);
|
||||||
|
|||||||
+16
-1
@@ -55,6 +55,21 @@ export const TOPICOS: Record<string, TopicoGuia> = {
|
|||||||
],
|
],
|
||||||
dica: 'Desligar o modo vida volta o celular aos gestos soltos de sempre e à sensibilidade dos Ajustes.',
|
dica: 'Desligar o modo vida volta o celular aos gestos soltos de sempre e à sensibilidade dos Ajustes.',
|
||||||
},
|
},
|
||||||
|
aprendizado: {
|
||||||
|
titulo: 'O que ele aprendeu',
|
||||||
|
emoji: '🎓',
|
||||||
|
onde: 'Ao vivo › O que ele aprendeu (com o modo vida ligado)',
|
||||||
|
resumo:
|
||||||
|
'Ele observa o próprio dia e melhora aos poucos, sem IA e sem internet: conta o que acontece em casa e ajusta o modo vida. À meia-noite fecha o dia, escreve as lições no diário e o que ficou para trás passa a pesar menos, porque o gosto das pessoas muda.',
|
||||||
|
itens: [
|
||||||
|
{ emoji: '❤️', nome: 'Do que vocês gostam', texto: 'Cada gesto ou atividade que ele faz com alguém por perto é uma tentativa. Carinho, brincadeira ou conversa até 1 minuto depois contam como "gostaram" (cutucada conta meio). Os que agradam passam a sair até 2 vezes mais; os ignorados, até a metade. Precisa de pelo menos 3 tentativas para ele formar opinião.' },
|
||||||
|
{ emoji: '🏠', nome: 'Rotina da casa', texto: 'Em que horas costuma ter gente por perto (pelo PIR e pela câmera). As 24 barras mostram cada hora. Quando a próxima hora costuma ser movimentada e a atual vazia, ele acorda do cochilo 15 minutos antes para estar de pé quando você chegar. Sono pesado e madrugada continuam respeitados.' },
|
||||||
|
{ emoji: '☾', nome: 'Acordar à toa', texto: 'Se o PIR acorda o rosto e ninguém fica nem interage em 3 minutos, foi à toa. Quando isso se repete na mesma hora em dias diferentes, ali ele passa a precisar de 1 ou 2 disparos a mais para acordar.' },
|
||||||
|
{ emoji: '📖', nome: 'Lições', texto: 'O que ele concluiu em cada dia, em primeira pessoa. Também aparecem no diário do cartão Vida.' },
|
||||||
|
{ emoji: '🧽', nome: 'Esquecer', texto: 'Apaga rotina, gostos e lições, e ele começa do zero. Desligar o aprendizado só para de usar o que aprendeu, sem apagar.' },
|
||||||
|
],
|
||||||
|
dica: 'Para ensinar rápido: quando ele fizer algo que você gostou, toque na tela do celular (vale como carinho) ou fale com ele logo em seguida.',
|
||||||
|
},
|
||||||
cuidados: {
|
cuidados: {
|
||||||
titulo: 'Cuidar do Carinha',
|
titulo: 'Cuidar do Carinha',
|
||||||
emoji: '❤️',
|
emoji: '❤️',
|
||||||
@@ -334,7 +349,7 @@ 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', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'atividades', 'criar', 'ideias', 'aparencia',
|
'inicio', 'vida', 'aprendizado', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'atividades', 'criar', 'ideias', 'aparencia',
|
||||||
'personalidade', 'acontecimentos', 'eventos', 'presenca', 'tela', 'sensores', 'sensibilidade',
|
'personalidade', 'acontecimentos', 'eventos', 'presenca', 'tela', 'sensores', 'sensibilidade',
|
||||||
'reacoes', 'conversa', 'movimento', 'atalhos',
|
'reacoes', 'conversa', 'movimento', 'atalhos',
|
||||||
];
|
];
|
||||||
|
|||||||
+17
-1
@@ -83,6 +83,7 @@ export interface EstadoServidor {
|
|||||||
api?: { provedor: string; modelo: string; configurada: boolean; ultimo_teste: unknown };
|
api?: { provedor: string; modelo: string; configurada: boolean; ultimo_teste: unknown };
|
||||||
pir?: { disponivel: boolean; erro: string; linha: string; ultimo: number | null };
|
pir?: { disponivel: boolean; erro: string; linha: string; ultimo: number | null };
|
||||||
vida?: Vida;
|
vida?: Vida;
|
||||||
|
aprendizado?: Aprendizado;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Modo vida (/vida/status e SSE 'vida'): necessidades de 0 a 100, 100 = satisfeito. */
|
/** Modo vida (/vida/status e SSE 'vida'): necessidades de 0 a 100, 100 = satisfeito. */
|
||||||
@@ -103,5 +104,20 @@ 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'; texto: string }[];
|
diario: { horario: number; tipo: 'acao' | 'sono' | 'emoji' | 'cuidado' | 'vida' | 'atividade' | 'aprendizado' | 'aprendi'; texto: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** O que ele aprendeu com o dia a dia (aprendizado.py). */
|
||||||
|
export interface Aprendizado {
|
||||||
|
config: { ligado: boolean };
|
||||||
|
/** Dias inteiros observados. */
|
||||||
|
dias: number;
|
||||||
|
/** 24 horas: chance de ter gente por perto (null = ainda sem dados nessa hora). */
|
||||||
|
rotina: (number | null)[];
|
||||||
|
/** 24 horas: disparos a mais do PIR para acordar (0 a 2), por ter acordado à toa ali. */
|
||||||
|
sono: number[];
|
||||||
|
/** Atos com opinião formada, do mais querido ao mais ignorado. peso multiplica a chance (0,5 a 2). */
|
||||||
|
gosto: { id: string; nome: string; tentativas: number; agradou: number; peso: number }[];
|
||||||
|
licoes: { dia: string; texto: string }[];
|
||||||
|
hoje: { dia: string; atos: number; respostas: Record<string, number>; acordou: number; a_toa: number };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import PresencaPir from '../lib/componentes/PresencaPir.svelte';
|
import PresencaPir from '../lib/componentes/PresencaPir.svelte';
|
||||||
import RostoPrevia from '../lib/componentes/RostoPrevia.svelte';
|
import RostoPrevia from '../lib/componentes/RostoPrevia.svelte';
|
||||||
import VidaCartao from '../lib/componentes/VidaCartao.svelte';
|
import VidaCartao from '../lib/componentes/VidaCartao.svelte';
|
||||||
|
import AprendizadoCartao from '../lib/componentes/AprendizadoCartao.svelte';
|
||||||
import { estado } from '../lib/estado.svelte';
|
import { estado } from '../lib/estado.svelte';
|
||||||
import { Rastreamento } from '../lib/rastreamento.svelte';
|
import { Rastreamento } from '../lib/rastreamento.svelte';
|
||||||
import { VideoAoVivo } from '../lib/video.svelte';
|
import { VideoAoVivo } from '../lib/video.svelte';
|
||||||
@@ -126,6 +127,7 @@ const pirAtivo = $derived(
|
|||||||
|
|
||||||
<div class="lista">
|
<div class="lista">
|
||||||
<VidaCartao />
|
<VidaCartao />
|
||||||
|
{#if estado.vida?.config.ligado}<AprendizadoCartao />{/if}
|
||||||
|
|
||||||
<Grupo titulo="Celular" ajuda="celular">
|
<Grupo titulo="Celular" ajuda="celular">
|
||||||
<Celular />
|
<Celular />
|
||||||
|
|||||||
Reference in New Issue
Block a user