feat(celular): rosto e app usam SSE e token; remove telas legadas

- carinha.html assina /eventos (expressao, app_config, conversa, reacao,
  recarregar) e volta ao polling de /fila sozinho se o SSE cair; foto dos
  ajustes a cada (re)conexao; token (?t=) vai em todo fetch ao servidor
- APK 0.8-token: token nas preferencias e no menu de toque longo, enviado
  na pagina, no HTTP do Java e ao llama-server (--api-key); trocar servidor
  por Intent exige o token salvo; camera/microfone so para o servidor
  configurado ou a copia local
- cerebro/conversa mandam a chave ao llama-server do S10
- servidor: evento SSE 'recarregar'; remove /camera/frame e /app/connect;
  enderecos antigos redirecionam para a secao do painel novo
- remove dashboard, painel, conversa, emojis, treinamento, braco, servos,
  api, scripts e CSS antigos, audio-robot.js e servir-local.bat
- logica pura do painel em web/src/lib/nucleo.ts com testes node --test
  (substitui test_movimentos.js e test_rastreamento.js)
- testes de navegador portados para o painel novo (rtc, rosto, rastreamento)
- README, docs/PAINEL.md e docs/SEGURANCA.md reescritos; versao 0.8.0;
  carinha-pc.service le /etc/carinha.env

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-05 20:11:58 -03:00
co-authored by Claude Opus 5.5
parent 673dea62e8
commit c5fd54c1d9
67 changed files with 626 additions and 5904 deletions
+3 -38
View File
@@ -3,18 +3,9 @@
* servidor em /rastreador-worker.js). Porta de painel-rastreamento.js + rastreamento-core.js.
*/
import type { AppConfig } from './tipos';
import type { AlvoRastreio, VideoAoVivo } from './video.svelte';
import { encaixe, escolherAlvo, type AlvoRastreio, type Face, type Ponto } from './nucleo';
import type { VideoAoVivo } from './video.svelte';
interface Ponto {
x: number;
y: number;
}
interface Face extends Ponto {
w: number;
h: number;
score: number;
pontos?: Ponto[];
}
interface Resultado {
faces: Face[];
hands: Ponto[][];
@@ -22,35 +13,9 @@ interface Resultado {
timestamp: number;
}
const valido = (p: Ponto | undefined) => !!p && Number.isFinite(p.x) && Number.isFinite(p.y);
const limitar = (v: number) => Math.max(0, Math.min(1, v));
const LIGACOES = [[0, 1], [1, 2], [2, 3], [3, 4], [0, 5], [5, 6], [6, 7], [7, 8], [5, 9], [9, 10], [10, 11], [11, 12],
[9, 13], [13, 14], [14, 15], [15, 16], [13, 17], [0, 17], [17, 18], [18, 19], [19, 20]];
export function escolherAlvo(faces: Face[], maos: Ponto[][], anterior: AlvoRastreio | null): AlvoRastreio | null {
let candidatos: AlvoRastreio[] = maos
.filter((h) => h.length === 21 && h.every(valido))
.map((h) => {
const palma = [0, 5, 9, 13, 17].map((i) => h[i]);
return { tipo: 'mao', x: palma.reduce((s, p) => s + p.x, 0) / 5, y: palma.reduce((s, p) => s + p.y, 0) / 5 };
});
if (!candidatos.length)
candidatos = faces
.filter((f) => valido(f) && f.w > 0 && f.h > 0)
.sort((a, b) => b.w * b.h - a.w * a.h)
.map((f) => ({ tipo: 'rosto', x: f.x + f.w / 2, y: f.y + f.h / 2 }));
if (!candidatos.length) return null;
if (anterior && anterior.tipo === candidatos[0].tipo)
candidatos.sort((a, b) => Math.hypot(a.x - anterior.x, a.y - anterior.y) - Math.hypot(b.x - anterior.x, b.y - anterior.y));
const t = candidatos[0];
return { ...t, x: limitar(t.x), y: limitar(t.y) };
}
function encaixe(w: number, h: number, vw: number, vh: number) {
const escala = Math.min(w / vw, h / vh);
return { x: Math.max(0, (w - vw * escala) / 2), y: Math.max(0, (h - vh * escala) / 2), w: vw * escala, h: vh * escala };
}
export class Rastreamento {
texto = $state('Rastreamento desligado.');
@@ -125,7 +90,7 @@ export class Rastreamento {
const p = pt(rosto);
c.strokeStyle = c.fillStyle = '#5cd6ff';
c.strokeRect(p.x, p.y, rosto.w * f.w, rosto.h * f.h);
c.fillText('Rosto ' + Math.round(rosto.score * 100) + '%', p.x, Math.max(15, p.y - 6));
c.fillText('Rosto ' + Math.round((rosto.score ?? 0) * 100) + '%', p.x, Math.max(15, p.y - 6));
for (const q of rosto.pontos || []) bola(pt(q));
}
for (const mao of r.hands) {