feat(criacao): estúdio para criar expressões, atividades e figurinhas com IA
Menu "Criar com IA" (Ao vivo › Gestos e Reações) com grade igual à dos favoritos. Para cada pedido escolhe-se o tipo (expressão, atividade ou figurinha) e a IA: Gemini, IA local (qualquer servidor compatível com OpenAI na rede, como o ROG Ally com LM Studio/llama-server) ou o Qwen do S10. - A IA não escreve código: responde uma receita de formato fixo (schema JSON) montada só com peças que o rosto já anima. O servidor valida, limita números e descarta peças desconhecidas antes de chegar ao celular. - Reaproveita o que existe: o prompt leva exemplos reais dos gestos e atividades atuais, e "Basear em" manda a receita de um gesto, atividade ou criação para a IA fazer uma variação. - Expressões viram sequências como as de sequencias(), com mãos e espiral. - Atividades usam uma cena genérica com o bonequinho (poses, pernas, pulo, balanço) e os objetos das cenas feitas à mão (bola, livro, xícara, notas, estrelas, coração, Zzz, chama, bolha, halter, microfone, controle, pincel, flor), com movimentos pular/girar/subir/orbitar. - Figurinhas reaproveitam o desenho e a biblioteca de reações. - A estrela põe expressões e atividades criadas nos favoritos (e nas teclas 1 a 9); a IA local é configurada na própria tela, com teste. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,9 @@ class Favoritos {
|
||||
export const favoritos = new Favoritos();
|
||||
|
||||
export const gesto = (nome: string) => tentar(() => postar('/comando', { tipo: 'gesto', valor: nome }));
|
||||
/** Favorito pode ser um gesto ou uma criação do estúdio ("criado:12"). */
|
||||
export const tocarFavorito = (g: string) =>
|
||||
g.startsWith('criado:') ? tentar(() => postar('/criacao/tocar', { id: Number(g.slice(7)) })) : gesto(g);
|
||||
export const rosto = (nome: string) => tentar(() => postar('/comando', { tipo: 'rosto', valor: nome }));
|
||||
export const cor = (rgb: [number, number, number]) => tentar(() => postar('/comando', { tipo: 'cor', valor: rgb }));
|
||||
export const formato = (nome: string) => tentar(() => postar('/comando', { tipo: 'formato', valor: nome }));
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { Star } from '@lucide/svelte';
|
||||
import { favoritos, gesto, GESTOS_INFO, gestosDisponiveis } from '../comandos.svelte';
|
||||
import { favoritos, GESTOS_INFO, gestosDisponiveis, tocarFavorito } from '../comandos.svelte';
|
||||
import { criacao } from '../criacao.svelte';
|
||||
|
||||
// compacto: só os favoritos com atalho 1–9 (dock). Completo: todos, com estrela para fixar.
|
||||
// compacto: só os favoritos com atalho 1–9 (dock), inclusive criações do estúdio. Completo: todos, com estrela.
|
||||
let { completo = false }: { completo?: boolean } = $props();
|
||||
const lista = $derived(completo ? gestosDisponiveis() : favoritos.lista.filter((g) => g in GESTOS_INFO));
|
||||
$effect(() => {
|
||||
if (!completo && !criacao.status && favoritos.lista.some((g) => g.startsWith('criado:'))) criacao.carregar();
|
||||
});
|
||||
function infoDe(g: string) {
|
||||
if (g.startsWith('criado:')) {
|
||||
const c = criacao.item(Number(g.slice(7)));
|
||||
return c ? { nome: c.nome, emoji: c.emoji } : null;
|
||||
}
|
||||
return GESTOS_INFO[g] ?? null;
|
||||
}
|
||||
const lista = $derived(completo ? gestosDisponiveis() : favoritos.lista.filter((g) => infoDe(g)));
|
||||
</script>
|
||||
|
||||
<div class="gestos" class:completo>
|
||||
{#each lista as g, i (g)}
|
||||
{@const info = GESTOS_INFO[g]}
|
||||
{@const info = infoDe(g)!}
|
||||
<div class="item">
|
||||
<button class="gesto" onclick={() => gesto(g)} title={completo || i > 8 ? info.nome : `${info.nome} (tecla ${i + 1})`}>
|
||||
<button class="gesto" class:criado={g.startsWith('criado:')} onclick={() => tocarFavorito(g)} title={completo || i > 8 ? info.nome : `${info.nome} (tecla ${i + 1})`}>
|
||||
<span class="emoji" aria-hidden="true">{info.emoji}</span>
|
||||
<span class="nome">{info.nome}</span>
|
||||
{#if !completo && i < 9}<kbd>{i + 1}</kbd>{/if}
|
||||
@@ -71,6 +82,13 @@
|
||||
color: var(--texto-2);
|
||||
text-align: center;
|
||||
}
|
||||
.gesto.criado::after {
|
||||
content: '✨';
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
kbd {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Estúdio de criação (/criacao/*): expressões, atividades e figurinhas feitas por IA.
|
||||
* O estado chega por SSE ('criacao') e é recarregado ao abrir a tela.
|
||||
*/
|
||||
import { pedir, postar } from './api';
|
||||
import { avisos, tentar } from './avisos.svelte';
|
||||
import { ouvir } from './estado.svelte';
|
||||
|
||||
export type TipoCriacao = 'expressao' | 'atividade' | 'figurinha';
|
||||
export type IaCriacao = 'gemini' | 'local' | 's10';
|
||||
|
||||
export interface ItemCriado {
|
||||
id: number;
|
||||
horario: number;
|
||||
tipo: TipoCriacao;
|
||||
nome: string;
|
||||
emoji: string;
|
||||
receita: any;
|
||||
ia: IaCriacao;
|
||||
pedido: string;
|
||||
}
|
||||
|
||||
export interface Provedor {
|
||||
nome: string;
|
||||
pronto: boolean;
|
||||
detalhe: string;
|
||||
motivo: string;
|
||||
url?: string;
|
||||
modelo?: string;
|
||||
chave?: boolean;
|
||||
}
|
||||
|
||||
export interface StatusCriacao {
|
||||
ocupado: { tipo: TipoCriacao; ia: IaCriacao; desde: number; pedido: string } | null;
|
||||
erro: string;
|
||||
provedores: Record<IaCriacao, Provedor>;
|
||||
criacoes: ItemCriado[];
|
||||
bases: Record<'expressao' | 'atividade', { id: string; nome: string; emoji: string }[]>;
|
||||
}
|
||||
|
||||
export const TIPOS: { id: TipoCriacao; nome: string; emoji: string; exemplo: string }[] = [
|
||||
{ id: 'expressao', nome: 'Expressão', emoji: '😊', exemplo: 'Ex.: desconfiado, olhando de lado com um olho meio fechado' },
|
||||
{ id: 'atividade', nome: 'Atividade', emoji: '🤹', exemplo: 'Ex.: fazendo malabarismo com 3 bolinhas, concentrado' },
|
||||
{ id: 'figurinha', nome: 'Figurinha', emoji: '🎨', exemplo: 'Ex.: um sol de óculos escuros sorrindo' },
|
||||
];
|
||||
|
||||
class Criacao {
|
||||
status = $state<StatusCriacao | null>(null);
|
||||
|
||||
async carregar() {
|
||||
try {
|
||||
this.status = await pedir<StatusCriacao>('/criacao/status');
|
||||
} catch (e) {
|
||||
avisos.erro(e);
|
||||
}
|
||||
}
|
||||
|
||||
item(id: number) {
|
||||
return this.status?.criacoes.find((c) => c.id === id) ?? null;
|
||||
}
|
||||
|
||||
async criar(tipo: TipoCriacao, pedido: string, ia: IaCriacao, base: string) {
|
||||
const r = await tentar(() => postar<StatusCriacao>('/criacao/criar', { tipo, pedido, ia, base: base || null }));
|
||||
if (r) this.status = r;
|
||||
return !!r;
|
||||
}
|
||||
|
||||
tocar(id: number) {
|
||||
return tentar(() => postar('/criacao/tocar', { id }));
|
||||
}
|
||||
|
||||
async apagar(id: number) {
|
||||
await tentar(() => postar('/criacao/apagar', { id }), 'Criação apagada.');
|
||||
}
|
||||
|
||||
async configurar(local: { url?: string; modelo?: string; chave?: string }) {
|
||||
const r = await tentar(() => postar<StatusCriacao>('/criacao/config', { local }), 'IA local salva.');
|
||||
if (r) this.status = r;
|
||||
}
|
||||
|
||||
async testar(ia: IaCriacao) {
|
||||
const r = await tentar(() => postar<{ mensagem: string }>('/criacao/testar', { ia }));
|
||||
if (r) avisos.ok(r.mensagem);
|
||||
}
|
||||
}
|
||||
|
||||
export const criacao = new Criacao();
|
||||
ouvir('criacao', (d: StatusCriacao) => {
|
||||
const antes = criacao.status;
|
||||
criacao.status = d;
|
||||
// terminou: avisa o resultado
|
||||
if (antes?.ocupado && !d.ocupado) {
|
||||
if (d.erro) avisos.erro(new Error(d.erro));
|
||||
else if (d.criacoes[0]) avisos.ok(`Criado: ${d.criacoes[0].emoji} ${d.criacoes[0].nome}`);
|
||||
}
|
||||
});
|
||||
|
||||
/** Favoritos podem guardar criações ("criado:12"): tocar no celular. */
|
||||
export const ehCriado = (g: string) => g.startsWith('criado:');
|
||||
export const idCriado = (g: string) => Number(g.slice(7));
|
||||
@@ -116,7 +116,13 @@ function aplicarFoto(f: EstadoServidor) {
|
||||
const tratadores: Record<string, (d: any) => 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' && !d.ordem.startsWith('humor ')) noFeed('expressao', 'Expressão: ' + d.ordem);
|
||||
if (d.ordem === 'calma' || /^(humor|ritmo|zanga|sono) /.test(d.ordem)) return;
|
||||
const criada = /^(expressao|atividade)_criada (.*)$/.exec(d.ordem);
|
||||
if (criada) {
|
||||
let nome = '';
|
||||
try { nome = JSON.parse(criada[2]).nome; } catch { /* ok */ }
|
||||
noFeed('expressao', (criada[1] === 'expressao' ? 'Expressão criada: ' : 'Atividade criada: ') + nome);
|
||||
} else noFeed('expressao', 'Expressão: ' + d.ordem);
|
||||
},
|
||||
telemetria: (d: Telemetria) => {
|
||||
estado.app = d;
|
||||
@@ -177,7 +183,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']) {
|
||||
'reacao', 'treinamento', 'historico', 'pir', 'vida', 'criacao']) {
|
||||
fonte.addEventListener(tipo, (e) => {
|
||||
const dados = JSON.parse((e as MessageEvent).data);
|
||||
tratadores[tipo]?.(dados);
|
||||
|
||||
+19
-2
@@ -179,6 +179,23 @@ export const TOPICOS: Record<string, TopicoGuia> = {
|
||||
{ emoji: '👆', nome: 'Parar', texto: 'Botão Parar no painel ou um toque na tela do celular. Dormir também encerra a atividade.' },
|
||||
],
|
||||
},
|
||||
criar: {
|
||||
titulo: 'Criar com IA',
|
||||
emoji: '🪄',
|
||||
onde: 'Ao vivo › Gestos › Criar com IA (ou Reações › Criar com IA)',
|
||||
resumo:
|
||||
'Escreva um pedido e uma IA cria uma expressão, uma atividade ou uma figurinha nova. Ela não escreve código: monta a criação só com peças que o Carinha já sabe animar, com exemplos reais do que já existe. Tudo é conferido no servidor antes de chegar ao celular.',
|
||||
itens: [
|
||||
{ emoji: '😊', nome: 'Expressão', texto: 'Sequência de olhos, boca, cor, olhar e mãozinhas, como os gestos favoritos. Ex.: "desconfiado, olhando de lado".' },
|
||||
{ emoji: '🤹', nome: 'Atividade', texto: 'O bonequinho faz algo com braços, pernas e objetos (bola, livro, notas, estrelas, halter, microfone, flor...). Ex.: "malabarismo com 3 bolinhas".' },
|
||||
{ emoji: '🎨', nome: 'Figurinha', texto: 'Desenho que aparece nos olhos ou em volta do rosto.' },
|
||||
{ emoji: '🧬', nome: 'Basear em', texto: 'Escolha um gesto, atividade ou criação que já existe e peça uma variação: a IA parte da receita dele.' },
|
||||
{ emoji: '✨', nome: 'Gemini', texto: 'O melhor resultado e o mais rápido (alguns segundos). Usa a chave paga.' },
|
||||
{ emoji: '🖥️', nome: 'IA local', texto: 'Um servidor na sua rede (LM Studio ou llama-server, como o ROG Ally). Grátis, mas pode levar alguns minutos e criar coisas mais simples. Configure em "IA local".' },
|
||||
{ emoji: '📱', nome: 'Qwen do S10', texto: 'O modelo pequeno do celular. Rápido e grátis, mas fraco para criar: bom só para testar.' },
|
||||
{ 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).' },
|
||||
],
|
||||
},
|
||||
personalidade: {
|
||||
titulo: 'IA local (Qwen)',
|
||||
emoji: '🧠',
|
||||
@@ -302,7 +319,7 @@ export const TOPICOS: Record<string, TopicoGuia> = {
|
||||
|
||||
/** Ordem do tutorial (página Guia). */
|
||||
export const ORDEM_GUIA = [
|
||||
'inicio', 'vida', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'atividades', 'aparencia',
|
||||
'inicio', 'vida', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'atividades', 'criar', 'aparencia',
|
||||
'personalidade', 'acontecimentos', 'eventos', 'presenca', 'tela', 'sensores', 'sensibilidade',
|
||||
'reacoes', 'conversa', 'movimento', 'atalhos',
|
||||
];
|
||||
@@ -310,5 +327,5 @@ export const ORDEM_GUIA = [
|
||||
/** Tópico de cada tela, para o ⓘ do topo. */
|
||||
export const TOPICO_DA_TELA: Record<string, string> = {
|
||||
'ao-vivo': 'inicio', eventos: 'eventos', conversa: 'conversa', reacoes: 'reacoes',
|
||||
movimento: 'movimento', ajustes: 'tela', rosto: 'aparencia', guia: 'inicio',
|
||||
movimento: 'movimento', ajustes: 'tela', rosto: 'aparencia', guia: 'inicio', criar: 'criar',
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ export function itensDaPaleta(): ItemPaleta[] {
|
||||
registrarNaPaleta(() => [
|
||||
...ROTAS.map((r) => ({ id: 'ir-' + r.id, grupo: 'Ir para', titulo: r.titulo, fazer: () => nav.ir(r.id) })),
|
||||
{ id: 'ir-rosto', grupo: 'Ir para', titulo: 'Rosto (estilo, formato e cor)', fazer: () => nav.ir('rosto') },
|
||||
{ id: 'ir-criar', grupo: 'Ir para', titulo: 'Criar com IA (expressões, atividades, figurinhas)', fazer: () => nav.ir('criar') },
|
||||
...Object.entries(ATIVIDADES_INFO).map(([id, a]) => ({
|
||||
id: 'atividade-' + id, grupo: 'Atividades', titulo: a.emoji + ' ' + a.nome, fazer: () => atividade(id),
|
||||
})),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Rotas por hash (#/conversa): o servidor só precisa entregar index.html em "/". */
|
||||
|
||||
export type Rota = 'ao-vivo' | 'eventos' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto' | 'guia';
|
||||
export type Rota = 'ao-vivo' | 'eventos' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto' | 'guia' | 'criar';
|
||||
|
||||
export const ROTAS: { id: Rota; titulo: string; atalho: string }[] = [
|
||||
{ id: 'ao-vivo', titulo: 'Ao vivo', atalho: 'g v' },
|
||||
@@ -15,6 +15,7 @@ export const ROTAS: { id: Rota; titulo: string; atalho: string }[] = [
|
||||
export const SECUNDARIAS: { id: Rota; titulo: string; volta: Rota }[] = [
|
||||
{ id: 'rosto', titulo: 'Rosto', volta: 'ao-vivo' },
|
||||
{ id: 'guia', titulo: 'Guia', volta: 'ajustes' },
|
||||
{ id: 'criar', titulo: 'Criar com IA', volta: 'reacoes' },
|
||||
];
|
||||
|
||||
export const tituloDa = (id: Rota) => [...ROTAS, ...SECUNDARIAS].find((r) => r.id === id)?.titulo ?? '';
|
||||
|
||||
Reference in New Issue
Block a user