feat: estrutura banco de dados para Multi-Tenancy com usuario_id e suporta roteiro continuo na IA
This commit is contained in:
@@ -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();
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user