feat(vida): ideias do dia com inspiração da internet e "olha o que eu aprendi"

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>
This commit is contained in:
julio
2026-10-08 08:13:03 -03:00
co-authored by Claude Opus 5.5
parent 43770c6d67
commit 4777ceb023
19 changed files with 1179 additions and 30 deletions
+35 -19
View File
@@ -283,6 +283,11 @@ class Criacao:
with self.db() as db:
db.execute('CREATE TABLE IF NOT EXISTS criacoes(id INTEGER PRIMARY KEY, horario REAL, tipo TEXT, nome TEXT, '
'emoji TEXT, receita TEXT, ia TEXT, pedido TEXT, excluida INTEGER DEFAULT 0)')
# origem: 'pedido' (alguém pediu no estúdio) ou 'dia' (ideia do dia); motivo e aprendi vêm da ideia do dia
colunas = {c[1] for c in db.execute('PRAGMA table_info(criacoes)')}
for coluna, tipo in (('origem', "TEXT DEFAULT 'pedido'"), ('motivo', "TEXT DEFAULT ''"), ('aprendi', "TEXT DEFAULT ''")):
if coluna not in colunas:
db.execute(f'ALTER TABLE criacoes ADD COLUMN {coluna} {tipo}')
@contextmanager
def db(self):
@@ -368,24 +373,29 @@ class Criacao:
self.avisar()
return self.status()
def gerar(self, tipo, pedido, ia, base=None, origem='pedido', motivo='', aprendi=''):
"""Pede a receita à IA, valida e guarda (síncrono). Devolve o id da criação."""
schema = dict(expressao=SCHEMA_EXPRESSAO, atividade=SCHEMA_ATIVIDADE, figurinha=SCHEMA_FIGURINHA)[tipo]
bruto = self.chamar(ia, montar_prompt(tipo, pedido, base), schema)
if tipo == 'expressao':
receita = validar_expressao(bruto)
elif tipo == 'atividade':
receita = validar_atividade(bruto)
else:
emoji = _emoji(bruto.pop('emoji', '')) if isinstance(bruto, dict) else '🎨'
desenho = validar_desenho(bruto)
receita = dict(nome=desenho['nome'][:30], emoji=emoji, desenho=desenho)
with self.lock, self.db() as db:
ident = db.execute('INSERT INTO criacoes(horario, tipo, nome, emoji, receita, ia, pedido, origem, motivo, aprendi) '
'VALUES (?,?,?,?,?,?,?,?,?,?)',
(time.time(), tipo, receita['nome'], receita['emoji'], json.dumps(receita, ensure_ascii=False),
ia, pedido, origem, motivo, aprendi)).lastrowid
db.execute('DELETE FROM criacoes WHERE id NOT IN (SELECT id FROM criacoes ORDER BY id DESC LIMIT ?)', (MAX_CRIACOES,))
return ident
def _criar(self, tipo, pedido, ia, base):
try:
schema = dict(expressao=SCHEMA_EXPRESSAO, atividade=SCHEMA_ATIVIDADE, figurinha=SCHEMA_FIGURINHA)[tipo]
bruto = self.chamar(ia, montar_prompt(tipo, pedido, base), schema)
if tipo == 'expressao':
receita = validar_expressao(bruto)
elif tipo == 'atividade':
receita = validar_atividade(bruto)
else:
emoji = _emoji(bruto.pop('emoji', '')) if isinstance(bruto, dict) else '🎨'
desenho = validar_desenho(bruto)
receita = dict(nome=desenho['nome'][:30], emoji=emoji, desenho=desenho)
with self.lock, self.db() as db:
ident = db.execute('INSERT INTO criacoes(horario, tipo, nome, emoji, receita, ia, pedido) VALUES (?,?,?,?,?,?,?)',
(time.time(), tipo, receita['nome'], receita['emoji'],
json.dumps(receita, ensure_ascii=False), ia, pedido)).lastrowid
db.execute('DELETE FROM criacoes WHERE id NOT IN (SELECT id FROM criacoes ORDER BY id DESC LIMIT ?)', (MAX_CRIACOES,))
self.tocar(ident) # mostra na hora no celular
self.tocar(self.gerar(tipo, pedido, ia, base)) # mostra na hora no celular
except Exception as e:
with self.lock:
self.erro = str(e)[:200]
@@ -397,12 +407,18 @@ class Criacao:
# ------------------------------------------------------------ biblioteca
def item(self, ident):
with self.db() as db:
row = db.execute('SELECT id, horario, tipo, nome, emoji, receita, ia, pedido FROM criacoes WHERE id=? AND excluida=0',
(ident,)).fetchone()
row = db.execute('SELECT id, horario, tipo, nome, emoji, receita, ia, pedido, origem, motivo, aprendi FROM criacoes '
'WHERE id=? AND excluida=0', (ident,)).fetchone()
if not row:
return None
return dict(id=row[0], horario=row[1], tipo=row[2], nome=row[3], emoji=row[4], receita=json.loads(row[5]),
ia=row[6], pedido=row[7])
ia=row[6], pedido=row[7], origem=row[8] or 'pedido', motivo=row[9] or '', aprendi=row[10] or '')
def atividades(self):
"""Atividades criadas (do estúdio e do dia): o modo vida sorteia entre elas."""
with self.db() as db:
rows = db.execute("SELECT id FROM criacoes WHERE excluida=0 AND tipo='atividade' ORDER BY id DESC LIMIT 60").fetchall()
return [i for i in (self.item(r[0]) for r in rows) if i]
def lista(self):
with self.db() as db: