feat: Adicionado gerenciador de camadas para a geração de cartazes e resolvido bug de resize

This commit is contained in:
2026-06-08 11:23:51 +00:00
parent cf5a46c86c
commit 002218fe13
3 changed files with 441 additions and 100 deletions
+94 -85
View File
@@ -1556,46 +1556,103 @@ Return ONLY the English prompt, no extra chat.`
});
app.post('/api/cartazes/decompose-scene', requireAuth, async (req, res) => {
const { tema, titulo } = req.body;
if (!tema) {
return res.status(400).json({ error: 'Tema é obrigatório.' });
}
const { tema, titulo, layers_manual } = req.body;
try {
console.log('[Decompose] Iniciando engenharia reversa do tema:', tema);
console.log('[Decompose] Recebida requisição:', { hasLayersManual: !!layers_manual, tema });
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
// Função interna para traduzir uma descrição PT→EN e gerar a imagem
const translateAndGenerate = async (descPT, isBackground) => {
let engPrompt = descPT;
try {
const refineContent = isBackground
? `Translate this Portuguese description of a scene BACKGROUND into a clean English image generation prompt.
The background must be COMPLETELY EMPTY — no people, no animals, no characters. Just the environment/scenery.
Specify cartoon vector illustration style, vibrant colors, kid-friendly, no text.
Portuguese: "${descPT}"
Return ONLY the English prompt, nothing else.`
: `Translate this Portuguese description of a single character or object into a clean English image generation prompt.
The element must be COMPLETELY ISOLATED on a PURE WHITE background (#FFFFFF), full body visible, no shadows, no background details.
Specify 3D Pixar cartoon sticker style, soft pastel colors, no text.
Portuguese: "${descPT}"
Return ONLY the English prompt, nothing else.`;
const r = await callMinimax({ messages: [{ role: 'user', content: refineContent }], temperature: 0.2, max_tokens: 150 });
if (r?.text) engPrompt = r.text.trim();
} catch (e) { console.error('[Decompose] Translate error:', e.message); }
try {
const suffix = isBackground
? 'Empty background scene, no people, no animals, cartoon illustration, vibrant colors, no text.'
: 'Full body visible, pure solid white background #FFFFFF, no shadows, no gradients, 3D Pixar sticker clipart style, no text.';
const genResp = await fetch(`${minmBase}/v1/image_generation`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'image-01', prompt: `${engPrompt}. ${suffix}`, n: 1 })
});
if (genResp.ok) {
const genData = await genResp.json();
const rawUrl = genData.data?.image_urls?.[0];
if (rawUrl) {
const imgFetch = await fetch(rawUrl);
if (imgFetch.ok) {
const buf = Buffer.from(await imgFetch.arrayBuffer());
const fname = `layer_${Date.now()}_${Math.floor(Math.random()*1000)}.jpg`;
const mediaDir = require('path').join(__dirname, 'public', 'generated-media');
if (!require('fs').existsSync(mediaDir)) require('fs').mkdirSync(mediaDir, { recursive: true });
const fpath = require('path').join(mediaDir, fname);
require('fs').writeFileSync(fpath, buf);
await backupMediaFile(fpath, buf);
return `/generated-media/${fname}`;
}
}
}
} catch (e) { console.error('[Decompose] Generate error:', e.message); }
return null;
};
// MODO NOVO: layers_manual enviado pelo layer manager
if (layers_manual && Array.isArray(layers_manual) && layers_manual.length > 0) {
const bgLayerDef = layers_manual.find(l => l.isBackground);
const elementLayerDefs = layers_manual.filter(l => !l.isBackground);
if (!bgLayerDef) return res.status(400).json({ error: 'Nenhuma camada de fundo definida.' });
console.log('[Decompose] Modo Manual - gerando', layers_manual.length, 'camadas em paralelo...');
// Gerar tudo em paralelo
const [backgroundUrl, ...elementUrls] = await Promise.all([
translateAndGenerate(bgLayerDef.description, true),
...elementLayerDefs.map(el => translateAndGenerate(el.description, false))
]);
const layers = elementLayerDefs.map((el, i) => ({
name: el.description,
url: elementUrls[i],
left: 10 + i * 25, top: 35, width: 30, height: 30
})).filter(l => l.url);
return res.json({ success: true, backgroundUrl, layers });
}
// MODO LEGADO: campo tema (compatibilidade)
if (!tema) return res.status(400).json({ error: 'Tema ou layers_manual são obrigatórios.' });
const agentConfig = readAgentConfig();
const agentName = agentConfig.agentName || "Kemily";
const segmentPrompt = `Você é a ${agentName}, assistente pedagógica.
Dada a descrição de uma imagem de cartaz infantil:
Tema: "${tema}"
Título: "${titulo || ''}"
Por favor, divida essa cena em componentes em camadas para que possamos montar um Canvas interativo.
Eu preciso de:
1. Um prompt para a imagem de fundo (background) sem NENHUM personagem, pessoa ou animal. Deve ser apenas o cenário de fundo vazio (ex: "an empty lawn inside a park with some trees under a blue sky, cartoon vector illustration style, no people, no animals, no text").
2. Uma lista de 2 a 3 elementos individuais importantes da cena que serão colocados como adesivos (stickers) por cima. Cada elemento deve ser descrito de forma isolada com fundo branco sólido (ex: "two happy cartoon kids holding hands, isolated on a solid white background, sticker style, no text").
Retorne a resposta estritamente no formato JSON abaixo, sem blocos de código markdown (\`\`\`json ou \`\`\$) e sem nenhum texto explicativo extra:
{
"backgroundPrompt": "descrição do fundo em inglês...",
"elements": [
{
"name": "Nome do Elemento (Ex: Crianças)",
"prompt": "descrição do elemento em inglês isolado em fundo branco..."
}
]
}`;
Tema: "${tema}" / Título: "${titulo || ''}"
Divida em: 1 fundo vazio + 2-3 elementos stickers (fundo branco puro).
Retorne JSON: { "backgroundPrompt":"...", "elements":[{"name":"...","prompt":"..."}] }`;
let jsonResponse = null;
try {
const chatRes = await callMinimax({
messages: [{ role: 'user', content: segmentPrompt }],
temperature: 0.2,
max_tokens: 800
});
let text = chatRes.text.trim();
text = text.replace(/```json|```/g, '').trim();
const chatRes = await callMinimax({ messages: [{ role: 'user', content: segmentPrompt }], temperature: 0.2, max_tokens: 600 });
let text = chatRes.text.trim().replace(/```json|```/g, '').trim();
jsonResponse = JSON.parse(text);
} catch (err) {
console.error('[Decompose] Erro ao obter JSON do Minimax:', err);
@@ -1603,68 +1660,20 @@ Retorne a resposta estritamente no formato JSON abaixo, sem blocos de código ma
if (!jsonResponse || !jsonResponse.backgroundPrompt || !jsonResponse.elements) {
jsonResponse = {
backgroundPrompt: `empty scene backdrop for ${tema}, cartoon vector style, no people, no animals, no text`,
backgroundPrompt: `${tema}, empty scene backdrop, cartoon vector style, no people, no animals, no text`,
elements: [
{ name: "Elemento Principal", prompt: `${tema}, cartoon sticker clipart style, completely isolated on a pure white background, no shadows, no background elements, no text` }
]
};
}
console.log('[Decompose] Planejamento da cena em camadas:', jsonResponse);
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
// Função para gerar uma imagem individual via Minimax
const generateAndDownload = async (promptText) => {
try {
console.log('[Decompose] Gerando imagem para o prompt:', promptText);
const genResp = await fetch(`${minmBase}/v1/image_generation`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'image-01',
prompt: `${promptText}. Complete full-body figure visible from head to toe without any cutoffs. Pure solid white background #FFFFFF, no shadows, no gradients, no gray areas, no drop shadows, no environment, kid-friendly 3D Pixar illustration style, soft pastel colors on the character only, no text, no words, no letters, no writing, no labels.`,
n: 1
})
});
if (genResp.ok) {
const genData = await genResp.json();
const rawImgUrl = genData.data?.image_urls?.[0];
if (rawImgUrl) {
const imgFetch = await fetch(rawImgUrl);
if (imgFetch.ok) {
const imgBuffer = Buffer.from(await imgFetch.arrayBuffer());
const fileName = `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}.jpg`;
const mediaDir = path.join(__dirname, 'public', 'generated-media');
if (!fs.existsSync(mediaDir)) fs.mkdirSync(mediaDir, { recursive: true });
const filePath = path.join(mediaDir, fileName);
fs.writeFileSync(filePath, imgBuffer);
await backupMediaFile(filePath, imgBuffer);
return `/generated-media/${fileName}`;
}
}
}
} catch (err) {
console.error('[Decompose] Erro na geração de camada:', err);
}
return null;
};
console.log('[Decompose] Planejamento da cena em camadas (Legado):', jsonResponse);
// Gerar fundo e stickers paralelamente
const promises = [];
promises.push(generateAndDownload(jsonResponse.backgroundPrompt));
for (const el of jsonResponse.elements) {
promises.push(generateAndDownload(el.prompt));
}
const results = await Promise.all(promises);
const backgroundUrl = results[0];
const elementUrls = results.slice(1);
const [backgroundUrl, ...elementUrls] = await Promise.all([
translateAndGenerate(jsonResponse.backgroundPrompt, true),
...jsonResponse.elements.map(el => translateAndGenerate(el.prompt, false))
]);
const layers = jsonResponse.elements.map((el, index) => {
return {