feat: Estúdio de Cenas em Camadas - IA desmonta imagem em elementos editáveis individualmente no canvas

This commit is contained in:
2026-06-08 10:34:41 +00:00
parent f16c1dc286
commit 837bda8c88
5 changed files with 484 additions and 7 deletions
+134
View File
@@ -1555,6 +1555,140 @@ 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.' });
}
try {
console.log('[Decompose] Iniciando engenharia reversa do tema:', tema);
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..."
}
]
}`;
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();
jsonResponse = JSON.parse(text);
} catch (err) {
console.error('[Decompose] Erro ao obter JSON do Minimax:', err);
}
if (!jsonResponse || !jsonResponse.backgroundPrompt || !jsonResponse.elements) {
jsonResponse = {
backgroundPrompt: `empty scene backdrop for ${tema}, cartoon vector style, no people, no animals, no text`,
elements: [
{ name: "Elemento Principal", prompt: `${tema}, cartoon vector style, isolated on a solid white background, sticker style, 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 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.`,
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;
};
// 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 layers = jsonResponse.elements.map((el, index) => {
return {
name: el.name,
url: elementUrls[index],
left: 20 + index * 25,
top: 35,
width: 30,
height: 30
};
}).filter(l => l.url !== null);
res.json({
success: true,
backgroundUrl,
layers
});
} catch (err) {
console.error('[Decompose] Erro no desmembramento:', err);
res.status(500).json({ error: 'Erro ao desmembrar a imagem: ' + err.message });
}
});
app.get('/api/cartazes/list', requireAuth, async (req, res) => {
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try {