"""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