Modernizacao completa do Estudio de Cartazes Pedagogicos: correcao de abertura, BNCC, upload de fotos, novo motor de IA, edicao direta e suite de exportacao

This commit is contained in:
2026-09-03 10:41:49 +00:00
parent fe6d392005
commit 1e9d80b69c
5 changed files with 1016 additions and 394 deletions
+212 -75
View File
@@ -2147,65 +2147,120 @@ app.delete('/api/poesias/:id', requireAuth, async (req, res) => {
// ROTAS DO CRIAR CARTAZES
// ============================================================
app.post('/api/cartazes/generate', requireAuth, async (req, res) => {
const { titulo, tema, corFundo, corTexto, layout, promptImagem, tamanho, orientacao } = req.body;
const {
titulo,
tema,
subtitulo,
rodape,
faixaEtaria,
campoBncc,
tomPedagogico,
turmaId,
corFundo,
corTexto,
layout,
promptImagem,
tamanho,
orientacao,
estiloCards
} = req.body;
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
if (!titulo || !tema || !layout) {
return res.status(400).json({ error: 'Título, tema e layout são obrigatórios.' });
if (!titulo || !tema) {
return res.status(400).json({ error: 'Título e tema/foco são obrigatórios.' });
}
try {
const agentConfig = readAgentConfig();
const agentName = agentConfig.agentName || "PedagogIA";
// 1. Gerar Conteúdo do Cartaz
const posterPrompt = `Você é a ${agentName}, assistente pedagógica da professora Camila Martella Gasparini Reifonas.
A professora Camila quer criar um Cartaz para sala de aula da Educação Infantil.
Detalhes do Cartaz:
- Título: "${titulo}"
- Tema / Foco Pedagógico: "${tema}"
- Tipo de Cartaz: "${layout}" (educativo, informativo, conscientizacao, cultural)
- Tamanho: "${tamanho || 'A4'}"
- Orientação: "${orientacao === 'paisagem' ? 'Paisagem/Horizontal' : 'Retrato/Vertical'}"
Por favor, gere o CONTEÚDO textual e estrutural do cartaz em HTML.
REGRAS DE IDIOMA E LOCALIZAÇÃO:
- O conteúdo gerado deve ser IMPRETERIVELMENTE em português do Brasil (pt-BR). Não use termos em outros idiomas de forma desnecessária.
REGRAS FUNDAMENTAIS E ESTÉTICAS DE DIAGRAMAÇÃO:
- O cartaz deve caber ESTRITAMENTE em UMA ÚNICA PÁGINA FÍSICA (Tamanho ${tamanho || 'A4'}, orientação ${orientacao === 'paisagem' ? 'Horizontal/Paisagem' : 'Vertical/Retrato'}). Não extrapole este espaço de forma alguma.
- Limite a quantidade de conteúdo: gere no máximo 2 ou 3 cards (divs de conteúdo) no total. Cada card deve ter pouquíssimo texto (no máximo 3 ou 4 itens curtos por card).
- Distribua a informação em CARDS (pequenos cartões ou blocos curtos) bem espaçados, parecidos com pôsteres infantis modernos, onde o texto NÃO ocupa uma folha corrida.
- Os textos devem ser extremamente curtos, diretos, visuais e fáceis de ler.
- Use emojis apropriados para crianças.
- Se for "educativo", estruture o conteúdo pedagógico de forma visual (ex: caixas para alfabeto, tabuada, regras gramaticais).
- Se for "informativo", destaque avisos gerais e orientações (ex: horários, cardápio, provas) em caixas curtas e legíveis.
- Se for "conscientizacao", use frases curtas de impacto sobre campanhas (ex: prevenção, saúde, coleta seletiva), acompanhadas de dicas em pequenos blocos.
- Se for "cultural", promova a atividade de lazer, esporte ou feira destacando o evento de forma animada e pontual.
- Considere a orientação ${orientacao === 'paisagem' ? 'Paisagem (horizontal)' : 'Retrato (vertical)'} ao organizar o fluxo: para paisagem, os cards devem ficar lado a lado (2 colunas horizontais); para retrato, eles se empilham verticalmente.
- Retorne apenas o formato HTML (use <div>, <p>, <ul>, <li>, <strong>, <span>). Não adicione <html> ou <body>, retorne apenas o conteúdo interno. Não inclua o título <h1> pois ele é colocado no topo pela interface.`;
let conteudoHtml = '';
// Tentar MiniMax M3
try {
console.log('[Cartazes] Gerando conteúdo com MiniMax...');
const result = await callMinimax({
messages: [{ role: 'user', content: posterPrompt }],
temperature: 0.7,
max_tokens: 1500
});
conteudoHtml = result.text;
} catch (err) {
console.error('[Cartazes] Erro MiniMax:', err.message);
// Buscar contexto da turma se fornecido
let turmaContexto = '';
if (turmaId) {
try {
const tRes = await dbPool.query('SELECT nome, ano_letivo, periodo FROM escola.turmas WHERE id = $1', [turmaId]);
if (tRes.rows.length > 0) {
const t = tRes.rows[0];
turmaContexto = `Turma Escolar: "${t.nome}" (${t.ano_letivo || '2026'}, período ${t.periodo || 'integral'})`;
}
} catch (tErr) {
console.warn('[Cartazes] Aviso ao buscar turma:', tErr.message);
}
}
// Fallback OpenRouter
if (!conteudoHtml && process.env.OPENROUTER_API_KEY) {
// 1. Gerar Conteúdo do Cartaz com Inteligência Pedagógica Avançada
const posterPrompt = `Você é a ${agentName}, assistente pedagógica de excelência da professora Camila Martella Gasparini Reifonas, especialista em Educação Infantil e Diretrizes da BNCC.
A professora Camila está criando um Cartaz Pedagógico de alta qualidade para a sala de aula da Educação Infantil.
DIRETRIZES DO PEDIDO:
- Título do Cartaz: "${titulo}"
- Tema / Foco Pedagógico: "${tema}"
- Subtítulo Pretendido: "${subtitulo || ''}"
- Faixa Etária: "${faixaEtaria || 'Educação Infantil (2 a 5 anos)'}"
- Campo de Experiência / BNCC: "${campoBncc || 'Convivência e Aprendizagem'}"
- Tom Pedagógico: "${tomPedagogico || 'Acolhedor, afetuoso e lúdico'}"
${turmaContexto ? `- Contexto: ${turmaContexto}` : ''}
- Formato Físico: Tamanho ${tamanho || 'A4'}, Orientação ${orientacao === 'paisagem' ? 'Paisagem/Horizontal' : 'Retrato/Vertical'}
- Tipo de Layout: "${layout || 'educativo'}" (educativo, combinados, rotina, informativo, conscientizacao, cultural)
REGRAS DE CONTEÚDO E DIAGRAMAÇÃO:
1. IDIOMA: Português do Brasil (pt-BR) impecável, carinhoso, claro e adequado para crianças e famílias.
2. ESPAÇO FÍSICO: O cartaz deve caber perfeitamente em UMA ÚNICA PÁGINA. Portanto, seja sintético, impactante e visual.
3. ESTRUTURA DE CARDS:
- Gere entre 3 a 5 itens/cards principais.
- Cada card DEVE começar com um emoji temático atrativo (ex: 🤝, 🌟, 🧸, 🧼, 🍎, 🎨, 📚).
- Frases curtas, afirmativas, positivas e de fácil memorização pelas crianças.
4. RETORNO OBRIGATÓRIO EM FORMATO JSON:
Responda ESTRITAMENTE com um objeto JSON válido no seguinte formato:
{
"titulo": "Título refinado e encantador para o topo do cartaz",
"subtitulo": "Subtítulo explicativo ou pedagógico (ex: faixa etária / campo BNCC)",
"badge": "Tag ou selo curto (ex: 🌟 BNCC • EI02EO ou 💡 Dica da Semana)",
"cards": [
"🤝 Frase ou combinado 1 com emoji inicial",
"🧸 Frase ou combinado 2 com emoji inicial",
"🧼 Frase ou combinado 3 com emoji inicial",
"👂 Frase ou combinado 4 com emoji inicial"
],
"rodape": "Mensagem carinhosa de fechamento no rodapé",
"promptImagem": "Sugestão detalhada em inglês para ilustrar este cartaz em estilo 3D Pixar ou vetor infantil colorido, SEM NENHUM TEXTO ESCRITO"
}
Não inclua comentários nem texto fora do bloco JSON.`;
let generatedData = null;
let conteudoHtml = '';
// Tentar chamada com MiniMax M3 (ou fallback automático em callMinimax)
try {
console.log('[Cartazes] Gerando cartaz com IA pedagógica...');
const result = await callMinimax({
messages: [{ role: 'user', content: posterPrompt }],
temperature: 0.6,
max_tokens: 1500
});
const rawText = result.text.trim();
// Tentar extrair JSON
const jsonMatch = rawText.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
generatedData = JSON.parse(jsonMatch[0]);
} catch (e) {
console.warn('[Cartazes] Erro ao parsear JSON da IA, usando fallback:', e.message);
}
}
if (!generatedData) {
conteudoHtml = rawText.replace(/```html|```/g, '').trim();
}
} catch (err) {
console.error('[Cartazes] Erro ao chamar IA principal:', err.message);
}
// Fallback OpenRouter se necessário
if (!generatedData && !conteudoHtml && process.env.OPENROUTER_API_KEY) {
try {
console.log('[Cartazes] Fallback OpenRouter...');
console.log('[Cartazes] Acionando fallback OpenRouter...');
const r = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
@@ -2215,50 +2270,86 @@ REGRAS FUNDAMENTAIS E ESTÉTICAS DE DIAGRAMAÇÃO:
body: JSON.stringify({
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
messages: [{ role: 'user', content: posterPrompt }],
temperature: 0.7,
temperature: 0.6,
max_tokens: 1500
})
});
if (r.ok) {
const data = await r.json();
conteudoHtml = data.choices?.[0]?.message?.content || '';
const raw = data.choices?.[0]?.message?.content || '';
const match = raw.match(/\{[\s\S]*\}/);
if (match) {
generatedData = JSON.parse(match[0]);
} else {
conteudoHtml = raw.replace(/```html|```/g, '').trim();
}
}
} catch (err) {
console.error('[Cartazes] Erro OpenRouter:', err.message);
console.error('[Cartazes] Erro OpenRouter fallback:', err.message);
}
}
// Se temos JSON estruturado, montamos os campos e o HTML diagramado
const finalTitulo = generatedData?.titulo || titulo;
const finalSubtitulo = generatedData?.subtitulo || subtitulo || tema;
const finalBadge = generatedData?.badge || (campoBncc ? `📚 ${campoBncc}` : '');
const finalCards = Array.isArray(generatedData?.cards) && generatedData.cards.length > 0
? generatedData.cards
: [
`🌟 ${tema}`,
`🤝 Vivência lúdica e cooperativa em sala`,
`✨ Cuidamos e aprendemos juntos todos os dias`
];
const finalRodape = generatedData?.rodape || rodape || (turmaContexto ? `${turmaContexto} • Professora Camila` : 'Educando com afeto, empatia e compromisso');
const finalPromptSugerido = generatedData?.promptImagem || promptImagem || `Cute children learning happily about ${tema}, 3D Pixar style, colorful, warm lighting, no text`;
// Montar HTML estilizado caso a interface use conteúdo rico
if (!conteudoHtml) {
conteudoHtml = `<h2>${titulo}</h2><p>Tema: ${tema}</p><p>Não foi possível gerar com IA. Edite o conteúdo livremente.</p>`;
const isLandscape = orientacao === 'paisagem';
const gridStyle = isLandscape
? 'display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; width: 100%;'
: 'display: flex; flex-direction: column; gap: 10px; width: 100%;';
const cardsHtml = finalCards.map(c => `
<div style="background: rgba(255, 255, 255, 0.45); backdrop-filter: blur(6px); border: 2px solid rgba(0, 0, 0, 0.08); border-radius: 12px; padding: 12px 16px; font-size: 1.05rem; font-weight: 600; display: flex; align-items: center; gap: 10px; box-shadow: 0 3px 8px rgba(0,0,0,0.04);">
<span>${c}</span>
</div>
`).join('');
conteudoHtml = `
${finalSubtitulo ? `<div style="text-align: center; font-size: 1.15rem; font-weight: 600; opacity: 0.9; margin-bottom: 12px; letter-spacing: 0.3px;">${finalSubtitulo}</div>` : ''}
<div style="${gridStyle}">${cardsHtml}</div>
${finalRodape ? `<div style="margin-top: auto; padding-top: 14px; text-align: center; font-size: 0.85rem; font-weight: 600; opacity: 0.75; border-top: 1px dashed rgba(0,0,0,0.15);">${finalRodape}</div>` : ''}
`.trim();
}
// Clean HTML from backticks
conteudoHtml = conteudoHtml.replace(/```html|```/g, '').trim();
// 2. Gerar Imagem Ilustrativa se solicitado
// 2. Gerar Imagem Ilustrativa com IA se solicitado
let generatedImageUrl = null;
if (promptImagem && promptImagem.trim().length > 3) {
const promptParaGerar = promptImagem?.trim() || (generatedData?.promptImagem ? generatedData.promptImagem : null);
if (promptParaGerar && promptParaGerar.length > 3) {
try {
console.log('[Cartazes] Refinando prompt de imagem para inglês (sem texto)...');
let refinedPrompt = promptImagem;
console.log('[Cartazes] Refinando prompt de imagem para estilo infantil e garantindo ZERO texto...');
let refinedPrompt = promptParaGerar;
try {
const refineRes = await callMinimax({
messages: [{
role: 'user',
content: `Translate the following Portuguese description into a clean English description suitable for an image generation prompt.
Make sure the prompt specifies a beautiful, colorful, kid-friendly vector/illustration style.
CRITICAL REQUIREMENT: Explicitly specify in the prompt that there should be absolutely no text, no words, no signs, no letters, no labels, no speech bubbles, and no writing of any kind.
Portuguese description: "${promptImagem}"
Return ONLY the English prompt, no extra chat.`
content: `Translate and refine this image request into an ultra-clean English prompt for an image generator.
Description: "${promptParaGerar}"
Subject context: Classroom poster for early childhood education (${tema}).
Art style: Friendly 3D Pixar character cartoon, vibrant soft colors, high resolution.
CRITICAL SAFETY INSTRUCTION: There must be ABSOLUTELY NO TEXT, no letters, no words, no signs, no speech bubbles, and no captions anywhere in the image.
Return ONLY the refined English prompt.`
}],
temperature: 0.3,
max_tokens: 250
temperature: 0.2,
max_tokens: 200
});
if (refineRes && refineRes.text) {
refinedPrompt = refineRes.text;
refinedPrompt = refineRes.text.replace(/["\n]/g, ' ').trim();
}
} catch (refineErr) {
console.error('[Cartazes] Erro ao refinar prompt:', refineErr);
console.error('[Cartazes] Erro ao refinar prompt:', refineErr.message);
}
console.log('[Cartazes] Gerando ilustração com prompt final:', refinedPrompt);
@@ -2271,7 +2362,7 @@ Return ONLY the English prompt, no extra chat.`
},
body: JSON.stringify({
model: 'image-01',
prompt: `${refinedPrompt}. Complete figures shown from top to bottom without cutoffs, kid-friendly illustration style, 3D Pixar, soft pastel colors, no text, no words, no letters, no writing, no labels.`,
prompt: `${refinedPrompt}, complete joyful composition, children book illustration, 3D Pixar, soft pastel lighting, clean background, no text, no words, no letters, no watermark.`,
n: 1
})
});
@@ -2293,9 +2384,12 @@ Return ONLY the English prompt, no extra chat.`
generatedImageUrl = `/generated-media/${fileName}`;
}
}
} else {
const errBody = await genResp.text();
console.warn('[Cartazes] Falha na API de imagem MiniMax:', genResp.status, errBody);
}
} catch (imgErr) {
console.error('[Cartazes] Erro na geração de imagem:', imgErr);
console.error('[Cartazes] Erro na geração de imagem:', imgErr.message);
}
}
@@ -2303,17 +2397,22 @@ Return ONLY the English prompt, no extra chat.`
const dbRes = await dbPool.query(
`INSERT INTO escola.cartazes (usuario_id, titulo, tema, cor_fundo, cor_texto, layout, conteudo, imagem_url)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at`,
[usuarioId, titulo, tema, corFundo || '#ffffff', corTexto || '#1e293b', layout, conteudoHtml, generatedImageUrl]
[usuarioId, finalTitulo, tema, corFundo || '#fff9eb', corTexto || '#2d3748', layout || 'educativo', conteudoHtml, generatedImageUrl]
);
res.json({
success: true,
id: dbRes.rows[0].id,
titulo,
titulo: finalTitulo,
subtitulo: finalSubtitulo,
badge: finalBadge,
cards: finalCards,
rodape: finalRodape,
promptImagem: finalPromptSugerido,
tema,
corFundo,
corTexto,
layout,
corFundo: corFundo || '#fff9eb',
corTexto: corTexto || '#2d3748',
layout: layout || 'educativo',
conteudo: conteudoHtml,
imagemUrl: generatedImageUrl,
createdAt: dbRes.rows[0].created_at
@@ -2325,6 +2424,44 @@ Return ONLY the English prompt, no extra chat.`
}
});
// Rota para upload direto de imagem/foto do professor para o cartaz
app.post('/api/cartazes/upload-image', requireAuth, async (req, res) => {
const { base64Data } = req.body;
if (!base64Data) {
return res.status(400).json({ error: 'Nenhuma imagem fornecida.' });
}
try {
const matches = base64Data.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
if (!matches || matches.length !== 3) {
return res.status(400).json({ error: 'Formato de imagem base64 inválido.' });
}
const imageBuffer = Buffer.from(matches[2], 'base64');
let fileExt = matches[1].split('/')[1] || 'png';
if (fileExt === 'jpeg') fileExt = 'jpg';
const mediaDir = path.join(__dirname, 'public', 'generated-media');
if (!fs.existsSync(mediaDir)) {
fs.mkdirSync(mediaDir, { recursive: true });
}
const fileName = `cartaz_upload_${Date.now()}_${Math.floor(Math.random() * 1000)}.${fileExt}`;
const filePath = path.join(mediaDir, fileName);
fs.writeFileSync(filePath, imageBuffer);
await backupMediaFile(filePath, imageBuffer);
res.json({
success: true,
imageUrl: `/generated-media/${fileName}`
});
} catch (err) {
console.error('[Cartazes Upload Image] Erro:', err);
res.status(500).json({ error: 'Falha ao salvar imagem enviada: ' + err.message });
}
});
app.post('/api/cartazes/decompose-scene', requireAuth, async (req, res) => {
const { tema, titulo, layers_manual } = req.body;