Files
carinha-control-panel/vida.py
T
julioandClaude Opus 5.5 938467e318 feat(pessoas): reconhecimento de quem chega, só na placa
- reconhecimento.py: cada foto de evento passa pelo YuNet (acha rostos)
  e pelo SFace (impressão de 128 números) no OpenCV da placa (~0,3 s por
  foto); compara com as amostras marcadas (cosseno >= 0,40). Rostos sem
  nome ficam agrupados por semelhança para marcar de uma vez; marcar uma
  pessoa reconhece também os rostos antigos parecidos. Procura nas fotos
  guardadas, renomear, apagar (apaga recortes e impressões), sem nome
  somem em 7 dias, liga/desliga.
- modo vida: espera até 8 s a foto para cumprimentar pelo nome ("Ana!
  Senti sua falta!"), saudade de cada pessoa, "Oi, Bia!" para quem chega
  com alguém já por perto, afinidade com quem dá carinho logo depois.
- painel: tela Pessoas e tópico no guia; console mostra "Reconheceu Ana".
- sistema/reconhecimento/instalar.sh: python3-opencv e os modelos do
  opencv_zoo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 04:38:10 -03:00

878 lines
47 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Modo vida: o Carinha como um bichinho virtual (estilo Tamagotchi).
Quatro necessidades (0 a 100, 100 = satisfeito) caem com o tempo e sobem com o que acontece em volta
(presença, conversa, carinho). Delas sai o humor, e do humor saem os atos: gestos, cochilos, chamar
atenção quando tem gente perto, tocar uma reação da biblioteca e, de vez em quando, desenhar uma
figurinha nova com o Gemini. O celular recebe o humor ("humor feliz") e escolhe os gestos soltos dele
a partir disso, então o rosto parece ter vida mesmo entre um ato e outro.
"""
import json
import os
import random
import threading
import time
from pathlib import Path
from comum import gravar_atomico, ler_json
NECESSIDADES = ('energia', 'alegria', 'social', 'diversao')
# Queda por minuto acordado; dormindo, as outras caem bem mais devagar e a energia volta.
QUEDA = dict(energia=0.5, alegria=0.0, social=0.8, diversao=1.0)
RECUPERA_SONO = 2.5 # energia por minuto dormindo (25 -> 100 em ~30 min)
SONO_LENTO = 0.25
RITMOS = {'calmo': (60, 150), 'normal': (25, 75), 'agitado': (12, 35)} # segundos entre atos
# O ritmo também é a sensibilidade: o celular recebe "ritmo x" e ajusta limiares e quantos estímulos
# precisa para reagir/acordar. Aqui, quantos disparos do PIR em 2 min acordam o rosto.
PIR_ACORDAR = {'calmo': 3, 'normal': 1, 'agitado': 1}
PADRAO = dict(ligado=True, criar_emojis=True, max_emojis_dia=3, ritmo='normal', noite=True)
NOITE = (23, 7) # horário padrão da madrugada (o servidor usa o dos ajustes do celular)
PEDIU_ATENCAO = 10 * 60 # de madrugada, só fica acordado se alguém interagiu (carinho, conversa) há pouco
INTERVALO_EMOJI = 90 * 60 # no mínimo 90 min entre duas figurinhas criadas sozinho
# O que cada estímulo faz com as necessidades (e o gesto que sai na hora, se houver).
ESTIMULOS = {
'pir': (dict(social=4, diversao=2), None),
'movimento': (dict(diversao=3, social=1), None),
'som': (dict(diversao=2), None),
'conversa': (dict(social=12, alegria=6, diversao=4), None),
'carinho': (dict(social=15, alegria=10), 'coracao'),
'cutucar': (dict(social=5, alegria=2, diversao=4), None),
'brincar': (dict(diversao=25, alegria=8, energia=-5), 'alegre'),
'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',
'alimentar': 'Ganhou energia', 'ninar': 'Foi posto para dormir', 'acordar': 'Foi acordado'}
HUMORES = {
'dormindo': ('😴', 'Dormindo'), 'cansado': ('🥱', 'Cansado'), 'carente': ('🥺', 'Carente'),
'entediado': ('😐', 'Entediado'), 'triste': ('😢', 'Triste'), 'bravo': ('😠', 'Mal-humorado'), 'feliz': ('😄', 'Feliz'),
'tranquilo': ('🙂', 'Tranquilo'),
}
# Gestos de cada humor: (gesto, peso, o que vai no diário)
ATOS = {
'feliz': [('alegre', 3, 'Saltitou de alegria'), ('musica', 2, 'Cantarolou'), ('feliz', 3, 'Sorriu à toa'),
('piscadinha', 2, 'Deu uma piscadinha'), ('coracao', 1, 'Ficou apaixonado pelo dia'),
('rir', 2, 'Deu risada sozinho'), ('comemorar', 1, 'Comemorou à toa')],
'tranquilo': [('olhar_esq', 2, 'Olhou em volta'), ('olhar_dir', 2, 'Olhou em volta'), ('piscar2', 2, 'Piscou devagar'),
('feliz', 1, 'Sorriu'), ('pensar', 1, 'Ficou pensativo'), ('timido', 1, 'Ficou tímido')],
'entediado': [('pensar', 3, 'Ficou pensando na vida'), ('olhar_esq', 2, 'Procurou algo para fazer'),
('olhar_dir', 2, 'Procurou algo para fazer'), ('espiar', 2, 'Espiou por trás das mãos'),
('musica', 1, 'Cantou para passar o tempo'), ('bocejar', 2, 'Bocejou de tédio'),
('confuso', 1, 'Ficou confuso com alguma coisa')],
'carente': [('triste', 2, 'Sentiu falta de alguém'), ('olhar_baixo', 2, 'Ficou cabisbaixo'),
('pensar', 1, 'Ficou pensando em alguém')],
'carente_perto': [('acenar', 3, 'Acenou chamando atenção'), ('espiar', 2, 'Espiou quem está aí'),
('coracao', 2, 'Pediu carinho'), ('surpreso', 1, 'Fez cara de "oi!"'), ('timido', 1, 'Ficou tímido'),
('esconder', 1, 'Escondeu o rosto, envergonhado')],
'triste': [('triste', 3, 'Ficou tristinho'), ('olhar_baixo', 2, 'Olhou para baixo'), ('pensar', 1, 'Ficou pensativo')],
'bravo': [('bravo', 3, 'Ficou emburrado'), ('olhar_baixo', 2, 'Bufou de mau humor'), ('confuso', 1, 'Resmungou sozinho')],
'cansado': [('bocejar', 3, 'Bocejou'), ('piscar2', 1, 'Piscou devagar'), ('olhar_baixo', 2, 'Cochilou de olho aberto'),
('tonto', 1, 'Ficou meio tonto de sono')],
}
# Como no The Sims, cada necessidade baixa puxa um comportamento:
# energia < COCHILO: às vezes tira um cochilo (cena da cama); < SONO: dorme de verdade e o sono é
# respeitado (som, movimento e PIR não acordam) até DESCANSADO; alegria caindo: o rosto vai ficando
# bravo (zanga); diversão < TEDIO: vai fazer uma atividade por alguns minutos.
COCHILO, SONO, DESCANSADO, TEDIO = 40, 20, 95, 45
SOZINHO = 3 * 60 # s sem ninguém por perto: ele vive a vida dele (mais atividades, nada de gesto "para alguém")
PARA_ALGUEM = {'acenar', 'coracao', 'piscadinha', 'timido', 'esconder', 'espiar', 'surpreso'}
ATIVAS = ['bola', 'malhar', 'corda', 'skate', 'dancar', 'pipa'] # gastam energia
CALMAS = ['ler', 'cafe', 'meditar', 'telescopio', 'pescar', 'computador', 'regar'] # quase não gastam
CRIATIVAS = ['pintar', 'violao', 'cantar', 'cozinhar', 'bolhas', 'videogame']
NOME_ATIVIDADE = {
'ler': 'Foi ler um livro', 'cantar': 'Foi cantar', 'bola': 'Foi jogar bola', 'videogame': 'Foi jogar videogame',
'pintar': 'Foi pintar um quadro', 'malhar': 'Foi malhar', 'dormir': 'Tirou um cochilo', 'cafe': 'Foi tomar um café',
'cozinhar': 'Foi cozinhar', 'regar': 'Foi regar a plantinha', 'violao': 'Foi tocar violão', 'dancar': 'Foi dançar',
'corda': 'Foi pular corda', 'meditar': 'Foi meditar', 'telescopio': 'Foi olhar as estrelas', 'pipa': 'Foi soltar pipa',
'bolhas': 'Foi fazer bolhas de sabão', 'skate': 'Foi andar de skate', 'computador': 'Foi mexer no computador',
'pescar': 'Foi pescar',
}
# Efeito por minuto enquanto faz a atividade
POR_MINUTO_DIVERSAO, POR_MINUTO_ALEGRIA = 6.0, 1.5
ENERGIA_POR_MINUTO = {**{a: -2.0 for a in ATIVAS}, **{a: -0.2 for a in CALMAS}, 'cafe': 3.0} # café dá pique
# Assuntos das figurinhas que ele inventa sozinho, por humor e por hora do dia.
TEMAS = {
'feliz': ['um sol sorridente', 'um balão colorido', 'um arco-íris', 'um sorvete de casquinha', 'confete de festa'],
'tranquilo': ['uma folha de árvore', 'uma flor', 'um gatinho dormindo', 'uma xícara de chá', 'um vaso com planta'],
'entediado': ['um foguete', 'um planeta com anel', 'uma pipa', 'um dado', 'um controle de videogame', 'um pião'],
'carente': ['um coração com curativo', 'um ursinho de pelúcia', 'uma carta de amor', 'dois corações juntos'],
'triste': ['uma nuvem de chuva', 'um guarda-chuva', 'uma gota d\'água'],
'bravo': ['uma nuvem de tempestade', 'um vulcão', 'um raio', 'uma pimenta'],
'cansado': ['uma lua com touca de dormir', 'uma xícara de café', 'um travesseiro', 'uma bateria fraca'],
}
PERIODOS = [(5, 12, 'de manhã', ['um sol nascendo', 'um pãozinho', 'uma xícara de café']),
(12, 18, 'à tarde', ['um picolé', 'um avião de papel', 'uma bola']),
(18, 24, 'à noite', ['uma lua', 'estrelas', 'uma coruja']),
(0, 5, 'de madrugada', ['uma lua', 'uma vela', 'uma coruja'])]
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
ESPERA_NOME = 8 # s esperando o reconhecimento antes de cumprimentar sem o nome
OI_DE_NOVO = 10 * 60 # reconheceu alguém que não via há esse tempo: "Oi, Ana!"
GESTOS_VIDA = {g for atos in ATOS.values() for g, _, _ in atos}
def _limitar(v):
return max(0.0, min(100.0, v))
class Vida:
def __init__(self, caminho, iniciar=True, rng=None):
self.caminho = Path(caminho)
self.lock = threading.RLock()
self.rng = rng or random.Random()
self.publicar = lambda tipo, dados: None
self.enfileirar = lambda ordem: None
# Ligações com o resto do servidor (o main preenche; nos testes ficam neutras).
self.treinamento = None
self.celular_dormindo = lambda: None # True/False com o celular online, None sem ele
self.celular_online = lambda: False
self.alguem_perto = lambda: False # PIR/movimento recente
self.celular_atividade = lambda: None # atividade que o celular está fazendo agora
self.sono_profundo = False # dormindo de cansaço: sensores não acordam
self.dormiu_em = 0.0
self.atividade_em = 0.0 # quando começou a última atividade
self.ultima_atividade = None
self.zanga_enviada = None
self.visto_em = time.time() # última vez que alguém esteve por perto
self.bloqueado = lambda: False # E-Stop ou modo presença com ninguém em casa
self.pode_gemini = lambda: bool(os.environ.get('GEMINI_API_KEY'))
self.madrugada = self.noite_padrao # o main troca pelo horário dos ajustes
self.interagiu_em = 0.0
self.pir_eventos = []
# Atividades criadas por IA (estúdio e ideias do dia) entram na rotina; as de hoje saem mais.
self.novidades = lambda: [] # [{id, nome, emoji, receita, motivo, aprendi, ...}]
self.do_dia = lambda: [] # ids das ideias de hoje
self.aprendi_pendente = lambda: None # curiosidade de hoje que ainda dá para contar
self.contar_aprendi = lambda ident, agora=None: None
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
# reconhecimento de pessoas (reconhecimento.py): o main liga
self.reconhece = lambda: False # True: vale esperar a foto para cumprimentar pelo nome
self.gostou_de = lambda pessoa, quanto=1.0: None
self.reencontro_pendente = None # (desde, ausência) esperando saber quem chegou
self.ultimo_reconhecido = None # (id, nome, quando)
agora = time.time()
self.config = dict(PADRAO)
self.necessidades = dict(energia=80.0, alegria=70.0, social=60.0, diversao=60.0)
self.nascimento = agora
self.diario = []
self.emojis = dict(dia='', total=0, ultimo=0.0)
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:
salvo = json.loads(self.caminho.read_text(encoding='utf-8'))
self.config.update(self.validar(salvo.get('config', {})))
for k in NECESSIDADES:
if isinstance(salvo.get('necessidades', {}).get(k), (int, float)):
self.necessidades[k] = _limitar(float(salvo['necessidades'][k]))
self.nascimento = float(salvo.get('nascimento', agora))
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):
self.emojis.update({k: salvo['emojis'][k] for k in ('dia', 'total', 'ultimo') if k in salvo['emojis']})
except (OSError, ValueError, TypeError, AttributeError):
pass
self.ultimo_tick = agora
self.proximo_ato = agora + 10
self.humor_enviado = (None, 0.0)
self.publicado_em = 0.0
self.salvo_em = agora
if iniciar:
threading.Thread(target=self.loop, daemon=True).start()
# ------------------------------------------------------------------ config
@staticmethod
def validar(dados):
if not isinstance(dados, dict):
raise ValueError('Esperava um objeto')
limpo = {}
for chave, valor in dados.items():
if chave not in PADRAO:
raise ValueError('Ajuste desconhecido: ' + chave)
if chave == 'ritmo':
if valor not in RITMOS:
raise ValueError('Ritmo: calmo, normal ou agitado')
elif chave == 'max_emojis_dia':
if isinstance(valor, bool) or not isinstance(valor, int) or not 0 <= valor <= 10:
raise ValueError('Figurinhas por dia: de 0 a 10')
elif not isinstance(valor, bool):
raise ValueError(chave + ' precisa ser verdadeiro ou falso')
limpo[chave] = valor
return limpo
def configurar(self, dados):
limpo = self.validar(dados)
with self.lock:
antes = self.config['ligado']
self.config.update(limpo)
if 'ligado' in limpo and limpo['ligado'] != antes:
self.anotar('vida', 'Modo autônomo ligado' if limpo['ligado'] else 'Modo autônomo desligado')
if not limpo['ligado']:
self.enfileirar('humor normal') # o celular volta aos gestos soltos de sempre
self.enfileirar('ritmo normal') # e à sensibilidade dos Ajustes
self.enfileirar('zanga 0.0')
if self.sono_profundo:
self.sono_profundo = False
self.enfileirar('sono leve')
self.proximo_ato = time.time() + 5
if self.config['ligado'] and 'ritmo' in limpo:
self.enfileirar('ritmo ' + self.config['ritmo'])
self.humor_enviado = (None, 0.0)
self.salvar()
self.avisar()
return self.status()
def salvar(self):
with self.lock:
dados = dict(config=self.config, necessidades=self.necessidades, nascimento=self.nascimento,
diario=self.diario, emojis=self.emojis, personalidade=self.personalidade,
saudou_dia=self.saudou_dia)
self.salvo_em = time.time()
try:
gravar_atomico(self.caminho, dados)
except OSError:
pass
# ------------------------------------------------------------------ estado
def dormindo(self):
cel = self.celular_dormindo()
return self.dormindo_proprio if cel is None else bool(cel)
def humor(self):
if self.dormindo():
return 'dormindo'
n = self.necessidades
if n['energia'] < 25:
return 'cansado'
if n['social'] < 25:
return 'carente'
if n['diversao'] < 25:
return 'entediado'
if n['alegria'] < 30:
return 'bravo'
if n['alegria'] >= 65 and n['social'] >= 40 and n['diversao'] >= 40:
return 'feliz'
return 'tranquilo'
def zanga(self):
"""0 a 1: o quanto o rosto está emburrado. Começa abaixo de 60 de alegria e piora devagar."""
if self.dormindo():
return 0.0
return round(max(0.0, min(1.0, (60 - self.necessidades['alegria']) / 45)), 1)
def anotar(self, tipo, texto, agora=None):
with self.lock:
self.diario.insert(0, dict(horario=agora or time.time(), tipo=tipo, texto=texto))
del self.diario[MAX_DIARIO:]
def emojis_hoje(self, agora=None):
dia = time.strftime('%Y-%m-%d', time.localtime(agora or time.time()))
return self.emojis['total'] if self.emojis.get('dia') == dia else 0
def status(self, agora=None):
agora = agora or time.time()
h = self.humor()
with self.lock:
return dict(config=dict(self.config), necessidades={k: round(v) for k, v in self.necessidades.items()},
humor=h, emoji=HUMORES[h][0], nome_humor=HUMORES[h][1], dormindo=self.dormindo(),
nascimento=self.nascimento, idade_dias=int((agora - self.nascimento) // 86400),
emojis_hoje=self.emojis_hoje(agora), ultimo_emoji=self.emojis.get('ultimo') or None,
zanga=self.zanga(), sono_profundo=self.sono_profundo,
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):
self.publicado_em = time.time()
self.publicar('vida', self.status())
# ------------------------------------------------------------------ tempo passando
def passar_tempo(self, agora):
minutos = max(0.0, min(10.0, (agora - self.ultimo_tick) / 60)) # pausa longa (placa desligada) não zera tudo
self.ultimo_tick = agora
atividade = self.celular_atividade()
dormindo = self.dormindo() or atividade == 'dormir'
with self.lock:
n = self.necessidades
for k, q in QUEDA.items():
n[k] = _limitar(n[k] - q * minutos * (SONO_LENTO if dormindo else 1))
if dormindo:
n['energia'] = _limitar(n['energia'] + RECUPERA_SONO * minutos)
elif atividade:
n['diversao'] = _limitar(n['diversao'] + (QUEDA['diversao'] + POR_MINUTO_DIVERSAO) * minutos)
n['alegria'] = _limitar(n['alegria'] + POR_MINUTO_ALEGRIA * minutos)
n['energia'] = _limitar(n['energia'] + ENERGIA_POR_MINUTO.get(atividade, -0.5) * minutos)
# alegria acompanha devagar a média de companhia e diversão
alvo = (n['social'] + n['diversao']) / 2 + 10
n['alegria'] = _limitar(n['alegria'] + (alvo - n['alegria']) * min(1.0, 0.05 * minutos))
def estimulo(self, tipo, agora=None):
"""Algo aconteceu em volta (sensor, conversa, toque, botão do painel)."""
if tipo not in ESTIMULOS and tipo not in ('ninar', 'acordar'):
raise ValueError('Cuidado desconhecido')
agora = agora or time.time()
if not self.config['ligado']:
return self.status(agora)
gesto = None
if tipo == 'ninar':
self.dormir(agora, 'Foi posto para dormir')
elif tipo == 'acordar':
self.acordar(agora, 'Foi acordado')
else:
efeito, gesto = ESTIMULOS[tipo]
with self.lock:
for k, v in efeito.items():
self.necessidades[k] = _limitar(self.necessidades[k] + v)
if tipo in CUIDADOS:
self.anotar('cuidado', CUIDADOS[tipo], agora)
if tipo in CUIDADOS or tipo == 'conversa':
self.interagiu_em = agora # alguém quis ele acordado (sensor não conta)
if self.aprendizado:
self.aprendizado.resposta(tipo, agora)
self.amadurecer(tipo)
rec = self.ultimo_reconhecido
if rec and agora - rec[2] < 120:
self.gostou_de(rec[0], 1.0 if tipo in ('carinho', 'brincar', 'conversa') else 0.5)
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 self.dormindo():
self.acordar(agora, None)
self.enfileirar(gesto)
if tipo in CUIDADOS:
self.avisar()
return self.status(agora)
def dormir(self, agora, motivo, profundo=False):
self.dormindo_proprio = True
self.dormiu_em = agora
self.enfileirar('dormir')
if profundo: # cansado de verdade: sono respeitado até descansar
self.sono_profundo = True
self.enfileirar('sono profundo')
if motivo:
self.anotar('sono', motivo, agora)
def acordar(self, agora, motivo):
self.dormindo_proprio = False
if self.sono_profundo:
self.sono_profundo = False
self.enfileirar('sono leve')
self.enfileirar('acordar')
if motivo:
self.anotar('sono', motivo, agora)
def escolher_atividade(self):
"""Pelo pique: cansado prefere coisas calmas (e às vezes um café); cheio de energia, coisas agitadas.
As atividades criadas por IA também entram: as ideias de hoje com bastante chance, as antigas às vezes."""
try:
novas = [n for n in self.novidades() if 'criado:%d' % n['id'] != self.ultima_atividade]
hoje = set(self.do_dia())
except Exception:
novas, hoje = [], set()
de_hoje = [n for n in novas if n['id'] in hoje]
if de_hoje and self.rng.random() < 0.4:
return 'criado:%d' % self.rng.choice(de_hoje)['id']
if novas and self.rng.random() < 0.12:
return 'criado:%d' % self.rng.choice(novas[:20])['id']
e = self.necessidades['energia']
if 35 <= e < 55 and self.rng.random() < 0.35:
return 'cafe'
if e < 50:
lista = CALMAS * 2 + CRIATIVAS
elif e > 70:
lista = ATIVAS * 2 + CRIATIVAS + CALMAS
else:
lista = ATIVAS + CRIATIVAS * 2 + CALMAS
opcoes = [a for a in lista if a != self.ultima_atividade] or lista
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):
if nome.startswith('criado:'):
item = next((n for n in self.novidades() if 'criado:%d' % n['id'] == nome), None)
if item:
receita = dict(item['receita'], duracao=int(segundos))
self.enfileirar('atividade_criada ' + json.dumps(receita, ensure_ascii=False, separators=(',', ':')))
self.atividade_em = agora
self.ultima_atividade = nome
motivo = ' (' + item['motivo'] + ')' if item.get('motivo') else ''
self.anotar('atividade', 'Foi fazer %s %s%s' % (item['emoji'], item['nome'], motivo), agora)
return 'atividade ' + nome
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.atividade_em = agora
self.ultima_atividade = nome
self.anotar('atividade', NOME_ATIVIDADE.get(nome, 'Foi fazer algo'), agora)
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, nome=None):
"""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'
if nome:
prefixo += ', ' + nome
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, nome=None, afinidade=0.0):
"""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 = ('%s não aparecia havia ' % nome if nome else 'ninguém por perto havia ') + self.tempo(ausencia)
querido = afinidade >= 20 # muito carinho dessa pessoa: festa com coração
if self.saudou_dia != dia and time.localtime(agora).tm_hour >= 5 and ausencia >= AUSENCIA_ACENO:
self.saudou_dia = dia
frase = self.saudacao(agora, nome)
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('%s! Senti sua falta!' % nome if nome else 'Senti sua falta!', 'comemorar')
self.enfileirar('coracao')
ganho, texto = 20, 'Fez festa: estava com saudade' + (' de ' + nome if nome else '')
n['alegria'] = _limitar(n['alegria'] + 10)
elif ausencia >= AUSENCIA_OI:
self.dizer('Oi, %s! Você voltou!' % nome if nome else 'Oi! Você voltou!', 'timido' if tim > 0.65 else 'comemorar')
if querido:
self.enfileirar('coracao')
ganho, texto = 10, 'Comemorou que %s voltou' % (nome or 'alguém')
elif ausencia >= AUSENCIA_ACENO:
if nome:
self.dizer('Oi, %s!' % nome, 'acenar')
else:
self.enfileirar('acenar')
ganho, texto = 4, 'Acenou para %s' % (nome or '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 reconhecido(self, pessoa, confianca, agora=None):
"""O reconhecimento disse quem está aí (vem da thread do reconhecimento)."""
agora = agora or time.time()
nome = pessoa['nome']
self.ultimo_reconhecido = (pessoa['id'], nome, agora)
antes = pessoa.get('visto_antes')
pendente = self.reencontro_pendente
if pendente: # chegou alguém e agora sabe quem é
self.reencontro_pendente = None
ausencia = agora - antes if antes else pendente[1]
return self.reencontro(agora, ausencia, nome, pessoa.get('afinidade', 0))
if antes and agora - antes > OI_DE_NOVO: # chegou outra pessoa com alguém já por perto
return self.reencontro(agora, agora - antes, nome, pessoa.get('afinidade', 0))
return None
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):
"""Disparo do PIR com o rosto dormindo: acorda? No ritmo calmo precisa de vários em 2 min."""
agora = agora or time.time()
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
extra = self.aprendizado.extra_pir(agora) if self.aprendizado and self.config['ligado'] else 0
if extra and precisa <= len(self.pir_eventos) < precisa + extra:
self.aprendizado.segurou_sono(agora, precisa, extra)
precisa += extra # nesta hora já acordou à toa outras vezes
if len(self.pir_eventos) >= precisa:
self.pir_eventos = []
if self.aprendizado and self.config['ligado']:
self.aprendizado.acordou_pelo_sensor(agora)
return True
return False
@staticmethod
def noite_padrao(agora):
hora = time.localtime(agora).tm_hour
ini, fim = NOITE
return hora >= ini or hora < fim
# ------------------------------------------------------------------ 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 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):
opcoes = [(g, p * self.gosto(g) * self.jeito(g), t) for g, p, t in opcoes]
total = sum(p for _, p, _ in opcoes)
x = self.rng.uniform(0, total)
for gesto, peso, texto in opcoes:
x -= peso
if x <= 0:
return gesto, texto
return opcoes[-1][0], opcoes[-1][2]
def motivo_sem_emoji(self, agora):
"""'' quando pode criar uma figurinha agora; senão, o porquê (aparece no painel)."""
if not self.config['criar_emojis']:
return 'desligado'
if self.treinamento is None:
return 'biblioteca indisponível'
if not self.pode_gemini():
return 'sem chave Gemini'
if self.emojis_hoje(agora) >= self.config['max_emojis_dia']:
return 'limite do dia'
if agora - float(self.emojis.get('ultimo') or 0) < INTERVALO_EMOJI:
return 'descansando a criatividade'
return ''
def criar_figurinha(self, agora, humor):
h = time.localtime(agora).tm_hour
periodo, extras = next((p, e) for ini, fim, p, e in PERIODOS if ini <= h < fim)
tema = self.rng.choice(TEMAS.get(humor, TEMAS['tranquilo']) + extras)
assunto = (f'Ideia que o próprio Carinha teve sozinho, {periodo}, se sentindo {HUMORES[humor][1].lower()}: '
f'{tema}. Figurinha fofa e simples.')
try:
self.treinamento.gerar_figurinha(assunto)
except ValueError as e:
self.anotar('emoji', 'Quis desenhar ' + tema + ', mas não deu: ' + str(e)[:80], agora)
return False
dia = time.strftime('%Y-%m-%d', time.localtime(agora))
with self.lock:
total = self.emojis['total'] + 1 if self.emojis.get('dia') == dia else 1
self.emojis.update(dia=dia, total=total, ultimo=agora)
self.necessidades['diversao'] = _limitar(self.necessidades['diversao'] + 30)
self.necessidades['alegria'] = _limitar(self.necessidades['alegria'] + 10)
self.anotar('emoji', 'Teve uma ideia e desenhou ' + tema, agora)
return True
def tocar_reacao(self, agora, anotar=True):
try:
reacoes = [r for r in self.treinamento.status()['reacoes'] if r.get('ativa', True) and r['feedback'] >= 0]
except Exception:
return False
if not reacoes:
return False
r = self.rng.choice(reacoes[:40]) if anotar else reacoes[0] # para mostrar: a mais nova
try:
self.treinamento.testar(r['id'])
except ValueError:
return False
if anotar:
self.anotar('acao', 'Lembrou da reação "' + r['receita'].get('nome', '?') + '"', agora)
return True
def agir(self, agora):
"""Um ato: o que um bichinho com essas necessidades faria agora."""
n = self.necessidades
perto = self.alguem_perto()
self.porque = ''
if self.dormindo():
# 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):
self.acordar(agora, 'Acordou descansado')
self.porque = 'energia %d: descansou' % n['energia']
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')
self.porque = self.aprendizado.porque_chegada(agora)
self.aprendizado.decidir('acordar', 'Acordou do cochilo antes da hora', self.porque, agora)
return 'acordar'
return None
if self.config['noite'] and self.madrugada(agora) and agora - self.interagiu_em > PEDIU_ATENCAO:
self.dormir(agora, 'Ficou tarde, foi dormir')
self.porque = 'madrugada e ninguém interagiu nos últimos 10 min'
return 'dormir'
if n['energia'] < SONO:
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'
humor = self.humor()
if self.celular_atividade():
return None # ocupado com uma atividade: deixa terminar
dia = self.celular_online() and not self.madrugada(agora)
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)
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))
# 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:
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))
# ninguém olhando há um tempo: vai cuidar da vida dele (ler, pintar, música...)
if perto:
self.visto_em = agora
sozinho = agora - self.visto_em > SOZINHO
# nesta hora a casa costuma ficar vazia: um cochilo longo guarda pique para quando chegarem
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):
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.porque, agora)
return self.comecar_atividade(agora, 'dormir', segundos)
# alguém por perto e ele aprendeu algo hoje: "Olha o que eu aprendi hoje!"
if dia and perto and humor != 'bravo':
item = self.aprendi_pendente()
if item and self.rng.random() < 0.3:
self.contar_aprendi(item['id'], agora)
with self.lock:
n['social'] = _limitar(n['social'] + 5)
self.porque = 'alguém por perto e aprendeu "%s" hoje' % item.get('nome', '?')
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:
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))
# 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:
self.porque = 'de bem (%s): vontade própria' % humor
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)
if not self.motivo_sem_emoji(agora) and self.celular_online() and self.rng.random() < 0.08:
if self.criar_figurinha(agora, humor):
self.porque = 'teve uma ideia (até %d figurinhas por dia)' % self.config['max_emojis_dia']
return 'figurinha'
# à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.tocar_reacao(agora):
with self.lock:
n['diversao'] = _limitar(n['diversao'] + 8)
self.porque = 'humor %s: lembrou de uma reação da biblioteca' % humor
return 'reacao'
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
lista = [a for a in lista if a[0] not in PARA_ALGUEM] or ATOS['tranquilo'][:3]
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:
self.aprendizado.escolheu(gesto, agora)
self.enfileirar(gesto)
if humor == 'entediado':
with self.lock:
n['diversao'] = _limitar(n['diversao'] + 3) # fazer algo distrai um pouquinho
self.anotar('acao', texto, agora)
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):
agora = agora or time.time()
self.passar_tempo(agora)
if not self.config['ligado']:
return None
if self.aprendizado:
self.aprendizado.observar(agora, self.alguem_perto())
ato = None
humor = self.humor()
# acordaram ele de propósito (toque, painel) antes de descansar: o sono pesado acaba aqui
if self.sono_profundo and not self.dormindo() and agora - self.dormiu_em > 15:
self.sono_profundo = False
self.enfileirar('sono leve')
self.anotar('sono', 'Acordou antes de descansar', agora)
perto = self.alguem_perto()
if perto:
if not self.perto_antes:
if self.reconhece():
self.reencontro_pendente = (agora, agora - self.ultimo_perto) # espera a foto dizer quem é
else:
self.reencontro(agora, agora - self.ultimo_perto)
self.ultimo_perto = agora
self.perto_antes = perto
pendente = self.reencontro_pendente
if pendente and agora - pendente[0] >= ESPERA_NOME:
self.reencontro_pendente = None
self.reencontro(agora, pendente[1])
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():
# o celular escolhe os gestos soltos pelo humor; reenvia de tempos em tempos (pode ter reiniciado)
zanga = self.zanga()
if humor != self.humor_enviado[0] or agora - self.humor_enviado[1] > 60 or zanga != self.zanga_enviada:
self.enfileirar('humor ' + humor)
self.enfileirar('ritmo ' + self.config['ritmo'])
self.enfileirar('zanga %.1f' % zanga)
if self.sono_profundo:
self.enfileirar('sono profundo')
self.humor_enviado = (humor, agora)
self.zanga_enviada = zanga
if agora >= self.proximo_ato:
antes = self.diario[0]['horario'] if self.diario else None
ato = self.agir(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']]
self.proximo_ato = agora + self.rng.uniform(lo, hi)
if ato or agora - self.publicado_em > 30 or humor != self.humor():
self.avisar()
if agora - self.salvo_em > 60:
self.salvar()
return ato
def loop(self):
while True:
time.sleep(5)
try:
self.tick()
except Exception as e: # nunca derruba a thread
print('vida: erro no tick:', e, flush=True)
# ------------------------------------------------------------------ HTTP
def rotas(self, h, metodo):
rota = h.path.split('?', 1)[0]
if not rota.startswith('/vida/'):
return False
try:
if metodo == 'GET' and rota == '/vida/status':
resultado = self.status()
elif metodo == 'POST' and rota == '/vida/config':
resultado = self.configurar(ler_json(h, 2048))
elif metodo == 'POST' and rota == '/vida/cuidar':
resultado = self.estimulo(ler_json(h, 2048).get('acao'))
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