Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d410dbf24 | ||
|
|
fd6a14054d | ||
|
|
784042335c | ||
|
|
cf54a93b53 |
@@ -613,6 +613,7 @@ if (MODO_PAINEL) {
|
|||||||
const m = e.data;
|
const m = e.data;
|
||||||
if (m.tipo === 'ordem' && typeof m.ordem === 'string') trata(m.ordem);
|
if (m.tipo === 'ordem' && typeof m.ordem === 'string') trata(m.ordem);
|
||||||
else if (m.tipo === 'config' && m.config) configurarApp(m.config);
|
else if (m.tipo === 'config' && m.config) configurarApp(m.config);
|
||||||
|
else if (m.tipo === 'sono' && !!m.dormindo !== r.dormindo) trata(m.dormindo ? 'dormir' : 'acordar');
|
||||||
else if (m.tipo === 'legenda' && typeof m.texto === 'string' && window.MotorRosto) window.MotorRosto.faixa('legenda', { texto: m.texto });
|
else if (m.tipo === 'legenda' && typeof m.texto === 'string' && window.MotorRosto) window.MotorRosto.faixa('legenda', { texto: m.texto });
|
||||||
});
|
});
|
||||||
} else if (new URLSearchParams(location.search).get('espelho') === '1') setInterval(aplicarAparencia, 1000);
|
} else if (new URLSearchParams(location.search).get('espelho') === '1') setInterval(aplicarAparencia, 1000);
|
||||||
|
|||||||
@@ -613,6 +613,7 @@ if (MODO_PAINEL) {
|
|||||||
const m = e.data;
|
const m = e.data;
|
||||||
if (m.tipo === 'ordem' && typeof m.ordem === 'string') trata(m.ordem);
|
if (m.tipo === 'ordem' && typeof m.ordem === 'string') trata(m.ordem);
|
||||||
else if (m.tipo === 'config' && m.config) configurarApp(m.config);
|
else if (m.tipo === 'config' && m.config) configurarApp(m.config);
|
||||||
|
else if (m.tipo === 'sono' && !!m.dormindo !== r.dormindo) trata(m.dormindo ? 'dormir' : 'acordar');
|
||||||
else if (m.tipo === 'legenda' && typeof m.texto === 'string' && window.MotorRosto) window.MotorRosto.faixa('legenda', { texto: m.texto });
|
else if (m.tipo === 'legenda' && typeof m.texto === 'string' && window.MotorRosto) window.MotorRosto.faixa('legenda', { texto: m.texto });
|
||||||
});
|
});
|
||||||
} else if (new URLSearchParams(location.search).get('espelho') === '1') setInterval(aplicarAparencia, 1000);
|
} else if (new URLSearchParams(location.search).get('espelho') === '1') setInterval(aplicarAparencia, 1000);
|
||||||
|
|||||||
+2
-1
@@ -92,7 +92,8 @@ systemctl restart carinha-pc
|
|||||||
echo "backup em `$backup"
|
echo "backup em `$backup"
|
||||||
"@
|
"@
|
||||||
Write-Host '== Instalando na placa'
|
Write-Host '== Instalando na placa'
|
||||||
$script -replace "`r", '' | & ssh $alvo 'bash -s'
|
# O PowerShell 5.1 põe BOM no stdin do ssh; sem tirar, o bash lê "<BOM>set -e" e não para no primeiro erro.
|
||||||
|
$script -replace "`r", '' | & ssh $alvo "sed '1s/^\xEF\xBB\xBF//' | bash -s"
|
||||||
if ($LASTEXITCODE) { throw 'instalação na placa falhou (o backup foi feito antes de qualquer remoção)' }
|
if ($LASTEXITCODE) { throw 'instalação na placa falhou (o backup foi feito antes de qualquer remoção)' }
|
||||||
|
|
||||||
Write-Host '== Conferindo'
|
Write-Host '== Conferindo'
|
||||||
|
|||||||
+33
-12
@@ -48,7 +48,8 @@ class Historico:
|
|||||||
pass
|
pass
|
||||||
self.ultimo = {} # tipo -> horário do último evento (intervalo mínimo)
|
self.ultimo = {} # tipo -> horário do último evento (intervalo mínimo)
|
||||||
self.pedido = None # (id do evento, monotonic) aguardando foto do app
|
self.pedido = None # (id do evento, monotonic) aguardando foto do app
|
||||||
self.pir = dict(disponivel=False, erro='', linha=os.environ.get('CARINHA_PIR', 'gpiochip1:70'), ultimo=None)
|
self.pir = dict(disponivel=False, erro='', linha=os.environ.get('CARINHA_PIR', 'gpiochip1:70'), ultimo=None,
|
||||||
|
ativo=False) # ativo = pino em nível alto agora (o HC-SR505 segura ~8 s e rearma)
|
||||||
self.processo_pir = None
|
self.processo_pir = None
|
||||||
with self.db() as db:
|
with self.db() as db:
|
||||||
db.execute('CREATE TABLE IF NOT EXISTS eventos(id INTEGER PRIMARY KEY, horario REAL, tipo TEXT, evento TEXT, nivel REAL, foto TEXT)')
|
db.execute('CREATE TABLE IF NOT EXISTS eventos(id INTEGER PRIMARY KEY, horario REAL, tipo TEXT, evento TEXT, nivel REAL, foto TEXT)')
|
||||||
@@ -119,13 +120,30 @@ class Historico:
|
|||||||
if not (self.config['presenca'] and self.config['pir'] and self.pir['disponivel']):
|
if not (self.config['presenca'] and self.config['pir'] and self.pir['disponivel']):
|
||||||
return False
|
return False
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
if self.pir['ativo']: # sensor ainda segurando: a pessoa continua ali
|
||||||
|
self.presenca['ultimo'] = agora
|
||||||
|
return False
|
||||||
if self.presenca['estado'] != 'presente' or agora - self.presenca['ultimo'] < self.config['ausencia'] * 60:
|
if self.presenca['estado'] != 'presente' or agora - self.presenca['ultimo'] < self.config['ausencia'] * 60:
|
||||||
return False
|
return False
|
||||||
self.presenca.update(estado='ausente', desde=agora)
|
self.presenca.update(estado='ausente', desde=agora)
|
||||||
self.ao_ausencia()
|
self.ao_ausencia()
|
||||||
self.publicar('pir', dict(ultimo=self.pir['ultimo'], presenca=dict(self.presenca)))
|
self.avisar_pir()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def avisar_pir(self):
|
||||||
|
self.publicar('pir', dict(ultimo=self.pir['ultimo'], ativo=self.pir['ativo'], presenca=dict(self.presenca)))
|
||||||
|
|
||||||
|
def nivel_pir(self, alto, agora=None, registrar=True):
|
||||||
|
"""Mudança no pino do PIR. Subida = presença (evento + foto); enquanto alto, a bolinha fica acesa."""
|
||||||
|
agora = agora if agora is not None else time.time()
|
||||||
|
self.pir['ativo'] = alto
|
||||||
|
if alto:
|
||||||
|
self.pir['ultimo'] = agora
|
||||||
|
self.marcar_presenca(agora)
|
||||||
|
self.avisar_pir()
|
||||||
|
if alto and registrar:
|
||||||
|
self.registrar('pir', 'presença')
|
||||||
|
|
||||||
def vigiar_ausencia(self):
|
def vigiar_ausencia(self):
|
||||||
while True:
|
while True:
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
@@ -240,7 +258,7 @@ class Historico:
|
|||||||
|
|
||||||
# ------------------------------------------------------------------ PIR
|
# ------------------------------------------------------------------ PIR
|
||||||
def vigiar_pir(self):
|
def vigiar_pir(self):
|
||||||
"""Mantém um `gpiomon` rodando enquanto o PIR estiver ligado; cada borda de subida = presença."""
|
"""Mantém um `gpiomon` rodando enquanto o PIR estiver ligado e acompanha o nível do pino."""
|
||||||
while True:
|
while True:
|
||||||
if not self.config['pir'] or os.name != 'posix' or not shutil.which('gpiomon'):
|
if not self.config['pir'] or os.name != 'posix' or not shutil.which('gpiomon'):
|
||||||
self.pir.update(disponivel=False, erro='' if self.config['pir'] else 'desligado')
|
self.pir.update(disponivel=False, erro='' if self.config['pir'] else 'desligado')
|
||||||
@@ -251,25 +269,28 @@ class Historico:
|
|||||||
chip, _, linha = self.pir['linha'].partition(':')
|
chip, _, linha = self.pir['linha'].partition(':')
|
||||||
try:
|
try:
|
||||||
# pull-down: com o sensor desligado o pino não fica "flutuando" (disparos falsos)
|
# pull-down: com o sensor desligado o pino não fica "flutuando" (disparos falsos)
|
||||||
self.processo_pir = subprocess.Popen(['gpiomon', '-c', chip, '-b', 'pull-down', '-e', 'rising', linha],
|
lido = subprocess.run(['gpioget', '-c', chip, '-b', 'pull-down', '--numeric', linha],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
self.processo_pir = subprocess.Popen(['gpiomon', '-c', chip, '-b', 'pull-down', '-e', 'both', '-F', '%E', linha],
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
self.pir.update(disponivel=True, erro='')
|
self.pir.update(disponivel=True, erro='')
|
||||||
|
# nível de partida: alguém já na frente não gera borda de subida, mas acende a bolinha
|
||||||
|
self.nivel_pir(lido.stdout.strip() == '1', registrar=False)
|
||||||
inicio = time.monotonic()
|
inicio = time.monotonic()
|
||||||
for _ in self.processo_pir.stdout:
|
for linha_evento in self.processo_pir.stdout:
|
||||||
if not self.config['pir']:
|
if not self.config['pir']:
|
||||||
break
|
break
|
||||||
if time.monotonic() - inicio < 2: # o pino "assenta" ao ligar o pull-down
|
alto = linha_evento.strip() == 'rising'
|
||||||
continue
|
# o pino "assenta" ao ligar o pull-down: acompanha o nível, mas não registra evento
|
||||||
self.pir['ultimo'] = time.time()
|
self.nivel_pir(alto, registrar=time.monotonic() - inicio >= 2)
|
||||||
self.marcar_presenca(self.pir['ultimo'])
|
|
||||||
self.publicar('pir', dict(ultimo=self.pir['ultimo'], presenca=dict(self.presenca))) # todo disparo, ao vivo
|
|
||||||
self.registrar('pir', 'presença')
|
|
||||||
erro = (self.processo_pir.stderr.read() or '').strip()[:200]
|
erro = (self.processo_pir.stderr.read() or '').strip()[:200]
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
erro = str(e)
|
erro = str(e)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
erro = 'gpioget não respondeu'
|
||||||
finally:
|
finally:
|
||||||
self.parar_pir()
|
self.parar_pir()
|
||||||
self.pir.update(disponivel=False, erro=erro if self.config['pir'] else 'desligado')
|
self.pir.update(disponivel=False, ativo=False, erro=erro if self.config['pir'] else 'desligado')
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
def parar_pir(self):
|
def parar_pir(self):
|
||||||
|
|||||||
@@ -93,6 +93,20 @@ class HistoricoTest(unittest.TestCase):
|
|||||||
self.h.marcar_presenca(3000)
|
self.h.marcar_presenca(3000)
|
||||||
self.assertFalse(self.h.checar_ausencia(9999))
|
self.assertFalse(self.h.checar_ausencia(9999))
|
||||||
|
|
||||||
|
def test_pir_segurando_conta_como_presenca(self):
|
||||||
|
avisos = []
|
||||||
|
self.h.publicar = lambda tipo, dados: avisos.append((tipo, dados))
|
||||||
|
self.h.pir['disponivel'] = True
|
||||||
|
self.h.nivel_pir(True, agora=1000) # subida: evento + bolinha acesa
|
||||||
|
self.assertTrue(self.h.pir['ativo'])
|
||||||
|
self.assertEqual(self.h.status()['eventos'][0]['tipo'], 'pir')
|
||||||
|
self.assertFalse(self.h.checar_ausencia(1000 + 10 * 60)) # pino ainda alto: não dorme
|
||||||
|
self.h.nivel_pir(False, agora=1000 + 10 * 60) # descida: só apaga a bolinha
|
||||||
|
self.assertFalse(self.h.pir['ativo'])
|
||||||
|
self.assertEqual(self.h.status()['total'], 1)
|
||||||
|
self.assertTrue(self.h.checar_ausencia(1000 + 15 * 60 + 1))
|
||||||
|
self.assertIn(('pir', True), [(t, d.get('ativo')) for t, d in avisos])
|
||||||
|
|
||||||
def test_apagar_e_limpar(self):
|
def test_apagar_e_limpar(self):
|
||||||
a = self.h.registrar('pir', agora=1); self.h.guardar_foto(a, JPEG)
|
a = self.h.registrar('pir', agora=1); self.h.guardar_foto(a, JPEG)
|
||||||
b = self.h.registrar('som', agora=1)
|
b = self.h.registrar('som', agora=1)
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { estado, ouvir } from '../estado.svelte';
|
import { estado, ouvir } from '../estado.svelte';
|
||||||
|
|
||||||
let { titulo = 'Prévia do rosto' }: { titulo?: string } = $props();
|
/** espelharSono: dorme/acorda junto com o celular (ele adormece sozinho no silêncio). */
|
||||||
|
let { titulo = 'Prévia do rosto', espelharSono = true }: { titulo?: string; espelharSono?: boolean } = $props();
|
||||||
let quadro: HTMLIFrameElement;
|
let quadro: HTMLIFrameElement;
|
||||||
let pronto = $state(false);
|
let pronto = $state(false);
|
||||||
|
|
||||||
@@ -20,6 +21,13 @@
|
|||||||
if (estado.appConfig) enviar({ tipo: 'config', config: $state.snapshot(estado.appConfig) });
|
if (estado.appConfig) enviar({ tipo: 'config', config: $state.snapshot(estado.appConfig) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sono que o celular decidiu sozinho (silêncio, toque, barulho) não passa pelo SSE de ordens:
|
||||||
|
// vem na telemetria. Sem celular online, a prévia fica como está.
|
||||||
|
const dormindoNoCelular = $derived(estado.celularOnline ? !!estado.app?.estado?.dormindo : null);
|
||||||
|
$effect(() => {
|
||||||
|
if (espelharSono && pronto && dormindoNoCelular !== null) enviar({ tipo: 'sono', dormindo: dormindoNoCelular });
|
||||||
|
});
|
||||||
|
|
||||||
let ultimaConversa = estado.conversa?.ultima?.id;
|
let ultimaConversa = estado.conversa?.ultima?.id;
|
||||||
const cancelar = [
|
const cancelar = [
|
||||||
ouvir('expressao', (d) => enviar({ tipo: 'ordem', ordem: d.ordem })),
|
ouvir('expressao', (d) => enviar({ tipo: 'ordem', ordem: d.ordem })),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class Estado {
|
|||||||
api = $state<EstadoServidor['api'] | null>(null);
|
api = $state<EstadoServidor['api'] | null>(null);
|
||||||
feed = $state<ItemFeed[]>([]);
|
feed = $state<ItemFeed[]>([]);
|
||||||
/** Sensor PIR da placa: último disparo (s, epoch) e se está ligado. */
|
/** Sensor PIR da placa: último disparo (s, epoch) e se está ligado. */
|
||||||
pir = $state<{ disponivel: boolean; ultimo: number | null; presenca?: { estado: 'presente' | 'ausente'; desde: number; ultimo: number } } | null>(null);
|
pir = $state<{ disponivel: boolean; ultimo: number | null; ativo?: boolean; presenca?: { estado: 'presente' | 'ausente'; desde: number; ultimo: number } } | null>(null);
|
||||||
/** Relógio de 1 s para derivar "online" sem esperar evento novo. */
|
/** Relógio de 1 s para derivar "online" sem esperar evento novo. */
|
||||||
agora = $state(Date.now());
|
agora = $state(Date.now());
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ function aplicarFoto(f: EstadoServidor) {
|
|||||||
estado.reacao = f.reacao ?? null;
|
estado.reacao = f.reacao ?? null;
|
||||||
if (f.treinamento) estado.treinamento = f.treinamento;
|
if (f.treinamento) estado.treinamento = f.treinamento;
|
||||||
if (f.api) estado.api = f.api;
|
if (f.api) estado.api = f.api;
|
||||||
if (f.pir) estado.pir = { disponivel: f.pir.disponivel, ultimo: f.pir.ultimo, presenca: (f.pir as any).presenca };
|
if (f.pir) estado.pir = { disponivel: f.pir.disponivel, ultimo: f.pir.ultimo, ativo: (f.pir as any).ativo, presenca: (f.pir as any).presenca };
|
||||||
}
|
}
|
||||||
|
|
||||||
const tratadores: Record<string, (d: any) => void> = {
|
const tratadores: Record<string, (d: any) => void> = {
|
||||||
@@ -142,7 +142,7 @@ const tratadores: Record<string, (d: any) => void> = {
|
|||||||
if (d?.receita?.nome) noFeed('reacao', 'Reação: ' + d.receita.nome);
|
if (d?.receita?.nome) noFeed('reacao', 'Reação: ' + d.receita.nome);
|
||||||
},
|
},
|
||||||
treinamento: (d) => (estado.treinamento = d),
|
treinamento: (d) => (estado.treinamento = d),
|
||||||
pir: (d: { ultimo: number; presenca?: any }) => (estado.pir = { disponivel: true, ultimo: d.ultimo, presenca: d.presenca }),
|
pir: (d: { ultimo: number; ativo?: boolean; presenca?: any }) => (estado.pir = { disponivel: true, ultimo: d.ultimo, ativo: d.ativo, presenca: d.presenca }),
|
||||||
historico: (d) => {
|
historico: (d) => {
|
||||||
const n = d?.novo;
|
const n = d?.novo;
|
||||||
if (n) noFeed('historico', { pir: '🚶 Presença no PIR', movimento: '👀 Movimento na câmera', som: '🔊 Som alto' }[n.tipo as string] ?? 'Detecção');
|
if (n) noFeed('historico', { pir: '🚶 Presença no PIR', movimento: '👀 Movimento na câmera', som: '🔊 Som alto' }[n.tipo as string] ?? 'Detecção');
|
||||||
|
|||||||
@@ -48,6 +48,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const cfg = $derived(estado.appConfig);
|
const cfg = $derived(estado.appConfig);
|
||||||
|
// Pontinho do PIR sobre a imagem: verde pulsando nos 8 s depois de cada disparo.
|
||||||
|
// aceso enquanto o pino do sensor está alto; servidor antigo (sem `ativo`) cai nos 8 s após o disparo
|
||||||
|
const pirAtivo = $derived(
|
||||||
|
estado.pir?.ativo ?? (!!estado.pir?.ultimo && estado.agora - estado.pir.ultimo * 1000 < 8000),
|
||||||
|
);
|
||||||
const transmitindo = $derived(!!cfg?.preview);
|
const transmitindo = $derived(!!cfg?.preview);
|
||||||
const hora = (t: number) => new Date(t).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
const hora = (t: number) => new Date(t).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
|
||||||
@@ -70,6 +75,12 @@
|
|||||||
<div class="ao-vivo">
|
<div class="ao-vivo">
|
||||||
<!-- Palco fixo: a câmera continua visível enquanto a lista rola -->
|
<!-- Palco fixo: a câmera continua visível enquanto a lista rola -->
|
||||||
<section class="palco" aria-label="Câmera e rosto">
|
<section class="palco" aria-label="Câmera e rosto">
|
||||||
|
{#if estado.pir?.disponivel}
|
||||||
|
<span class="ponto-pir" class:ativo={pirAtivo} title="Sensor de presença (PIR)" aria-live="polite">
|
||||||
|
<span class="luz" aria-hidden="true"></span>
|
||||||
|
PIR{pirAtivo ? ' · presença' : ''}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
<div class="quadro camera" class:mini={rostoGrande}>
|
<div class="quadro camera" class:mini={rostoGrande}>
|
||||||
<!-- svelte-ignore a11y_media_has_caption -->
|
<!-- svelte-ignore a11y_media_has_caption -->
|
||||||
<video bind:this={elVideo} muted playsinline autoplay hidden={!video?.exibindo}></video>
|
<video bind:this={elVideo} muted playsinline autoplay hidden={!video?.exibindo}></video>
|
||||||
@@ -300,6 +311,41 @@
|
|||||||
.espaco {
|
.espaco {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
.ponto-pir {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
left: var(--e-3);
|
||||||
|
top: var(--e-3);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px 4px 7px;
|
||||||
|
border-radius: var(--raio-pill);
|
||||||
|
background: rgb(0 0 0 / 0.55);
|
||||||
|
color: #c7cdd4;
|
||||||
|
font: 600 11px var(--fonte);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: color var(--transicao);
|
||||||
|
}
|
||||||
|
.ponto-pir .luz {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #6b7280;
|
||||||
|
transition: background var(--transicao);
|
||||||
|
}
|
||||||
|
.ponto-pir.ativo {
|
||||||
|
color: #b8f5c9;
|
||||||
|
}
|
||||||
|
.ponto-pir.ativo .luz {
|
||||||
|
background: #34c759;
|
||||||
|
animation: pulsoPir 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulsoPir {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgb(52 199 89 / 0.7); }
|
||||||
|
50% { box-shadow: 0 0 0 7px rgb(52 199 89 / 0); }
|
||||||
|
}
|
||||||
/* ---------- lista */
|
/* ---------- lista */
|
||||||
.lista {
|
.lista {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -249,7 +249,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="lateral">
|
<aside class="lateral">
|
||||||
<div class="quadro"><RostoPrevia bind:this={previa} titulo="Prévia das reações" /></div>
|
<div class="quadro"><RostoPrevia bind:this={previa} titulo="Prévia das reações" espelharSono={false} /></div>
|
||||||
<p class="fraco pequeno-texto">Clique numa reação para ver aqui sem mandar ao celular.</p>
|
<p class="fraco pequeno-texto">Clique numa reação para ver aqui sem mandar ao celular.</p>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
|
|
||||||
<section class="bancada" aria-label="Prévia">
|
<section class="bancada" aria-label="Prévia">
|
||||||
<div class="celular">
|
<div class="celular">
|
||||||
<div class="tela"><RostoPrevia bind:this={previa} titulo="Prévia do rosto" /></div>
|
<div class="tela"><RostoPrevia bind:this={previa} titulo="Prévia do rosto" espelharSono={false} /></div>
|
||||||
<span class="camera-frontal" aria-hidden="true"></span>
|
<span class="camera-frontal" aria-hidden="true"></span>
|
||||||
<span class="etiqueta">{nomeRosto(atual).toUpperCase()}{aceitaFormato ? ' · ' + (FORMATOS_INFO[formatoAtual] ?? formatoAtual).toUpperCase() : ''}</span>
|
<span class="etiqueta">{nomeRosto(atual).toUpperCase()}{aceitaFormato ? ' · ' + (FORMATOS_INFO[formatoAtual] ?? formatoAtual).toUpperCase() : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user