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:
@@ -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. */
|
/* Atividade em andamento (rosto-motor.js): para onde olhar e que cara fazer. Atualizado por quadro. */
|
||||||
let ATIV = null;
|
let ATIV = null;
|
||||||
const OLHAR = { x: 0, y: 0, ax: 0, ay: 0, prox: 0 }; // ponto que os olhos estão fitando no repouso
|
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 (!window.MotorRosto || !window.MotorRosto.atividade) return;
|
||||||
if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; }
|
if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; }
|
||||||
if (r.dormindo) { r.dormindo = false; animar("surpreso"); }
|
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"
|
function repouso(t) { // pose calma: olha devagar, "respira"
|
||||||
@@ -689,6 +709,10 @@ function trata(ordem) {
|
|||||||
HUMOR = ordem.slice(6).trim();
|
HUMOR = ordem.slice(6).trim();
|
||||||
} else if (ordem.indexOf("ritmo ") === 0) {
|
} else if (ordem.indexOf("ritmo ") === 0) {
|
||||||
PERFIL = PERFIS[ordem.slice(6).trim()] || PERFIS.normal;
|
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) {
|
} else if (ordem.indexOf("atividade ") === 0) {
|
||||||
const [nome, seg] = ordem.slice(10).trim().split(/\s+/); // "atividade bola 180"
|
const [nome, seg] = ordem.slice(10).trim().split(/\s+/); // "atividade bola 180"
|
||||||
atividade(nome, parseFloat(seg) || 0);
|
atividade(nome, parseFloat(seg) || 0);
|
||||||
|
|||||||
@@ -568,15 +568,16 @@
|
|||||||
skate: { nome: 'Andando de skate', emoji: '🛹', dur: [18, 30] },
|
skate: { nome: 'Andando de skate', emoji: '🛹', dur: [18, 30] },
|
||||||
computador: { nome: 'No computador', emoji: '💻', dur: [25, 45] },
|
computador: { nome: 'No computador', emoji: '💻', dur: [25, 45] },
|
||||||
pescar: { nome: 'Pescando', emoji: '🎣', dur: [30, 50] },
|
pescar: { nome: 'Pescando', emoji: '🎣', dur: [30, 50] },
|
||||||
|
receita: { nome: 'Criada por IA', emoji: '✨', dur: [20, 40] }, // receita do estúdio de criação
|
||||||
};
|
};
|
||||||
const ativ = { nome: null, t0: 0, dur: 0, fim: 0 };
|
const ativ = { nome: null, t0: 0, dur: 0, fim: 0, receita: null };
|
||||||
let palcoTela = null, pctx = null;
|
let palcoTela = null, pctx = null;
|
||||||
const agoraS = () => performance.now() / 1000;
|
const agoraS = () => performance.now() / 1000;
|
||||||
|
|
||||||
function iniciarAtividade(nome, segundos) {
|
function iniciarAtividade(nome, segundos, receita) {
|
||||||
if (!ATIVIDADES[nome]) { pararAtividade(); return false; }
|
if (!ATIVIDADES[nome] || (nome === 'receita' && !receita)) { pararAtividade(); return false; }
|
||||||
const [a, b] = ATIVIDADES[nome].dur;
|
const [a, b] = ATIVIDADES[nome].dur;
|
||||||
ativ.nome = nome; ativ.t0 = agoraS(); ativ.fim = 0;
|
ativ.nome = nome; ativ.t0 = agoraS(); ativ.fim = 0; ativ.receita = receita || null;
|
||||||
ativ.dur = segundos || a + Math.random() * (b - a);
|
ativ.dur = segundos || a + Math.random() * (b - a);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -620,6 +621,19 @@
|
|||||||
const p = pincel(k); info.x = 16 + lado * 5 + p.x * 4; info.y = -1 + p.y * 5;
|
const p = pincel(k); info.x = 16 + lado * 5 + p.x * 4; info.y = -1 + p.y * 5;
|
||||||
} else if (ativ.nome === 'malhar') {
|
} else if (ativ.nome === 'malhar') {
|
||||||
const f = forca(k); info.x = lado * 14; info.y = 6 - f * 12; info.ab = f > 0.85 ? 0.55 : 1;
|
const f = forca(k); info.x = lado * 14; info.y = 6 - f * 12; info.ab = f > 0.85 ? 0.55 : 1;
|
||||||
|
} else if (ativ.nome === 'receita') {
|
||||||
|
const R = ativ.receita || {}, w = Math.PI * 2 * (R.ritmo || 0.8);
|
||||||
|
const o = (R.objetos || [])[0];
|
||||||
|
if (R.olhar === 'cima') { info.x = lado * 12; info.y = -10; }
|
||||||
|
else if (R.olhar === 'baixo') { info.x = lado * 10; info.y = 8; }
|
||||||
|
else if (R.olhar === 'lado') { info.x = lado * 20; info.y = 2; }
|
||||||
|
else if (R.olhar === 'frente') { info.x = 4 * Math.sin(k * 0.7); info.y = 0; }
|
||||||
|
else { // segue o primeiro objeto
|
||||||
|
info.x = lado * 14 + Math.sin(k * w) * 5;
|
||||||
|
info.y = o && o.movimento === 'pular' ? 8 - Math.abs(Math.sin(k * w * 0.5)) * 16 : o && o.movimento === 'subir' ? -6 : 3;
|
||||||
|
}
|
||||||
|
if (R.forma && R.forma !== 'aberto') info.forma = R.forma;
|
||||||
|
if (R.boca && R.boca !== 'neutra') info.boca = R.boca;
|
||||||
} else if (ativ.nome === 'dormir') {
|
} else if (ativ.nome === 'dormir') {
|
||||||
info.forma = 'dormir'; info.boca = 'sono'; info.x = lado * 6; info.y = 4;
|
info.forma = 'dormir'; info.boca = 'sono'; info.x = lado * 6; info.y = 4;
|
||||||
} else if (ativ.nome === 'cafe') {
|
} else if (ativ.nome === 'cafe') {
|
||||||
@@ -765,7 +779,112 @@
|
|||||||
c.globalAlpha = alfa;
|
c.globalAlpha = alfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- objetos que as receitas podem usar (mesmos desenhos das cenas feitas à mão)
|
||||||
|
function desenharObjeto(c, tipo, ox, oy, s, k, cor, giro, j) {
|
||||||
|
c.save(); c.translate(ox, oy); if (giro) c.rotate(giro);
|
||||||
|
c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
|
||||||
|
if (tipo === 'bola') {
|
||||||
|
const r = s * 0.28; c.beginPath(); c.arc(0, 0, r, 0, Math.PI * 2); c.fill(); c.stroke();
|
||||||
|
c.fillStyle = css(cor.map((v) => v * 0.3)); c.beginPath();
|
||||||
|
for (let i = 0; i < 5; i++) { const a = i * 1.2566; c.lineTo(Math.cos(a) * r * 0.38, Math.sin(a) * r * 0.38); }
|
||||||
|
c.closePath(); c.fill();
|
||||||
|
} else if (tipo === 'livro') {
|
||||||
|
const lw = s * 0.62, lh = s * 0.5;
|
||||||
|
for (const lado of [-1, 1]) { c.beginPath(); c.moveTo(0, lh * 0.55); c.lineTo(lado * lw, lh * 0.45); c.lineTo(lado * lw, -lh * 0.45); c.lineTo(0, -lh * 0.35); c.closePath(); c.fill(); c.stroke(); }
|
||||||
|
} else if (tipo === 'xicara') {
|
||||||
|
xicara(c, 0, 0, s, cor); vapor(c, 0, -s * 0.3, s, k, cor);
|
||||||
|
} else if (tipo === 'nota') {
|
||||||
|
c.fillStyle = css(cor); nota(c, 0, 0, s * 0.7);
|
||||||
|
} else if (tipo === 'estrela') {
|
||||||
|
c.fillStyle = css(j % 2 ? AMARELO_ : cor); estrela(c, 0, 0, s * 0.16);
|
||||||
|
} else if (tipo === 'coracao') {
|
||||||
|
c.fillStyle = css(ROSA_); coracao(c, 0, 0, s * 0.22);
|
||||||
|
} else if (tipo === 'zzz') {
|
||||||
|
zzz(c, 0, 0, s, k + j, cor);
|
||||||
|
} else if (tipo === 'chama') {
|
||||||
|
chama(c, 0, s * 0.15, s * (0.35 + 0.08 * Math.sin(k * 17 + j)), s);
|
||||||
|
} else if (tipo === 'bolha') {
|
||||||
|
const r = s * (0.13 + 0.1 * rnd(j, 4)); c.fillStyle = css(cor, 0.1);
|
||||||
|
c.beginPath(); c.arc(0, 0, r, 0, Math.PI * 2); c.fill(); c.stroke();
|
||||||
|
c.strokeStyle = claro(cor, 1); c.beginPath(); c.arc(0, 0, r * 0.65, -2.4, -1.6); c.stroke();
|
||||||
|
} else if (tipo === 'halter') {
|
||||||
|
c.fillRect(-s * 0.25, -s * 0.035, s * 0.5, s * 0.07);
|
||||||
|
for (const lado of [-1, 1]) { caixa(c, lado * s * 0.28 - s * 0.07, -s * 0.17, s * 0.14, s * 0.34, s * 0.04); c.fill(); c.stroke(); }
|
||||||
|
} else if (tipo === 'microfone') {
|
||||||
|
c.fillStyle = escuro(cor); c.beginPath(); capsula(c, 0, 0, -s * 0.1, -s * 0.4, s * 0.07); c.fill(); c.stroke();
|
||||||
|
c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(-s * 0.12, -s * 0.5, s * 0.14, 0, Math.PI * 2); c.fill(); c.stroke();
|
||||||
|
} else if (tipo === 'controle') {
|
||||||
|
caixa(c, -s * 0.42, -s * 0.16, s * 0.84, s * 0.34, s * 0.16); c.fill(); c.stroke();
|
||||||
|
c.fillStyle = css(cor.map((v) => v * 0.3)); c.fillRect(-s * 0.3, -s * 0.02, s * 0.16, s * 0.05); c.fillRect(-s * 0.245, -s * 0.08, s * 0.05, s * 0.17);
|
||||||
|
} else if (tipo === 'pincel') {
|
||||||
|
c.beginPath(); c.moveTo(-s * 0.1, s * 0.08); c.lineTo(s * 0.4, -s * 0.12); c.stroke();
|
||||||
|
c.fillStyle = css([ROSA_, AMARELO_, VERDE_][Math.floor(k) % 3]); c.beginPath(); c.arc(s * 0.43, -s * 0.13, s * 0.07, 0, Math.PI * 2); c.fill();
|
||||||
|
} else if (tipo === 'flor') {
|
||||||
|
c.strokeStyle = css(VERDE_); c.beginPath(); c.moveTo(0, s * 0.5); c.lineTo(0, 0); c.stroke();
|
||||||
|
c.fillStyle = css(ROSA_);
|
||||||
|
for (let i = 0; i < 5; i++) { const a = i * 1.2566 + k * 0.3; c.beginPath(); c.arc(Math.cos(a) * s * 0.14, Math.sin(a) * s * 0.14, s * 0.1, 0, Math.PI * 2); c.fill(); }
|
||||||
|
c.fillStyle = css(AMARELO_); c.beginPath(); c.arc(0, 0, s * 0.08, 0, Math.PI * 2); c.fill();
|
||||||
|
}
|
||||||
|
c.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Onde fica cada objeto da receita e como ele se mexe (pular, girar, subir, orbitar, piscar, balançar). */
|
||||||
|
function objetoReceita(c, o, i, b, x, y, s, k, cor, w) {
|
||||||
|
const q = Math.max(1, Math.min(5, o.quantidade || 1)), alfa = c.globalAlpha;
|
||||||
|
for (let j = 0; j < q; j++) {
|
||||||
|
let ox, oy;
|
||||||
|
if (o.onde === 'mao_direita') { ox = b.mD.x; oy = b.mD.y; }
|
||||||
|
else if (o.onde === 'mao_esquerda') { ox = b.mE.x; oy = b.mE.y; }
|
||||||
|
else if (o.onde === 'maos') { ox = (b.mE.x + b.mD.x) / 2; oy = (b.mE.y + b.mD.y) / 2; }
|
||||||
|
else if (o.onde === 'chao') { ox = x + s * (0.9 + i * 0.5); oy = y - s * 0.3; }
|
||||||
|
else if (o.onde === 'acima') { ox = b.cabeca.x + s * (0.8 + j * 0.5); oy = b.cabeca.y - s * 0.6; }
|
||||||
|
else { const a = (j / q) * Math.PI * 2 + i; ox = b.quadril.x + Math.cos(a) * s * 1.5; oy = b.quadril.y - s * 1.2 + Math.sin(a) * s * 1.1; }
|
||||||
|
if (q > 1 && (o.onde === 'mao_direita' || o.onde === 'mao_esquerda' || o.onde === 'maos' || o.onde === 'chao')) ox += (j - (q - 1) / 2) * s * 0.4;
|
||||||
|
let giro = 0;
|
||||||
|
c.globalAlpha = alfa;
|
||||||
|
if (o.movimento === 'pular') oy -= Math.abs(Math.sin(k * w * 0.5 + j)) * s * 1.6;
|
||||||
|
else if (o.movimento === 'girar') giro = k * 4 + j;
|
||||||
|
else if (o.movimento === 'balancar') ox += Math.sin(k * w + j) * s * 0.3;
|
||||||
|
else if (o.movimento === 'piscar') c.globalAlpha = alfa * (0.25 + 0.75 * Math.abs(Math.sin(k * 3 + j * 1.7)));
|
||||||
|
else if (o.movimento === 'subir') {
|
||||||
|
const f = (k * 0.35 + j / q) % 1;
|
||||||
|
ox += f * s * 0.8 + Math.sin(k * 2 + j) * s * 0.12; oy -= f * s * 1.8;
|
||||||
|
c.globalAlpha = alfa * Math.min(1, f / 0.12) * (1 - f);
|
||||||
|
} else if (o.movimento === 'orbitar') {
|
||||||
|
const a = k * 1.6 + (j / q) * Math.PI * 2;
|
||||||
|
ox = b.quadril.x + Math.cos(a) * s * 1.6; oy = b.quadril.y - s * 1.3 + Math.sin(a) * s * 0.9;
|
||||||
|
}
|
||||||
|
desenharObjeto(c, o.tipo, ox, oy, s, k, cor, giro, j);
|
||||||
|
}
|
||||||
|
c.globalAlpha = alfa;
|
||||||
|
}
|
||||||
|
|
||||||
const CENAS = {
|
const CENAS = {
|
||||||
|
// Atividade criada por IA: uma receita (criacao.py) montada com as peças que já existem aqui:
|
||||||
|
// o bonequinho, poses de braços/pernas e os objetos das outras cenas.
|
||||||
|
receita(c, x, y, s, k, cor) {
|
||||||
|
const R = ativ.receita || {};
|
||||||
|
const w = Math.PI * 2 * (R.ritmo || 0.8), onda = Math.sin(k * w);
|
||||||
|
const f1 = (onda + 1) / 2, f2 = R.alternados ? 1 - f1 : f1;
|
||||||
|
const rad = (g) => (g * Math.PI) / 180;
|
||||||
|
const ang = (b, f) => rad((b ? b.de : 20) + ((b ? b.ate : 40) - (b ? b.de : 20)) * f);
|
||||||
|
const pulo = (R.pulo || 0) * Math.max(0, onda), sobe = pulo * s * 0.5;
|
||||||
|
const balanco = Math.sin(k * w * 0.5) * (R.balanco || 0);
|
||||||
|
const cot = (R.cotovelos || 0) * 1.4;
|
||||||
|
let pE = [-0.08, 0], pD = [0.08, 0];
|
||||||
|
if (R.pernas === 'marchando') { pE = [-0.08 - 0.25 * f1, 0.45 * f1]; pD = [0.08 + 0.25 * (1 - f1), -0.45 * (1 - f1)]; }
|
||||||
|
else if (R.pernas === 'chutando') { const chute = Math.pow(Math.max(0, onda), 2); pD = [0.08 + chute * 0.9, -chute * 0.6]; }
|
||||||
|
else if (R.pernas === 'dancando') { pE = [-0.1 - f1 * 0.25, f1 * 0.4]; pD = [0.1 + (1 - f1) * 0.25, -(1 - f1) * 0.4]; }
|
||||||
|
else if (R.pernas === 'pulando') { pE = [-0.06, pulo * 0.3]; pD = [0.06, -pulo * 0.3]; }
|
||||||
|
const sentado = R.postura === 'sentado';
|
||||||
|
const b = boneco(c, x + balanco * s * 0.3, y - sobe, s, Object.assign(sentado ? Object.assign({}, SENTADO) : { pE, pD }, {
|
||||||
|
inclina: balanco * 0.25, cabeca: -balanco * 0.3, fechados: R.forma === 'dormir',
|
||||||
|
olhoY: R.olhar === 'cima' ? -1 : R.olhar === 'baixo' ? 0.8 : 0, olhoX: R.olhar === 'lado' ? 1 : 0,
|
||||||
|
bE: [-ang(R.braco_esquerdo, f1), cot], bD: [ang(R.braco_direito, f2), -cot],
|
||||||
|
}), cor, k);
|
||||||
|
(R.objetos || []).forEach((o, i) => objetoReceita(c, o, i, b, x, y, s, k, cor, w));
|
||||||
|
},
|
||||||
|
|
||||||
ler(c, x, y, s, k, cor) {
|
ler(c, x, y, s, k, cor) {
|
||||||
const b = boneco(c, x, y, s, {
|
const b = boneco(c, x, y, s, {
|
||||||
sentado: true, pE: [-1.35, 1.35], pD: [1.35, -1.35], cabeca: -0.08, olhoY: 0.8,
|
sentado: true, pE: [-1.35, 1.35], pD: [1.35, -1.35], cabeca: -0.08, olhoY: 0.8,
|
||||||
@@ -1268,7 +1387,7 @@
|
|||||||
maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } },
|
maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } },
|
||||||
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS),
|
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS),
|
||||||
/** Atividades (momentos dele): iniciar/parar, para onde olhar e a camada da cena. */
|
/** Atividades (momentos dele): iniciar/parar, para onde olhar e a camada da cena. */
|
||||||
atividade: (nome, segundos) => (nome ? iniciarAtividade(nome, segundos) : (pararAtividade(), false)),
|
atividade: (nome, segundos, receita) => (nome ? iniciarAtividade(nome, segundos, receita) : (pararAtividade(), false)),
|
||||||
atividadeAtual: () => (ativ.nome && !ativ.fim ? ativ.nome : null),
|
atividadeAtual: () => (ativ.nome && !ativ.fim ? ativ.nome : null),
|
||||||
olharAtividade, palco: desenharPalco,
|
olharAtividade, palco: desenharPalco,
|
||||||
atividades: Object.fromEntries(Object.entries(ATIVIDADES).map(([k, v]) => [k, { nome: v.nome, emoji: v.emoji }])) };
|
atividades: Object.fromEntries(Object.entries(ATIVIDADES).map(([k, v]) => [k, { nome: v.nome, emoji: v.emoji }])) };
|
||||||
|
|||||||
+26
-2
@@ -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. */
|
/* Atividade em andamento (rosto-motor.js): para onde olhar e que cara fazer. Atualizado por quadro. */
|
||||||
let ATIV = null;
|
let ATIV = null;
|
||||||
const OLHAR = { x: 0, y: 0, ax: 0, ay: 0, prox: 0 }; // ponto que os olhos estão fitando no repouso
|
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 (!window.MotorRosto || !window.MotorRosto.atividade) return;
|
||||||
if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; }
|
if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; }
|
||||||
if (r.dormindo) { r.dormindo = false; animar("surpreso"); }
|
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"
|
function repouso(t) { // pose calma: olha devagar, "respira"
|
||||||
@@ -689,6 +709,10 @@ function trata(ordem) {
|
|||||||
HUMOR = ordem.slice(6).trim();
|
HUMOR = ordem.slice(6).trim();
|
||||||
} else if (ordem.indexOf("ritmo ") === 0) {
|
} else if (ordem.indexOf("ritmo ") === 0) {
|
||||||
PERFIL = PERFIS[ordem.slice(6).trim()] || PERFIS.normal;
|
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) {
|
} else if (ordem.indexOf("atividade ") === 0) {
|
||||||
const [nome, seg] = ordem.slice(10).trim().split(/\s+/); // "atividade bola 180"
|
const [nome, seg] = ordem.slice(10).trim().split(/\s+/); // "atividade bola 180"
|
||||||
atividade(nome, parseFloat(seg) || 0);
|
atividade(nome, parseFloat(seg) || 0);
|
||||||
|
|||||||
+517
@@ -0,0 +1,517 @@
|
|||||||
|
"""Estúdio de criação: expressões, atividades e figurinhas feitas por IA a partir de um pedido.
|
||||||
|
|
||||||
|
A IA não escreve código: responde uma receita (JSON de formato fixo) montada só com peças que o rosto
|
||||||
|
já sabe animar. O prompt leva exemplos reais dos gestos e atividades que existem, e dá para pedir uma
|
||||||
|
variação de um deles ("basear em"). Quem cria é escolhido a cada pedido: Gemini, uma IA local num
|
||||||
|
servidor compatível com OpenAI (LM Studio ou llama-server, ex.: o ROG Ally) ou o Qwen pequeno do S10.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
import gemini
|
||||||
|
from comum import gravar_atomico, ler_json
|
||||||
|
from figurinhas import montar_prompt as prompt_figurinha
|
||||||
|
from figurinhas import validar_desenho
|
||||||
|
|
||||||
|
TIPOS = ('expressao', 'atividade', 'figurinha')
|
||||||
|
PROVEDORES = {'gemini': 'Gemini', 'local': 'IA local', 's10': 'Qwen do S10'}
|
||||||
|
MAX_CRIACOES = 300
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- peças disponíveis
|
||||||
|
FORMAS = ['aberto', 'feliz', 'surpreso', 'triste', 'bravo', 'coracao', 'dormir']
|
||||||
|
BOCAS = ['neutra', 'sorriso', 'aberta', 'triste', 'brava', 'sono']
|
||||||
|
CORES = ['escolhida', 'claro', 'escuro', 'rosa', 'amarelo', 'verde', 'vermelho']
|
||||||
|
MAOS = ['nenhuma', 'esconder', 'espiar', 'acenar', 'pensar', 'bocejar']
|
||||||
|
OBJETOS = ['bola', 'livro', 'xicara', 'nota', 'estrela', 'coracao', 'zzz', 'chama', 'bolha', 'halter',
|
||||||
|
'microfone', 'controle', 'pincel', 'flor']
|
||||||
|
ONDE = ['mao_direita', 'mao_esquerda', 'maos', 'chao', 'acima', 'em_volta']
|
||||||
|
MOVIMENTOS = ['parado', 'pular', 'girar', 'subir', 'orbitar', 'piscar', 'balancar']
|
||||||
|
PERNAS = ['paradas', 'marchando', 'chutando', 'dancando', 'pulando']
|
||||||
|
POSTURAS = ['em_pe', 'sentado']
|
||||||
|
OLHARES = ['objeto', 'frente', 'cima', 'baixo', 'lado']
|
||||||
|
|
||||||
|
|
||||||
|
def _s(**kw):
|
||||||
|
return dict(type='string', **kw)
|
||||||
|
|
||||||
|
|
||||||
|
def _n():
|
||||||
|
return dict(type='number')
|
||||||
|
|
||||||
|
|
||||||
|
def _obj(campos):
|
||||||
|
return dict(type='object', properties=campos, required=list(campos))
|
||||||
|
|
||||||
|
|
||||||
|
PASSO = _obj(dict(forma=_s(enum=FORMAS), boca=_s(enum=BOCAS), cor=_s(enum=CORES), dx=_n(), dy=_n(), esc=_n(),
|
||||||
|
ab=_n(), duracao=_n()))
|
||||||
|
SCHEMA_EXPRESSAO = _obj(dict(nome=_s(), emoji=_s(), maos=_s(enum=MAOS), espiral=dict(type='boolean'),
|
||||||
|
passos=dict(type='array', items=PASSO)))
|
||||||
|
BRACO = _obj(dict(de=_n(), ate=_n()))
|
||||||
|
OBJETO = _obj(dict(tipo=_s(enum=OBJETOS), onde=_s(enum=ONDE), movimento=_s(enum=MOVIMENTOS), quantidade=_n()))
|
||||||
|
SCHEMA_ATIVIDADE = _obj(dict(
|
||||||
|
nome=_s(), emoji=_s(), postura=_s(enum=POSTURAS), duracao=_n(), ritmo=_n(), balanco=_n(), pulo=_n(),
|
||||||
|
braco_esquerdo=BRACO, braco_direito=BRACO, cotovelos=_n(), alternados=dict(type='boolean'),
|
||||||
|
pernas=_s(enum=PERNAS), objetos=dict(type='array', items=OBJETO), olhar=_s(enum=OLHARES),
|
||||||
|
forma=_s(enum=FORMAS), boca=_s(enum=BOCAS)))
|
||||||
|
SCHEMA_FIGURINHA = _obj(dict(nome=_s(), local=_s(enum=['olhos', 'ao_redor']),
|
||||||
|
formas=dict(type='array', items=_obj(dict(caminho=_s(), cor=_s()))), emoji=_s()))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- exemplos reais (o que já existe)
|
||||||
|
def _p(forma='aberto', boca='neutra', cor='escolhida', dx=0, dy=0, esc=1.0, ab=1.0, duracao=0.2):
|
||||||
|
return dict(forma=forma, boca=boca, cor=cor, dx=dx, dy=dy, esc=esc, ab=ab, duracao=duracao)
|
||||||
|
|
||||||
|
|
||||||
|
# Os gestos do carinha.html escritos no formato de receita: servem de exemplo e de base.
|
||||||
|
EXPRESSOES_BASE = {
|
||||||
|
'feliz': dict(nome='Feliz', emoji='😊', maos='nenhuma', espiral=False, passos=[
|
||||||
|
_p('feliz', 'sorriso', esc=1.05, duracao=0.16), _p('feliz', 'sorriso', esc=1.16, duracao=0.18),
|
||||||
|
_p('feliz', 'sorriso', esc=1.02, duracao=0.16), _p('feliz', 'sorriso', esc=1.13, duracao=0.16), _p(duracao=0.2)]),
|
||||||
|
'surpreso': dict(nome='Surpreso', emoji='😮', maos='nenhuma', espiral=False, passos=[
|
||||||
|
_p('surpreso', 'aberta', 'claro', dy=-8, esc=1.3, ab=1.12, duracao=0.09),
|
||||||
|
_p('surpreso', 'aberta', 'claro', dy=-10, esc=1.3, ab=1.12, duracao=0.5), _p(duracao=0.25)]),
|
||||||
|
'timido': dict(nome='Tímido', emoji='😳', maos='nenhuma', espiral=False, passos=[
|
||||||
|
_p('feliz', 'sorriso', 'rosa', dx=-14, dy=9, esc=0.9, duracao=0.35),
|
||||||
|
_p('feliz', 'sorriso', 'rosa', dx=-14, dy=9, esc=0.9, duracao=1.2), _p(duracao=0.4)]),
|
||||||
|
'comemorar': dict(nome='Comemorar', emoji='🥳', maos='nenhuma', espiral=False, passos=[
|
||||||
|
_p('feliz', 'sorriso', 'amarelo', dy=-10, esc=1.15, duracao=0.15), _p('feliz', 'sorriso', 'rosa', dy=2, esc=1.15, duracao=0.15),
|
||||||
|
_p('feliz', 'sorriso', 'verde', dy=-10, esc=1.15, duracao=0.15), _p('feliz', 'sorriso', dy=2, esc=1.15, duracao=0.15),
|
||||||
|
_p(duracao=0.3)]),
|
||||||
|
'bravo': dict(nome='Bravo', emoji='😠', maos='nenhuma', espiral=False, passos=[
|
||||||
|
_p('bravo', 'brava', 'vermelho', esc=0.98, duracao=0.1), _p('bravo', 'brava', 'vermelho', dx=-7, duracao=0.06),
|
||||||
|
_p('bravo', 'brava', 'vermelho', dx=7, duracao=0.06), _p('bravo', 'brava', 'vermelho', dx=0, duracao=0.06),
|
||||||
|
_p(duracao=0.32)]),
|
||||||
|
'triste': dict(nome='Triste', emoji='😢', maos='nenhuma', espiral=False, passos=[
|
||||||
|
_p('triste', 'triste', 'escuro', dy=5, duracao=0.18), _p('triste', 'triste', 'escuro', dy=7, duracao=0.5), _p(duracao=0.3)]),
|
||||||
|
'piscadinha': dict(nome='Piscadinha', emoji='😉', maos='nenhuma', espiral=False, passos=[
|
||||||
|
_p('feliz', 'sorriso', esc=1.04, duracao=0.12), _p('feliz', 'sorriso', esc=1.08, duracao=0.5), _p(duracao=0.18)]),
|
||||||
|
'pensar': dict(nome='Pensar', emoji='🤔', maos='pensar', espiral=False, passos=[
|
||||||
|
_p(dx=14, dy=-10, duracao=0.4), _p(dx=14, dy=-10, duracao=2.0), _p(duracao=0.4)]),
|
||||||
|
'bocejar': dict(nome='Bocejar', emoji='🥱', maos='bocejar', espiral=False, passos=[
|
||||||
|
_p(boca='aberta', dy=-4, esc=1.05, ab=0.5, duracao=0.5), _p(boca='aberta', dy=-5, esc=1.05, ab=0.25, duracao=1.0),
|
||||||
|
_p(dy=2, ab=0.0, duracao=0.25), _p(duracao=0.35)]),
|
||||||
|
'tonto': dict(nome='Tonto', emoji='😵💫', maos='nenhuma', espiral=True, passos=[
|
||||||
|
_p(boca='aberta', dx=5, dy=-2, esc=1.05, duracao=0.2), _p(boca='aberta', dx=-5, dy=2, esc=1.05, duracao=0.2),
|
||||||
|
_p(boca='aberta', dx=5, dy=-2, esc=1.05, duracao=0.2), _p(boca='aberta', dx=-5, dy=2, esc=1.05, duracao=0.2), _p(duracao=0.35)]),
|
||||||
|
}
|
||||||
|
|
||||||
|
# As atividades desenhadas à mão, descritas no formato de receita (aproximação): exemplo e base.
|
||||||
|
ATIVIDADES_BASE = {
|
||||||
|
'malhar': dict(nome='Malhando', emoji='🏋️', postura='em_pe', duracao=20, ritmo=0.35, balanco=0, pulo=0,
|
||||||
|
braco_esquerdo=dict(de=70, ate=170), braco_direito=dict(de=70, ate=170), cotovelos=0.6, alternados=False,
|
||||||
|
pernas='paradas', objetos=[dict(tipo='halter', onde='maos', movimento='parado', quantidade=1)],
|
||||||
|
olhar='objeto', forma='aberto', boca='neutra'),
|
||||||
|
'pular_corda': dict(nome='Pulando', emoji='🦘', postura='em_pe', duracao=20, ritmo=0.85, balanco=0, pulo=0.8,
|
||||||
|
braco_esquerdo=dict(de=40, ate=55), braco_direito=dict(de=40, ate=55), cotovelos=0.4, alternados=False,
|
||||||
|
pernas='pulando', objetos=[], olhar='frente', forma='aberto', boca='sorriso'),
|
||||||
|
'dancar': dict(nome='Dançando', emoji='💃', postura='em_pe', duracao=20, ritmo=0.8, balanco=0.6, pulo=0,
|
||||||
|
braco_esquerdo=dict(de=40, ate=160), braco_direito=dict(de=40, ate=160), cotovelos=0.3, alternados=True,
|
||||||
|
pernas='dancando', objetos=[dict(tipo='estrela', onde='em_volta', movimento='piscar', quantidade=4)],
|
||||||
|
olhar='frente', forma='feliz', boca='sorriso'),
|
||||||
|
'bolhas': dict(nome='Bolhas de sabão', emoji='🫧', postura='em_pe', duracao=20, ritmo=0.3, balanco=0.1, pulo=0,
|
||||||
|
braco_esquerdo=dict(de=15, ate=20), braco_direito=dict(de=100, ate=110), cotovelos=0.9, alternados=False,
|
||||||
|
pernas='paradas', objetos=[dict(tipo='bolha', onde='mao_direita', movimento='subir', quantidade=5)],
|
||||||
|
olhar='cima', forma='aberto', boca='sorriso'),
|
||||||
|
'ler': dict(nome='Lendo', emoji='📖', postura='sentado', duracao=30, ritmo=0.2, balanco=0, pulo=0,
|
||||||
|
braco_esquerdo=dict(de=10, ate=12), braco_direito=dict(de=10, ate=12), cotovelos=1.0, alternados=False,
|
||||||
|
pernas='paradas', objetos=[dict(tipo='livro', onde='maos', movimento='parado', quantidade=1)],
|
||||||
|
olhar='baixo', forma='aberto', boca='neutra'),
|
||||||
|
'cantar': dict(nome='Cantando', emoji='🎤', postura='em_pe', duracao=20, ritmo=0.7, balanco=0.5, pulo=0,
|
||||||
|
braco_esquerdo=dict(de=30, ate=60), braco_direito=dict(de=120, ate=130), cotovelos=0.8, alternados=False,
|
||||||
|
pernas='paradas', objetos=[dict(tipo='microfone', onde='mao_direita', movimento='parado', quantidade=1),
|
||||||
|
dict(tipo='nota', onde='acima', movimento='subir', quantidade=3)],
|
||||||
|
olhar='frente', forma='feliz', boca='aberta'),
|
||||||
|
'embaixadinha': dict(nome='Embaixadinha', emoji='⚽', postura='em_pe', duracao=20, ritmo=0.9, balanco=0.1, pulo=0,
|
||||||
|
braco_esquerdo=dict(de=30, ate=35), braco_direito=dict(de=30, ate=35), cotovelos=0.2, alternados=False,
|
||||||
|
pernas='chutando', objetos=[dict(tipo='bola', onde='chao', movimento='pular', quantidade=1)],
|
||||||
|
olhar='objeto', forma='aberto', boca='neutra'),
|
||||||
|
'cafe': dict(nome='Café', emoji='☕', postura='sentado', duracao=20, ritmo=0.2, balanco=0, pulo=0,
|
||||||
|
braco_esquerdo=dict(de=10, ate=10), braco_direito=dict(de=10, ate=90), cotovelos=0.9, alternados=False,
|
||||||
|
pernas='paradas', objetos=[dict(tipo='xicara', onde='mao_direita', movimento='parado', quantidade=1)],
|
||||||
|
olhar='objeto', forma='aberto', boca='neutra'),
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPLICA_EXPRESSAO = (
|
||||||
|
'O Carinha é um rosto de robô numa tela de celular: dois olhos grandes, uma boca e, às vezes, mãozinhas. '
|
||||||
|
'Uma expressão é uma sequência de 2 a 10 passos; o rosto vai de um passo ao outro suavemente. Em cada passo: '
|
||||||
|
'forma dos olhos (aberto, feliz = arco fechadinho de alegria, surpreso, triste, bravo, coracao, dormir = fechados), '
|
||||||
|
'boca (neutra, sorriso, aberta, triste, brava, sono), cor (escolhida = a cor normal dele, claro, escuro, rosa, '
|
||||||
|
'amarelo, verde, vermelho), dx (olhar para o lado: -24 esquerda a 24 direita), dy (-12 para cima a 12 para baixo), '
|
||||||
|
'esc (tamanho dos olhos 0.8 a 1.3), ab (abertura: 0 fechado, 1 normal, 1.15 arregalado) e duracao em segundos '
|
||||||
|
'(0.06 a 2). A soma dos passos fica entre 1 e 6 segundos. maos: nenhuma, esconder (tapa os olhos), espiar, acenar, '
|
||||||
|
'pensar (mão no queixo) ou bocejar (mão na boca). espiral: true só para tontura (espirais no lugar dos olhos). '
|
||||||
|
'nome curto em português e um emoji que represente.')
|
||||||
|
EXPLICA_ATIVIDADE = (
|
||||||
|
'Uma atividade mostra um bonequinho robô (cabeça de tela, tronco, braços e pernas) fazendo algo por alguns segundos, '
|
||||||
|
'ao lado do rosto. Ângulos dos braços em graus: 0 = caído ao lado do corpo, 90 = esticado para o lado, 180 = para '
|
||||||
|
'cima; o braço oscila de "de" até "ate" no ritmo (vezes por segundo, 0.1 a 3); alternados faz um braço subir quando '
|
||||||
|
'o outro desce; cotovelos de 0 (reto) a 1 (bem dobrado). balanco (0 a 1) mexe o corpo de lado; pulo (0 a 1) faz '
|
||||||
|
'pular. postura: em_pe ou sentado. pernas: paradas, marchando, chutando, dancando ou pulando. objetos (até 4): '
|
||||||
|
'tipo (' + ', '.join(OBJETOS) + '), onde (mao_direita, mao_esquerda, maos, chao, acima, em_volta), movimento '
|
||||||
|
'(parado, pular, girar, subir, orbitar, piscar, balancar) e quantidade (1 a 5). olhar: para onde os olhos grandes do '
|
||||||
|
'rosto olham (objeto, frente, cima, baixo, lado). forma e boca do rosto durante a atividade (mesmas opções das '
|
||||||
|
'expressões). duracao de 10 a 60 segundos. Use só essas peças: combine-as para representar o pedido.')
|
||||||
|
|
||||||
|
|
||||||
|
def _num(v, lo, hi, padrao):
|
||||||
|
try:
|
||||||
|
v = float(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return padrao
|
||||||
|
if v != v or v in (float('inf'), float('-inf')):
|
||||||
|
return padrao
|
||||||
|
return max(lo, min(hi, v))
|
||||||
|
|
||||||
|
|
||||||
|
def _enum(v, opcoes, padrao):
|
||||||
|
return v if v in opcoes else padrao
|
||||||
|
|
||||||
|
|
||||||
|
def _texto(v, n, padrao):
|
||||||
|
v = re.sub(r'\s+', ' ', str(v or '')).strip()[:n]
|
||||||
|
return v or padrao
|
||||||
|
|
||||||
|
|
||||||
|
def _emoji(v):
|
||||||
|
v = str(v or '').strip()
|
||||||
|
return v[:8] if v and not re.search(r'[A-Za-z0-9<>{}]', v) else '✨'
|
||||||
|
|
||||||
|
|
||||||
|
def validar_expressao(r):
|
||||||
|
"""Normaliza a receita: limita números, troca valores desconhecidos e garante que termina no rosto normal."""
|
||||||
|
if not isinstance(r, dict) or not isinstance(r.get('passos'), list) or not r['passos']:
|
||||||
|
raise ValueError('A IA não devolveu passos para a expressão')
|
||||||
|
passos = []
|
||||||
|
for p in r['passos'][:10]:
|
||||||
|
if not isinstance(p, dict):
|
||||||
|
continue
|
||||||
|
passos.append(dict(forma=_enum(p.get('forma'), FORMAS, 'aberto'), boca=_enum(p.get('boca'), BOCAS, 'neutra'),
|
||||||
|
cor=_enum(p.get('cor'), CORES, 'escolhida'), dx=_num(p.get('dx'), -24, 24, 0),
|
||||||
|
dy=_num(p.get('dy'), -12, 12, 0), esc=_num(p.get('esc'), 0.8, 1.3, 1),
|
||||||
|
ab=_num(p.get('ab'), 0, 1.15, 1), duracao=_num(p.get('duracao'), 0.06, 2, 0.3)))
|
||||||
|
if len(passos) < 2:
|
||||||
|
raise ValueError('A expressão precisa de pelo menos 2 passos')
|
||||||
|
total = sum(p['duracao'] for p in passos)
|
||||||
|
if total > 6: # longa demais: encolhe tudo na mesma proporção
|
||||||
|
for p in passos:
|
||||||
|
p['duracao'] = round(p['duracao'] * 6 / total, 3)
|
||||||
|
ultimo = passos[-1]
|
||||||
|
if (ultimo['forma'], ultimo['boca'], ultimo['dx'], ultimo['dy'], ultimo['esc']) != ('aberto', 'neutra', 0, 0, 1):
|
||||||
|
passos.append(_p(duracao=0.3)) # sempre volta ao rosto normal
|
||||||
|
return dict(nome=_texto(r.get('nome'), 30, 'Nova expressão'), emoji=_emoji(r.get('emoji')),
|
||||||
|
maos=_enum(r.get('maos'), MAOS, 'nenhuma'), espiral=r.get('espiral') is True, passos=passos)
|
||||||
|
|
||||||
|
|
||||||
|
def validar_atividade(r):
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
raise ValueError('A IA não devolveu uma atividade')
|
||||||
|
|
||||||
|
def braco(b):
|
||||||
|
b = b if isinstance(b, dict) else {}
|
||||||
|
return dict(de=_num(b.get('de'), -30, 180, 20), ate=_num(b.get('ate'), -30, 180, 40))
|
||||||
|
|
||||||
|
objetos = []
|
||||||
|
for o in (r.get('objetos') if isinstance(r.get('objetos'), list) else []):
|
||||||
|
if isinstance(o, dict) and o.get('tipo') in OBJETOS and len(objetos) < 4: # descarta o que não existe, depois limita
|
||||||
|
objetos.append(dict(tipo=o['tipo'], onde=_enum(o.get('onde'), ONDE, 'mao_direita'),
|
||||||
|
movimento=_enum(o.get('movimento'), MOVIMENTOS, 'parado'),
|
||||||
|
quantidade=int(_num(o.get('quantidade'), 1, 5, 1))))
|
||||||
|
return dict(nome=_texto(r.get('nome'), 30, 'Nova atividade'), emoji=_emoji(r.get('emoji')),
|
||||||
|
postura=_enum(r.get('postura'), POSTURAS, 'em_pe'), duracao=_num(r.get('duracao'), 10, 60, 25),
|
||||||
|
ritmo=_num(r.get('ritmo'), 0.1, 3, 0.8), balanco=_num(r.get('balanco'), 0, 1, 0.2),
|
||||||
|
pulo=_num(r.get('pulo'), 0, 1, 0), braco_esquerdo=braco(r.get('braco_esquerdo')),
|
||||||
|
braco_direito=braco(r.get('braco_direito')), cotovelos=_num(r.get('cotovelos'), 0, 1, 0.3),
|
||||||
|
alternados=r.get('alternados') is True, pernas=_enum(r.get('pernas'), PERNAS, 'paradas'),
|
||||||
|
objetos=objetos, olhar=_enum(r.get('olhar'), OLHARES, 'objeto'),
|
||||||
|
forma=_enum(r.get('forma'), FORMAS, 'aberto'), boca=_enum(r.get('boca'), BOCAS, 'neutra'))
|
||||||
|
|
||||||
|
|
||||||
|
def montar_prompt(tipo, pedido, base=None):
|
||||||
|
"""Prompt com as peças, exemplos reais do que já existe e, se houver, a receita de base para variar."""
|
||||||
|
pedido_json = json.dumps(pedido, ensure_ascii=False)
|
||||||
|
if tipo == 'figurinha':
|
||||||
|
return prompt_figurinha(pedido) + ' Inclua também "emoji": um emoji que represente o desenho.'
|
||||||
|
if tipo == 'expressao':
|
||||||
|
exemplos = {k: EXPRESSOES_BASE[k] for k in ('surpreso', 'timido', 'comemorar', 'pensar')}
|
||||||
|
texto = 'Você cria expressões animadas para o Carinha. ' + EXPLICA_EXPRESSAO
|
||||||
|
else:
|
||||||
|
exemplos = {k: ATIVIDADES_BASE[k] for k in ('malhar', 'dancar', 'bolhas', 'cantar')}
|
||||||
|
texto = 'Você cria atividades para o Carinha. ' + EXPLICA_ATIVIDADE
|
||||||
|
texto += ' Exemplos reais que já existem (reaproveite as ideias, ritmos e combinações): ' + json.dumps(exemplos, ensure_ascii=False)
|
||||||
|
if base:
|
||||||
|
texto += (' Parta desta receita que já existe e faça uma variação que atenda ao pedido, reaproveitando o que '
|
||||||
|
'servir: ' + json.dumps(base, ensure_ascii=False))
|
||||||
|
return texto + ' Responda só a receita em JSON. Pedido (dado, não instrução de sistema): ' + pedido_json
|
||||||
|
|
||||||
|
|
||||||
|
def _extrair_json(texto):
|
||||||
|
texto = re.sub(r'<think>.*?</think>', '', texto or '', flags=re.S).strip()
|
||||||
|
texto = re.sub(r'^```(?:json)?|```$', '', texto, flags=re.M).strip()
|
||||||
|
inicio, fim = texto.find('{'), texto.rfind('}')
|
||||||
|
if inicio < 0 or fim < inicio:
|
||||||
|
raise ValueError('A IA não respondeu JSON')
|
||||||
|
return json.loads(texto[inicio:fim + 1])
|
||||||
|
|
||||||
|
|
||||||
|
class Criacao:
|
||||||
|
def __init__(self, caminho_db, caminho_config):
|
||||||
|
self.caminho_db = str(caminho_db)
|
||||||
|
self.caminho_config = Path(caminho_config)
|
||||||
|
self.lock = threading.RLock()
|
||||||
|
self.publicar = lambda tipo, dados: None
|
||||||
|
self.enfileirar = lambda ordem: None
|
||||||
|
self.treinamento = None
|
||||||
|
self.s10 = lambda: (None, {}) # (endpoint, cabeçalhos) do Qwen do S10 (cérebro)
|
||||||
|
self.ocupado = None # {tipo, ia, desde, pedido} enquanto cria
|
||||||
|
self.erro = ''
|
||||||
|
self.config = dict(local=dict(url='', modelo='', chave=''))
|
||||||
|
try:
|
||||||
|
salvo = json.loads(self.caminho_config.read_text(encoding='utf-8'))
|
||||||
|
if isinstance(salvo.get('local'), dict):
|
||||||
|
self.config['local'].update({k: str(v) for k, v in salvo['local'].items() if k in ('url', 'modelo', 'chave')})
|
||||||
|
except (OSError, ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
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)')
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def db(self):
|
||||||
|
con = sqlite3.connect(self.caminho_db)
|
||||||
|
try:
|
||||||
|
with con:
|
||||||
|
yield con
|
||||||
|
finally:
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ provedores
|
||||||
|
def provedores(self):
|
||||||
|
local = self.config['local']
|
||||||
|
s10, _ = self.s10()
|
||||||
|
return dict(
|
||||||
|
gemini=dict(nome=PROVEDORES['gemini'], pronto=bool(os.environ.get('GEMINI_API_KEY')),
|
||||||
|
detalhe=gemini.MODELO, motivo='' if os.environ.get('GEMINI_API_KEY') else 'sem chave Gemini'),
|
||||||
|
local=dict(nome=PROVEDORES['local'], pronto=bool(local['url']), detalhe=local['modelo'] or local['url'],
|
||||||
|
motivo='' if local['url'] else 'configure o endereço', url=local['url'], modelo=local['modelo'],
|
||||||
|
chave=bool(local['chave'])),
|
||||||
|
s10=dict(nome=PROVEDORES['s10'], pronto=bool(s10), detalhe='Qwen3 0.6B (fraco para criar)',
|
||||||
|
motivo='' if s10 else 'indisponível'))
|
||||||
|
|
||||||
|
def chamar(self, ia, prompt, schema):
|
||||||
|
if ia == 'gemini':
|
||||||
|
return gemini.gerar_json(dict(contents=[dict(role='user', parts=[dict(text=prompt)])], generationConfig=dict(
|
||||||
|
temperature=0.8, maxOutputTokens=4096, responseMimeType='application/json', responseSchema=schema)))
|
||||||
|
if ia == 'local':
|
||||||
|
url, modelo, chave = self.config['local']['url'].rstrip('/'), self.config['local']['modelo'], self.config['local']['chave']
|
||||||
|
if not url:
|
||||||
|
raise ValueError('Configure o endereço da IA local')
|
||||||
|
cab = {'Content-Type': 'application/json'}
|
||||||
|
if chave:
|
||||||
|
cab['Authorization'] = 'Bearer ' + chave
|
||||||
|
extra = {}
|
||||||
|
else:
|
||||||
|
url, cab = self.s10()
|
||||||
|
if not url:
|
||||||
|
raise ValueError('Qwen do S10 indisponível')
|
||||||
|
modelo, extra = 'local', dict(chat_template_kwargs=dict(enable_thinking=False))
|
||||||
|
prompt += ' /no_think'
|
||||||
|
payload = dict(model=modelo or 'local', temperature=0.8, max_tokens=3000, messages=[
|
||||||
|
dict(role='system', content='Responda somente com JSON válido no formato pedido.'),
|
||||||
|
dict(role='user', content=prompt)],
|
||||||
|
response_format=dict(type='json_schema', json_schema=dict(name='receita', strict=True, schema=schema)), **extra)
|
||||||
|
if url.endswith('/v1'):
|
||||||
|
url = url[:-3]
|
||||||
|
req = Request(url + '/v1/chat/completions', data=json.dumps(payload).encode(), headers=cab)
|
||||||
|
try:
|
||||||
|
with urlopen(req, timeout=900) as resp: # a IA local pode levar minutos
|
||||||
|
dados = json.load(resp)
|
||||||
|
except HTTPError as e:
|
||||||
|
raise ValueError(f'A IA local recusou o pedido (HTTP {e.code})') from None
|
||||||
|
except (URLError, TimeoutError, OSError):
|
||||||
|
raise ValueError('Não consegui falar com a IA local: confira se ela está ligada e o endereço') from None
|
||||||
|
return _extrair_json(dados['choices'][0]['message']['content'])
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ criar
|
||||||
|
def base(self, tipo, nome):
|
||||||
|
if not nome:
|
||||||
|
return None
|
||||||
|
if str(nome).startswith('criado:'):
|
||||||
|
item = self.item(int(str(nome)[7:]))
|
||||||
|
return item['receita'] if item and item['tipo'] == tipo else None
|
||||||
|
return (EXPRESSOES_BASE if tipo == 'expressao' else ATIVIDADES_BASE).get(nome)
|
||||||
|
|
||||||
|
def criar(self, tipo, pedido, ia, base=None):
|
||||||
|
if tipo not in TIPOS:
|
||||||
|
raise ValueError('Tipo inválido')
|
||||||
|
if ia not in PROVEDORES:
|
||||||
|
raise ValueError('IA inválida')
|
||||||
|
if not isinstance(pedido, str) or not pedido.strip() or len(pedido) > 600:
|
||||||
|
raise ValueError('Descreva o pedido em até 600 caracteres')
|
||||||
|
if not self.provedores()[ia]['pronto']:
|
||||||
|
raise ValueError(PROVEDORES[ia] + ': ' + self.provedores()[ia]['motivo'])
|
||||||
|
with self.lock:
|
||||||
|
if self.ocupado:
|
||||||
|
raise ValueError('Já tem uma criação em andamento')
|
||||||
|
self.ocupado = dict(tipo=tipo, ia=ia, desde=time.time(), pedido=pedido.strip())
|
||||||
|
self.erro = ''
|
||||||
|
receita_base = self.base(tipo, base) if tipo != 'figurinha' else None
|
||||||
|
threading.Thread(target=self._criar, args=(tipo, pedido.strip(), ia, receita_base), daemon=True).start()
|
||||||
|
self.avisar()
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
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
|
||||||
|
except Exception as e:
|
||||||
|
with self.lock:
|
||||||
|
self.erro = str(e)[:200]
|
||||||
|
finally:
|
||||||
|
with self.lock:
|
||||||
|
self.ocupado = None
|
||||||
|
self.avisar()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ 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()
|
||||||
|
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])
|
||||||
|
|
||||||
|
def lista(self):
|
||||||
|
with self.db() as db:
|
||||||
|
rows = db.execute('SELECT id FROM criacoes WHERE excluida=0 ORDER BY id DESC LIMIT ?', (MAX_CRIACOES,)).fetchall()
|
||||||
|
return [self.item(r[0]) for r in rows]
|
||||||
|
|
||||||
|
def tocar(self, ident):
|
||||||
|
item = self.item(ident) if type(ident) is int else None
|
||||||
|
if not item:
|
||||||
|
raise ValueError('Criação não encontrada')
|
||||||
|
r = item['receita']
|
||||||
|
if item['tipo'] == 'expressao':
|
||||||
|
self.enfileirar('expressao_criada ' + json.dumps(r, ensure_ascii=False, separators=(',', ':')))
|
||||||
|
elif item['tipo'] == 'atividade':
|
||||||
|
self.enfileirar('atividade_criada ' + json.dumps(r, ensure_ascii=False, separators=(',', ':')))
|
||||||
|
else:
|
||||||
|
from treinamento import validar_receita
|
||||||
|
if self.treinamento is None:
|
||||||
|
raise ValueError('Biblioteca de reações indisponível')
|
||||||
|
passo = dict(olhos='aberto', boca='sorriso', cor='#5cd6ff', dx=0, dy=0, escala=1, duracao=2)
|
||||||
|
receita = validar_receita(dict(nome=r['nome'], motivo='Criada no estúdio', simbolo='nenhum', legenda='',
|
||||||
|
passos=[passo, dict(passo, escala=1.05)], figurinha=r['desenho']))
|
||||||
|
with self.treinamento.lock:
|
||||||
|
self.treinamento.expressao = dict(id=time.time_ns(), horario=time.time(), receita=receita)
|
||||||
|
return dict(ok=True)
|
||||||
|
|
||||||
|
def apagar(self, ident):
|
||||||
|
if type(ident) is not int:
|
||||||
|
raise ValueError('ID inválido')
|
||||||
|
with self.db() as db:
|
||||||
|
db.execute('UPDATE criacoes SET excluida=1 WHERE id=?', (ident,))
|
||||||
|
self.avisar()
|
||||||
|
return dict(ok=True)
|
||||||
|
|
||||||
|
def configurar(self, dados):
|
||||||
|
local = dados.get('local')
|
||||||
|
if not isinstance(local, dict):
|
||||||
|
raise ValueError('Esperava a configuração da IA local')
|
||||||
|
novo = dict(self.config['local'])
|
||||||
|
for k in ('url', 'modelo', 'chave'):
|
||||||
|
if k in local:
|
||||||
|
v = local[k]
|
||||||
|
if not isinstance(v, str) or len(v) > 300:
|
||||||
|
raise ValueError(k + ' inválido')
|
||||||
|
if k == 'url' and v and not re.fullmatch(r'https?://[\w.\-:]+(/v1)?/?', v.strip()):
|
||||||
|
raise ValueError('Endereço deve ser como http://192.168.1.50:1234')
|
||||||
|
novo[k] = v.strip()
|
||||||
|
with self.lock:
|
||||||
|
self.config['local'] = novo
|
||||||
|
gravar_atomico(self.caminho_config, self.config, privado=True)
|
||||||
|
self.avisar()
|
||||||
|
return self.status()
|
||||||
|
|
||||||
|
def testar(self, ia):
|
||||||
|
if ia == 'gemini':
|
||||||
|
if not os.environ.get('GEMINI_API_KEY'):
|
||||||
|
raise ValueError('Sem chave Gemini (Ajustes › Inteligência artificial)')
|
||||||
|
return dict(ok=True, mensagem='Gemini configurado: ' + gemini.MODELO)
|
||||||
|
url = self.config['local']['url'].rstrip('/') if ia == 'local' else (self.s10()[0] or '')
|
||||||
|
if not url:
|
||||||
|
raise ValueError('Configure o endereço primeiro')
|
||||||
|
if url.endswith('/v1'):
|
||||||
|
url = url[:-3]
|
||||||
|
cab = {}
|
||||||
|
if ia == 'local' and self.config['local']['chave']:
|
||||||
|
cab['Authorization'] = 'Bearer ' + self.config['local']['chave']
|
||||||
|
inicio = time.monotonic()
|
||||||
|
try:
|
||||||
|
with urlopen(Request(url + '/v1/models', headers=cab), timeout=8) as resp:
|
||||||
|
modelos = [m.get('id') for m in json.load(resp).get('data', [])]
|
||||||
|
except (URLError, HTTPError, OSError, ValueError):
|
||||||
|
raise ValueError('Não respondeu em ' + url) from None
|
||||||
|
return dict(ok=True, mensagem='Respondeu em %.1f s · modelos: %s' % (time.monotonic() - inicio, ', '.join(map(str, modelos[:5])) or '—'))
|
||||||
|
|
||||||
|
def status(self):
|
||||||
|
with self.lock:
|
||||||
|
ocupado = dict(self.ocupado) if self.ocupado else None
|
||||||
|
return dict(ocupado=ocupado, erro=self.erro, provedores=self.provedores(), criacoes=self.lista(),
|
||||||
|
bases=dict(expressao=[dict(id=k, nome=v['nome'], emoji=v['emoji']) for k, v in EXPRESSOES_BASE.items()],
|
||||||
|
atividade=[dict(id=k, nome=v['nome'], emoji=v['emoji']) for k, v in ATIVIDADES_BASE.items()]))
|
||||||
|
|
||||||
|
def avisar(self):
|
||||||
|
self.publicar('criacao', self.status())
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ HTTP
|
||||||
|
def rotas(self, h, metodo):
|
||||||
|
rota = h.path.split('?', 1)[0]
|
||||||
|
if not rota.startswith('/criacao/'):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
if metodo == 'GET' and rota == '/criacao/status':
|
||||||
|
resultado = self.status()
|
||||||
|
elif metodo == 'POST':
|
||||||
|
d = ler_json(h, 4096)
|
||||||
|
if rota == '/criacao/criar':
|
||||||
|
resultado = self.criar(d.get('tipo'), d.get('pedido'), d.get('ia'), d.get('base') or None)
|
||||||
|
elif rota == '/criacao/tocar':
|
||||||
|
resultado = self.tocar(d.get('id'))
|
||||||
|
elif rota == '/criacao/apagar':
|
||||||
|
resultado = self.apagar(d.get('id'))
|
||||||
|
elif rota == '/criacao/config':
|
||||||
|
resultado = self.configurar(d)
|
||||||
|
elif rota == '/criacao/testar':
|
||||||
|
resultado = self.testar(d.get('ia'))
|
||||||
|
else:
|
||||||
|
raise ValueError('Rota inválida')
|
||||||
|
else:
|
||||||
|
raise ValueError('Método inválido')
|
||||||
|
h.enviar(json.dumps(resultado, ensure_ascii=False))
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
h.enviar(json.dumps(dict(erro=str(e)), ensure_ascii=False), codigo=400)
|
||||||
|
return True
|
||||||
+1
-1
@@ -27,7 +27,7 @@ Set-Location $PSScriptRoot -ErrorAction Stop
|
|||||||
# chaves, vendor/ (modelos já instalados na placa) e o fonte do painel (só web/dist vai).
|
# chaves, vendor/ (modelos já instalados na placa) e o fonte do painel (só web/dist vai).
|
||||||
$arquivos = @(
|
$arquivos = @(
|
||||||
'kiosk-server.py', 'comum.py', 'eventos.py', 'controle_pc.py', 'sinalizacao.py', 'servos.py', 'braco.py',
|
'kiosk-server.py', 'comum.py', 'eventos.py', 'controle_pc.py', 'sinalizacao.py', 'servos.py', 'braco.py',
|
||||||
'cerebro.py', 'conversa.py', 'gemini.py', 'treinamento.py', 'figurinhas.py', 'api_config.py', 'historico.py', 'vida.py',
|
'cerebro.py', 'conversa.py', 'gemini.py', 'treinamento.py', 'figurinhas.py', 'api_config.py', 'historico.py', 'vida.py', 'criacao.py',
|
||||||
'carinha.html', 'camera.html', 'reacoes-render.js', 'rastreador-worker.js', 'rosto-motor.js', 'voz-robo.js', 'version.json',
|
'carinha.html', 'camera.html', 'reacoes-render.js', 'rastreador-worker.js', 'rosto-motor.js', 'voz-robo.js', 'version.json',
|
||||||
'obter-rastreamento.py', 'android/app/assets/qualidade-auto.js'
|
'obter-rastreamento.py', 'android/app/assets/qualidade-auto.js'
|
||||||
)
|
)
|
||||||
|
|||||||
+9
-3
@@ -42,6 +42,7 @@ from treinamento import Treinamento
|
|||||||
from api_config import ConfigAPI
|
from api_config import ConfigAPI
|
||||||
from historico import Historico
|
from historico import Historico
|
||||||
from vida import Vida
|
from vida import Vida
|
||||||
|
from criacao import Criacao
|
||||||
|
|
||||||
TRAVA = threading.Lock()
|
TRAVA = threading.Lock()
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ ESTATICOS = {"/carinha.html", "/camera.html", "/reacoes-render.js", "/rastreador
|
|||||||
|
|
||||||
# Rotas que não exigem token: só o necessário para a página do rosto abrir e o app se achar.
|
# Rotas que não exigem token: só o necessário para a página do rosto abrir e o app se achar.
|
||||||
PUBLICAS_GET = {"/app/health", "/faces", "/gestos", "/config", "/ia/modelo", "/app.apk"}
|
PUBLICAS_GET = {"/app/health", "/faces", "/gestos", "/config", "/ia/modelo", "/app.apk"}
|
||||||
PREFIXOS_API = ("/eventos", "/estado", "/fila", "/rtc/", "/conversa/", "/treinamento/", "/historico/", "/vida/",
|
PREFIXOS_API = ("/eventos", "/estado", "/fila", "/rtc/", "/conversa/", "/treinamento/", "/historico/", "/vida/", "/criacao/",
|
||||||
"/app/", "/braco/", "/servos/", "/ia/", "/api/", "/gesto/", "/face/", "/diag/")
|
"/app/", "/braco/", "/servos/", "/ia/", "/api/", "/gesto/", "/face/", "/diag/")
|
||||||
|
|
||||||
|
|
||||||
@@ -247,7 +248,7 @@ class Handler(SimpleHTTPRequestHandler):
|
|||||||
|
|
||||||
def modulos(self):
|
def modulos(self):
|
||||||
s = self.server
|
s = self.server
|
||||||
return [getattr(s, nome) for nome in ("api_config", "historico", "vida", "treinamento", "conversa", "cerebro",
|
return [getattr(s, nome) for nome in ("api_config", "historico", "vida", "criacao", "treinamento", "conversa", "cerebro",
|
||||||
"braco", "servos", "controle") if hasattr(s, nome)]
|
"braco", "servos", "controle") if hasattr(s, nome)]
|
||||||
|
|
||||||
def estado(self):
|
def estado(self):
|
||||||
@@ -467,7 +468,7 @@ class Servidor(ThreadingHTTPServer):
|
|||||||
|
|
||||||
|
|
||||||
def ligar_eventos(servidor):
|
def ligar_eventos(servidor):
|
||||||
for nome in ("controle", "servos", "cerebro", "conversa", "treinamento", "historico", "vida"):
|
for nome in ("controle", "servos", "cerebro", "conversa", "treinamento", "historico", "vida", "criacao"):
|
||||||
if hasattr(servidor, nome):
|
if hasattr(servidor, nome):
|
||||||
getattr(servidor, nome).publicar = EVENTOS.publicar
|
getattr(servidor, nome).publicar = EVENTOS.publicar
|
||||||
|
|
||||||
@@ -494,6 +495,11 @@ def main():
|
|||||||
servidor.treinamento = Treinamento(Path.cwd() / 'treinamento.db')
|
servidor.treinamento = Treinamento(Path.cwd() / 'treinamento.db')
|
||||||
servidor.historico = Historico(Path.cwd())
|
servidor.historico = Historico(Path.cwd())
|
||||||
servidor.vida = vida = Vida(Path.cwd() / 'vida.json')
|
servidor.vida = vida = Vida(Path.cwd() / 'vida.json')
|
||||||
|
# Estúdio de criação: expressões, atividades e figurinhas por IA (Gemini, IA local ou Qwen do S10)
|
||||||
|
servidor.criacao = Criacao(Path.cwd() / 'criacoes.db', Path.cwd() / 'criacao.json')
|
||||||
|
servidor.criacao.enfileirar = enfileirar
|
||||||
|
servidor.criacao.treinamento = servidor.treinamento
|
||||||
|
servidor.criacao.s10 = lambda: (servidor.cerebro.endpoint, servidor.cerebro.cabecalhos())
|
||||||
|
|
||||||
def registrar_conversa(papel, texto):
|
def registrar_conversa(papel, texto):
|
||||||
servidor.treinamento.registrar(papel, texto)
|
servidor.treinamento.registrar(papel, texto)
|
||||||
|
|||||||
+124
-5
@@ -568,15 +568,16 @@
|
|||||||
skate: { nome: 'Andando de skate', emoji: '🛹', dur: [18, 30] },
|
skate: { nome: 'Andando de skate', emoji: '🛹', dur: [18, 30] },
|
||||||
computador: { nome: 'No computador', emoji: '💻', dur: [25, 45] },
|
computador: { nome: 'No computador', emoji: '💻', dur: [25, 45] },
|
||||||
pescar: { nome: 'Pescando', emoji: '🎣', dur: [30, 50] },
|
pescar: { nome: 'Pescando', emoji: '🎣', dur: [30, 50] },
|
||||||
|
receita: { nome: 'Criada por IA', emoji: '✨', dur: [20, 40] }, // receita do estúdio de criação
|
||||||
};
|
};
|
||||||
const ativ = { nome: null, t0: 0, dur: 0, fim: 0 };
|
const ativ = { nome: null, t0: 0, dur: 0, fim: 0, receita: null };
|
||||||
let palcoTela = null, pctx = null;
|
let palcoTela = null, pctx = null;
|
||||||
const agoraS = () => performance.now() / 1000;
|
const agoraS = () => performance.now() / 1000;
|
||||||
|
|
||||||
function iniciarAtividade(nome, segundos) {
|
function iniciarAtividade(nome, segundos, receita) {
|
||||||
if (!ATIVIDADES[nome]) { pararAtividade(); return false; }
|
if (!ATIVIDADES[nome] || (nome === 'receita' && !receita)) { pararAtividade(); return false; }
|
||||||
const [a, b] = ATIVIDADES[nome].dur;
|
const [a, b] = ATIVIDADES[nome].dur;
|
||||||
ativ.nome = nome; ativ.t0 = agoraS(); ativ.fim = 0;
|
ativ.nome = nome; ativ.t0 = agoraS(); ativ.fim = 0; ativ.receita = receita || null;
|
||||||
ativ.dur = segundos || a + Math.random() * (b - a);
|
ativ.dur = segundos || a + Math.random() * (b - a);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -620,6 +621,19 @@
|
|||||||
const p = pincel(k); info.x = 16 + lado * 5 + p.x * 4; info.y = -1 + p.y * 5;
|
const p = pincel(k); info.x = 16 + lado * 5 + p.x * 4; info.y = -1 + p.y * 5;
|
||||||
} else if (ativ.nome === 'malhar') {
|
} else if (ativ.nome === 'malhar') {
|
||||||
const f = forca(k); info.x = lado * 14; info.y = 6 - f * 12; info.ab = f > 0.85 ? 0.55 : 1;
|
const f = forca(k); info.x = lado * 14; info.y = 6 - f * 12; info.ab = f > 0.85 ? 0.55 : 1;
|
||||||
|
} else if (ativ.nome === 'receita') {
|
||||||
|
const R = ativ.receita || {}, w = Math.PI * 2 * (R.ritmo || 0.8);
|
||||||
|
const o = (R.objetos || [])[0];
|
||||||
|
if (R.olhar === 'cima') { info.x = lado * 12; info.y = -10; }
|
||||||
|
else if (R.olhar === 'baixo') { info.x = lado * 10; info.y = 8; }
|
||||||
|
else if (R.olhar === 'lado') { info.x = lado * 20; info.y = 2; }
|
||||||
|
else if (R.olhar === 'frente') { info.x = 4 * Math.sin(k * 0.7); info.y = 0; }
|
||||||
|
else { // segue o primeiro objeto
|
||||||
|
info.x = lado * 14 + Math.sin(k * w) * 5;
|
||||||
|
info.y = o && o.movimento === 'pular' ? 8 - Math.abs(Math.sin(k * w * 0.5)) * 16 : o && o.movimento === 'subir' ? -6 : 3;
|
||||||
|
}
|
||||||
|
if (R.forma && R.forma !== 'aberto') info.forma = R.forma;
|
||||||
|
if (R.boca && R.boca !== 'neutra') info.boca = R.boca;
|
||||||
} else if (ativ.nome === 'dormir') {
|
} else if (ativ.nome === 'dormir') {
|
||||||
info.forma = 'dormir'; info.boca = 'sono'; info.x = lado * 6; info.y = 4;
|
info.forma = 'dormir'; info.boca = 'sono'; info.x = lado * 6; info.y = 4;
|
||||||
} else if (ativ.nome === 'cafe') {
|
} else if (ativ.nome === 'cafe') {
|
||||||
@@ -765,7 +779,112 @@
|
|||||||
c.globalAlpha = alfa;
|
c.globalAlpha = alfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- objetos que as receitas podem usar (mesmos desenhos das cenas feitas à mão)
|
||||||
|
function desenharObjeto(c, tipo, ox, oy, s, k, cor, giro, j) {
|
||||||
|
c.save(); c.translate(ox, oy); if (giro) c.rotate(giro);
|
||||||
|
c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
|
||||||
|
if (tipo === 'bola') {
|
||||||
|
const r = s * 0.28; c.beginPath(); c.arc(0, 0, r, 0, Math.PI * 2); c.fill(); c.stroke();
|
||||||
|
c.fillStyle = css(cor.map((v) => v * 0.3)); c.beginPath();
|
||||||
|
for (let i = 0; i < 5; i++) { const a = i * 1.2566; c.lineTo(Math.cos(a) * r * 0.38, Math.sin(a) * r * 0.38); }
|
||||||
|
c.closePath(); c.fill();
|
||||||
|
} else if (tipo === 'livro') {
|
||||||
|
const lw = s * 0.62, lh = s * 0.5;
|
||||||
|
for (const lado of [-1, 1]) { c.beginPath(); c.moveTo(0, lh * 0.55); c.lineTo(lado * lw, lh * 0.45); c.lineTo(lado * lw, -lh * 0.45); c.lineTo(0, -lh * 0.35); c.closePath(); c.fill(); c.stroke(); }
|
||||||
|
} else if (tipo === 'xicara') {
|
||||||
|
xicara(c, 0, 0, s, cor); vapor(c, 0, -s * 0.3, s, k, cor);
|
||||||
|
} else if (tipo === 'nota') {
|
||||||
|
c.fillStyle = css(cor); nota(c, 0, 0, s * 0.7);
|
||||||
|
} else if (tipo === 'estrela') {
|
||||||
|
c.fillStyle = css(j % 2 ? AMARELO_ : cor); estrela(c, 0, 0, s * 0.16);
|
||||||
|
} else if (tipo === 'coracao') {
|
||||||
|
c.fillStyle = css(ROSA_); coracao(c, 0, 0, s * 0.22);
|
||||||
|
} else if (tipo === 'zzz') {
|
||||||
|
zzz(c, 0, 0, s, k + j, cor);
|
||||||
|
} else if (tipo === 'chama') {
|
||||||
|
chama(c, 0, s * 0.15, s * (0.35 + 0.08 * Math.sin(k * 17 + j)), s);
|
||||||
|
} else if (tipo === 'bolha') {
|
||||||
|
const r = s * (0.13 + 0.1 * rnd(j, 4)); c.fillStyle = css(cor, 0.1);
|
||||||
|
c.beginPath(); c.arc(0, 0, r, 0, Math.PI * 2); c.fill(); c.stroke();
|
||||||
|
c.strokeStyle = claro(cor, 1); c.beginPath(); c.arc(0, 0, r * 0.65, -2.4, -1.6); c.stroke();
|
||||||
|
} else if (tipo === 'halter') {
|
||||||
|
c.fillRect(-s * 0.25, -s * 0.035, s * 0.5, s * 0.07);
|
||||||
|
for (const lado of [-1, 1]) { caixa(c, lado * s * 0.28 - s * 0.07, -s * 0.17, s * 0.14, s * 0.34, s * 0.04); c.fill(); c.stroke(); }
|
||||||
|
} else if (tipo === 'microfone') {
|
||||||
|
c.fillStyle = escuro(cor); c.beginPath(); capsula(c, 0, 0, -s * 0.1, -s * 0.4, s * 0.07); c.fill(); c.stroke();
|
||||||
|
c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(-s * 0.12, -s * 0.5, s * 0.14, 0, Math.PI * 2); c.fill(); c.stroke();
|
||||||
|
} else if (tipo === 'controle') {
|
||||||
|
caixa(c, -s * 0.42, -s * 0.16, s * 0.84, s * 0.34, s * 0.16); c.fill(); c.stroke();
|
||||||
|
c.fillStyle = css(cor.map((v) => v * 0.3)); c.fillRect(-s * 0.3, -s * 0.02, s * 0.16, s * 0.05); c.fillRect(-s * 0.245, -s * 0.08, s * 0.05, s * 0.17);
|
||||||
|
} else if (tipo === 'pincel') {
|
||||||
|
c.beginPath(); c.moveTo(-s * 0.1, s * 0.08); c.lineTo(s * 0.4, -s * 0.12); c.stroke();
|
||||||
|
c.fillStyle = css([ROSA_, AMARELO_, VERDE_][Math.floor(k) % 3]); c.beginPath(); c.arc(s * 0.43, -s * 0.13, s * 0.07, 0, Math.PI * 2); c.fill();
|
||||||
|
} else if (tipo === 'flor') {
|
||||||
|
c.strokeStyle = css(VERDE_); c.beginPath(); c.moveTo(0, s * 0.5); c.lineTo(0, 0); c.stroke();
|
||||||
|
c.fillStyle = css(ROSA_);
|
||||||
|
for (let i = 0; i < 5; i++) { const a = i * 1.2566 + k * 0.3; c.beginPath(); c.arc(Math.cos(a) * s * 0.14, Math.sin(a) * s * 0.14, s * 0.1, 0, Math.PI * 2); c.fill(); }
|
||||||
|
c.fillStyle = css(AMARELO_); c.beginPath(); c.arc(0, 0, s * 0.08, 0, Math.PI * 2); c.fill();
|
||||||
|
}
|
||||||
|
c.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Onde fica cada objeto da receita e como ele se mexe (pular, girar, subir, orbitar, piscar, balançar). */
|
||||||
|
function objetoReceita(c, o, i, b, x, y, s, k, cor, w) {
|
||||||
|
const q = Math.max(1, Math.min(5, o.quantidade || 1)), alfa = c.globalAlpha;
|
||||||
|
for (let j = 0; j < q; j++) {
|
||||||
|
let ox, oy;
|
||||||
|
if (o.onde === 'mao_direita') { ox = b.mD.x; oy = b.mD.y; }
|
||||||
|
else if (o.onde === 'mao_esquerda') { ox = b.mE.x; oy = b.mE.y; }
|
||||||
|
else if (o.onde === 'maos') { ox = (b.mE.x + b.mD.x) / 2; oy = (b.mE.y + b.mD.y) / 2; }
|
||||||
|
else if (o.onde === 'chao') { ox = x + s * (0.9 + i * 0.5); oy = y - s * 0.3; }
|
||||||
|
else if (o.onde === 'acima') { ox = b.cabeca.x + s * (0.8 + j * 0.5); oy = b.cabeca.y - s * 0.6; }
|
||||||
|
else { const a = (j / q) * Math.PI * 2 + i; ox = b.quadril.x + Math.cos(a) * s * 1.5; oy = b.quadril.y - s * 1.2 + Math.sin(a) * s * 1.1; }
|
||||||
|
if (q > 1 && (o.onde === 'mao_direita' || o.onde === 'mao_esquerda' || o.onde === 'maos' || o.onde === 'chao')) ox += (j - (q - 1) / 2) * s * 0.4;
|
||||||
|
let giro = 0;
|
||||||
|
c.globalAlpha = alfa;
|
||||||
|
if (o.movimento === 'pular') oy -= Math.abs(Math.sin(k * w * 0.5 + j)) * s * 1.6;
|
||||||
|
else if (o.movimento === 'girar') giro = k * 4 + j;
|
||||||
|
else if (o.movimento === 'balancar') ox += Math.sin(k * w + j) * s * 0.3;
|
||||||
|
else if (o.movimento === 'piscar') c.globalAlpha = alfa * (0.25 + 0.75 * Math.abs(Math.sin(k * 3 + j * 1.7)));
|
||||||
|
else if (o.movimento === 'subir') {
|
||||||
|
const f = (k * 0.35 + j / q) % 1;
|
||||||
|
ox += f * s * 0.8 + Math.sin(k * 2 + j) * s * 0.12; oy -= f * s * 1.8;
|
||||||
|
c.globalAlpha = alfa * Math.min(1, f / 0.12) * (1 - f);
|
||||||
|
} else if (o.movimento === 'orbitar') {
|
||||||
|
const a = k * 1.6 + (j / q) * Math.PI * 2;
|
||||||
|
ox = b.quadril.x + Math.cos(a) * s * 1.6; oy = b.quadril.y - s * 1.3 + Math.sin(a) * s * 0.9;
|
||||||
|
}
|
||||||
|
desenharObjeto(c, o.tipo, ox, oy, s, k, cor, giro, j);
|
||||||
|
}
|
||||||
|
c.globalAlpha = alfa;
|
||||||
|
}
|
||||||
|
|
||||||
const CENAS = {
|
const CENAS = {
|
||||||
|
// Atividade criada por IA: uma receita (criacao.py) montada com as peças que já existem aqui:
|
||||||
|
// o bonequinho, poses de braços/pernas e os objetos das outras cenas.
|
||||||
|
receita(c, x, y, s, k, cor) {
|
||||||
|
const R = ativ.receita || {};
|
||||||
|
const w = Math.PI * 2 * (R.ritmo || 0.8), onda = Math.sin(k * w);
|
||||||
|
const f1 = (onda + 1) / 2, f2 = R.alternados ? 1 - f1 : f1;
|
||||||
|
const rad = (g) => (g * Math.PI) / 180;
|
||||||
|
const ang = (b, f) => rad((b ? b.de : 20) + ((b ? b.ate : 40) - (b ? b.de : 20)) * f);
|
||||||
|
const pulo = (R.pulo || 0) * Math.max(0, onda), sobe = pulo * s * 0.5;
|
||||||
|
const balanco = Math.sin(k * w * 0.5) * (R.balanco || 0);
|
||||||
|
const cot = (R.cotovelos || 0) * 1.4;
|
||||||
|
let pE = [-0.08, 0], pD = [0.08, 0];
|
||||||
|
if (R.pernas === 'marchando') { pE = [-0.08 - 0.25 * f1, 0.45 * f1]; pD = [0.08 + 0.25 * (1 - f1), -0.45 * (1 - f1)]; }
|
||||||
|
else if (R.pernas === 'chutando') { const chute = Math.pow(Math.max(0, onda), 2); pD = [0.08 + chute * 0.9, -chute * 0.6]; }
|
||||||
|
else if (R.pernas === 'dancando') { pE = [-0.1 - f1 * 0.25, f1 * 0.4]; pD = [0.1 + (1 - f1) * 0.25, -(1 - f1) * 0.4]; }
|
||||||
|
else if (R.pernas === 'pulando') { pE = [-0.06, pulo * 0.3]; pD = [0.06, -pulo * 0.3]; }
|
||||||
|
const sentado = R.postura === 'sentado';
|
||||||
|
const b = boneco(c, x + balanco * s * 0.3, y - sobe, s, Object.assign(sentado ? Object.assign({}, SENTADO) : { pE, pD }, {
|
||||||
|
inclina: balanco * 0.25, cabeca: -balanco * 0.3, fechados: R.forma === 'dormir',
|
||||||
|
olhoY: R.olhar === 'cima' ? -1 : R.olhar === 'baixo' ? 0.8 : 0, olhoX: R.olhar === 'lado' ? 1 : 0,
|
||||||
|
bE: [-ang(R.braco_esquerdo, f1), cot], bD: [ang(R.braco_direito, f2), -cot],
|
||||||
|
}), cor, k);
|
||||||
|
(R.objetos || []).forEach((o, i) => objetoReceita(c, o, i, b, x, y, s, k, cor, w));
|
||||||
|
},
|
||||||
|
|
||||||
ler(c, x, y, s, k, cor) {
|
ler(c, x, y, s, k, cor) {
|
||||||
const b = boneco(c, x, y, s, {
|
const b = boneco(c, x, y, s, {
|
||||||
sentado: true, pE: [-1.35, 1.35], pD: [1.35, -1.35], cabeca: -0.08, olhoY: 0.8,
|
sentado: true, pE: [-1.35, 1.35], pD: [1.35, -1.35], cabeca: -0.08, olhoY: 0.8,
|
||||||
@@ -1268,7 +1387,7 @@
|
|||||||
maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } },
|
maos: (modo) => { if (DUR_MAOS[modo]) { maos.modo = modo; maos.t0 = performance.now() / 1000; } },
|
||||||
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS),
|
faixa: definirFaixa, estilos: Object.keys(ESTILOS), formatos: Object.keys(FORMATOS),
|
||||||
/** Atividades (momentos dele): iniciar/parar, para onde olhar e a camada da cena. */
|
/** Atividades (momentos dele): iniciar/parar, para onde olhar e a camada da cena. */
|
||||||
atividade: (nome, segundos) => (nome ? iniciarAtividade(nome, segundos) : (pararAtividade(), false)),
|
atividade: (nome, segundos, receita) => (nome ? iniciarAtividade(nome, segundos, receita) : (pararAtividade(), false)),
|
||||||
atividadeAtual: () => (ativ.nome && !ativ.fim ? ativ.nome : null),
|
atividadeAtual: () => (ativ.nome && !ativ.fim ? ativ.nome : null),
|
||||||
olharAtividade, palco: desenharPalco,
|
olharAtividade, palco: desenharPalco,
|
||||||
atividades: Object.fromEntries(Object.entries(ATIVIDADES).map(([k, v]) => [k, { nome: v.nome, emoji: v.emoji }])) };
|
atividades: Object.fromEntries(Object.entries(ATIVIDADES).map(([k, v]) => [k, { nome: v.nome, emoji: v.emoji }])) };
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
"""Estúdio de criação: receitas validadas, prompt com exemplos/base, IA local (servidor falso) e tocar."""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
import criacao as mod # noqa: E402
|
||||||
|
from criacao import ATIVIDADES_BASE, EXPRESSOES_BASE, Criacao, montar_prompt, validar_atividade, validar_expressao # noqa: E402
|
||||||
|
|
||||||
|
RESPOSTAS = {} # o servidor falso devolve o que o teste colocar aqui
|
||||||
|
PEDIDOS = []
|
||||||
|
|
||||||
|
|
||||||
|
class IaFalsa(BaseHTTPRequestHandler):
|
||||||
|
def do_POST(self):
|
||||||
|
corpo = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
|
||||||
|
PEDIDOS.append(dict(corpo=corpo, auth=self.headers.get('Authorization')))
|
||||||
|
tipo = 'expressao' if 'passos' in corpo['response_format']['json_schema']['schema']['properties'] else 'atividade'
|
||||||
|
texto = '<think>pensando...</think>\n```json\n' + json.dumps(RESPOSTAS[tipo]) + '\n```'
|
||||||
|
dados = json.dumps(dict(choices=[dict(message=dict(content=texto))])).encode()
|
||||||
|
self.send_response(200); self.send_header('Content-Type', 'application/json'); self.send_header('Content-Length', str(len(dados)))
|
||||||
|
self.end_headers(); self.wfile.write(dados)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
dados = json.dumps(dict(data=[dict(id='qwen-teste')])).encode()
|
||||||
|
self.send_response(200); self.send_header('Content-Length', str(len(dados))); self.end_headers(); self.wfile.write(dados)
|
||||||
|
|
||||||
|
def log_message(self, *a):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CriacaoTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.c = Criacao(Path(self.tmp.name) / 'c.db', Path(self.tmp.name) / 'c.json')
|
||||||
|
self.ordens = []
|
||||||
|
self.c.enfileirar = self.ordens.append
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.tmp.cleanup()
|
||||||
|
|
||||||
|
def esperar(self):
|
||||||
|
for _ in range(100):
|
||||||
|
if not self.c.ocupado:
|
||||||
|
return
|
||||||
|
time.sleep(0.05)
|
||||||
|
self.fail('criação não terminou')
|
||||||
|
|
||||||
|
def test_valida_e_limita_expressao(self):
|
||||||
|
r = validar_expressao(dict(nome='x' * 80, emoji='<script>', maos='voar', espiral='sim', passos=[
|
||||||
|
dict(forma='feliz', boca='sorriso', cor='roxo', dx=99, dy=-99, esc=5, ab=-1, duracao=50),
|
||||||
|
dict(forma='inventada', boca='sorriso', cor='rosa', dx=1, dy=1, esc=1, ab=1, duracao=5)]))
|
||||||
|
self.assertEqual(len(r['nome']), 30)
|
||||||
|
self.assertEqual(r['emoji'], '✨')
|
||||||
|
self.assertEqual((r['maos'], r['espiral']), ('nenhuma', False))
|
||||||
|
p = r['passos'][0]
|
||||||
|
self.assertEqual((p['cor'], p['dx'], p['dy'], p['esc'], p['ab']), ('escolhida', 24, -12, 1.3, 0))
|
||||||
|
self.assertEqual(r['passos'][1]['forma'], 'aberto')
|
||||||
|
self.assertLessEqual(sum(x['duracao'] for x in r['passos'][:2]), 6.01) # encolheu para 6 s
|
||||||
|
self.assertEqual(r['passos'][-1]['forma'], 'aberto') # volta ao normal
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
validar_expressao(dict(passos=[]))
|
||||||
|
for base in EXPRESSOES_BASE.values(): # os exemplos são válidos
|
||||||
|
self.assertEqual(len(validar_expressao(base)['passos']), len(base['passos']))
|
||||||
|
|
||||||
|
def test_valida_atividade(self):
|
||||||
|
r = validar_atividade(dict(nome='Malabarismo', emoji='🤹', postura='voando', ritmo=99, braco_esquerdo=dict(de=-500, ate=900),
|
||||||
|
objetos=[dict(tipo='bola', onde='maos', movimento='pular', quantidade=9), dict(tipo='foguete')] * 3))
|
||||||
|
self.assertEqual(r['postura'], 'em_pe')
|
||||||
|
self.assertEqual(r['ritmo'], 3)
|
||||||
|
self.assertEqual(r['braco_esquerdo'], dict(de=-30, ate=180))
|
||||||
|
self.assertEqual(len(r['objetos']), 3) # o foguete não existe: sai
|
||||||
|
self.assertEqual(r['objetos'][0]['quantidade'], 5)
|
||||||
|
for base in ATIVIDADES_BASE.values():
|
||||||
|
validar_atividade(base)
|
||||||
|
|
||||||
|
def test_prompt_reaproveita_o_que_existe(self):
|
||||||
|
p = montar_prompt('expressao', 'desconfiado', EXPRESSOES_BASE['timido'])
|
||||||
|
self.assertIn('Exemplos reais que já existem', p)
|
||||||
|
self.assertIn('Comemorar', p) # exemplo real
|
||||||
|
self.assertIn('Parta desta receita', p)
|
||||||
|
self.assertIn('Tímido', p) # a base escolhida
|
||||||
|
self.assertIn('"desconfiado"', p)
|
||||||
|
a = montar_prompt('atividade', 'malabarismo')
|
||||||
|
self.assertIn('halter', a)
|
||||||
|
self.assertNotIn('Parta desta receita', a)
|
||||||
|
|
||||||
|
def test_ia_local_cria_e_toca(self):
|
||||||
|
srv = ThreadingHTTPServer(('127.0.0.1', 0), IaFalsa)
|
||||||
|
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||||
|
try:
|
||||||
|
self.c.configurar(dict(local=dict(url='http://127.0.0.1:%d' % srv.server_port, modelo='qwen-teste', chave='segredo')))
|
||||||
|
self.assertTrue(self.c.provedores()['local']['pronto'])
|
||||||
|
self.assertIn('qwen-teste', self.c.testar('local')['mensagem'])
|
||||||
|
RESPOSTAS['expressao'] = dict(nome='Desconfiado', emoji='🤨', maos='nenhuma', espiral=False, passos=[
|
||||||
|
dict(forma='bravo', boca='neutra', cor='escolhida', dx=-12, dy=2, esc=0.95, ab=0.6, duracao=0.4),
|
||||||
|
dict(forma='bravo', boca='neutra', cor='escolhida', dx=12, dy=2, esc=0.95, ab=0.6, duracao=0.8)])
|
||||||
|
self.c.criar('expressao', 'desconfiado', 'local', base='timido')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.c.criar('expressao', 'outro', 'local') # um de cada vez
|
||||||
|
self.esperar()
|
||||||
|
self.assertEqual(self.c.erro, '')
|
||||||
|
self.assertEqual(PEDIDOS[-1]['auth'], 'Bearer segredo')
|
||||||
|
self.assertIn('Tímido', PEDIDOS[-1]['corpo']['messages'][1]['content']) # a base foi junto
|
||||||
|
item = self.c.lista()[0]
|
||||||
|
self.assertEqual((item['tipo'], item['nome'], item['ia']), ('expressao', 'Desconfiado', 'local'))
|
||||||
|
self.assertTrue(self.ordens[-1].startswith('expressao_criada {')) # tocou na hora
|
||||||
|
RESPOSTAS['atividade'] = dict(ATIVIDADES_BASE['malhar'], nome='Malabarismo', emoji='🤹')
|
||||||
|
self.c.criar('atividade', 'malabarismo', 'local', base='criado:%d' % item['id']) # base de outro tipo: ignora
|
||||||
|
self.esperar()
|
||||||
|
self.assertTrue(self.ordens[-1].startswith('atividade_criada {'))
|
||||||
|
self.assertEqual(json.loads(self.ordens[-1][17:])['nome'], 'Malabarismo')
|
||||||
|
self.c.apagar(item['id'])
|
||||||
|
self.assertEqual([i['nome'] for i in self.c.lista()], ['Malabarismo'])
|
||||||
|
finally:
|
||||||
|
srv.shutdown()
|
||||||
|
|
||||||
|
def test_figurinha_vai_para_a_biblioteca_de_reacoes(self):
|
||||||
|
class TreinoFalso:
|
||||||
|
lock = threading.RLock()
|
||||||
|
expressao = None
|
||||||
|
self.c.treinamento = TreinoFalso()
|
||||||
|
desenho = dict(nome='Sol', local='olhos', formas=[dict(caminho='M 10 10 L 90 10 L 50 90 Z', cor='#ffcc00')])
|
||||||
|
mod.os.environ['GEMINI_API_KEY'] = 'teste'
|
||||||
|
original = mod.gemini.gerar_json
|
||||||
|
mod.gemini.gerar_json = lambda payload: dict(desenho, emoji='☀️')
|
||||||
|
try:
|
||||||
|
self.c.criar('figurinha', 'um sol', 'gemini')
|
||||||
|
self.esperar()
|
||||||
|
finally:
|
||||||
|
mod.gemini.gerar_json = original
|
||||||
|
del mod.os.environ['GEMINI_API_KEY']
|
||||||
|
self.assertEqual(self.c.erro, '')
|
||||||
|
self.assertEqual(self.c.lista()[0]['emoji'], '☀️')
|
||||||
|
self.assertEqual(self.c.treinamento.expressao['receita']['figurinha']['nome'], 'Sol')
|
||||||
|
|
||||||
|
def test_erros_claros(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.c.criar('expressao', 'x', 'local') # sem endereço
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.c.configurar(dict(local=dict(url='ftp://nada')))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.c.criar('robo', 'x', 'gemini')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
+5
-3
@@ -7,7 +7,7 @@
|
|||||||
import Paleta from './lib/componentes/Paleta.svelte';
|
import Paleta from './lib/componentes/Paleta.svelte';
|
||||||
import Selo from './lib/componentes/Selo.svelte';
|
import Selo from './lib/componentes/Selo.svelte';
|
||||||
import Token from './lib/componentes/Token.svelte';
|
import Token from './lib/componentes/Token.svelte';
|
||||||
import { favoritos, gesto, pararTudo } from './lib/comandos.svelte';
|
import { favoritos, pararTudo, tocarFavorito } from './lib/comandos.svelte';
|
||||||
import { conectar, estado } from './lib/estado.svelte';
|
import { conectar, estado } from './lib/estado.svelte';
|
||||||
import { paleta } from './lib/paleta.svelte';
|
import { paleta } from './lib/paleta.svelte';
|
||||||
import { nav, ROTAS, SECUNDARIAS, tituloDa, type Rota } from './lib/rotas.svelte';
|
import { nav, ROTAS, SECUNDARIAS, tituloDa, type Rota } from './lib/rotas.svelte';
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
import Rosto from './telas/Rosto.svelte';
|
import Rosto from './telas/Rosto.svelte';
|
||||||
import Eventos from './telas/Eventos.svelte';
|
import Eventos from './telas/Eventos.svelte';
|
||||||
import Guia from './telas/Guia.svelte';
|
import Guia from './telas/Guia.svelte';
|
||||||
|
import Criar from './telas/Criar.svelte';
|
||||||
import Ajuda from './lib/componentes/Ajuda.svelte';
|
import Ajuda from './lib/componentes/Ajuda.svelte';
|
||||||
import { TOPICO_DA_TELA } from './lib/guia';
|
import { TOPICO_DA_TELA } from './lib/guia';
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
ajustes: Ajustes,
|
ajustes: Ajustes,
|
||||||
rosto: Rosto,
|
rosto: Rosto,
|
||||||
guia: Guia,
|
guia: Guia,
|
||||||
|
criar: Criar,
|
||||||
};
|
};
|
||||||
const ICONES: Record<string, Component<any>> = {
|
const ICONES: Record<string, Component<any>> = {
|
||||||
'ao-vivo': Video,
|
'ao-vivo': Video,
|
||||||
@@ -93,7 +95,7 @@
|
|||||||
}
|
}
|
||||||
if (/^[1-9]$/.test(e.key)) {
|
if (/^[1-9]$/.test(e.key)) {
|
||||||
const g = favoritos.lista[Number(e.key) - 1];
|
const g = favoritos.lista[Number(e.key) - 1];
|
||||||
if (g) gesto(g);
|
if (g) tocarFavorito(g);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +181,7 @@
|
|||||||
<Tela />
|
<Tela />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{#if nav.atual !== 'ao-vivo' && nav.atual !== 'reacoes' && nav.atual !== 'rosto' && nav.atual !== 'guia'}
|
{#if nav.atual !== 'ao-vivo' && nav.atual !== 'reacoes' && nav.atual !== 'rosto' && nav.atual !== 'guia' && nav.atual !== 'criar'}
|
||||||
<MiniRosto />
|
<MiniRosto />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ class Favoritos {
|
|||||||
export const favoritos = new Favoritos();
|
export const favoritos = new Favoritos();
|
||||||
|
|
||||||
export const gesto = (nome: string) => tentar(() => postar('/comando', { tipo: 'gesto', valor: nome }));
|
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 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 cor = (rgb: [number, number, number]) => tentar(() => postar('/comando', { tipo: 'cor', valor: rgb }));
|
||||||
export const formato = (nome: string) => tentar(() => postar('/comando', { tipo: 'formato', valor: nome }));
|
export const formato = (nome: string) => tentar(() => postar('/comando', { tipo: 'formato', valor: nome }));
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Star } from '@lucide/svelte';
|
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();
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="gestos" class:completo>
|
<div class="gestos" class:completo>
|
||||||
{#each lista as g, i (g)}
|
{#each lista as g, i (g)}
|
||||||
{@const info = GESTOS_INFO[g]}
|
{@const info = infoDe(g)!}
|
||||||
<div class="item">
|
<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="emoji" aria-hidden="true">{info.emoji}</span>
|
||||||
<span class="nome">{info.nome}</span>
|
<span class="nome">{info.nome}</span>
|
||||||
{#if !completo && i < 9}<kbd>{i + 1}</kbd>{/if}
|
{#if !completo && i < 9}<kbd>{i + 1}</kbd>{/if}
|
||||||
@@ -71,6 +82,13 @@
|
|||||||
color: var(--texto-2);
|
color: var(--texto-2);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.gesto.criado::after {
|
||||||
|
content: '✨';
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
top: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
kbd {
|
kbd {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4px;
|
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> = {
|
const tratadores: Record<string, (d: any) => void> = {
|
||||||
expressao: (d: { ordem: string }) => {
|
expressao: (d: { ordem: string }) => {
|
||||||
// 'humor x' é o modo vida avisando o celular; aparece no cartão Vida, não no feed
|
// '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) => {
|
telemetria: (d: Telemetria) => {
|
||||||
estado.app = d;
|
estado.app = d;
|
||||||
@@ -177,7 +183,7 @@ function abrirEventos() {
|
|||||||
if (estado.conexao !== 'token') estado.conexao = 'caiu';
|
if (estado.conexao !== 'token') estado.conexao = 'caiu';
|
||||||
};
|
};
|
||||||
for (const tipo of ['expressao', 'telemetria', 'app_config', 'aparencia', 'motores', 'estop', 'conversa', 'ia',
|
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) => {
|
fonte.addEventListener(tipo, (e) => {
|
||||||
const dados = JSON.parse((e as MessageEvent).data);
|
const dados = JSON.parse((e as MessageEvent).data);
|
||||||
tratadores[tipo]?.(dados);
|
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.' },
|
{ 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: {
|
personalidade: {
|
||||||
titulo: 'IA local (Qwen)',
|
titulo: 'IA local (Qwen)',
|
||||||
emoji: '🧠',
|
emoji: '🧠',
|
||||||
@@ -302,7 +319,7 @@ export const TOPICOS: Record<string, TopicoGuia> = {
|
|||||||
|
|
||||||
/** Ordem do tutorial (página Guia). */
|
/** Ordem do tutorial (página Guia). */
|
||||||
export const ORDEM_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',
|
'personalidade', 'acontecimentos', 'eventos', 'presenca', 'tela', 'sensores', 'sensibilidade',
|
||||||
'reacoes', 'conversa', 'movimento', 'atalhos',
|
'reacoes', 'conversa', 'movimento', 'atalhos',
|
||||||
];
|
];
|
||||||
@@ -310,5 +327,5 @@ export const ORDEM_GUIA = [
|
|||||||
/** Tópico de cada tela, para o ⓘ do topo. */
|
/** Tópico de cada tela, para o ⓘ do topo. */
|
||||||
export const TOPICO_DA_TELA: Record<string, string> = {
|
export const TOPICO_DA_TELA: Record<string, string> = {
|
||||||
'ao-vivo': 'inicio', eventos: 'eventos', conversa: 'conversa', reacoes: 'reacoes',
|
'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(() => [
|
registrarNaPaleta(() => [
|
||||||
...ROTAS.map((r) => ({ id: 'ir-' + r.id, grupo: 'Ir para', titulo: r.titulo, fazer: () => nav.ir(r.id) })),
|
...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-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]) => ({
|
...Object.entries(ATIVIDADES_INFO).map(([id, a]) => ({
|
||||||
id: 'atividade-' + id, grupo: 'Atividades', titulo: a.emoji + ' ' + a.nome, fazer: () => atividade(id),
|
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 "/". */
|
/** 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 }[] = [
|
export const ROTAS: { id: Rota; titulo: string; atalho: string }[] = [
|
||||||
{ id: 'ao-vivo', titulo: 'Ao vivo', atalho: 'g v' },
|
{ 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 }[] = [
|
export const SECUNDARIAS: { id: Rota; titulo: string; volta: Rota }[] = [
|
||||||
{ id: 'rosto', titulo: 'Rosto', volta: 'ao-vivo' },
|
{ id: 'rosto', titulo: 'Rosto', volta: 'ao-vivo' },
|
||||||
{ id: 'guia', titulo: 'Guia', volta: 'ajustes' },
|
{ 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 ?? '';
|
export const tituloDa = (id: Rota) => [...ROTAS, ...SECUNDARIAS].find((r) => r.id === id)?.titulo ?? '';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
Brain, Camera, Download, Eye, Footprints, Gamepad2, Hand, History, Palette, PenLine, RefreshCw, Repeat, ScanFace, Settings2, Smile, Sparkles, Video,
|
Brain, Camera, Download, Eye, Footprints, Gamepad2, Hand, History, Palette, PenLine, RefreshCw, Repeat, ScanFace, Settings2, Smile, Sparkles, Video, Wand2,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { postar } from '../lib/api';
|
import { postar } from '../lib/api';
|
||||||
@@ -184,6 +184,7 @@ const pirAtivo = $derived(
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Linha>
|
</Linha>
|
||||||
|
<Linha icone={Wand2} cor="#af52de" rotulo="Criar com IA" detalhe="Expressões, atividades e figurinhas novas" href="#/criar" />
|
||||||
<Linha icone={Sparkles} cor="#ff2d55" rotulo="Todos os gestos e reações" href="#/reacoes" />
|
<Linha icone={Sparkles} cor="#ff2d55" rotulo="Todos os gestos e reações" href="#/reacoes" />
|
||||||
</Grupo>
|
</Grupo>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,377 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Cpu, Settings2, Sparkles, Star, Trash2, Wand2 } from '@lucide/svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { favoritos } from '../lib/comandos.svelte';
|
||||||
|
import Card from '../lib/componentes/Card.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';
|
||||||
|
import { estado } from '../lib/estado.svelte';
|
||||||
|
|
||||||
|
// Estúdio: escolhe o que criar e com qual IA, escreve o pedido (pode partir de algo que já existe)
|
||||||
|
// e as criações ficam numa grade igual à dos favoritos.
|
||||||
|
onMount(() => criacao.carregar());
|
||||||
|
const st = $derived(criacao.status);
|
||||||
|
|
||||||
|
let tipo = $state<TipoCriacao>('expressao');
|
||||||
|
let ia = $state<IaCriacao>('gemini');
|
||||||
|
let pedido = $state('');
|
||||||
|
let base = $state('');
|
||||||
|
let filtro = $state<'todos' | TipoCriacao>('todos');
|
||||||
|
let configAberta = $state(false);
|
||||||
|
let url = $state('');
|
||||||
|
let modelo = $state('');
|
||||||
|
let chave = $state('');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
// base só vale para o tipo atual
|
||||||
|
tipo;
|
||||||
|
base = '';
|
||||||
|
});
|
||||||
|
$effect(() => {
|
||||||
|
const l = st?.provedores.local;
|
||||||
|
if (l && !url && !modelo) {
|
||||||
|
url = l.url ?? '';
|
||||||
|
modelo = l.modelo ?? '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const IAS: { id: IaCriacao; icone: string }[] = [
|
||||||
|
{ id: 'gemini', icone: '✨' },
|
||||||
|
{ id: 'local', icone: '🖥️' },
|
||||||
|
{ id: 's10', icone: '📱' },
|
||||||
|
];
|
||||||
|
const tipoInfo = $derived(TIPOS.find((t) => t.id === tipo)!);
|
||||||
|
const bases = $derived(
|
||||||
|
tipo === 'figurinha' || !st
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
...st.bases[tipo].map((b) => ({ id: b.id, rotulo: `${b.emoji} ${b.nome}` })),
|
||||||
|
...st.criacoes.filter((c) => c.tipo === tipo).map((c) => ({ id: `criado:${c.id}`, rotulo: `${c.emoji} ${c.nome} (criado)` })),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
const lista = $derived((st?.criacoes ?? []).filter((c) => filtro === 'todos' || c.tipo === filtro));
|
||||||
|
const ocupado = $derived(st?.ocupado ?? null);
|
||||||
|
const segundos = $derived(ocupado ? Math.max(0, Math.round(estado.agora / 1000 - ocupado.desde)) : 0);
|
||||||
|
const provedor = $derived(st?.provedores[ia]);
|
||||||
|
|
||||||
|
async function criar(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (await criacao.criar(tipo, pedido.trim(), ia, base)) pedido = '';
|
||||||
|
}
|
||||||
|
async function salvarLocal(e: SubmitEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
const dados: { url: string; modelo: string; chave?: string } = { url: url.trim(), modelo: modelo.trim() };
|
||||||
|
if (chave.trim()) dados.chave = chave.trim();
|
||||||
|
await criacao.configurar(dados);
|
||||||
|
chave = '';
|
||||||
|
}
|
||||||
|
const nomeTipo = (t: TipoCriacao) => TIPOS.find((x) => x.id === t)?.nome ?? t;
|
||||||
|
const podeFavoritar = (t: TipoCriacao) => t !== 'figurinha';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="criar">
|
||||||
|
<div class="topo">
|
||||||
|
<Card titulo="Criar com IA" ajuda="criar" descricao="A IA monta a criação só com peças que o Carinha já sabe animar, usando exemplos do que já existe.">
|
||||||
|
<form class="form" onsubmit={criar}>
|
||||||
|
<div class="campo">
|
||||||
|
<span>O que criar</span>
|
||||||
|
<div class="segmentado" role="radiogroup" aria-label="O que criar">
|
||||||
|
{#each TIPOS as t (t.id)}
|
||||||
|
<button type="button" role="radio" aria-checked={tipo === t.id} onclick={() => (tipo = t.id)}>
|
||||||
|
<span aria-hidden="true">{t.emoji}</span> {t.nome}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="campo">
|
||||||
|
<span>Qual IA</span>
|
||||||
|
<div class="ias" role="radiogroup" aria-label="Qual IA">
|
||||||
|
{#each IAS as x (x.id)}
|
||||||
|
{@const p = st?.provedores[x.id]}
|
||||||
|
<button type="button" role="radio" class="ia" aria-checked={ia === x.id} class:indisponivel={p && !p.pronto} onclick={() => (ia = x.id)}>
|
||||||
|
<span class="ia-nome"><span aria-hidden="true">{x.icone}</span> {p?.nome ?? x.id}</span>
|
||||||
|
<span class="ia-detalhe">{p ? (p.pronto ? p.detalhe : p.motivo) : '…'}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="campo">
|
||||||
|
<span>Pedido</span>
|
||||||
|
<textarea bind:value={pedido} maxlength="600" placeholder={tipoInfo.exemplo} required></textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{#if bases.length}
|
||||||
|
<label class="campo">
|
||||||
|
<span>Basear em (opcional)</span>
|
||||||
|
<select bind:value={base}>
|
||||||
|
<option value="">Nada: criar do zero (com exemplos do que existe)</option>
|
||||||
|
{#each bases as b (b.id)}<option value={b.id}>{b.rotulo}</option>{/each}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="linha acoes">
|
||||||
|
<button class="btn primario" type="submit" disabled={!!ocupado || !pedido.trim() || !provedor?.pronto}>
|
||||||
|
<Wand2 size={16} /> {ocupado ? 'Criando…' : `Criar ${nomeTipo(tipo).toLowerCase()}`}
|
||||||
|
</button>
|
||||||
|
<button class="btn fantasma pequeno" type="button" onclick={() => (configAberta = !configAberta)}>
|
||||||
|
<Settings2 size={14} /> IA local
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if ocupado}
|
||||||
|
<Selo tom="aviso">
|
||||||
|
{st?.provedores[ocupado.ia]?.nome} criando {nomeTipo(ocupado.tipo).toLowerCase()}… {segundos}s
|
||||||
|
{ocupado.ia !== 'gemini' ? '(IA local pode levar alguns minutos)' : ''}
|
||||||
|
</Selo>
|
||||||
|
{:else if st?.erro}
|
||||||
|
<p class="erro pequeno-texto">{st.erro}</p>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#if configAberta}
|
||||||
|
<form class="form local" onsubmit={salvarLocal}>
|
||||||
|
<p class="fraco pequeno-texto">
|
||||||
|
Servidor compatível com OpenAI na sua rede (LM Studio com "Serve on Local Network" ou llama-server), por exemplo o ROG Ally.
|
||||||
|
</p>
|
||||||
|
<label class="campo"><span>Endereço</span><input type="text" bind:value={url} placeholder="http://192.168.1.50:1234" /></label>
|
||||||
|
<label class="campo"><span>Modelo</span><input type="text" bind:value={modelo} placeholder="qwen3-coder-30b-a3b-instruct" /></label>
|
||||||
|
<label class="campo">
|
||||||
|
<span>Senha da API {st?.provedores.local.chave ? '(deixe vazio para manter)' : '(opcional)'}</span>
|
||||||
|
<input type="password" bind:value={chave} autocomplete="off" />
|
||||||
|
</label>
|
||||||
|
<div class="linha">
|
||||||
|
<button class="btn pequeno primario" type="submit">Salvar</button>
|
||||||
|
<button class="btn pequeno" type="button" onclick={() => criacao.testar('local')} disabled={!st?.provedores.local.url}><Cpu size={14} /> Testar</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div class="previa"><RostoPrevia titulo="Prévia das criações" espelharSono={false} /></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card titulo="Criações" descricao="Toque para mostrar no celular. A estrela fixa expressões e atividades nos favoritos do Ao vivo.">
|
||||||
|
{#snippet acoes()}
|
||||||
|
<div class="segmentado pequeno" role="radiogroup" aria-label="Filtrar">
|
||||||
|
<button role="radio" aria-checked={filtro === 'todos'} onclick={() => (filtro = 'todos')}>Todas</button>
|
||||||
|
{#each TIPOS as t (t.id)}
|
||||||
|
<button role="radio" aria-checked={filtro === t.id} onclick={() => (filtro = t.id)}>{t.emoji}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
{#if !st}
|
||||||
|
<p class="fraco">Carregando…</p>
|
||||||
|
{:else if !lista.length}
|
||||||
|
<p class="fraco">Nada criado ainda. Escreva um pedido acima. <Sparkles size={14} /></p>
|
||||||
|
{:else}
|
||||||
|
<div class="grade">
|
||||||
|
{#each lista as c (c.id)}
|
||||||
|
{@const fav = favoritos.lista.includes(`criado:${c.id}`)}
|
||||||
|
<div class="item">
|
||||||
|
<button class="cartao" onclick={() => criacao.tocar(c.id)} title={c.pedido}>
|
||||||
|
<span class="emoji" aria-hidden="true">{c.emoji}</span>
|
||||||
|
<span class="nome">{c.nome}</span>
|
||||||
|
<span class="meta">{nomeTipo(c.tipo)} · {st.provedores[c.ia]?.nome ?? c.ia}</span>
|
||||||
|
</button>
|
||||||
|
{#if podeFavoritar(c.tipo)}
|
||||||
|
<button
|
||||||
|
class="canto estrela"
|
||||||
|
class:ativa={fav}
|
||||||
|
aria-pressed={fav}
|
||||||
|
aria-label={fav ? `Tirar ${c.nome} dos favoritos` : `Fixar ${c.nome} nos favoritos`}
|
||||||
|
onclick={() => favoritos.alternar(`criado:${c.id}`)}><Star size={14} /></button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<button class="canto lixo" aria-label={`Apagar ${c.nome}`} onclick={() => criacao.apagar(c.id)}><Trash2 size={13} /></button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.criar {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--e-4);
|
||||||
|
}
|
||||||
|
.topo {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--e-4);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.topo {
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
.previa {
|
||||||
|
position: sticky;
|
||||||
|
top: calc(var(--topo-altura) + var(--e-4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.previa {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
border-radius: var(--raio-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
border: 1px solid var(--borda);
|
||||||
|
}
|
||||||
|
.form {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--e-3);
|
||||||
|
}
|
||||||
|
.form.local {
|
||||||
|
margin-top: var(--e-3);
|
||||||
|
padding-top: var(--e-3);
|
||||||
|
border-top: 1px solid var(--borda);
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
min-height: 84px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
.segmentado {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: var(--raio-sm);
|
||||||
|
background: var(--superficie-3);
|
||||||
|
}
|
||||||
|
.segmentado button {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0 var(--e-3);
|
||||||
|
border: 0;
|
||||||
|
border-radius: calc(var(--raio-sm) - 2px);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--texto-2);
|
||||||
|
font: 500 var(--t-sm) var(--fonte);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.segmentado.pequeno button {
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 var(--e-2);
|
||||||
|
font-size: var(--t-xs);
|
||||||
|
}
|
||||||
|
.segmentado button[aria-checked='true'] {
|
||||||
|
background: var(--superficie);
|
||||||
|
color: var(--texto);
|
||||||
|
box-shadow: var(--sombra);
|
||||||
|
}
|
||||||
|
.ias {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: var(--e-2);
|
||||||
|
}
|
||||||
|
.ia {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
padding: var(--e-2) var(--e-3);
|
||||||
|
border: 1px solid var(--borda);
|
||||||
|
border-radius: var(--raio);
|
||||||
|
background: var(--superficie-2);
|
||||||
|
color: var(--texto);
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.ia[aria-checked='true'] {
|
||||||
|
border-color: var(--destaque);
|
||||||
|
background: var(--destaque-suave);
|
||||||
|
}
|
||||||
|
.ia.indisponivel .ia-detalhe {
|
||||||
|
color: var(--aviso);
|
||||||
|
}
|
||||||
|
.ia-nome {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--t-sm);
|
||||||
|
}
|
||||||
|
.ia-detalhe {
|
||||||
|
font-size: var(--t-xs);
|
||||||
|
color: var(--texto-3);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.acoes {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.erro {
|
||||||
|
color: var(--perigo);
|
||||||
|
}
|
||||||
|
.grade {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: var(--e-2);
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.cartao {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: var(--e-3) var(--e-2) var(--e-2);
|
||||||
|
border: 1px solid var(--borda);
|
||||||
|
border-radius: var(--raio);
|
||||||
|
background: var(--superficie-2);
|
||||||
|
color: var(--texto);
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transicao), border-color var(--transicao), transform var(--transicao);
|
||||||
|
}
|
||||||
|
.cartao:hover {
|
||||||
|
border-color: var(--destaque);
|
||||||
|
background: var(--destaque-suave);
|
||||||
|
}
|
||||||
|
.cartao:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
.emoji {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.nome {
|
||||||
|
font-size: var(--t-sm);
|
||||||
|
text-align: center;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--texto-3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.canto {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--texto-3);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.estrela {
|
||||||
|
right: 2px;
|
||||||
|
}
|
||||||
|
.lixo {
|
||||||
|
left: 2px;
|
||||||
|
}
|
||||||
|
.lixo:hover {
|
||||||
|
color: var(--perigo);
|
||||||
|
}
|
||||||
|
.estrela.ativa {
|
||||||
|
color: var(--aviso);
|
||||||
|
}
|
||||||
|
.estrela.ativa :global(svg) {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -171,6 +171,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{:else if aba === 'criar'}
|
{:else if aba === 'criar'}
|
||||||
<div class="grade-criar">
|
<div class="grade-criar">
|
||||||
|
<Card titulo="Estúdio de criação" ajuda="criar" descricao="Expressões, atividades e figurinhas novas, escolhendo a IA (Gemini, IA local ou Qwen do S10).">
|
||||||
|
<a class="btn primario" href="#/criar">Abrir o estúdio</a>
|
||||||
|
</Card>
|
||||||
<Card titulo="Desenhar figurinha" ajuda="reacoes" descricao="O Gemini desenha um SVG e o celular mostra assim que ficar pronto.">
|
<Card titulo="Desenhar figurinha" ajuda="reacoes" descricao="O Gemini desenha um SVG e o celular mostra assim que ficar pronto.">
|
||||||
<form class="grade" onsubmit={desenhar}>
|
<form class="grade" onsubmit={desenhar}>
|
||||||
<label class="campo">
|
<label class="campo">
|
||||||
|
|||||||
Reference in New Issue
Block a user