From 4777ceb0230b7a24c49b8eb94c35b613281e14e2 Mon Sep 17 00:00:00 2001 From: julio Date: Thu, 8 Oct 2026 08:13:03 -0300 Subject: [PATCH] =?UTF-8?q?feat(vida):=20ideias=20do=20dia=20com=20inspira?= =?UTF-8?q?=C3=A7=C3=A3o=20da=20internet=20e=20"olha=20o=20que=20eu=20apre?= =?UTF-8?q?ndi"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- android/app/assets/carinha.html | 23 +- android/app/assets/rosto-motor.js | 2 +- .../java/br/carinha/rosto/MainActivity.java | 2 + carinha.html | 23 +- controle_pc.py | 9 + criacao.py | 54 ++- deploy.ps1 | 2 +- inspiracao.py | 441 ++++++++++++++++++ kiosk-server.py | 29 +- rosto-motor.js | 2 +- tests/test_inspiracao.py | 204 ++++++++ tests/test_servidor.py | 12 + vida.py | 37 +- web/src/lib/componentes/IdeiasDoDia.svelte | 262 +++++++++++ web/src/lib/criacao.svelte.ts | 3 + web/src/lib/estado.svelte.ts | 4 +- web/src/lib/guia.ts | 17 +- web/src/lib/inspiracao.svelte.ts | 78 ++++ web/src/telas/Criar.svelte | 5 +- 19 files changed, 1179 insertions(+), 30 deletions(-) create mode 100644 inspiracao.py create mode 100644 tests/test_inspiracao.py create mode 100644 web/src/lib/componentes/IdeiasDoDia.svelte create mode 100644 web/src/lib/inspiracao.svelte.ts diff --git a/android/app/assets/carinha.html b/android/app/assets/carinha.html index 74e69ac..b54a948 100644 --- a/android/app/assets/carinha.html +++ b/android/app/assets/carinha.html @@ -80,6 +80,11 @@ + + + {#snippet acoes()} + + {/snippet} + + {#if !st || !cfg} +

Carregando…

+ {:else} + {#if st.ocupado} + Procurando ideias na internet e criando as atividades… + {:else if st.erro} +

{st.erro}{st.tentativas ? ` (tentativa ${st.tentativas} de 3; tenta de novo em 30 min)` : ''}

+ {/if} + + {#if st.ideias.length} +
    + {#each st.ideias as i (i.id)} + {@const contou = st.contadas[String(i.id)] ?? 0} +
  • + +
    + {i.nome} + {#if i.motivo}{i.motivo}{/if} + {#if i.aprendi}

    {i.aprendi}

    {/if} +
    +
    + + {#if i.aprendi} + + {/if} +
    +
  • + {/each} +
+ {:else} +

{vazio}

+ {/if} + + {#if sinais.length} +
+ O que ele viu hoje na internet +
    + {#each sinais as s (s.f)} +
  • + {ICONES[s.f]} {st.fontes[s.f]} + {Array.isArray(s.valor) ? s.valor.slice(0, 8).join(' · ') : s.valor} +
  • + {/each} +
+
+ {/if} + + + {#if ajustesAbertos} +
+ inspiracao.configurar({ ligado: v })} /> + inspiracao.configurar({ aprendi: v })} + /> +
+ + +
+
+ Cidade do tempo: {cfg.cidade.nome} +
+ + +
+
+
+ De onde tirar inspiração + {#each Object.entries(st.fontes) as [f, nome] (f)} + fonte(f as Fonte, v)} /> + {/each} +
+
+ {/if} + {/if} +
+ + diff --git a/web/src/lib/criacao.svelte.ts b/web/src/lib/criacao.svelte.ts index 05f4cf4..6480392 100644 --- a/web/src/lib/criacao.svelte.ts +++ b/web/src/lib/criacao.svelte.ts @@ -18,6 +18,9 @@ export interface ItemCriado { receita: any; ia: IaCriacao; pedido: string; + origem?: 'pedido' | 'dia'; + motivo?: string; + aprendi?: string; } export interface Provedor { diff --git a/web/src/lib/estado.svelte.ts b/web/src/lib/estado.svelte.ts index 6b94b76..1f7a328 100644 --- a/web/src/lib/estado.svelte.ts +++ b/web/src/lib/estado.svelte.ts @@ -117,6 +117,8 @@ const tratadores: Record void> = { expressao: (d: { ordem: string }) => { // 'humor x' é o modo vida avisando o celular; aparece no cartão Vida, não no feed if (d.ordem === 'calma' || /^(humor|ritmo|zanga|sono) /.test(d.ordem)) return; + // contar o que aprendeu já aparece no diário do cartão Vida + if (d.ordem.startsWith('aprendi ')) return; const criada = /^(expressao|atividade)_criada (.*)$/.exec(d.ordem); if (criada) { let nome = ''; @@ -183,7 +185,7 @@ function abrirEventos() { if (estado.conexao !== 'token') estado.conexao = 'caiu'; }; for (const tipo of ['expressao', 'telemetria', 'app_config', 'aparencia', 'motores', 'estop', 'conversa', 'ia', - 'reacao', 'treinamento', 'historico', 'pir', 'vida', 'criacao']) { + 'reacao', 'treinamento', 'historico', 'pir', 'vida', 'criacao', 'inspiracao']) { fonte.addEventListener(tipo, (e) => { const dados = JSON.parse((e as MessageEvent).data); tratadores[tipo]?.(dados); diff --git a/web/src/lib/guia.ts b/web/src/lib/guia.ts index c787c79..a6e77be 100644 --- a/web/src/lib/guia.ts +++ b/web/src/lib/guia.ts @@ -196,6 +196,21 @@ export const TOPICOS: Record = { { emoji: '⭐', nome: 'Favoritos', texto: 'A estrela põe expressões e atividades criadas na grade de favoritos do Ao vivo (e nas teclas 1 a 9).' }, ], }, + ideias: { + titulo: 'Ideias do dia', + emoji: '💡', + onde: 'Criar com IA › Ideias do dia', + resumo: + 'Todo dia, a partir do horário escolhido, ele procura inspiração na internet e o Gemini cria atividades novas (3 por padrão). A IA descarta tragédias, política e afins: só entram temas leves. As novidades entram na rotina do modo vida, as de hoje com mais chance, e ficam na grade do estúdio para tocar, favoritar ou apagar.', + itens: [ + { emoji: '🌦️', nome: 'Tempo da cidade', texto: 'Chuva, sol, frio e calor de hoje na cidade escolhida (Open-Meteo). Ex.: dia de chuva vira "lendo com chá".' }, + { emoji: '📅', nome: 'Datas comemorativas', texto: 'Feriados (BrasilAPI) e datas como Dia das Crianças, Dia da Pizza, Halloween e Natal, de hoje e dos próximos dias.' }, + { emoji: '📜', nome: 'Neste dia na história', texto: 'Fatos que aconteceram neste dia, da Wikipédia em português.' }, + { emoji: '📈', nome: 'Assuntos em alta', texto: 'O que o pessoal está procurando hoje no Google Brasil. Política e notícias tristes são ignoradas.' }, + { emoji: '🤓', nome: 'Olha o que eu aprendi hoje', texto: 'Cada ideia vem com uma curiosidade. Quando tem alguém perto, ele conta, com legenda e voz de robô (até 2 vezes por dia cada uma). O botão "Contar" faz ele contar na hora.' }, + { emoji: '💳', nome: 'Custo', texto: 'Uma chamada ao Gemini para escolher as ideias e uma por atividade: 4 por dia com o padrão de 3.' }, + ], + }, personalidade: { titulo: 'IA local (Qwen)', emoji: '🧠', @@ -319,7 +334,7 @@ export const TOPICOS: Record = { /** Ordem do tutorial (página Guia). */ export const ORDEM_GUIA = [ - 'inicio', 'vida', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'atividades', 'criar', 'aparencia', + 'inicio', 'vida', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'atividades', 'criar', 'ideias', 'aparencia', 'personalidade', 'acontecimentos', 'eventos', 'presenca', 'tela', 'sensores', 'sensibilidade', 'reacoes', 'conversa', 'movimento', 'atalhos', ]; diff --git a/web/src/lib/inspiracao.svelte.ts b/web/src/lib/inspiracao.svelte.ts new file mode 100644 index 0000000..5218d7e --- /dev/null +++ b/web/src/lib/inspiracao.svelte.ts @@ -0,0 +1,78 @@ +/** + * Ideias do dia (/criacao/dia): todo dia o Carinha cria atividades novas inspiradas no tempo, nas + * datas, no "neste dia" da Wikipédia e no que está em alta, e conta o que aprendeu com cada uma. + * O estado chega por SSE ('inspiracao') e é recarregado ao abrir a tela. + */ +import { pedir, postar } from './api'; +import { avisos, tentar } from './avisos.svelte'; +import { ouvir } from './estado.svelte'; + +export type Fonte = 'tempo' | 'datas' | 'historia' | 'alta'; + +export interface ConfigInspiracao { + ligado: boolean; + por_dia: number; + hora: number; + aprendi: boolean; + cidade: { nome: string; lat: number; lon: number }; + fontes: Record; +} + +export interface IdeiaDoDia { + id: number; + nome: string; + emoji: string; + motivo: string; + aprendi: string; + horario: number; +} + +export interface StatusInspiracao { + config: ConfigInspiracao; + fontes: Record; + ocupado: boolean; + pode: boolean; + dia: string; + erro: string; + tentativas: number; + feito_em: number | null; + sinais: Partial>; + contadas: Record; + ideias: IdeiaDoDia[]; +} + +class Inspiracao { + status = $state(null); + + async carregar() { + try { + this.status = await pedir('/criacao/dia'); + } catch (e) { + avisos.erro(e); + } + } + + async configurar(dados: Partial> & { cidade?: string }, aviso?: string) { + const r = await tentar(() => postar('/criacao/dia/config', dados), aviso); + if (r) this.status = r; + return !!r; + } + + agora() { + return tentar(() => postar('/criacao/dia/agora', {}), 'Procurando ideias na internet…'); + } + + contar(id: number) { + return tentar(() => postar('/criacao/dia/contar', { id })); + } +} + +export const inspiracao = new Inspiracao(); +ouvir('inspiracao', (d: StatusInspiracao) => { + const antes = inspiracao.status; + inspiracao.status = d; + if (antes?.ocupado && !d.ocupado) { + if (d.erro) avisos.erro(new Error(d.erro)); + else if (d.ideias.length > (antes.ideias?.length ?? 0)) avisos.ok(`Ideias novas: ${d.ideias.slice(antes.ideias.length).map((i) => i.emoji + ' ' + i.nome).join(', ')}`); + } +}); diff --git a/web/src/telas/Criar.svelte b/web/src/telas/Criar.svelte index 51e1f25..b96eac2 100644 --- a/web/src/telas/Criar.svelte +++ b/web/src/telas/Criar.svelte @@ -3,6 +3,7 @@ import { onMount } from 'svelte'; import { favoritos } from '../lib/comandos.svelte'; import Card from '../lib/componentes/Card.svelte'; + import IdeiasDoDia from '../lib/componentes/IdeiasDoDia.svelte'; import RostoPrevia from '../lib/componentes/RostoPrevia.svelte'; import Selo from '../lib/componentes/Selo.svelte'; import { criacao, TIPOS, type IaCriacao, type TipoCriacao } from '../lib/criacao.svelte'; @@ -153,6 +154,8 @@
+ + {#snippet acoes()}
@@ -174,7 +177,7 @@ {#if podeFavoritar(c.tipo)}