Todo dia, a partir das 9h, o Carinha cria 3 atividades novas (inspiracao.py): - sinais de fontes abertas: tempo da cidade (Open-Meteo), datas comemorativas da semana (lista fixa + feriados da BrasilAPI), "neste dia" da Wikipédia e assuntos em alta no Google Trends Brasil; - o Gemini escolhe ideias leves (descarta tragédia, política e afins) e cada uma vira atividade pelo estúdio (Criacao.gerar), com motivo e uma curiosidade; - até 3 tentativas por dia, 30 min entre elas; botão "Criar mais agora"; - as atividades criadas entram na rotina do modo vida (as de hoje com 40% de chance) e, com alguém perto, ele conta a curiosidade: legenda e voz de robô (ordem "aprendi", até 2x por dia cada); - painel: cartão "Ideias do dia" no estúdio (Mostrar, Contar, o que ele viu na internet, ajustes: quantas, horário, cidade, fontes) e tópico no guia. Correções achadas no caminho: - WebView de fábrica do S10 (Chrome 102) não tem AbortSignal.timeout: o celular nunca mandava o status e a placa o via offline; - arquivos estáticos com Cache-Control no-cache e o app com LOAD_NO_CACHE: o WebView seguia com uma carinha.html velha depois de deploy; - status do celular recusado vai para o journal (1 linha por minuto); - legenda mais estreita para não cobrir a bateria. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
205 lines
9.7 KiB
Python
205 lines
9.7 KiB
Python
"""Ideias do dia: sinais da internet (falsos), filtro pela IA, criação das atividades, horário e "aprendi"."""
|
|
import json
|
|
import random
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
from criacao import ATIVIDADES_BASE, Criacao # noqa: E402
|
|
from inspiracao import Inspiracao, descrever_tempo # noqa: E402
|
|
from vida import Vida # noqa: E402
|
|
|
|
TEMPO = dict(current=dict(temperature_2m=12.4, weather_code=63, is_day=1),
|
|
daily=dict(temperature_2m_max=[15.2], temperature_2m_min=[9.8], precipitation_probability_max=[80],
|
|
weather_code=[63]))
|
|
FERIADOS = [dict(date='2026-10-12', name='Nossa Senhora Aparecida', type='national')]
|
|
HISTORIA = dict(selected=[dict(year=1957, text='A União Soviética lança o Sputnik 1, o primeiro satélite artificial.')],
|
|
events=[dict(year=1871, text='Começa o grande incêndio de Chicago.')])
|
|
ALTA = ('<?xml version="1.0"?><rss><channel><item><title>semana mundial do espaço</title></item>'
|
|
'<item><title>eleição municipal</title></item></channel></rss>')
|
|
# 8 de outubro de 2026, 10h (horário de Brasília)
|
|
AGORA = time.mktime((2026, 10, 8, 10, 0, 0, 0, 0, -1))
|
|
|
|
|
|
def baixar_falso(url, timeout=10):
|
|
if 'open-meteo.com/v1/forecast' in url:
|
|
return json.dumps(TEMPO).encode()
|
|
if 'brasilapi' in url:
|
|
return json.dumps(FERIADOS).encode()
|
|
if 'onthisday' in url:
|
|
return json.dumps(HISTORIA).encode()
|
|
if 'trends.google' in url:
|
|
return ALTA.encode()
|
|
if 'geocoding' in url:
|
|
return json.dumps(dict(results=[dict(name='Curitiba', admin1='Paraná', latitude=-25.4284, longitude=-49.2733)])).encode()
|
|
raise OSError('sem internet')
|
|
|
|
|
|
class InspiracaoTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
pasta = Path(self.tmp.name)
|
|
self.c = Criacao(pasta / 'c.db', pasta / 'c.json')
|
|
self.prompts = []
|
|
ideias = dict(ideias=[
|
|
dict(pedido='bonequinho de astronauta pulando com estrelas em volta', motivo='Semana Mundial do Espaço',
|
|
fonte='alta', aprendi='Sabia que o Sputnik foi o primeiro satélite, em 1957?'),
|
|
dict(pedido='sentado lendo um livro com uma xícara, chuva lá fora', motivo='Chuva em Londrina', fonte='tempo',
|
|
aprendi='A chuva enche os rios e rega as plantas!'),
|
|
dict(pedido='dançando com estrelas para o Dia das Crianças', motivo='Dia das Crianças chegando', fonte='datas',
|
|
aprendi='O Dia das Crianças no Brasil é em 12 de outubro.')])
|
|
|
|
def chamar(ia, prompt, schema):
|
|
self.prompts.append(prompt)
|
|
if 'ideias' in schema['properties']:
|
|
return ideias
|
|
return dict(ATIVIDADES_BASE['dancar'], nome='Atividade %d' % len(self.prompts))
|
|
self.c.chamar = chamar
|
|
self.i = Inspiracao(pasta / 'i.json', self.c, iniciar=False)
|
|
self.i.baixar = baixar_falso
|
|
self.i.pode_gemini = lambda: True
|
|
self.ordens, self.diario = [], []
|
|
self.i.enfileirar = self.ordens.append
|
|
self.i.anotar = lambda tipo, texto, agora=None: self.diario.append((tipo, texto))
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def test_sinais_das_quatro_fontes(self):
|
|
s = self.i.coletar(AGORA)
|
|
self.assertIn('12 °C e chuva', s['tempo'])
|
|
self.assertIn('80% de chance de chuva', s['tempo'])
|
|
self.assertTrue(any('Nossa Senhora Aparecida' in d for d in s['datas']))
|
|
self.assertIn('em 4 dias (12/10): Dia das Crianças', s['datas'])
|
|
self.assertTrue(any('Sputnik' in f for f in s['historia']))
|
|
self.assertEqual(s['alta'], ['semana mundial do espaço', 'eleição municipal'])
|
|
self.assertEqual(descrever_tempo(95), 'tempestade com trovões')
|
|
|
|
def test_fonte_fora_do_ar_nao_impede_as_outras_e_fontes_desligadas(self):
|
|
def so_tempo(url, timeout=10):
|
|
if 'forecast' in url:
|
|
return json.dumps(TEMPO).encode()
|
|
raise OSError('fora do ar')
|
|
self.i.baixar = so_tempo
|
|
self.assertEqual(list(self.i.coletar(AGORA)), ['tempo', 'datas']) # as datas fixas não dependem da internet
|
|
self.i.baixar = baixar_falso
|
|
self.i.configurar(dict(fontes=dict(alta=False, historia=False)))
|
|
self.assertEqual(sorted(self.i.coletar(AGORA)), ['datas', 'tempo'])
|
|
with self.assertRaises(ValueError):
|
|
self.i.configurar(dict(fontes=dict(tempo=False, datas=False))) # sobraria nenhuma
|
|
|
|
def test_cria_as_ideias_do_dia_na_hora_certa(self):
|
|
cedo = AGORA - 3 * 3600 # 7h: antes das 9h não cria
|
|
self.assertFalse(self.i.passo(cedo))
|
|
self.assertTrue(self.i.passo(AGORA))
|
|
self.assertEqual(len(self.i.hoje['ids']), 3)
|
|
itens = self.i.itens_hoje()
|
|
self.assertEqual([i['origem'] for i in itens], ['dia'] * 3)
|
|
self.assertEqual(itens[0]['motivo'], 'Semana Mundial do Espaço')
|
|
self.assertIn('Sputnik', itens[0]['aprendi'])
|
|
# o prompt pede só temas leves e leva os sinais e o que ele já tem
|
|
prompt = self.prompts[0]
|
|
self.assertIn('política', prompt)
|
|
self.assertIn('Londrina', prompt)
|
|
self.assertIn('Dançando', prompt)
|
|
self.assertIn('quinta-feira, 8 de outubro de 2026', prompt)
|
|
self.assertTrue(any(t == 'ideia' for t, _ in self.diario))
|
|
self.assertFalse(self.i.passo(AGORA + 60)) # já fez as de hoje
|
|
# dia seguinte: começa de novo
|
|
self.assertTrue(self.i.passo(AGORA + 86400))
|
|
self.assertEqual(len(self.i.hoje['ids']), 3)
|
|
|
|
def test_sem_internet_tenta_de_novo_depois_e_desiste_no_dia(self):
|
|
self.i.baixar = lambda url, timeout=10: (_ for _ in ()).throw(OSError('sem rede'))
|
|
chamadas = []
|
|
|
|
def ia_fora(ia, prompt, schema):
|
|
chamadas.append(prompt)
|
|
raise ValueError('Gemini indisponível')
|
|
self.c.chamar = ia_fora
|
|
self.assertTrue(self.i.passo(AGORA))
|
|
self.assertIn('Gemini indisponível', self.i.hoje['erro'])
|
|
self.assertFalse(self.i.passo(AGORA + 60)) # espera 30 min
|
|
self.assertTrue(self.i.passo(AGORA + 31 * 60))
|
|
self.assertTrue(self.i.passo(AGORA + 62 * 60))
|
|
self.assertFalse(self.i.passo(AGORA + 93 * 60)) # 3 tentativas por dia
|
|
self.assertEqual(len(chamadas), 3)
|
|
self.assertEqual(self.i.hoje['ids'], [])
|
|
|
|
def test_sem_gemini_ou_desligado_nao_cria(self):
|
|
self.i.pode_gemini = lambda: False
|
|
self.assertFalse(self.i.passo(AGORA))
|
|
self.i.pode_gemini = lambda: True
|
|
self.i.configurar(dict(ligado=False))
|
|
self.assertFalse(self.i.passo(AGORA))
|
|
|
|
def test_aprendi_hoje_conta_ate_duas_vezes(self):
|
|
self.i.passo(AGORA)
|
|
item = self.i.pendente()
|
|
self.assertIsNotNone(item)
|
|
self.i.contar(item['id'])
|
|
self.i.contar(item['id'])
|
|
self.assertTrue(self.ordens[0].startswith('aprendi Sabia que o Sputnik'))
|
|
self.assertNotEqual(self.i.pendente()['id'], item['id']) # já contou 2x: passa para a próxima
|
|
self.i.configurar(dict(aprendi=False))
|
|
self.assertIsNone(self.i.pendente())
|
|
|
|
def test_cidade_pelo_nome_e_validacao(self):
|
|
st = self.i.configurar(dict(cidade='curitiba', por_dia=2, hora=14))
|
|
self.assertEqual(st['config']['cidade'], dict(nome='Curitiba (Paraná)', lat=-25.4284, lon=-49.2733))
|
|
self.assertEqual((st['config']['por_dia'], st['config']['hora']), (2, 14))
|
|
for ruim in (dict(por_dia=9), dict(hora=23), dict(ligado='sim'), dict(outra=1)):
|
|
with self.assertRaises(ValueError):
|
|
self.i.configurar(ruim)
|
|
outra = Inspiracao(Path(self.tmp.name) / 'i.json', self.c, iniciar=False)
|
|
self.assertEqual(outra.config['por_dia'], 2)
|
|
|
|
|
|
class VidaComNovidadesTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.v = Vida(Path(self.tmp.name) / 'vida.json', iniciar=False, rng=random.Random(3))
|
|
self.ordens = []
|
|
self.v.enfileirar = self.ordens.append
|
|
self.v.celular_online = lambda: True
|
|
self.v.celular_dormindo = lambda: False
|
|
self.v.madrugada = lambda agora: False
|
|
self.nova = dict(id=7, nome='Astronauta', emoji='🚀', motivo='Semana do Espaço', aprendi='Sabia que...',
|
|
receita=dict(ATIVIDADES_BASE['dancar'], nome='Astronauta'))
|
|
self.v.novidades = lambda: [self.nova]
|
|
self.v.do_dia = lambda: [7]
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def test_ideias_de_hoje_entram_na_rotina(self):
|
|
escolhas = [self.v.escolher_atividade() for _ in range(200)]
|
|
self.assertGreater(escolhas.count('criado:7'), 40)
|
|
ato = self.v.comecar_atividade(time.time(), 'criado:7', 150)
|
|
self.assertEqual(ato, 'atividade criado:7')
|
|
ordem = self.ordens[-1]
|
|
self.assertTrue(ordem.startswith('atividade_criada '))
|
|
self.assertEqual(json.loads(ordem[17:])['duracao'], 150)
|
|
self.assertIn('Astronauta (Semana do Espaço)', self.v.diario[0]['texto'])
|
|
# não repete a mesma em seguida
|
|
self.v.ultima_atividade = 'criado:7'
|
|
self.assertNotIn('criado:7', [self.v.escolher_atividade() for _ in range(50)])
|
|
|
|
def test_conta_o_que_aprendeu_com_gente_perto(self):
|
|
contadas = []
|
|
self.v.aprendi_pendente = lambda: self.nova
|
|
self.v.contar_aprendi = lambda ident, agora=None: contadas.append(ident)
|
|
self.v.alguem_perto = lambda: True
|
|
self.v.atividade_em = time.time() # sem folga para atividades
|
|
for _ in range(30):
|
|
if self.v.agir(time.time()) == 'aprendi':
|
|
break
|
|
self.assertEqual(contadas, [7])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|