feat: initial commit - Carinha Control Panel v1.0 com UI glassmorphism, watch-bar, emojis, braco robotico e conversa IA
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
public class BrilhoTest {
|
||||
public static void main(String[] args) throws Exception {
|
||||
Class<?> type;
|
||||
try { type = Class.forName("br.carinha.rosto.BrilhoRepouso"); }
|
||||
catch (ClassNotFoundException e) { throw new AssertionError("Economia de tela ainda não implementada"); }
|
||||
java.lang.reflect.Method method = type.getMethod("nivel", boolean.class, float.class,
|
||||
float.class, long.class, long.class, boolean.class, boolean.class);
|
||||
check(method, .08f, true, .7f, .08f, 31000L, 30000L, false, false);
|
||||
check(method, .7f, true, .7f, .08f, 0L, 30000L, false, false);
|
||||
check(method, .7f, true, .7f, .08f, 31000L, 30000L, true, false);
|
||||
check(method, .08f, true, .7f, .08f, 1000L, 30000L, false, true);
|
||||
check(method, .7f, false, .7f, .08f, 60000L, 30000L, false, true);
|
||||
System.out.println("Brilho em repouso, retorno e economia desligada: OK");
|
||||
}
|
||||
static void check(java.lang.reflect.Method m, float want, Object... args) throws Exception {
|
||||
float actual = (Float) m.invoke(null, args);
|
||||
if (Math.abs(actual - want) > .001f) throw new AssertionError(actual + " != " + want);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import br.carinha.rosto.Sensores;
|
||||
public class SensoresTest {
|
||||
public static void main(String[] args) {
|
||||
if (!Sensores.ativo(true, false, false)) throw new AssertionError("acordado");
|
||||
if (Sensores.ativo(true, false, true)) throw new AssertionError("camera dormindo");
|
||||
if (!Sensores.ativo(true, true, true)) throw new AssertionError("mic dormindo");
|
||||
if (Sensores.ativo(false, true, true)) throw new AssertionError("desligado global");
|
||||
System.out.println("Sensores: OK");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class YuvFrameTest {
|
||||
public static void main(String[] args) throws Exception {
|
||||
Class<?> type;
|
||||
try { type = Class.forName("br.carinha.rosto.YuvFrame"); }
|
||||
catch (ClassNotFoundException e) { throw new AssertionError("Conversão YUV ainda não implementada"); }
|
||||
ByteBuffer[] planes = {ByteBuffer.wrap(new byte[]{16,32,0,0,48,64}),
|
||||
ByteBuffer.wrap(new byte[]{100,0}), ByteBuffer.wrap(new byte[]{(byte)150,0})};
|
||||
byte[] actual = (byte[]) type.getMethod("nv21", int.class, int.class,
|
||||
ByteBuffer[].class, int[].class, int[].class).invoke(null, 2, 2, planes,
|
||||
new int[]{4,2,2}, new int[]{1,2,2});
|
||||
byte[] expected = {16,32,48,64,(byte)150,100};
|
||||
if (!Arrays.equals(actual, expected)) throw new AssertionError(Arrays.toString(actual));
|
||||
System.out.println("YUV strides -> NV21: OK");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import unittest,tempfile,os,json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
import gemini
|
||||
from api_config import ConfigAPI
|
||||
class APIConfigTest(unittest.TestCase):
|
||||
def test_persistencia_e_sem_chave_na_resposta(self):
|
||||
with tempfile.TemporaryDirectory() as d,patch.dict(os.environ,{'GEMINI_API_KEY':'teste-chave-original-123456'}),patch.object(gemini,'MODELO','modelo-original'):
|
||||
p=Path(d)/'api.json';c=ConfigAPI(p);secret='nova-chave-de-teste-123456'
|
||||
status=c.salvar(dict(modelo='gemini-flash-lite-latest',chave=secret))
|
||||
self.assertNotIn(secret,json.dumps(status));self.assertTrue(status['configurada'])
|
||||
c.salvar(dict(modelo='gemini-flash-latest'));self.assertEqual(json.loads(p.read_text())['chave'],secret)
|
||||
loaded=ConfigAPI(p);self.assertEqual(loaded.status()['modelo'],'gemini-flash-latest')
|
||||
def test_teste_de_conexao_e_validacao(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
c=ConfigAPI(Path(d)/'api.json')
|
||||
with self.assertRaises(ValueError):c.salvar(dict(modelo='../invalido',chave='abc'))
|
||||
with patch('gemini.gerar_json',return_value={'ok':True}):self.assertTrue(c.testar()['ok'])
|
||||
with patch('gemini.gerar_json',side_effect=RuntimeError('segredo')):
|
||||
result=c.testar();self.assertFalse(result['ok']);self.assertNotIn('segredo',json.dumps(result))
|
||||
@@ -0,0 +1,36 @@
|
||||
import unittest,tempfile,sqlite3,json
|
||||
from pathlib import Path
|
||||
from treinamento import Treinamento,validar_receita
|
||||
class BibliotecaTest(unittest.TestCase):
|
||||
def test_criar_editar_excluir_desfazer_persistente(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d)/'treino.db';t=Treinamento(p,iniciar=False)
|
||||
t.alterar_reacao('criar',dados=dict(nome='Ideia',motivo='Uma ideia',caractere='💡',local='olhos'))
|
||||
row=t.status()['reacoes'][0];ident=row['id']
|
||||
t.alterar_reacao('editar',ident,dict(nome='Treino',motivo='Musculação',caractere='💪',local='olhos'))
|
||||
self.assertEqual(t.status()['reacoes'][0]['receita']['emoji']['caractere'],'💪')
|
||||
t=Treinamento(p,iniciar=False);t.alterar_reacao('desfazer')
|
||||
self.assertEqual(t.status()['reacoes'][0]['receita']['nome'],'Ideia')
|
||||
t.alterar_reacao('ativar',ident,dict(ativa=False));self.assertFalse(t.status()['reacoes'][0]['ativa'])
|
||||
t.alterar_reacao('desfazer');self.assertTrue(t.status()['reacoes'][0]['ativa'])
|
||||
t.alterar_reacao('feedback',ident,dict(valor=-1));self.assertEqual(t.status()['reacoes'][0]['feedback'],-1)
|
||||
t.alterar_reacao('desfazer');self.assertEqual(t.status()['reacoes'][0]['feedback'],0)
|
||||
t.alterar_reacao('duplicar',ident);self.assertEqual(len(t.status()['reacoes']),2)
|
||||
t.alterar_reacao('desfazer');self.assertEqual(len(t.status()['reacoes']),1)
|
||||
t.alterar_reacao('excluir',ident);self.assertEqual(t.status()['reacoes'],[])
|
||||
t.alterar_reacao('desfazer');self.assertEqual(t.status()['reacoes'][0]['id'],ident)
|
||||
def test_migracao_preserva_svg(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d)/'treino.db'
|
||||
recipe=dict(nome='Desenho',motivo='Original',simbolo='nenhum',legenda='',passos=[dict(olhos='aberto',boca='sorriso',cor='#5cd6ff',dx=0,dy=0,escala=1,duracao=2)],figurinha=dict(nome='SVG',local='olhos',formas=[dict(caminho='M 10 10 L 90 90 Z',cor='#123456')]))
|
||||
with sqlite3.connect(p) as db:
|
||||
db.execute('CREATE TABLE reacoes(id INTEGER PRIMARY KEY,horario REAL,receita TEXT,feedback INTEGER DEFAULT 0)');db.execute('INSERT INTO reacoes VALUES (1,0,?,1)',(json.dumps(recipe),))
|
||||
db.close()
|
||||
t=Treinamento(p,iniciar=False);self.assertEqual(t.status()['reacoes'][0]['receita'],recipe)
|
||||
t.alterar_reacao('editar',1,dict(nome='Emoji',motivo='Mudança',caractere='💡',local='olhos'))
|
||||
t.alterar_reacao('desfazer');self.assertEqual(t.status()['reacoes'][0]['receita'],recipe)
|
||||
def test_edicao_invalida_nao_modifica(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
t=Treinamento(Path(d)/'treino.db',iniciar=False)
|
||||
with self.assertRaises(ValueError):t.alterar_reacao('criar',dados=dict(nome='Teste',motivo='Teste',caractere='<script>',local='olhos'))
|
||||
self.assertEqual(t.status()['reacoes'],[])
|
||||
@@ -0,0 +1,45 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from servos import ControleServos
|
||||
from braco import Braco
|
||||
|
||||
class HW:
|
||||
def __init__(self): self.calls=[]
|
||||
def pulse(self,ch,us): self.calls.append((ch,us))
|
||||
def off(self,ch): self.calls.append((ch,None))
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp=tempfile.TemporaryDirectory(); self.hw=HW()
|
||||
self.servos=ControleServos(Path(self.tmp.name)/'servos.json',self.hw)
|
||||
self.arm=Braco(Path(self.tmp.name)/'braco.json',self.servos)
|
||||
def tearDown(self): self.servos.close(); self.tmp.cleanup()
|
||||
def test_no_startup_movement(self):
|
||||
self.assertEqual(self.hw.calls,[])
|
||||
self.assertEqual(len(self.arm.status()['articulacoes']),7)
|
||||
def test_calibration_single_and_pair(self):
|
||||
self.arm.calibrate(1,1700)
|
||||
self.assertEqual(self.hw.calls,[(1,1700)])
|
||||
self.hw.calls.clear();self.arm.calibrate(5,1800)
|
||||
self.assertEqual(self.hw.calls,[(5,1800),(6,1200)])
|
||||
with self.assertRaises(ValueError):self.arm.calibrate(1,2600)
|
||||
def test_pair_mirrors_and_all_channels_expire(self):
|
||||
self.arm.move([90,90,90,90,90,95,90])
|
||||
self.assertIn((5,1556),self.hw.calls); self.assertIn((6,1444),self.hw.calls)
|
||||
self.assertEqual(set(self.servos.active),set(range(8)))
|
||||
self.servos.expire(float('inf')); self.assertEqual(len(self.servos.active),0)
|
||||
def test_full_validation_before_any_write(self):
|
||||
for pose in [[0]*7,[90]*6,[90]*6+[True]]:
|
||||
with self.assertRaises(ValueError): self.arm.move(pose)
|
||||
self.assertEqual(self.hw.calls,[])
|
||||
def test_save_and_import_invalid_poses_are_atomic(self):
|
||||
data=self.arm.status(); data['posturas']=[dict(nome='Centro',angulos=[90]*7)]
|
||||
self.arm.save(data)
|
||||
other=Braco(self.arm.path,self.servos)
|
||||
self.assertEqual(other.status()['posturas'],data['posturas'])
|
||||
data['posturas'].append(dict(nome='Inválida',angulos=[180]*7))
|
||||
with self.assertRaises(ValueError): self.arm.save(data)
|
||||
self.assertEqual(len(self.arm.status()['posturas']),1)
|
||||
|
||||
if __name__=='__main__': unittest.main()
|
||||
@@ -0,0 +1,37 @@
|
||||
from playwright.sync_api import sync_playwright
|
||||
import json
|
||||
with sync_playwright() as p:
|
||||
b=p.chromium.launch(headless=True); page=b.new_page(viewport={'width':1400,'height':1000})
|
||||
cfg=dict(disponivel=True,erro='',calibracao=[dict(canal=i,minimo=1400,maximo=1600) for i in range(8)],posturas=[],articulacoes=[dict(nome=n,canal=c) for n,c in zip(['Garra','Punho 1','Punho 2','Braço superior','Braço inferior','Antebraço','Base'],[0,1,2,3,4,5,7])])
|
||||
calls=[]; errors=[]
|
||||
def handle(route):
|
||||
if route.request.method=='POST':
|
||||
data=route.request.post_data_json; calls.append((route.request.url,data))
|
||||
if route.request.url.endswith('/config'): cfg.update(data)
|
||||
route.fulfill(content_type='application/json',body=json.dumps(cfg))
|
||||
page.route('**/braco/status',handle); page.route('**/braco/config',handle); page.route('**/braco/mover',handle); page.route('**/braco/parar',handle)
|
||||
page.on('pageerror',lambda e:errors.append(str(e)))
|
||||
page.goto('http://192.168.1.35:8100/braco');page.wait_for_selector('#joint6')
|
||||
assert page.locator('#sliders input').count()==7
|
||||
assert not page.locator('#enable').is_checked()
|
||||
page.locator('#joint1').evaluate("e=>{e.value=95;e.dispatchEvent(new Event('input'))}");page.wait_for_timeout(200)
|
||||
assert calls==[]
|
||||
page.locator('#enable').check();assert calls==[]
|
||||
page.locator('#joint1').evaluate("e=>{e.value=85;e.dispatchEvent(new Event('input'))}");page.wait_for_timeout(650)
|
||||
moves=[d for u,d in calls if u.endswith('/mover')]
|
||||
assert moves and moves[-1]['angulos'][1]==85,moves
|
||||
assert len(moves[0]['angulos'])==7
|
||||
page.locator('#stop').click();page.wait_for_timeout(200)
|
||||
assert calls[-1][0].endswith('/parar');assert not page.locator('#enable').is_checked()
|
||||
page.on('dialog',lambda d:d.accept('Teste centro'))
|
||||
page.locator('#savePose').click();page.wait_for_timeout(250)
|
||||
assert '1 posturas' in page.locator('#count').inner_text()
|
||||
page.locator('#enable').check();page.locator('#play').click();page.wait_for_timeout(1100)
|
||||
assert 'Sequência concluída' in page.locator('#console').inner_text()
|
||||
page.locator('#stop').click();page.wait_for_timeout(200)
|
||||
page.screenshot(path='C:/Users/JullioPC/Documents/Codex/2026-10-04/o/outputs/carinha-pc/docs/braco-painel.png',full_page=True)
|
||||
page.set_viewport_size({'width':390,'height':844})
|
||||
assert page.evaluate('document.documentElement.scrollWidth<=innerWidth')
|
||||
assert not errors,errors
|
||||
print('OK: sete sliders, opt-in, velocidade, parar, salvar/reproduzir, mobile; comandos simulados')
|
||||
b.close()
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Integration test against a running local server; browser page is the real face."""
|
||||
from playwright.sync_api import sync_playwright
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
import tempfile
|
||||
import threading
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
spec = importlib.util.spec_from_file_location('kiosk_browser', ROOT / 'kiosk-server.py')
|
||||
kiosk = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(kiosk)
|
||||
|
||||
|
||||
def main():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
kiosk.ARQUIVO_APARENCIA = str(Path(tmp) / 'aparencia.json')
|
||||
server = kiosk.Servidor(('127.0.0.1', 0), kiosk.Handler)
|
||||
server.controle = kiosk.ControlePC(Path(tmp) / 'app.json')
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
browser_test('http://127.0.0.1:' + str(server.server_port))
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join()
|
||||
|
||||
|
||||
def browser_test(base):
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
page.goto(base + '/carinha.html?auto=0&camera=0')
|
||||
page.wait_for_selector('#tela')
|
||||
page.request.post(base + '/app/config', data={'volume': 0.65, 'sompausa': 35, 'reagir': False})
|
||||
page.wait_for_timeout(1500)
|
||||
assert page.evaluate('window.eilik.audio.volume') == 0.65, 'Volume remoto não aplicado'
|
||||
assert page.evaluate('window.eilik.ouvido().segSilencio') == 35, 'Sono remoto não aplicado'
|
||||
page.evaluate('window.eilik.movimento(0.8, 0.1, 0.5)')
|
||||
assert page.evaluate('window.eilik.presenca().olharX') == 0, 'Reação desligada precisa manter olhos no centro'
|
||||
page.request.post(base+'/app/config', data={'reagir':True})
|
||||
page.wait_for_timeout(1100)
|
||||
page.evaluate("window.eilik.soltar(); window.eilik.rastrear({tipo:'mao',x:.9,y:.5,atividade:false})")
|
||||
assert page.evaluate('window.eilik.presenca().olharX') > 20, 'Olhos devem seguir alvo em uma amostra'
|
||||
page.evaluate('window.eilik.movimento(.8,.1,.5)')
|
||||
assert page.evaluate('window.eilik.presenca().olharX') > 20, 'Detector simples não pode disputar alvo recente'
|
||||
page.wait_for_timeout(400)
|
||||
page.evaluate('window.eilik.movimento(.8,.1,.5)')
|
||||
assert page.evaluate('window.eilik.presenca().olharX') < 0, 'Alvo expirado deve liberar detector simples'
|
||||
page.evaluate("window.eilik.trata('dormir'); window.eilik.rastrear({tipo:'mao',x:.9,y:.5,atividade:true})")
|
||||
assert page.evaluate('window.eilik.face.dormindo'), 'Sono deve respeitar câmera desativada'
|
||||
page.evaluate('window.eilik.soltar()')
|
||||
page.request.post(base + '/config', data={'rosto': 'miudo', 'cor': [255, 120, 0]})
|
||||
page.wait_for_timeout(800)
|
||||
assert page.evaluate('window.eilik.rostoAtual()') == 'miudo'
|
||||
assert page.evaluate('window.eilik.face.cor') == [255, 120, 0], 'Cor deve persistir em repouso'
|
||||
page.request.post(base + '/app/config', data={'boca':False, 'sobrancelhas':True})
|
||||
page.wait_for_timeout(1200)
|
||||
assert not page.locator('#boca').is_visible(), 'Boca deve poder ser desligada'
|
||||
assert page.locator('#sobrancelha1').is_visible(), 'Sobrancelhas devem poder ser ligadas'
|
||||
panel = browser.new_page(viewport={'width': 1440, 'height': 1000})
|
||||
errors = []
|
||||
panel.on('pageerror', lambda error: errors.append(str(error) + '\n' + error.stack))
|
||||
panel.goto(base + '/painel')
|
||||
panel.wait_for_selector('#gestos button')
|
||||
panel.locator('#ajuste-sensorMic').uncheck()
|
||||
panel.wait_for_timeout(400)
|
||||
assert panel.request.get(base + '/app/config').json()['sensorMic'] is False
|
||||
panel.locator('#ajuste-cameraSono').check()
|
||||
panel.wait_for_timeout(400)
|
||||
panel.locator('#ajuste-cameraLado').select_option('environment')
|
||||
panel.wait_for_timeout(400)
|
||||
c = panel.request.get(base + '/app/config').json()
|
||||
assert c['cameraSono'] is True and c['cameraLado'] == 'environment'
|
||||
panel.get_by_role('button', name='Ligar câmera').click()
|
||||
panel.wait_for_timeout(500)
|
||||
assert panel.request.get(base + '/app/config').json()['preview'] is True
|
||||
panel.get_by_role('button', name='Desligar câmera').click()
|
||||
panel.wait_for_timeout(500)
|
||||
assert panel.request.get(base + '/app/config').json()['preview'] is False
|
||||
assert not errors, errors
|
||||
work = Path(__file__).resolve().parents[3] / 'work'
|
||||
panel.screenshot(path=str(work / 'painel-desktop.png'), full_page=True)
|
||||
panel.set_viewport_size({'width': 390, 'height': 844})
|
||||
panel.screenshot(path=str(work / 'painel-mobile.png'), full_page=True)
|
||||
assert panel.evaluate('document.documentElement.scrollWidth <= innerWidth'), 'Overflow mobile'
|
||||
browser.close()
|
||||
print('Remote config, gesture queue, camera toggle and responsive panel: OK')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,8 @@
|
||||
import unittest
|
||||
from cerebro import validar_intencao
|
||||
class Tests(unittest.TestCase):
|
||||
def test_only_allowlisted_expression(self):
|
||||
self.assertEqual(validar_intencao({'gesto':'feliz'}),'feliz')
|
||||
for data in [{'gesto':'motor 2500'},{'gesto':'feliz','pulso':2500},[],{'gesto':'dormir'}]:
|
||||
with self.assertRaises(ValueError):validar_intencao(data)
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,143 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
spec = importlib.util.spec_from_file_location('kiosk', ROOT / 'kiosk-server.py')
|
||||
kiosk = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(kiosk)
|
||||
|
||||
|
||||
class ControleTest(unittest.TestCase):
|
||||
def test_tracking_controls_are_independent(self):
|
||||
c = json.loads(self.request('/app/config')[1])
|
||||
self.assertTrue(c.get('rastrearRosto'))
|
||||
self.assertTrue(c.get('rastrearMaos'))
|
||||
self.assertTrue(c.get('mostrarRastreamento'))
|
||||
self.assertEqual(self.request('/app/config', {'seguirRastreamento': False, 'rastrearMaos': False})[0],200)
|
||||
c = json.loads(self.request('/app/config')[1])
|
||||
self.assertTrue(c['rastrearRosto'])
|
||||
self.assertFalse(c['rastrearMaos'])
|
||||
|
||||
def test_sensor_and_camera_preferences(self):
|
||||
c = json.loads(self.request('/app/config')[1])
|
||||
self.assertTrue(c.get('sensorMic'))
|
||||
self.assertTrue(c.get('sensorCamera'))
|
||||
self.assertTrue(c.get('micSono'))
|
||||
self.assertFalse(c.get('cameraSono', True))
|
||||
self.assertEqual(c.get('videoResolucao'), 'auto')
|
||||
self.assertEqual(self.request('/app/config', {'sensorMic': False, 'cameraSono': True,
|
||||
'cameraLado': 'environment', 'videoResolucao': '2160', 'videoFPS': 60})[0], 200)
|
||||
self.assertEqual(self.request('/app/config', {'cameraLado': 'invalida'})[0], 400)
|
||||
self.assertEqual(self.request('/app/config', {'videoFPS': 999})[0], 400)
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.server = kiosk.Servidor(('127.0.0.1', 0), kiosk.Handler)
|
||||
if hasattr(kiosk, 'ControlePC'):
|
||||
self.server.controle = kiosk.ControlePC(Path(self.tmp.name) / 'app.json')
|
||||
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
||||
self.thread.start()
|
||||
self.url = 'http://127.0.0.1:%d' % self.server.server_port
|
||||
kiosk.FILA.clear()
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
self.thread.join()
|
||||
self.tmp.cleanup()
|
||||
|
||||
def request(self, route, data=None, kind='application/json'):
|
||||
body = json.dumps(data).encode() if isinstance(data, dict) else data
|
||||
req = urllib.request.Request(self.url + route, data=body, headers={'Content-Type': kind})
|
||||
try:
|
||||
with urllib.request.urlopen(req) as r:
|
||||
return r.status, r.read()
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read()
|
||||
|
||||
def test_config_is_persisted_without_consuming_gestures(self):
|
||||
kiosk.FILA.append('feliz')
|
||||
code, body = self.request('/app/config', {'volume': 0.7, 'sompausa': 40})
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(json.loads(body)['config']['volume'], 0.7)
|
||||
self.assertEqual(self.request('/app/config')[0], 200)
|
||||
self.assertEqual(kiosk.FILA, ['feliz'])
|
||||
self.assertEqual(json.loads((Path(self.tmp.name) / 'app.json').read_text())['sompausa'], 40)
|
||||
|
||||
def test_invalid_configuration_does_not_partially_apply(self):
|
||||
code, _ = self.request('/app/config', {'volume': 0.8, 'sompausa': -2})
|
||||
self.assertEqual(code, 400)
|
||||
code, body = self.request('/app/config')
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(json.loads(body)['volume'], 0.35)
|
||||
|
||||
def test_telemetry_tracks_phone_not_panel(self):
|
||||
self.assertEqual(self.request('/app/status', {'rosto': 'miudo', 'microfone': 0.42})[0], 200)
|
||||
code, body = self.request('/app/status')
|
||||
self.assertEqual(code, 200)
|
||||
self.assertTrue(json.loads(body)['online'])
|
||||
self.assertEqual(json.loads(body)['estado']['rosto'], 'miudo')
|
||||
|
||||
def test_frame_is_hidden_when_disabled_and_after_expiry(self):
|
||||
jpeg = b'\xff\xd8fixture\xff\xd9'
|
||||
self.assertEqual(self.request('/camera/frame', jpeg, 'image/jpeg')[0], 409)
|
||||
self.request('/app/config', {'preview': True})
|
||||
self.assertEqual(self.request('/camera/frame', jpeg, 'image/jpeg')[0], 200)
|
||||
self.assertEqual(self.request('/camera/frame'), (200, jpeg))
|
||||
self.server.controle.frame_at -= 4
|
||||
self.assertEqual(self.request('/camera/frame')[0], 204)
|
||||
self.request('/camera/frame', jpeg, 'image/jpeg')
|
||||
self.request('/app/config', {'preview': False})
|
||||
self.assertEqual(self.request('/camera/frame')[0], 204)
|
||||
|
||||
def test_rejects_non_jpeg_and_unknown_settings(self):
|
||||
self.request('/app/config', {'preview': True})
|
||||
self.assertEqual(self.request('/camera/frame', b'hello', 'image/jpeg')[0], 400)
|
||||
self.assertEqual(self.request('/app/config', {'preview': 'false'})[0], 400)
|
||||
self.assertEqual(self.request('/app/config', {'extra': 1})[0], 400)
|
||||
|
||||
def test_screen_power_settings_are_saved(self):
|
||||
code, body = self.request('/app/config', {'economia': True, 'brilho': 0.65, 'brilhoRepouso': 0.06, 'repousoTela': 30})
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(json.loads(body)['config']['brilhoRepouso'], 0.06)
|
||||
|
||||
def test_server_does_not_expose_source_or_signing_key(self):
|
||||
self.assertEqual(self.request('/controle_pc.py')[0], 404)
|
||||
self.assertEqual(self.request('/android/keystore.jks')[0], 404)
|
||||
|
||||
def test_mouth_and_eyebrows_are_independent(self):
|
||||
code, body = self.request('/app/config', {'boca': False, 'sobrancelhas': True})
|
||||
self.assertEqual(code, 200)
|
||||
self.assertFalse(json.loads(body)['config']['boca'])
|
||||
self.assertTrue(json.loads(body)['config']['sobrancelhas'])
|
||||
|
||||
def test_rtc_signaling_keeps_messages_until_ack(self):
|
||||
self.request('/app/config', {'preview': True})
|
||||
code, body = self.request('/rtc/session', {'acao': 'abrir'})
|
||||
self.assertEqual(code, 200)
|
||||
sid = json.loads(body)['id']
|
||||
code, _ = self.request('/rtc/send', {'id': sid, 'de': 'pc', 'mensagem': {'tipo': 'offer', 'sdp': 'example'}})
|
||||
self.assertEqual(code, 200)
|
||||
code, first = self.request('/rtc/poll?lado=app&desde=0')
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(json.loads(first)['mensagens'][0]['mensagem']['tipo'], 'offer')
|
||||
self.assertEqual(self.request('/rtc/poll?lado=app&desde=0')[1], first)
|
||||
self.assertEqual(json.loads(self.request('/rtc/poll?lado=pc&desde=0')[1])['mensagens'], [])
|
||||
|
||||
def test_rtc_old_session_cannot_inject_into_new_session(self):
|
||||
self.request('/app/config', {'preview': True})
|
||||
old = json.loads(self.request('/rtc/session', {'acao':'abrir'})[1])['id']
|
||||
self.request('/rtc/session', {'acao':'abrir'})
|
||||
self.assertEqual(self.request('/rtc/send', {'id':old, 'de':'app', 'mensagem':{'tipo':'answer','sdp':'old'}})[0], 409)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,50 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from conversa import Memoria, validar_resposta
|
||||
from conversa import resposta_factual
|
||||
from conversa import mensagens_modelo
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
class TestConversa(unittest.TestCase):
|
||||
def test_mudanca_de_assunto_nao_reinjeta_resposta_anterior(self):
|
||||
history=[dict(role='user',content='Por que o céu é azul?'),dict(role='assistant',content='O céu é azul porque a luz passa pela atmosfera.'),dict(role='user',content='Está bravo?')]
|
||||
messages=mensagens_modelo(history,[dict(texto='O usuário se chama Jullio')])
|
||||
self.assertEqual([m['role'] for m in messages],['system','user'])
|
||||
self.assertIn('Está bravo?',messages[-1]['content'])
|
||||
self.assertNotIn('O céu é azul porque',str(messages))
|
||||
self.assertIn('Jullio',messages[0]['content'])
|
||||
def test_contexto_nao_reusa_respostas_antigas_defeituosas(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path=Path(d)/'memoria.db'
|
||||
import sqlite3
|
||||
with sqlite3.connect(path) as db:
|
||||
db.execute('CREATE TABLE mensagens(id INTEGER PRIMARY KEY,papel TEXT,texto TEXT)')
|
||||
db.execute("INSERT INTO mensagens(papel,texto) VALUES ('assistant','Estou brincando com você')")
|
||||
db.close()
|
||||
m=Memoria(path);m.registrar('user','Quanto é 2 + 3?')
|
||||
self.assertEqual(len(m.historico()),2)
|
||||
self.assertEqual(m.historico(contexto=True),[{'role':'user','content':'Quanto é 2 + 3?'}])
|
||||
def test_horario_usa_relogio_e_nao_modelo(self):
|
||||
agora=datetime(2026,10,5,9,42,tzinfo=timezone(timedelta(hours=-3)))
|
||||
self.assertIn('09:42',resposta_factual('Que horas são?',agora)['texto'])
|
||||
self.assertIsNone(resposta_factual('Quanto é 2 + 3?',agora))
|
||||
def test_memoria_persiste_e_apaga(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path=Path(d)/'memoria.db'
|
||||
m=Memoria(path)
|
||||
ident=m.lembrar('Meu nome é Jullio')
|
||||
self.assertEqual(Memoria(path).listar()[0]['texto'],'Meu nome é Jullio')
|
||||
m.apagar(ident)
|
||||
self.assertEqual(m.listar(),[])
|
||||
def test_resposta_limitada_sem_comandos(self):
|
||||
self.assertEqual(validar_resposta({'texto':'Olá!','gesto':'feliz'})['texto'],'Olá!')
|
||||
self.assertEqual(validar_resposta({'texto':'Bom dia, Carinha!','gesto':'feliz'})['texto'],'Bom dia!')
|
||||
for data in ({'texto':'x','gesto':'motor'}, {'texto':'x'*281,'gesto':'feliz'}, {'texto':'','gesto':'feliz'}):
|
||||
with self.assertRaises(ValueError):validar_resposta(data)
|
||||
def test_limpeza_remove_historico_e_memoria(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
m=Memoria(Path(d)/'memoria.db');m.lembrar('Gosto de azul');m.registrar('user','Oi')
|
||||
m.limpar()
|
||||
self.assertEqual(m.historico(),[]);self.assertEqual(m.listar(),[])
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import unittest
|
||||
from figurinhas import validar_desenho
|
||||
class TestFigurinhas(unittest.TestCase):
|
||||
def test_validacao_sem_svg_executavel(self):
|
||||
drawing=dict(nome='Coração',local='olhos',formas=[dict(caminho='M 10 10 L 90 90 Z',cor='#ff78aa')])
|
||||
self.assertEqual(validar_desenho(drawing),drawing)
|
||||
for path in ('<script>alert(1)</script>','M 1 2 L 9999 10','M 1 2 url(x)'):
|
||||
drawing['formas'][0]['caminho']=path
|
||||
with self.assertRaises(ValueError):validar_desenho(drawing)
|
||||
@@ -0,0 +1,21 @@
|
||||
import unittest
|
||||
from gemini import payload_gemini, extrair_resposta
|
||||
|
||||
class TestGemini(unittest.TestCase):
|
||||
def test_catalogo_limita_ids_da_reacao(self):
|
||||
payload=payload_gemini([],[],'Vamos festejar!',[dict(id=7,receita=dict(nome='Festa',motivo='Conquista'))])
|
||||
self.assertEqual(payload['generationConfig']['responseSchema']['properties']['reacao']['enum'],['0','7'])
|
||||
def test_nao_envia_perguntas_sem_resposta_de_chamadas_falhas(self):
|
||||
payload=payload_gemini([{'role':'user','content':'pergunta que falhou'}],[],'pergunta atual')
|
||||
self.assertEqual(len(payload['contents']),1)
|
||||
self.assertEqual(payload['contents'][0]['parts'][0]['text'],'pergunta atual')
|
||||
def test_contexto_e_memoria_sem_chave_no_payload(self):
|
||||
payload=payload_gemini([{'role':'user','content':'Está bravo?'},{'role':'assistant','content':'Não!'}],[{'texto':'Meu nome é Jullio'}],'Oi')
|
||||
self.assertEqual(payload['contents'][-1]['parts'][0]['text'],'Oi')
|
||||
self.assertEqual(payload['contents'][1]['role'],'model')
|
||||
self.assertIn('Jullio',str(payload['systemInstruction']))
|
||||
self.assertNotIn('api_key',str(payload))
|
||||
def test_conteudo_ignora_thought_e_exige_json(self):
|
||||
data={'candidates':[{'content':{'parts':[{'text':'segredo','thought':True},{'text':'{"texto":"Oi!","gesto":"feliz"}'}]}}]}
|
||||
self.assertEqual(extrair_resposta(data),{'texto':'Oi!','gesto':'feliz'})
|
||||
with self.assertRaises(ValueError):extrair_resposta({'candidates':[]})
|
||||
@@ -0,0 +1,41 @@
|
||||
from playwright.sync_api import sync_playwright
|
||||
import json
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
calls = []
|
||||
rows = [dict(nome=f'Servo {i+1}',canal=i,minimo=1400,maximo=1600,centro=1500) for i in range(6)]
|
||||
def handle(route):
|
||||
if route.request.method == 'POST': calls.append((route.request.url, route.request.post_data_json))
|
||||
route.fulfill(content_type='application/json',body=json.dumps(dict(disponivel=True,erro='',servos=rows,ativos=[])))
|
||||
page.route('**/servos/status', handle)
|
||||
page.route('**/servos/mover', handle)
|
||||
page.route('**/servos/parar', handle)
|
||||
page.goto('http://192.168.1.35:8100/servos')
|
||||
page.wait_for_selector('.card')
|
||||
live = page.locator('#live')
|
||||
assert live.count() == 1, 'Modo ao vivo ausente'
|
||||
assert not live.is_checked()
|
||||
slider = page.locator('input[type=range]').first
|
||||
slider.evaluate("e=>{e.value=1450;e.dispatchEvent(new Event('input'))}")
|
||||
page.wait_for_timeout(200)
|
||||
assert calls == []
|
||||
live.check()
|
||||
assert calls == [], 'Ativar modo não deve mover'
|
||||
slider.evaluate("e=>{for(let i=0;i<30;i++){e.value=1400+(i%20)*5;e.dispatchEvent(new Event('input'))}e.value=1600;e.dispatchEvent(new Event('input'))}")
|
||||
page.wait_for_timeout(500)
|
||||
moves = [data for url,data in calls if url.endswith('/mover')]
|
||||
assert 1 <= len(moves) <= 6, len(moves)
|
||||
assert moves[-1] == dict(indice=0,pulso=1600)
|
||||
page.locator('#stop').click()
|
||||
page.wait_for_timeout(200)
|
||||
assert not live.is_checked()
|
||||
assert calls[-1][0].endswith('/parar')
|
||||
count = len(calls)
|
||||
slider.evaluate("e=>{e.value=1400;e.dispatchEvent(new Event('input'))}")
|
||||
page.wait_for_timeout(200)
|
||||
assert len(calls) == count
|
||||
page.locator('input[type=text]').first.fill('Teste')
|
||||
assert live.is_disabled()
|
||||
print('OK: live opt-in, limite de envio, último valor, parada cancela fila, alterações bloqueiam; hardware simulado')
|
||||
browser.close()
|
||||
@@ -0,0 +1,9 @@
|
||||
const assert=require('node:assert/strict');
|
||||
const {sequencias}=require('../movimentos-core.js');
|
||||
const ranges=Array.from({length:7},()=>[81,99]);
|
||||
const moves=sequencias(ranges);
|
||||
assert.equal(moves.length,5);
|
||||
for(const move of moves){assert.ok(move.passos.length>=3);for(const step of move.passos){assert.equal(step.length,7);step.forEach(v=>assert.ok(v>=81&&v<=99));}}
|
||||
assert.ok(moves.some(m=>m.passos.some(s=>s.filter(v=>v!==90).length>=3)));
|
||||
assert.deepEqual(moves[0].passos.at(-1),Array(7).fill(90));
|
||||
console.log('OK: cinco sequências, limites e coordenação');
|
||||
@@ -0,0 +1,28 @@
|
||||
const assert = require('node:assert/strict');
|
||||
let Auto;
|
||||
try { Auto = require('../android/app/assets/qualidade-auto.js'); }
|
||||
catch (e) { assert.fail('Qualidade automática ainda não implementada'); }
|
||||
let q = new Auto();
|
||||
assert.equal(q.level, 0);
|
||||
q.update({fps: 15, bitrate: 400000, rtt: .5});
|
||||
assert.equal(q.level, 1);
|
||||
q.update({fps: 15, bitrate: 350000, rtt: .5});
|
||||
assert.equal(q.level, 2);
|
||||
q.update({fps: 30, bitrate: 3000000, rtt: .02});
|
||||
assert.equal(q.level, 2, 'Uma leitura boa não pode causar oscilação');
|
||||
q.update({fps: 30, bitrate: 3000000, rtt: .02});
|
||||
q.update({fps: 30, bitrate: 3000000, rtt: .02});
|
||||
q.update({fps: 30, bitrate: 3000000, rtt: .02});
|
||||
assert.equal(q.level, 1);
|
||||
assert.equal(q.parameters().maxFramerate, 30);
|
||||
console.log('Qualidade automática: reduz com congestionamento e sobe após estabilidade: OK');
|
||||
assert.equal(typeof Auto.constraints, 'function', 'Seleção de câmera/qualidade ausente');
|
||||
const v = Auto.constraints({cameraLado:'environment',videoResolucao:'2160',videoFPS:60}, {});
|
||||
assert.equal(v.facingMode.exact, 'environment');
|
||||
assert.equal(v.width.exact, 3840);
|
||||
assert.equal(v.frameRate.exact, 60);
|
||||
const maximum = Auto.constraints({videoResolucao:'max',videoFPS:-1}, {width:{max:7680},height:{max:4320},frameRate:{max:120}});
|
||||
assert.equal(maximum.width.ideal, 7680);
|
||||
assert.equal(maximum.frameRate.ideal, 120);
|
||||
assert.equal(maximum.frameRate.max, undefined, 'Sem teto artificial');
|
||||
assert.equal(new Auto(60, 20000000).parameters().maxFramerate, 60);
|
||||
@@ -0,0 +1,19 @@
|
||||
const assert = require('node:assert/strict');
|
||||
let R;
|
||||
try { R = require('../rastreamento-core.js'); } catch(e) { assert.fail('Rastreamento ausente'); }
|
||||
const faces = [{x:.2,y:.1,w:.3,h:.4,score:.9}];
|
||||
const hands = [Array.from({length:21},()=>({x:.8,y:.6}))];
|
||||
assert.equal(R.target(faces,hands).tipo,'mao');
|
||||
assert.equal(R.target(faces,[]).tipo,'rosto');
|
||||
assert.equal(R.target([],[]),null);
|
||||
assert.equal(R.target([{x:NaN,y:0,w:1,h:1}],[]),null);
|
||||
const rect = R.fit(1000,500,640,480);
|
||||
assert.ok(Math.abs(rect.x - (1000/2 - (500*640/480)/2)) < 1e-9);
|
||||
assert.equal(rect.y,0);
|
||||
assert.equal(R.point({x:0,y:0},rect).x,rect.x);
|
||||
const gate = new R.FreshTarget();
|
||||
gate.accept({tipo:'mao',x:.8,y:.5,atividade:true},100);
|
||||
assert.equal(gate.get(200).x,.8);
|
||||
assert.equal(gate.get(451),null,'Alvo antigo precisa expirar');
|
||||
assert.equal(gate.accept({x:Infinity,y:0},500),false);
|
||||
console.log('Rastreamento: prioridade, coordenadas, validação e expiração OK');
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Real WebRTC sender and panel in Chromium, using a synthetic camera."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
spec = importlib.util.spec_from_file_location('kiosk_rtc', ROOT / 'kiosk-server.py')
|
||||
kiosk = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(kiosk)
|
||||
|
||||
|
||||
class Handler(kiosk.Handler):
|
||||
def do_GET(self):
|
||||
assets = {'/test-sender': 'transmissao.html', '/qualidade-auto.js': 'qualidade-auto.js'}
|
||||
if self.path in assets:
|
||||
path = ROOT / 'android/app/assets' / assets[self.path]
|
||||
return self.enviar(path.read_bytes(), 'text/html' if self.path == '/test-sender' else 'text/javascript')
|
||||
return super().do_GET()
|
||||
|
||||
|
||||
def main():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
server = kiosk.Servidor(('127.0.0.1', 0), Handler)
|
||||
server.controle = kiosk.ControlePC(Path(tmp) / 'config.json')
|
||||
server.controle.atualizar({'preview': True,'rastrearRosto':False,'rastrearMaos':False})
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
base = 'http://127.0.0.1:' + str(server.server_port)
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, args=['--use-fake-device-for-media-stream=fps=30','--use-fake-ui-for-media-stream'])
|
||||
context = browser.new_context()
|
||||
sender = context.new_page()
|
||||
errors = []
|
||||
sender.on('pageerror', lambda e: errors.append(str(e)))
|
||||
sender.add_init_script("""
|
||||
window.CarinhaRTC = {
|
||||
enviar(json) { fetch('/rtc/send',{method:'POST',headers:{'Content-Type':'application/json'},body:json}).catch(console.error); },
|
||||
movimento() {},
|
||||
rastrear(json) { window.testAlvo=JSON.parse(json); },
|
||||
estado(json) {
|
||||
fetch('/app/status',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({rosto:'carinha',video:JSON.parse(json)})}).catch(console.error);
|
||||
}
|
||||
};
|
||||
""")
|
||||
sender.goto(base + '/test-sender')
|
||||
sender.evaluate("""() => {
|
||||
let id = '', cursor = 0, busy = false;
|
||||
window.testPoll = setInterval(async () => {
|
||||
if (busy) return; busy = true;
|
||||
try {
|
||||
const packet = await (await fetch('/rtc/poll?lado=app&desde='+cursor+'&id='+id)).json();
|
||||
if ((packet.id||'') !== id) { cursor=0; id=packet.id||''; }
|
||||
packet.config = await (await fetch('/app/config')).json();
|
||||
window.rtcReceber(packet);
|
||||
for (const msg of packet.mensagens) cursor=Math.max(cursor,msg.seq);
|
||||
} finally { busy=false; }
|
||||
},200);
|
||||
}""")
|
||||
sender.request.post(base + '/app/status', data={'rosto':'carinha'})
|
||||
panel = context.new_page()
|
||||
panel.on('pageerror', lambda e: errors.append(str(e)))
|
||||
panel.goto(base + '/painel')
|
||||
panel.wait_for_function("document.getElementById('cameraVideo').videoWidth > 0", timeout=20000)
|
||||
first = panel.evaluate("document.getElementById('cameraVideo').getVideoPlaybackQuality().totalVideoFrames")
|
||||
panel.wait_for_timeout(2200)
|
||||
count = panel.evaluate("document.getElementById('cameraVideo').getVideoPlaybackQuality().totalVideoFrames") - first
|
||||
assert count >= 45, 'Vídeo não está fluido: %s quadros em 2.2 s' % count
|
||||
assert panel.locator('#cameraVideo').is_visible()
|
||||
assert 'fps' in panel.locator('#videoQualidade').inner_text()
|
||||
assert not errors, errors
|
||||
print('Vídeo WebRTC real: %s quadros em 2.2 s; %s' % (count, panel.locator('#videoQualidade').inner_text()))
|
||||
sender.request.post(base+'/app/config',data={'rastrearMaos':True})
|
||||
sender.wait_for_function('config.rastrearMaos === true')
|
||||
panel.wait_for_function('CarinhaVideo.rastreamentoConectado()')
|
||||
panel.evaluate("CarinhaVideo.rastrear({tipo:'mao',x:.8,y:.3,atividade:true})")
|
||||
sender.wait_for_function("window.testAlvo?.tipo === 'mao' && testAlvo.x === .8")
|
||||
panel.evaluate('CarinhaVideo.rastrear(null)')
|
||||
sender.wait_for_function('window.testAlvo === null')
|
||||
sender.request.post(base+'/app/config',data={'rastrearMaos':False})
|
||||
panel.locator('#ajuste-videoResolucao').select_option('480')
|
||||
sender.wait_for_function("stream && stream.getVideoTracks()[0].getSettings().height === 480", timeout=10000)
|
||||
panel.locator('#ajuste-videoFPS').select_option('240')
|
||||
sender.wait_for_function("aviso.includes('indisponível')", timeout=10000)
|
||||
assert sender.evaluate('stream.getVideoTracks()[0].readyState') == 'live'
|
||||
panel.locator('#ajuste-sensorCamera').uncheck()
|
||||
panel.wait_for_function("document.getElementById('cameraVideo').srcObject === null", timeout=10000)
|
||||
sender.wait_for_function('stream === null', timeout=10000)
|
||||
panel.locator('#ajuste-sensorCamera').check()
|
||||
panel.wait_for_function("document.getElementById('cameraVideo').videoWidth > 0 && document.getElementById('cameraVideo').srcObject !== null", timeout=15000)
|
||||
assert not errors, errors
|
||||
panel.get_by_role('button', name='Desligar câmera').click()
|
||||
panel.wait_for_timeout(1400)
|
||||
assert panel.evaluate("document.getElementById('cameraVideo').srcObject === null")
|
||||
preview = context.new_page()
|
||||
preview.on('pageerror', lambda e: errors.append(str(e)))
|
||||
preview.goto(base + '/camera.html?lado=user&resolucao=480&fps=30')
|
||||
preview.wait_for_function("document.getElementById('video').videoHeight === 480", timeout=10000)
|
||||
assert not errors, errors
|
||||
browser.close()
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from servos import ControleServos, PCA
|
||||
|
||||
class Fake:
|
||||
def __init__(self): self.calls = []
|
||||
def pulse(self, channel, value): self.calls.append((channel, value))
|
||||
def off(self, channel): self.calls.append((channel, None))
|
||||
|
||||
class ServoTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.hw = Fake()
|
||||
self.c = ControleServos(Path(self.tmp.name) / 'servos.json', self.hw)
|
||||
def tearDown(self):
|
||||
self.c.close()
|
||||
self.tmp.cleanup()
|
||||
def test_open_does_not_move(self):
|
||||
self.assertEqual(len(self.c.status()['servos']), 6)
|
||||
self.assertEqual(self.hw.calls, [])
|
||||
def test_limits_and_types(self):
|
||||
for pulse in [1399, 1601, True, float('nan')]:
|
||||
with self.assertRaises(ValueError): self.c.move(0, pulse)
|
||||
self.assertEqual(self.hw.calls, [])
|
||||
def test_valid_pulse_and_stop(self):
|
||||
self.c.move(0, 1500)
|
||||
self.assertEqual(self.hw.calls[-1], (0, 1500))
|
||||
self.c.stop()
|
||||
self.assertIn((0, None), self.hw.calls)
|
||||
def test_duplicate_channel_and_atomic_config(self):
|
||||
config = self.c.status()['servos']
|
||||
config[1]['canal'] = 0
|
||||
with self.assertRaises(ValueError): self.c.save(config)
|
||||
self.assertEqual(self.c.status()['servos'][1]['canal'], 1)
|
||||
def test_expiry_turns_off(self):
|
||||
self.c.move(1, 1500)
|
||||
self.c.expire(float('inf'))
|
||||
self.assertEqual(self.hw.calls[-1], (1, None))
|
||||
def test_config_persists_and_supports_sixteen(self):
|
||||
rows = [dict(nome=f'Motor {i}',canal=i,minimo=1400,maximo=1600,centro=1500) for i in range(16)]
|
||||
self.c.save(rows)
|
||||
other = ControleServos(self.c.path, self.hw)
|
||||
try: self.assertEqual(other.status()['servos'], rows)
|
||||
finally: other.close()
|
||||
def test_frequency_initialization_disables_all_old_outputs(self):
|
||||
hw = PCA.__new__(PCA)
|
||||
hw.ready = False
|
||||
writes = []
|
||||
hw.write = lambda reg, values: writes.append((reg, values))
|
||||
hw.read = lambda reg: 0x11
|
||||
hw.pulse(1, 1500)
|
||||
release = next(i for i, value in enumerate(writes) if value[0] == 10)
|
||||
for channel in range(16):
|
||||
self.assertIn((9 + 4*channel, [0x10]), writes[:release])
|
||||
self.assertIn((0xFE, [121]), writes)
|
||||
self.assertIn((10, [0, 0, 51, 1]), writes)
|
||||
def test_global_register_cannot_overwrite_pulse_or_enable_other_channels(self):
|
||||
hw = PCA.__new__(PCA)
|
||||
hw.ready = False
|
||||
registers = {}
|
||||
def write(reg, values):
|
||||
for offset, value in enumerate(values):
|
||||
address = reg + offset
|
||||
registers[address] = value
|
||||
if 0xFA <= address <= 0xFD:
|
||||
for channel in range(16):
|
||||
registers[6 + 4*channel + address-0xFA] = value
|
||||
hw.write = write
|
||||
hw.read = lambda reg: 0x21
|
||||
hw.pulse(1, 1500)
|
||||
self.assertEqual(registers[12] + ((registers[13] & 15) << 8), 307)
|
||||
for channel in range(16):
|
||||
if channel != 1: self.assertTrue(registers[9 + 4*channel] & 16)
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Inferencia MediaPipe real, com modelos locais e imagens oficiais de teste."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
FIXTURES=ROOT.parents[1]/'work/tracking-fixtures'
|
||||
sys.path.insert(0,str(ROOT))
|
||||
spec=importlib.util.spec_from_file_location('kiosk_tracking',ROOT/'kiosk-server.py')
|
||||
kiosk=importlib.util.module_from_spec(spec);spec.loader.exec_module(kiosk)
|
||||
|
||||
class Handler(kiosk.Handler):
|
||||
def do_GET(self):
|
||||
if self.path in ('/test-portrait','/test-hands'):
|
||||
name='portrait.jpg' if self.path=='/test-portrait' else 'right_hands.jpg'
|
||||
return self.enviar((FIXTURES/name).read_bytes(),'image/jpeg')
|
||||
super().do_GET()
|
||||
|
||||
def main():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
server=kiosk.Servidor(('127.0.0.1',0),Handler)
|
||||
server.controle=kiosk.ControlePC(Path(tmp)/'config.json')
|
||||
thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start()
|
||||
base='http://127.0.0.1:'+str(server.server_port)
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
browser=p.chromium.launch(headless=True)
|
||||
page=browser.new_page();errors=[];external=[]
|
||||
page.on('pageerror',lambda e: errors.append(str(e)))
|
||||
page.on('console',lambda m: print('WORKER',m.text[:180]) if m.type=='error' else None)
|
||||
def route(r):
|
||||
if not r.request.url.startswith(base): external.append(r.request.url);r.abort()
|
||||
else:r.continue_()
|
||||
page.route('**/*',route)
|
||||
page.goto(base+'/painel')
|
||||
results=page.evaluate("""async () => {
|
||||
const worker=new Worker('/rastreador-worker.js');
|
||||
const send=msg=>new Promise((resolve,reject)=>{
|
||||
worker.onmessage=({data})=>data.tipo==='erro'?reject(Error(data.erro)):resolve(data);
|
||||
worker.onerror=e=>reject(Error(e.message));
|
||||
worker.postMessage(msg,msg.bitmap?[msg.bitmap]:[]);
|
||||
});
|
||||
const config={rosto:true,maos:true};
|
||||
await send({config});
|
||||
const a=await createImageBitmap(await (await fetch('/test-portrait')).blob());
|
||||
const face=await send({config,bitmap:a,timestamp:1000});
|
||||
const b=await createImageBitmap(await (await fetch('/test-hands')).blob());
|
||||
const hand=await send({config,bitmap:b,timestamp:2000});
|
||||
worker.terminate();return {face,hand};
|
||||
}""")
|
||||
assert results['face']['faces'], 'Rosto da imagem oficial nao detectado'
|
||||
assert results['hand']['hands'], 'Maos da imagem oficial nao detectadas'
|
||||
assert len(results['hand']['hands'][0])==21
|
||||
print('Inferencia real: %s rosto(s), %s mao(s); %.1f/%.1f ms' % (
|
||||
len(results['face']['faces']),len(results['hand']['hands']),results['face']['ms'],results['hand']['ms']))
|
||||
# Feed the same real worker into the panel, using a local canvas stream.
|
||||
page.request.post(base+'/app/config',data={'preview':True})
|
||||
page.request.post(base+'/app/status',data={'rosto':'carinha','dormindo':False})
|
||||
page.evaluate("""async () => {
|
||||
const image=await createImageBitmap(await (await fetch('/test-hands')).blob());
|
||||
const c=document.createElement('canvas');c.width=640;c.height=Math.round(640*image.height/image.width);
|
||||
const ctx=c.getContext('2d');ctx.drawImage(image,0,0,c.width,c.height);image.close();
|
||||
window.testStream=c.captureStream(30);
|
||||
window.testDraw=setInterval(()=>ctx.drawImage(c,0,0),33);
|
||||
const v=document.getElementById('cameraVideo');v.srcObject=testStream;v.hidden=false;await v.play();
|
||||
window.testTargets=[];
|
||||
window.CarinhaVideo={rastrear(t){testTargets.push(t)},parar(){},atualizar(){}};
|
||||
window.CarinhaRastreamento.atualizar({online:true,estado:{}}, {preview:true,sensorCamera:true,
|
||||
rastrearRosto:true,rastrearMaos:true,mostrarRastreamento:true,seguirRastreamento:true});
|
||||
}""")
|
||||
page.wait_for_function("window.CarinhaRastreamento.estado().result?.hands.length > 0",timeout=15000)
|
||||
assert page.locator('#trackingOverlay').is_visible()
|
||||
assert page.evaluate("testTargets.some(t=>t?.tipo==='mao')")
|
||||
work=ROOT.parents[1]/'work';page.screenshot(path=str(work/'rastreamento-teste.png'),full_page=True)
|
||||
page.locator('#ajuste-mostrarRastreamento').uncheck()
|
||||
page.wait_for_function("document.getElementById('trackingOverlay').hidden",timeout=5000)
|
||||
page.locator('#ajuste-seguirRastreamento').uncheck()
|
||||
page.wait_for_timeout(1200)
|
||||
assert page.evaluate('testTargets[testTargets.length-1] === null')
|
||||
page.evaluate("CarinhaRastreamento.atualizar({online:true,estado:{dormindo:true}},{preview:true,sensorCamera:true,cameraSono:false,rastrearRosto:true,rastrearMaos:true})")
|
||||
assert not page.evaluate('CarinhaRastreamento.estado().active')
|
||||
assert not external,external
|
||||
assert not errors,errors
|
||||
browser.close()
|
||||
print('Overlay, alvo, controles, sono e inferencia sem acesso externo: OK')
|
||||
finally:
|
||||
server.shutdown();server.server_close();thread.join()
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,30 @@
|
||||
import unittest,tempfile
|
||||
from pathlib import Path
|
||||
from treinamento import Treinamento,validar_receita
|
||||
|
||||
class TestTreinamento(unittest.TestCase):
|
||||
def test_automatico_so_gera_apos_hora_com_atividade_nova(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
t=Treinamento(Path(d)/'treino.db',iniciar=False);inicio=t.last
|
||||
with t.db() as db:db.execute("INSERT INTO config VALUES ('automatico','true')")
|
||||
calls=[];t.gerar=lambda:calls.append(True)
|
||||
self.assertFalse(t.verificar_hora(inicio+3601))
|
||||
t.registrar('user','Uma nova conversa')
|
||||
self.assertFalse(t.verificar_hora(inicio+3599))
|
||||
self.assertTrue(t.verificar_hora(inicio+3601))
|
||||
self.assertFalse(t.verificar_hora(inicio+7202))
|
||||
self.assertEqual(len(calls),1)
|
||||
def test_receita_rejeita_codigo_e_movimentos_fora_limite(self):
|
||||
step=dict(olhos='feliz',boca='sorriso',cor='#5cd6ff',dx=0,dy=0,escala=1,duracao=.5)
|
||||
recipe=dict(nome='Alegria',motivo='Conquista',simbolo='estrela',legenda='Boa!',passos=[step])
|
||||
self.assertEqual(validar_receita(recipe)['nome'],'Alegria')
|
||||
with self.assertRaises(ValueError):validar_receita(dict(recipe,codigo='alert(1)'))
|
||||
step['dx']=1000
|
||||
with self.assertRaises(ValueError):validar_receita(recipe)
|
||||
def test_rotina_diario_persistem(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
t=Treinamento(Path(d)/'treino.db',iniciar=False)
|
||||
t.rotina('Gosto de música');t.registrar('user','Ouvi uma música')
|
||||
saved=Treinamento(Path(d)/'treino.db',iniciar=False).status()
|
||||
self.assertEqual(saved['rotina'][0]['texto'],'Gosto de música')
|
||||
self.assertEqual(saved['diario'][0]['texto'],'Ouvi uma música')
|
||||
Reference in New Issue
Block a user