feat(criacao): estúdio para criar expressões, atividades e figurinhas com IA
Menu "Criar com IA" (Ao vivo › Gestos e Reações) com grade igual à dos favoritos. Para cada pedido escolhe-se o tipo (expressão, atividade ou figurinha) e a IA: Gemini, IA local (qualquer servidor compatível com OpenAI na rede, como o ROG Ally com LM Studio/llama-server) ou o Qwen do S10. - A IA não escreve código: responde uma receita de formato fixo (schema JSON) montada só com peças que o rosto já anima. O servidor valida, limita números e descarta peças desconhecidas antes de chegar ao celular. - Reaproveita o que existe: o prompt leva exemplos reais dos gestos e atividades atuais, e "Basear em" manda a receita de um gesto, atividade ou criação para a IA fazer uma variação. - Expressões viram sequências como as de sequencias(), com mãos e espiral. - Atividades usam uma cena genérica com o bonequinho (poses, pernas, pulo, balanço) e os objetos das cenas feitas à mão (bola, livro, xícara, notas, estrelas, coração, Zzz, chama, bolha, halter, microfone, controle, pincel, flor), com movimentos pular/girar/subir/orbitar. - Figurinhas reaproveitam o desenho e a biblioteca de reações. - A estrela põe expressões e atividades criadas nos favoritos (e nas teclas 1 a 9); a IA local é configurada na própria tela, com teste. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""Estúdio de criação: receitas validadas, prompt com exemplos/base, IA local (servidor falso) e tocar."""
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
import criacao as mod # noqa: E402
|
||||
from criacao import ATIVIDADES_BASE, EXPRESSOES_BASE, Criacao, montar_prompt, validar_atividade, validar_expressao # noqa: E402
|
||||
|
||||
RESPOSTAS = {} # o servidor falso devolve o que o teste colocar aqui
|
||||
PEDIDOS = []
|
||||
|
||||
|
||||
class IaFalsa(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
corpo = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
|
||||
PEDIDOS.append(dict(corpo=corpo, auth=self.headers.get('Authorization')))
|
||||
tipo = 'expressao' if 'passos' in corpo['response_format']['json_schema']['schema']['properties'] else 'atividade'
|
||||
texto = '<think>pensando...</think>\n```json\n' + json.dumps(RESPOSTAS[tipo]) + '\n```'
|
||||
dados = json.dumps(dict(choices=[dict(message=dict(content=texto))])).encode()
|
||||
self.send_response(200); self.send_header('Content-Type', 'application/json'); self.send_header('Content-Length', str(len(dados)))
|
||||
self.end_headers(); self.wfile.write(dados)
|
||||
|
||||
def do_GET(self):
|
||||
dados = json.dumps(dict(data=[dict(id='qwen-teste')])).encode()
|
||||
self.send_response(200); self.send_header('Content-Length', str(len(dados))); self.end_headers(); self.wfile.write(dados)
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
|
||||
class CriacaoTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.c = Criacao(Path(self.tmp.name) / 'c.db', Path(self.tmp.name) / 'c.json')
|
||||
self.ordens = []
|
||||
self.c.enfileirar = self.ordens.append
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def esperar(self):
|
||||
for _ in range(100):
|
||||
if not self.c.ocupado:
|
||||
return
|
||||
time.sleep(0.05)
|
||||
self.fail('criação não terminou')
|
||||
|
||||
def test_valida_e_limita_expressao(self):
|
||||
r = validar_expressao(dict(nome='x' * 80, emoji='<script>', maos='voar', espiral='sim', passos=[
|
||||
dict(forma='feliz', boca='sorriso', cor='roxo', dx=99, dy=-99, esc=5, ab=-1, duracao=50),
|
||||
dict(forma='inventada', boca='sorriso', cor='rosa', dx=1, dy=1, esc=1, ab=1, duracao=5)]))
|
||||
self.assertEqual(len(r['nome']), 30)
|
||||
self.assertEqual(r['emoji'], '✨')
|
||||
self.assertEqual((r['maos'], r['espiral']), ('nenhuma', False))
|
||||
p = r['passos'][0]
|
||||
self.assertEqual((p['cor'], p['dx'], p['dy'], p['esc'], p['ab']), ('escolhida', 24, -12, 1.3, 0))
|
||||
self.assertEqual(r['passos'][1]['forma'], 'aberto')
|
||||
self.assertLessEqual(sum(x['duracao'] for x in r['passos'][:2]), 6.01) # encolheu para 6 s
|
||||
self.assertEqual(r['passos'][-1]['forma'], 'aberto') # volta ao normal
|
||||
with self.assertRaises(ValueError):
|
||||
validar_expressao(dict(passos=[]))
|
||||
for base in EXPRESSOES_BASE.values(): # os exemplos são válidos
|
||||
self.assertEqual(len(validar_expressao(base)['passos']), len(base['passos']))
|
||||
|
||||
def test_valida_atividade(self):
|
||||
r = validar_atividade(dict(nome='Malabarismo', emoji='🤹', postura='voando', ritmo=99, braco_esquerdo=dict(de=-500, ate=900),
|
||||
objetos=[dict(tipo='bola', onde='maos', movimento='pular', quantidade=9), dict(tipo='foguete')] * 3))
|
||||
self.assertEqual(r['postura'], 'em_pe')
|
||||
self.assertEqual(r['ritmo'], 3)
|
||||
self.assertEqual(r['braco_esquerdo'], dict(de=-30, ate=180))
|
||||
self.assertEqual(len(r['objetos']), 3) # o foguete não existe: sai
|
||||
self.assertEqual(r['objetos'][0]['quantidade'], 5)
|
||||
for base in ATIVIDADES_BASE.values():
|
||||
validar_atividade(base)
|
||||
|
||||
def test_prompt_reaproveita_o_que_existe(self):
|
||||
p = montar_prompt('expressao', 'desconfiado', EXPRESSOES_BASE['timido'])
|
||||
self.assertIn('Exemplos reais que já existem', p)
|
||||
self.assertIn('Comemorar', p) # exemplo real
|
||||
self.assertIn('Parta desta receita', p)
|
||||
self.assertIn('Tímido', p) # a base escolhida
|
||||
self.assertIn('"desconfiado"', p)
|
||||
a = montar_prompt('atividade', 'malabarismo')
|
||||
self.assertIn('halter', a)
|
||||
self.assertNotIn('Parta desta receita', a)
|
||||
|
||||
def test_ia_local_cria_e_toca(self):
|
||||
srv = ThreadingHTTPServer(('127.0.0.1', 0), IaFalsa)
|
||||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||
try:
|
||||
self.c.configurar(dict(local=dict(url='http://127.0.0.1:%d' % srv.server_port, modelo='qwen-teste', chave='segredo')))
|
||||
self.assertTrue(self.c.provedores()['local']['pronto'])
|
||||
self.assertIn('qwen-teste', self.c.testar('local')['mensagem'])
|
||||
RESPOSTAS['expressao'] = dict(nome='Desconfiado', emoji='🤨', maos='nenhuma', espiral=False, passos=[
|
||||
dict(forma='bravo', boca='neutra', cor='escolhida', dx=-12, dy=2, esc=0.95, ab=0.6, duracao=0.4),
|
||||
dict(forma='bravo', boca='neutra', cor='escolhida', dx=12, dy=2, esc=0.95, ab=0.6, duracao=0.8)])
|
||||
self.c.criar('expressao', 'desconfiado', 'local', base='timido')
|
||||
with self.assertRaises(ValueError):
|
||||
self.c.criar('expressao', 'outro', 'local') # um de cada vez
|
||||
self.esperar()
|
||||
self.assertEqual(self.c.erro, '')
|
||||
self.assertEqual(PEDIDOS[-1]['auth'], 'Bearer segredo')
|
||||
self.assertIn('Tímido', PEDIDOS[-1]['corpo']['messages'][1]['content']) # a base foi junto
|
||||
item = self.c.lista()[0]
|
||||
self.assertEqual((item['tipo'], item['nome'], item['ia']), ('expressao', 'Desconfiado', 'local'))
|
||||
self.assertTrue(self.ordens[-1].startswith('expressao_criada {')) # tocou na hora
|
||||
RESPOSTAS['atividade'] = dict(ATIVIDADES_BASE['malhar'], nome='Malabarismo', emoji='🤹')
|
||||
self.c.criar('atividade', 'malabarismo', 'local', base='criado:%d' % item['id']) # base de outro tipo: ignora
|
||||
self.esperar()
|
||||
self.assertTrue(self.ordens[-1].startswith('atividade_criada {'))
|
||||
self.assertEqual(json.loads(self.ordens[-1][17:])['nome'], 'Malabarismo')
|
||||
self.c.apagar(item['id'])
|
||||
self.assertEqual([i['nome'] for i in self.c.lista()], ['Malabarismo'])
|
||||
finally:
|
||||
srv.shutdown()
|
||||
|
||||
def test_figurinha_vai_para_a_biblioteca_de_reacoes(self):
|
||||
class TreinoFalso:
|
||||
lock = threading.RLock()
|
||||
expressao = None
|
||||
self.c.treinamento = TreinoFalso()
|
||||
desenho = dict(nome='Sol', local='olhos', formas=[dict(caminho='M 10 10 L 90 10 L 50 90 Z', cor='#ffcc00')])
|
||||
mod.os.environ['GEMINI_API_KEY'] = 'teste'
|
||||
original = mod.gemini.gerar_json
|
||||
mod.gemini.gerar_json = lambda payload: dict(desenho, emoji='☀️')
|
||||
try:
|
||||
self.c.criar('figurinha', 'um sol', 'gemini')
|
||||
self.esperar()
|
||||
finally:
|
||||
mod.gemini.gerar_json = original
|
||||
del mod.os.environ['GEMINI_API_KEY']
|
||||
self.assertEqual(self.c.erro, '')
|
||||
self.assertEqual(self.c.lista()[0]['emoji'], '☀️')
|
||||
self.assertEqual(self.c.treinamento.expressao['receita']['figurinha']['nome'], 'Sol')
|
||||
|
||||
def test_erros_claros(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.c.criar('expressao', 'x', 'local') # sem endereço
|
||||
with self.assertRaises(ValueError):
|
||||
self.c.configurar(dict(local=dict(url='ftp://nada')))
|
||||
with self.assertRaises(ValueError):
|
||||
self.c.criar('robo', 'x', 'gemini')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user