Files
carinha-control-panel/painel.html

380 lines
17 KiB
HTML

<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Câmera & Rosto · Central Carinha</title>
<script>
if (window.location.search.includes('clean=1')) {
document.documentElement.classList.add('is-clean-embed');
}
</script>
<link rel="stylesheet" href="/dashboard.css">
<link rel="stylesheet" href="/painel.css">
<script src="/dashboard.js" defer></script>
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
<div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:16px;margin-bottom:12px">
<div>
<h1>Câmera & Rosto</h1>
<p id="bateriaCelular" class="texto-fraco" role="status" style="margin:4px 0 0">Bateria do celular: aguardando dados.</p>
</div>
<div style="display:flex;gap:10px;align-items:center">
<span class="badge ativo"><span class="badge-dot"></span> Monitoramento Contínuo</span>
</div>
</div>
<div class="sub" style="margin-bottom:18px">Controle o rostinho no celular e acompanhe o que ele vê. O monitor da câmera e do rosto fica fixado na tela enquanto você ajusta gestos e comportamentos.</div>
<!-- WORKSPACE STICKY: Câmera e Rosto fixados na tela durante a rolagem de configurações -->
<div class="painel-workspace">
<!-- Coluna Fixa (Sticky Monitor) -->
<aside class="painel-monitor-sticky">
<!-- Câmera ao Vivo -->
<section class="cartao" style="margin-bottom:18px">
<div class="cabecalho">
<h2>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--ciano)" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
Olhar da Carinha
</h2>
<div style="display:flex;gap:8px;align-items:center">
<button id="bSnapshot" type="button" title="Capturar imagem da câmera">📸 Foto</button>
<button id="toggleCamera" class="principal" disabled>Ligar câmera</button>
</div>
</div>
<div class="visao">
<video id="cameraVideo" autoplay muted playsinline hidden aria-label="Vídeo ao vivo da câmera do celular"></video>
<img id="cameraImagem" alt="Imagem ao vivo da câmera do celular" hidden>
<canvas id="trackingOverlay" aria-label="Marcações do rastreamento" style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none" hidden></canvas>
<p id="cameraMensagem">Ligue a câmera para acompanhar o ambiente. A imagem aparece aqui; o celular continua mostrando o rosto.</p>
</div>
<p class="texto-fraco" id="videoQualidade" style="margin:8px 0 4px">Qualidade automática · buscando 30 fps</p>
<div id="ajustesRastreamento" class="ajustes-grade" style="margin-top:8px"></div>
<p id="trackingEstado" class="texto-fraco" role="status" style="margin:4px 0">Aguardando vídeo para rastrear no PC.</p>
<div class="sensores">
<label>Microfone <span id="micValor">—</span><meter id="micNivel" min="0" max="1" value="0"></meter></label>
<label>Movimento <span id="movValor">—</span><meter id="movNivel" min="0" max="1" value="0"></meter></label>
</div>
<p class="texto-fraco" id="sensorEstado" style="margin:4px 0">Aguardando o app no celular.</p>
</section>
<!-- Prévia da Tela do Celular -->
<section class="cartao">
<div class="cabecalho">
<h2>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--ciano)" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/></svg>
Tela do Galaxy S10
</h2>
<span id="conexaoEstado" class="online">Aguardando</span>
</div>
<iframe class="rosto-preview" style="height:165px;margin-top:4px" src="/carinha.html?espelho=1&amp;camera=0&amp;audio=0" title="Prévia da aparência escolhida" tabindex="-1"></iframe>
<p class="texto-fraco" style="margin:6px 0 10px">Prévia da aparência. <span id="rostoConfirmado">Aguardando confirmação do celular.</span></p>
<button id="recarregarTela" type="button" style="width:100%;margin-bottom:10px">🔄 Recarregar tela do celular</button>
<details>
<summary>Conexão ADB e Rede</summary>
<form id="conectarForm" style="margin-top:10px">
<label for="celularIP">IP do celular</label>
<input id="celularIP" type="text" value="192.168.1.16" required>
<label for="pcIP">IP do servidor na rede</label>
<input id="pcIP" type="text" value="192.168.1.35" required>
<button id="conectarBotao" type="submit" style="width:100%">Conectar e abrir o app</button>
</form>
<p id="conectarEstado" class="texto-fraco" role="status" style="margin-top:6px">O celular e o PC precisam estar na mesma rede, com ADB Wi-Fi ativado.</p>
</details>
</section>
</aside>
<!-- Coluna Rolável (Controles & Configurações) -->
<main class="painel-controls-scroll">
<!-- Gestos -->
<section class="cartao">
<div class="cabecalho">
<h2>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--ciano)" stroke-width="2"><path d="M12 2v4"/><path d="M12 18v4"/><path d="M4.93 4.93l2.83 2.83"/><path d="M16.24 16.24l2.83 2.83"/><path d="M2 12h4"/><path d="M18 12h4"/><path d="M4.93 19.07l2.83-2.83"/><path d="M16.24 7.76l2.83-2.83"/></svg>
Disparo de Gestos
</h2>
<span class="badge" style="font-size:11px">Fila de Ações</span>
</div>
<p style="font-size:13px;margin:4px 0 12px">Clique para acionar a expressão física no robô com feedback sonoro instantâneo:</p>
<div class="linha" id="gestos" style="gap:10px"></div>
</section>
<!-- Rosto e Cor dos Olhos -->
<div class="grade" style="grid-template-columns:1fr 1fr">
<div class="cartao">
<h2>Rosto</h2>
<p style="font-size:13px;margin:4px 0 10px">Selecione a proporção e formato:</p>
<div class="linha" id="rostos"></div>
<div class="estado" id="estadoRosto" style="margin-top:12px;font-size:13.5px">—</div>
</div>
<div class="cartao">
<h2>Cor dos olhos</h2>
<p style="font-size:13px;margin:4px 0 10px">Paleta com brilho neon:</p>
<div class="linha" id="cores"></div>
<div class="olho">
<input type="color" id="corLivre" value="#5cd6ff">
<span style="font-size:13px">escolher outra cor</span>
</div>
<div class="previa" id="previa" style="margin-top:12px">
<div class="bolha" id="bolha1"></div>
<div class="bolha" id="bolha2"></div>
</div>
</div>
</div>
<!-- Aparência Atual -->
<div class="cartao">
<h2>Aparência Atual & Demos</h2>
<div class="estado" id="estadoAparencia">—</div>
<div class="linha" style="margin-top:14px">
<button id="bPadrao">Voltar ao padrão</button>
<button id="bAbracar" class="primary">Demo: acordar + feliz + coração</button>
</div>
</div>
<!-- Personalidade e IA local -->
<section class="cartao">
<details style="margin:0;border:0;padding:0">
<summary style="font-size:16px">Personalidade e IA Local (Qwen embarcado)</summary>
<div style="margin-top:12px">
<label style="display:flex;align-items:center;gap:10px">
<input id="iaAtiva" type="checkbox">
<span>Ativar cérebro autônomo do personagem</span>
</label>
<p class="texto-fraco">Qwen3 decide expressões a partir de sons e movimento. Processamento no celular; sem comandos de motores.</p>
<div style="display:flex;gap:10px;align-items:center;margin-top:10px">
<select id="iaEvento" aria-label="Evento para testar IA" style="margin:0">
<option value="saudacao">Alguém me cumprimentou</option>
<option value="som">Ouvi um som</option>
<option value="movimento">Vi movimento</option>
<option value="sozinho">Estou sozinho</option>
</select>
<button id="iaTestar" type="button">Testar reação</button>
</div>
<p id="iaEstado" role="status" style="margin-top:8px;font-size:13px">Carregando IA…</p>
</div>
</details>
</section>
<!-- Comportamento e Ajustes -->
<section class="cartao ajustes">
<div class="cabecalho">
<h2>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--ciano)" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
Comportamento & Sensores
</h2>
<span class="badge" style="font-size:11px">Tempo Real</span>
</div>
<div id="ajustesBasicos" class="ajustes-grade"></div>
<details>
<summary>Sensibilidade e tempos de reação</summary>
<div id="ajustesAvancados" class="ajustes-grade"></div>
</details>
<div id="configEstado" role="status" style="margin-top:14px;color:var(--ciano)">Carregando configurações…</div>
</section>
</main>
</div>
<div class="rodape" style="margin-top:32px">
Servidor no Orange Pi. O vídeo é transmitido ao navegador e não é gravado.
</div>
<script>
"use strict";
/* -------------------------------------------------------------------------
Painel de controle da carinha (servido pelo kiosk-server.py em /painel).
Ele conversa com as MESMAS rotas que a pagina da cara usa:
GET /gestos lista de gestos
GET /faces lista de rostos
GET /config aparencia guardada (rosto + cor)
GET /gesto/<nome> enfileira um gesto
POST /config guarda e aplica rosto/cor
------------------------------------------------------------------------- */
const elGestos = document.getElementById("gestos");
const elRostos = document.getElementById("rostos");
const elCores = document.getElementById("cores");
const elCorLivre = document.getElementById("corLivre");
const elEstadoRosto = document.getElementById("estadoRosto");
const elEstadoAparencia = document.getElementById("estadoAparencia");
const bPadrao = document.getElementById("bPadrao");
const bAbracar = document.getElementById("bAbracar");
const bolha1 = document.getElementById("bolha1");
const bolha2 = document.getElementById("bolha2");
const PADRAO = { rosto: "carinha", cor: [92, 214, 255] };
const PALETA = ["#5CD6FF", "#96E8FF", "#FF7070", "#FFB070", "#FFD860", "#78FFA0",
"#82AAFF", "#FF70B0", "#F5FAFF"];
let aparencia = Object.assign({}, PADRAO);
const paraHex = (c) => "#" + c.map((v) => Math.max(0, Math.min(255, Math.round(v)))
.toString(16).padStart(2, "0")).join("");
const deHex = (h) => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16),
parseInt(h.slice(5, 7), 16)];
const mesmoTom = (a, b) => a && b && a.every((v, i) => Math.abs(v - b[i]) < 6);
async function pedir(rota, opcoes) {
try {
const r = await fetch(rota, opcoes);
if (!r.ok) throw new Error("HTTP " + r.status);
return await r.json();
} catch (e) {
estado("Falha ao falar com o servidor (" + e.message + ")");
return null;
}
}
function estado(texto) {
elEstadoAparencia.innerHTML = texto;
}
function pintarPrevia() {
const c = paraHex(aparencia.cor || PADRAO.cor);
bolha1.style.background = c;
bolha2.style.background = c;
const hex = paraHex(aparencia.cor || PADRAO.cor);
elCorLivre.value = hex;
for (const b of elCores.children) b.classList.toggle("on", b.dataset.hex === hex);
for (const b of elRostos.children)
b.classList.toggle("on", b.dataset.rosto === aparencia.rosto);
elEstadoRosto.innerHTML = "Em uso: <b>" + (aparencia.rosto || "?") + "</b>";
estado("Rosto <b>" + (aparencia.rosto || "?") + "</b> · cor dos olhos <b>" + hex +
"</b><br><span style='color:#7f97ab'>configuração guardada no PC</span>");
}
/* Manda a aparencia para o servidor: ele guarda E enfileira a ordem certa. */
async function salvarAparencia(parte) {
const dados = {};
if (parte === "rosto") dados.rosto = aparencia.rosto;
if (parte === "cor") dados.cor = aparencia.cor;
const r = await pedir("/config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(dados),
});
if (r && r.aparencia) {
aparencia = Object.assign(aparencia, r.aparencia);
pintarPrevia();
}
}
function montarGestos(lista) {
elGestos.innerHTML = "";
for (const nome of lista) {
const b = document.createElement("button");
b.textContent = nome.replace(/_/g, " ");
b.onclick = async () => {
if (window.RobotAudio) window.RobotAudio.tocarGesto(nome);
const resposta = await pedir("/gesto/" + encodeURIComponent(nome));
if (resposta) estado("Gesto enviado: <b>" + nome + "</b>");
};
elGestos.appendChild(b);
}
}
function montarRostos(lista) {
elRostos.innerHTML = "";
for (const nome of lista) {
const b = document.createElement("button");
b.textContent = nome;
b.dataset.rosto = nome;
b.onclick = () => {
if (window.RobotAudio) window.RobotAudio.click();
aparencia.rosto = nome; pintarPrevia(); salvarAparencia("rosto");
};
elRostos.appendChild(b);
}
}
function montarCores() {
elCores.innerHTML = "";
for (const hex of PALETA) {
const b = document.createElement("button");
b.className = "swatch";
b.dataset.hex = hex;
b.style.background = hex;
b.title = hex;
b.onclick = () => {
if (window.RobotAudio) window.RobotAudio.click();
aparencia.cor = deHex(hex); pintarPrevia(); salvarAparencia("cor");
};
elCores.appendChild(b);
}
}
// Botão de Snapshot / Foto
const elSnapshot = document.getElementById("bSnapshot");
if (elSnapshot) {
elSnapshot.onclick = () => {
const video = document.getElementById("cameraVideo");
const img = document.getElementById("cameraImagem");
const canvas = document.createElement("canvas");
let ok = false;
if (video && !video.hidden && video.videoWidth > 0) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext("2d").drawImage(video, 0, 0);
ok = true;
} else if (img && !img.hidden && img.naturalWidth > 0) {
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
canvas.getContext("2d").drawImage(img, 0, 0);
ok = true;
}
if (ok) {
if (window.RobotAudio) window.RobotAudio.click();
const a = document.createElement("a");
a.download = "carinha-foto-" + Date.now() + ".png";
a.href = canvas.toDataURL("image/png");
a.click();
estado("Foto salva da câmera!");
} else {
alert("Ligue a câmera e aguarde o vídeo para tirar uma foto.");
}
};
}
elCorLivre.oninput = () => { aparencia.cor = deHex(elCorLivre.value); pintarPrevia();
salvarAparencia("cor"); };
bPadrao.onclick = () => {
aparencia = { rosto: PADRAO.rosto, cor: PADRAO.cor.slice() };
pintarPrevia();
salvarAparencia("rosto").then(() => salvarAparencia("cor"));
};
bAbracar.onclick = async () => {
await pedir("/gesto/acordar");
setTimeout(() => pedir("/gesto/feliz"), 900);
setTimeout(() => pedir("/gesto/coracao"), 2200);
estado("Demo enviada: <b>acordar → feliz → coração</b>");
};
/* Monta as listas a partir do servidor (rostos, gestos, aparencia guardada). */
async function iniciar() {
const cfg = await pedir("/config");
if (cfg) aparencia = Object.assign(PADRAO, cfg);
const rostos = (await pedir("/faces")) || { rostos: ["eilik", "carinha", "miudo"] };
montarRostos(rostos.rostos || []);
const gestos = (await pedir("/gestos")) || { gestos: [] };
montarGestos(gestos.gestos || []);
montarCores();
pintarPrevia();
}
iniciar();
</script>
<script src="/painel-controle.js"></script>
<script src="/painel-video.js"></script>
<script src="/rastreamento-core.js"></script>
<script src="/painel-rastreamento.js"></script>
</body>
</html>