From 6372f99275244c247c52cce0857a1595bae1a1f9 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Wed, 3 Jun 2026 14:01:44 +0000 Subject: [PATCH] feat: estrutura banco de dados para Multi-Tenancy com usuario_id e suporta roteiro continuo na IA --- scratch/show_tables.js | 38 +++++++++++++ server.js | 123 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 scratch/show_tables.js diff --git a/scratch/show_tables.js b/scratch/show_tables.js new file mode 100644 index 0000000..b10a672 --- /dev/null +++ b/scratch/show_tables.js @@ -0,0 +1,38 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL +}); + +async function main() { + try { + const tablesRes = await pool.query(` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'escola' + ORDER BY table_name; + `); + + console.log('--- TABELAS NO ESQUEMA escola ---'); + for (const row of tablesRes.rows) { + console.log(`\nTabela: ${row.table_name}`); + const columnsRes = await pool.query(` + SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_schema = 'escola' AND table_name = $1 + ORDER BY ordinal_position; + `, [row.table_name]); + + for (const col of columnsRes.rows) { + console.log(` - ${col.column_name}: ${col.data_type} (null: ${col.is_nullable}, default: ${col.column_default})`); + } + } + } catch (err) { + console.error('Erro ao ler tabelas:', err); + } finally { + await pool.end(); + } +} + +main(); diff --git a/server.js b/server.js index 8a75005..10e4839 100644 --- a/server.js +++ b/server.js @@ -307,7 +307,7 @@ Retorne APENAS a letra formatada em versos com quebras de linha. Não adicione n // 6. Retornar dados para o frontend res.json({ success: true, - lyrics: lyrics, + lyrics: lyrics, audioUrl: audioUrl }); @@ -323,14 +323,58 @@ Retorne APENAS a letra formatada em versos com quebras de linha. Não adicione n // 1. Gerar o roteiro (script) dos quadrinhos usando MiniMax-M3 app.post('/api/comics/generate-script', requireAuth, async (req, res) => { - const { tema, quantidade, baloes, proporcao, personagens, cenario } = req.body; + const { + tema, + quantidade, + baloes, + proporcao, + personagens, + cenario, + existingPanels, + character_description, + continuationType // 'extend' ou 'new_story' + } = req.body; if (!tema) { return res.status(400).json({ error: 'O tema dos quadrinhos é obrigatório.' }); } try { - const prompt = `Crie uma história em quadrinhos pedagógica infantil com o tema: "${tema}". + let prompt = ''; + if (existingPanels && Array.isArray(existingPanels) && existingPanels.length > 0) { + const startPanelNum = continuationType === 'extend' ? existingPanels.length + 1 : 1; + + prompt = `Você está continuando ou se baseando em um projeto de história em quadrinhos existente. +DADOS DO PROJETO EXISTENTE: +- Descrição visual física e de estilo constante dos personagens (em inglês): "${character_description || ''}" +- Painéis já existentes (roteiro anterior): +${existingPanels.map(p => ` * Quadrinho ${p.panel_number || p.panelNumber}: [Visual] ${p.image_prompt || p.imagePrompt || 'N/A'} | [Fala/Legenda] ${p.dialogue}`).join('\n')} + +INSTRUÇÕES DE GERAÇÃO: +Tipo de continuidade: ${continuationType === 'extend' ? 'Estender a mesma história (continuação cronológica direta, criando novos painéis sequenciais)' : 'Criar uma nova história separada (spin-off com os mesmos personagens, recomeçando do painel 1)'}. +Tema/Objetivo da nova fase: "${tema}". +Gere exatamente ${quantidade} novos quadrinhos. +Baloes de falas: ${baloes ? 'Sim, cada quadrinho deve conter um diálogo/fala extremamente conciso e curto, contendo no máximo 10 palavras no total.' : 'Não, a história é silenciosa, sem falas, apenas a descrição da cena'}. +Proporção visual: ${proporcao}. +Cenário/Local principal da nova fase: ${cenario}. + +Gere um JSON perfeitamente válido contendo: +{ + "title": "${continuationType === 'extend' ? 'Manter o título original ou propor um subtítulo condizente' : 'Novo Título da História'}", + "character_description": "${character_description || 'Descrição visual constante dos personagens em inglês'}", + "panels": [ + { + "panel_number": ${startPanelNum}, + "image_prompt": "Prompt visual detalhado em inglês para o gerador de imagens da MiniMax descrevendo a cena deste quadrinho. IMPORTANTE: Use enquadramento afastado (wide angle, medium shot, characters seen from a distance) para deixar espaço livre no topo da imagem para o texto. O estilo deve ser de ilustração de livro infantil de alta qualidade, 3D Pixar, traço limpo. Você DEVE incluir e respeitar estritamente a descrição visual física de cada personagem participante neste quadrinho conforme definido nos dados do projeto existente acima, para evitar qualquer alucinação de roupas, cores ou visual físico.", + "dialogue": "Falas do quadrinho em português (ex: 'Lucas: Veja aquilo!') com NO MÁXIMO 10 palavras, ou narração curta caso não tenha balões." + }, + ... + ] +} + +Responda APENAS o JSON válido, sem qualquer texto explicativo antes ou depois.`; + } else { + prompt = `Crie uma história em quadrinhos pedagógica infantil com o tema: "${tema}". A história deve conter exatamente ${quantidade} quadrinhos. Baloes de falas: ${baloes ? 'Sim, cada quadrinho deve conter um diálogo/fala extremamente conciso e curto, contendo no máximo 10 palavras no total.' : 'Não, a história é silenciosa, sem falas, apenas a descrição da cena'}. Proporção visual: ${proporcao}. @@ -352,6 +396,7 @@ Gere um JSON perfeitamente válido contendo: } Responda APENAS o JSON válido, sem qualquer texto explicativo antes ou depois.`; + } const r = await callMinimax({ system: "Você é um roteirista pedagógico especialista em histórias em quadrinhos infantis para educação básica. Você responde apenas com estruturas JSON válidas.", @@ -627,7 +672,77 @@ function writeObsIndex(idx) { // Inicializar banco de dados e migrar dados se estiver vazio async function initDatabase() { try { - // 0. Garantir que a tabela de modelos exista e tenha dados padrão + // Garantir extensões UUID no banco + try { + await dbPool.query('CREATE EXTENSION IF NOT EXISTS "pgcrypto";'); + await dbPool.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'); + } catch (extErr) { + console.warn('[Database Init] Aviso ao habilitar extensões UUID (pode requerer privilégios de superusuário):', extErr.message); + } + + // 0. Estruturar banco de dados para Multi-Tenancy (Individualização por Professora) + // Garantir que as tabelas existentes tenham a coluna usuario_id + await dbPool.query(` + ALTER TABLE escola.modelos ADD COLUMN IF NOT EXISTS usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'; + ALTER TABLE escola.observacoes ADD COLUMN IF NOT EXISTS usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'; + ALTER TABLE escola.conhecimento ADD COLUMN IF NOT EXISTS usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'; + ALTER TABLE escola.config ADD COLUMN IF NOT EXISTS usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'; + `); + + // Criar tabelas novas de conversas (histórico de chat) e criações (quadrinhos) com usuario_id + await dbPool.query(` + -- Conversas + CREATE TABLE IF NOT EXISTS escola.conversas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', + titulo VARCHAR(255) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + + -- Mensagens + CREATE TABLE IF NOT EXISTS escola.mensagens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + conversa_id UUID REFERENCES escola.conversas(id) ON DELETE CASCADE, + sender VARCHAR(50) NOT NULL, -- 'user' ou 'assistant' + content TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + + -- Projetos de HQ (Fábrica de Quadrinhos) + CREATE TABLE IF NOT EXISTS escola.projetos_comics ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', + titulo VARCHAR(255) NOT NULL, + tema TEXT, + cenario TEXT, + proporcao VARCHAR(10) DEFAULT '16:9', + character_description_global TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + + -- Painéis individuais de cada HQ + CREATE TABLE IF NOT EXISTS escola.comics_panels ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + projeto_id UUID REFERENCES escola.projetos_comics(id) ON DELETE CASCADE, + panel_number INTEGER NOT NULL, + image_url TEXT NOT NULL, + image_prompt TEXT, + dialogue TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + + -- Biblioteca de personagens persistentes para reuso + CREATE TABLE IF NOT EXISTS escola.comics_characters ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', + nome VARCHAR(100) NOT NULL, + descricao_fisica_en TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + `); + + // 0.1. Garantir que a tabela de modelos exista e tenha dados padrão await dbPool.query(` CREATE TABLE IF NOT EXISTS escola.modelos ( id SERIAL PRIMARY KEY,