✨ feat: Estúdio de Cenas em Camadas - IA desmonta imagem em elementos editáveis individualmente no canvas
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user