✨ feat: Adicionado gerenciador de camadas para a geração de cartazes e resolvido bug de resize
This commit is contained in:
+326
-4
@@ -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
@@ -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">
|
||||
|
||||
@@ -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