380 lines
17 KiB
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&camera=0&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>
|