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:
@@ -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 já é 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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user