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:
julio
2026-10-06 22:22:52 -03:00
co-authored by Claude Opus 5.5
parent ff06f02705
commit 05e82dfd75
19 changed files with 1522 additions and 32 deletions
+26 -2
View File
@@ -540,11 +540,31 @@ function passo(dt) { // avanca a fila; true se ha gesto em
/* Atividade em andamento (rosto-motor.js): para onde olhar e que cara fazer. Atualizado por quadro. */
let ATIV = null;
const OLHAR = { x: 0, y: 0, ax: 0, ay: 0, prox: 0 }; // ponto que os olhos estão fitando no repouso
function atividade(nome, segundos) {
function atividade(nome, segundos, receita) {
if (!window.MotorRosto || !window.MotorRosto.atividade) return;
if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; }
if (r.dormindo) { r.dormindo = false; animar("surpreso"); }
if (window.MotorRosto.atividade(nome, segundos) && nome === "cantar") tocarSom("musica");
if (window.MotorRosto.atividade(nome, segundos, receita) && nome === "cantar") tocarSom("musica");
}
/* Expressão criada no estúdio (criacao.py): receita já validada no servidor, só números e nomes de
peças. Vira uma sequência igual às de sequencias(), com mãos e espiral se a receita pedir. */
function corDaReceita(nome) {
return { claro: "claro", escuro: "escuro", rosa: ROSA, amarelo: AMARELO, verde: VERDE, vermelho: VERM }[nome] || CIANO;
}
function tocarExpressaoCriada(texto) {
let rc;
try { rc = JSON.parse(texto); } catch (e) { return; }
if (!rc || !Array.isArray(rc.passos) || !rc.passos.length) return;
if (r.dormindo) r.dormindo = false;
r.fila = rc.passos.slice(0, 12).map((p) => [{
forma: p.forma || "aberto", forma2: null, boca: p.boca || "neutra", cor: corDaReceita(p.cor),
dx: +p.dx || 0, dy: +p.dy || 0, esc: +p.esc || 1, ab: typeof p.ab === "number" ? p.ab : 1,
}, Math.max(0.05, Math.min(2, +p.duracao || 0.3))]);
proximo();
const total = r.fila.reduce((a, passo) => a + passo[1], 0);
if (rc.espiral) ESPIRAL.ate = performance.now() / 1000 + total;
if (rc.maos && rc.maos !== "nenhuma" && window.MotorRosto && window.MotorRosto.maos) window.MotorRosto.maos(rc.maos);
}
function repouso(t) { // pose calma: olha devagar, "respira"
@@ -689,6 +709,10 @@ function trata(ordem) {
HUMOR = ordem.slice(6).trim();
} else if (ordem.indexOf("ritmo ") === 0) {
PERFIL = PERFIS[ordem.slice(6).trim()] || PERFIS.normal;
} else if (ordem.indexOf("expressao_criada ") === 0) {
tocarExpressaoCriada(ordem.slice(17));
} else if (ordem.indexOf("atividade_criada ") === 0) {
try { const rc = JSON.parse(ordem.slice(17)); atividade("receita", +rc.duracao || 25, rc); } catch (e) {}
} else if (ordem.indexOf("atividade ") === 0) {
const [nome, seg] = ordem.slice(10).trim().split(/\s+/); // "atividade bola 180"
atividade(nome, parseFloat(seg) || 0);