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
+326 -4
View File
@@ -7601,13 +7601,335 @@ document.addEventListener('DOMContentLoaded', () => {
let dragStartWidth, dragStartHeight;
let activeAssetTab = 'postit';
// --- ESTÚDIO DE CENAS EM CAMADAS ---
// --- ESTÚDIO DE CENAS EM CAMADAS (novo layer manager) ---
const btnDecomposeScene = document.getElementById('btnDecomposeScene');
const sceneDecomposePrompt = document.getElementById('sceneDecomposePrompt');
const btnAddSceneLayer = document.getElementById('btnAddSceneLayer');
const sceneLayerManagerList = document.getElementById('sceneLayerManagerList');
const sceneDecomposeLoader = document.getElementById('sceneDecomposeLoader');
const sceneDecomposeStatus = document.getElementById('sceneDecomposeStatus');
const sceneLayersList = document.getElementById('sceneLayersList');
const sceneLayersItems = document.getElementById('sceneLayersItems');
// Estado das camadas
let sceneLayers = [];
let sceneLayerCounter = 0;
// Cria uma linha de camada no painel
function createSceneLayerRow(id, isBackground) {
const row = document.createElement('div');
row.dataset.layerId = id;
row.style.cssText = `display:flex; align-items:center; gap:6px; padding:6px 8px;
background:rgba(255,255,255,0.04); border:1px solid rgba(139,92,246,0.2);
border-radius:8px; transition: border-color 0.2s;`;
const badge = document.createElement('div');
badge.style.cssText = `min-width:22px; height:22px; border-radius:50%;
background:${isBackground ? 'rgba(16,163,127,0.3)' : 'rgba(139,92,246,0.3)'};
display:flex; align-items:center; justify-content:center; font-size:0.7rem; font-weight:700;
color:${isBackground ? '#10a37f' : '#a78bfa'}; flex-shrink:0;`;
badge.textContent = isBackground ? '🌄' : sceneLayers.filter(l => !l.isBackground).length;
const input = document.createElement('input');
input.type = 'text';
input.placeholder = isBackground
? 'Cenário de fundo (Ex: parque de diversões)'
: 'Elemento (Ex: menino de 6 anos)';
input.style.cssText = `flex:1; background:transparent; border:none; outline:none;
color:var(--text-primary); font-size:0.82rem; padding:2px 0; min-width:0;`;
input.addEventListener('input', () => {
const layer = sceneLayers.find(l => l.id === id);
if (layer) layer.description = input.value;
});
// Botões de ação
const btnVisibility = document.createElement('button');
btnVisibility.title = 'Ocultar/Mostrar camada no canvas';
btnVisibility.innerHTML = '👁️';
btnVisibility.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.9rem;
padding:2px; opacity:1; transition:opacity 0.2s; flex-shrink:0;`;
let visible = true;
btnVisibility.addEventListener('click', () => {
const layer = sceneLayers.find(l => l.id === id);
if (!layer || !layer.canvasItem) return;
visible = !visible;
layer.visible = visible;
layer.canvasItem.style.display = visible ? '' : 'none';
btnVisibility.style.opacity = visible ? '1' : '0.3';
btnVisibility.title = visible ? 'Ocultar camada' : 'Mostrar camada';
});
const btnPin = document.createElement('button');
btnPin.title = 'Fixar posição (impede arrastar)';
btnPin.innerHTML = '📌';
btnPin.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.9rem;
padding:2px; opacity:0.4; transition:opacity 0.2s; flex-shrink:0;`;
let pinned = false;
btnPin.addEventListener('click', () => {
const layer = sceneLayers.find(l => l.id === id);
if (!layer || !layer.canvasItem) return;
pinned = !pinned;
layer.pinned = pinned;
layer.canvasItem.style.pointerEvents = pinned ? 'none' : '';
layer.canvasItem.style.cursor = pinned ? 'default' : 'move';
btnPin.style.opacity = pinned ? '1' : '0.4';
btnPin.title = pinned ? 'Desafixar posição' : 'Fixar posição';
});
const btnDelete = document.createElement('button');
btnDelete.title = 'Remover camada';
btnDelete.innerHTML = '🗑️';
btnDelete.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.9rem;
padding:2px; opacity:0.6; transition:opacity 0.2s; flex-shrink:0;`;
btnDelete.addEventListener('mouseenter', () => btnDelete.style.opacity = '1');
btnDelete.addEventListener('mouseleave', () => btnDelete.style.opacity = '0.6');
btnDelete.addEventListener('click', () => {
// Remove do canvas se já gerado
const layer = sceneLayers.find(l => l.id === id);
if (layer?.canvasItem) layer.canvasItem.remove();
// Remove do state
sceneLayers = sceneLayers.filter(l => l.id !== id);
// Remove do DOM
row.remove();
// Reindexar badges dos elementos
reindexLayerBadges();
});
row.appendChild(badge);
row.appendChild(input);
row.appendChild(btnVisibility);
row.appendChild(btnPin);
if (!isBackground) row.appendChild(btnDelete); // Fundo não pode ser deletado da lista
return { row, input, badge };
}
function reindexLayerBadges() {
let elIdx = 0;
sceneLayers.forEach(l => {
if (!l.isBackground && l.rowBadge) {
l.rowBadge.textContent = ++elIdx;
}
});
}
function addSceneLayerEntry(isBackground = false) {
const id = `sl_${++sceneLayerCounter}`;
const layerObj = { id, description: '', isBackground, visible: true, pinned: false, canvasItem: null };
sceneLayers.push(layerObj);
const { row, input, badge } = createSceneLayerRow(id, isBackground);
layerObj.rowInput = input;
layerObj.rowBadge = badge;
if (sceneLayerManagerList) sceneLayerManagerList.appendChild(row);
input.focus();
return layerObj;
}
// Inicializa com camada de fundo padrão
addSceneLayerEntry(true);
if (btnAddSceneLayer) {
btnAddSceneLayer.addEventListener('click', () => {
addSceneLayerEntry(false);
reindexLayerBadges();
});
}
// ===== REMOÇÃO DE FUNDO BRANCO VIA CANVAS API =====
async function removeWhiteBackground(imgUrl, threshold = 235) {
return new Promise((resolve) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const offscreen = document.createElement('canvas');
offscreen.width = img.naturalWidth;
offscreen.height = img.naturalHeight;
const ctx = offscreen.getContext('2d', { willReadFrequently: true });
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, offscreen.width, offscreen.height);
const data = imageData.data;
const softRange = 20;
for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i + 1], b = data[i + 2];
const minChannel = Math.min(r, g, b);
if (minChannel >= threshold) {
data[i + 3] = 0;
} else if (minChannel >= threshold - softRange) {
const ratio = (minChannel - (threshold - softRange)) / softRange;
data[i + 3] = Math.round((1 - ratio) * data[i + 3]);
}
}
ctx.putImageData(imageData, 0, 0);
resolve(offscreen.toDataURL('image/png'));
};
img.onerror = () => resolve(imgUrl);
img.src = imgUrl;
});
}
// ===== ADICIONA ITEM DE CAMADA NO CANVAS =====
function addSceneLayerToCanvas(layerData) {
if (!cartazPrintableArea) return null;
const item = document.createElement('div');
item.className = 'canvas-item canvas-scene-layer';
item.setAttribute('data-layer-name', layerData.name || '');
item.setAttribute('data-rotation', '0');
item.style.cssText = `position:absolute; background:transparent; box-sizing:border-box;`;
item.style.zIndex = layerData.isBackground ? '0' : String(10 + (layerData.zOffset || 0));
// Cria a imagem SEM usar innerHTML (preserva o handle de resize)
const imgEl = document.createElement('img');
imgEl.style.cssText = `width:100%; height:100%; object-fit:${layerData.isBackground ? 'cover' : 'contain'}; pointer-events:none; display:block;`;
imgEl.src = layerData.url;
item.appendChild(imgEl);
// Handle de resize — criado uma única vez e permanece
const handle = document.createElement('div');
handle.className = 'canvas-resize-handle';
item.appendChild(handle);
if (layerData.isBackground) {
item.style.left = '0';
item.style.top = '0';
item.style.width = '100%';
item.style.height = '100%';
item.title = '🖼️ Cenário de Fundo';
} else {
item.style.left = (layerData.left || 10) + '%';
item.style.top = (layerData.top || 10) + '%';
item.style.width = (layerData.width || 30) + '%';
item.style.height = (layerData.height || 30) + '%';
item.title = '🎬 ' + (layerData.name || 'Elemento');
// Remove fundo branco: atualiza apenas o src da img (preserva o handle)
removeWhiteBackground(layerData.url).then(transparentUrl => {
imgEl.src = transparentUrl;
});
}
cartazPrintableArea.appendChild(item);
setupCanvasItemEvents(item);
return item;
}
// ===== BOTÃO CRIAR CENA =====
if (btnDecomposeScene) {
btnDecomposeScene.addEventListener('click', async () => {
// Validar que há pelo menos fundo + 1 elemento com descrição
const bgLayer = sceneLayers.find(l => l.isBackground);
if (!bgLayer?.description?.trim()) {
showToast('Descreva o cenário de fundo (Camada 🌄)!', 'warning');
if (bgLayer?.rowInput) bgLayer.rowInput.focus();
return;
}
const elementLayers = sceneLayers.filter(l => !l.isBackground && l.description?.trim());
if (elementLayers.length === 0) {
showToast('Adicione pelo menos um elemento ( Adicionar Camada)!', 'warning');
return;
}
if (!canvasModeActive) {
if (cartazEditArea && cartazEditArea.style.display !== 'none') {
btnEditCartazToggle && btnEditCartazToggle.click();
}
enterCanvasMode();
}
btnDecomposeScene.disabled = true;
btnDecomposeScene.style.opacity = '0.6';
if (sceneDecomposeLoader) sceneDecomposeLoader.style.display = 'flex';
// Remover camadas anteriores do canvas
cartazPrintableArea.querySelectorAll('.canvas-scene-layer').forEach(el => el.remove());
sceneLayers.forEach(l => { l.canvasItem = null; });
// Gerar cada camada individualmente (uma por uma para acompanhar progresso)
const totalLayers = 1 + elementLayers.length; // fundo + elementos
let done = 0;
const updateStatus = () => {
done++;
if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = `Gerando camada ${done}/${totalLayers}...`;
};
try {
const titulo = activeCartazData?.titulo || '';
const token = localStorage.getItem('token');
// Gera todas as imagens em paralelo via API (usando os prompts diretos das camadas)
const allPrompts = [
{ isBackground: true, description: bgLayer.description, layerRef: bgLayer },
...elementLayers.map(l => ({ isBackground: false, description: l.description, layerRef: l }))
];
if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = `Enviando para a IA...`;
const response = await fetch('/api/cartazes/decompose-scene', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({
titulo,
// Mandamos a estrutura de camadas diretamente (novo campo layers_manual)
layers_manual: allPrompts.map(p => ({
isBackground: p.isBackground,
description: p.description
}))
})
});
if (!response.ok) {
const errData = await response.json().catch(() => ({}));
throw new Error(errData.error || 'Erro na geração da cena.');
}
const data = await response.json();
if (!data.success) throw new Error('Resposta inválida do servidor.');
if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = 'Montando as camadas no canvas...';
// Fundo
if (data.backgroundUrl && bgLayer) {
bgLayer.canvasItem = addSceneLayerToCanvas({
url: data.backgroundUrl, name: bgLayer.description, isBackground: true
});
}
// Elementos
const spread = [
{ left: 8, top: 32 }, { left: 42, top: 38 },
{ left: 64, top: 28 }, { left: 22, top: 56 }
];
if (Array.isArray(data.layers)) {
data.layers.forEach((layer, idx) => {
if (!layer.url) return;
const elLayerRef = elementLayers[idx];
const pos = spread[idx % spread.length];
const canvasItem = addSceneLayerToCanvas({
url: layer.url,
name: layer.name || elLayerRef?.description || `Elemento ${idx + 1}`,
isBackground: false,
left: pos.left, top: pos.top, width: 30, height: 30,
zOffset: idx + 1
});
if (elLayerRef) elLayerRef.canvasItem = canvasItem;
});
}
showToast('🎬 Cena montada! Use os botões 👁️ 📌 🗑️ para controlar cada camada.', 'success');
} catch (err) {
console.error('[Cenas em Camadas]', err);
showToast('Erro ao criar a cena: ' + err.message, 'error');
} finally {
btnDecomposeScene.disabled = false;
btnDecomposeScene.style.opacity = '1';
if (sceneDecomposeLoader) sceneDecomposeLoader.style.display = 'none';
}
});
}
// Cria um item de camada de imagem direto no canvas (background ou sticker)
// Remoção de fundo branco via Canvas API — gera PNG com transparência real
+21 -11
View File
@@ -1885,17 +1885,31 @@
<!-- 🎬 ESTÚDIO DE CENAS EM CAMADAS -->
<div id="scenesStudioPanel" style="background: linear-gradient(135deg, rgba(139,92,246,0.12), rgba(59,130,246,0.08)); border: 1px solid rgba(139,92,246,0.3); border-radius: 12px; padding: 14px; display: flex; flex-direction: column; gap: 10px;">
<!-- Header -->
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.3rem;">🎬</span>
<div>
<div style="flex:1;">
<div style="font-weight: 700; color: var(--text-primary); font-size: 0.9rem;">Estúdio de Cenas em Camadas</div>
<div style="font-size: 0.72rem; color: var(--text-secondary); margin-top: 1px;">A IA desmonta a cena em elementos editáveis individualmente</div>
<div style="font-size: 0.72rem; color: var(--text-secondary); margin-top: 1px;">Cada linha = uma camada gerada separadamente pela IA</div>
</div>
</div>
<textarea id="sceneDecomposePrompt" class="obs-input" rows="3" placeholder="Descreva a cena... Ex: Duas crianças e um cachorrinho num gramado de parque, com um escorregador ao fundo" style="resize: vertical; font-size: 0.85rem; padding: 10px; border-radius: 8px; line-height: 1.4;"></textarea>
<button id="btnDecomposeScene" class="btn-save-settings" style="background: linear-gradient(135deg, #8b5cf6, #3b82f6); color: white; border: none; padding: 8px 14px; font-size: 0.82rem; font-weight: 700; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: opacity 0.2s;">
<span>✨ Criar Cena em Camadas</span>
<!-- Lista dinâmica de camadas -->
<div id="sceneLayerManagerList" style="display: flex; flex-direction: column; gap: 6px;">
<!-- Camadas inseridas por JS -->
</div>
<!-- Botão Adicionar Camada -->
<button id="btnAddSceneLayer" style="background: rgba(139,92,246,0.15); border: 1px dashed rgba(139,92,246,0.5); color: rgba(139,92,246,1); padding: 7px; font-size: 0.82rem; font-weight: 600; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; transition: all 0.2s; width: 100%;">
Adicionar Camada
</button>
<!-- Botão Criar Cena -->
<button id="btnDecomposeScene" style="background: linear-gradient(135deg, #8b5cf6, #3b82f6); color: white; border: none; padding: 9px 14px; font-size: 0.85rem; font-weight: 700; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: opacity 0.2s;">
✨ Criar Cena
</button>
<!-- Loading state -->
<div id="sceneDecomposeLoader" style="display: none; flex-direction: column; gap: 8px; align-items: center; padding: 10px; background: rgba(0,0,0,0.15); border-radius: 8px;">
<div style="display: flex; gap: 6px; align-items: center;">
@@ -1903,13 +1917,9 @@
<div class="loading-dot" style="width:8px;height:8px;border-radius:50%;background:#8b5cf6;animation:dot-pulse 1.2s ease-in-out 0.4s infinite;"></div>
<div class="loading-dot" style="width:8px;height:8px;border-radius:50%;background:#8b5cf6;animation:dot-pulse 1.2s ease-in-out 0.8s infinite;"></div>
</div>
<div id="sceneDecomposeStatus" style="font-size: 0.78rem; color: var(--text-secondary); text-align: center;">Planejando a cena com IA...</div>
</div>
<!-- Camadas geradas -->
<div id="sceneLayersList" style="display: none; flex-direction: column; gap: 6px;">
<div style="font-size: 0.75rem; color: var(--text-secondary); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">📋 Camadas no Canvas:</div>
<div id="sceneLayersItems" style="display: flex; flex-direction: column; gap: 4px;"></div>
<div id="sceneDecomposeStatus" style="font-size: 0.78rem; color: var(--text-secondary); text-align: center;">Gerando camadas...</div>
</div>
</div>
<div class="settings-group">
+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 {