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>
This commit is contained in:
julio
2026-10-11 04:38:10 -03:00
co-authored by Claude Opus 5.5
parent 2933407b9e
commit 938467e318
13 changed files with 1079 additions and 21 deletions
+51 -11
View File
@@ -124,6 +124,8 @@ GESTOS_TRACO = {'brincadeira': {'rir', 'comemorar', 'alegre', 'musica', 'piscadi
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}
@@ -172,6 +174,11 @@ class Vida:
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)
@@ -359,6 +366,9 @@ class Vida:
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)
@@ -453,10 +463,12 @@ class Vida:
def tempo(seg):
return '%d min' % round(seg / 60) if seg < 3600 else '%dh%02d' % (seg // 3600, seg % 3600 // 60)
def saudacao(self, agora):
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:
@@ -478,29 +490,35 @@ class Vida:
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):
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 = 'ninguém por perto havia ' + self.tempo(ausencia)
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)
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('Senti sua falta!', 'comemorar')
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'
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! Você voltou!', 'timido' if tim > 0.65 else 'comemorar')
ganho, texto = 10, 'Comemorou que alguém voltou'
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:
self.enfileirar('acenar')
ganho, texto = 4, 'Acenou para quem voltou'
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:
@@ -510,6 +528,21 @@ class Vida:
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
@@ -769,9 +802,16 @@ class Vida:
perto = self.alguem_perto()
if perto:
if not self.perto_antes:
self.reencontro(agora, agora - self.ultimo_perto)
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