✨ feat: Adicionado gerenciador de camadas para a geração de cartazes e resolvido bug de resize
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user