feat: estrutura banco de dados para Multi-Tenancy com usuario_id e suporta roteiro continuo na IA

This commit is contained in:
2026-06-03 14:01:44 +00:00
parent e55f02faa3
commit 6372f99275
2 changed files with 157 additions and 4 deletions
+38
View File
@@ -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();
+119 -4
View File
@@ -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 // 6. Retornar dados para o frontend
res.json({ res.json({
success: true, success: true,
lyrics: lyrics, lyrics: lyrics,
audioUrl: audioUrl 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 // 1. Gerar o roteiro (script) dos quadrinhos usando MiniMax-M3
app.post('/api/comics/generate-script', requireAuth, async (req, res) => { 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) { if (!tema) {
return res.status(400).json({ error: 'O tema dos quadrinhos é obrigatório.' }); return res.status(400).json({ error: 'O tema dos quadrinhos é obrigatório.' });
} }
try { 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. 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'}. 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}. 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.`; Responda APENAS o JSON válido, sem qualquer texto explicativo antes ou depois.`;
}
const r = await callMinimax({ 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.", 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 // Inicializar banco de dados e migrar dados se estiver vazio
async function initDatabase() { async function initDatabase() {
try { 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(` await dbPool.query(`
CREATE TABLE IF NOT EXISTS escola.modelos ( CREATE TABLE IF NOT EXISTS escola.modelos (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,